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,699 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright instrument — Pipeline Instrumentation & Feedback Loops ║
|
|
4
|
+
# ║ Records predicted vs actual metrics · Enables learning · Trend analysis ║
|
|
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
|
+
format_duration() {
|
|
38
|
+
local secs="$1"
|
|
39
|
+
if [[ "$secs" -ge 3600 ]]; then
|
|
40
|
+
printf "%dh %dm %ds" $((secs/3600)) $((secs%3600/60)) $((secs%60))
|
|
41
|
+
elif [[ "$secs" -ge 60 ]]; then
|
|
42
|
+
printf "%dm %ds" $((secs/60)) $((secs%60))
|
|
43
|
+
else
|
|
44
|
+
printf "%ds" "$secs"
|
|
45
|
+
fi
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
format_tokens() {
|
|
49
|
+
local tokens="$1"
|
|
50
|
+
if [[ "$tokens" -ge 1000000 ]]; then
|
|
51
|
+
printf "%.1fM" "$(echo "scale=1; $tokens / 1000000" | bc)"
|
|
52
|
+
elif [[ "$tokens" -ge 1000 ]]; then
|
|
53
|
+
printf "%.1fK" "$(echo "scale=1; $tokens / 1000" | bc)"
|
|
54
|
+
else
|
|
55
|
+
printf "%d" "$tokens"
|
|
56
|
+
fi
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
format_cost() {
|
|
60
|
+
printf '$%.2f' "$(echo "scale=2; $1" | bc)"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
percent_delta() {
|
|
64
|
+
local predicted="$1"
|
|
65
|
+
local actual="$2"
|
|
66
|
+
if [[ "$predicted" -eq 0 ]]; then
|
|
67
|
+
echo "N/A"
|
|
68
|
+
return
|
|
69
|
+
fi
|
|
70
|
+
local delta
|
|
71
|
+
delta=$(echo "scale=0; ($actual - $predicted) * 100 / $predicted" | bc)
|
|
72
|
+
if [[ "$delta" -ge 0 ]]; then
|
|
73
|
+
printf "+%d%%" "$delta"
|
|
74
|
+
else
|
|
75
|
+
printf "%d%%" "$delta"
|
|
76
|
+
fi
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
80
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
81
|
+
|
|
82
|
+
emit_event() {
|
|
83
|
+
local event_type="$1"
|
|
84
|
+
shift
|
|
85
|
+
local json_fields=""
|
|
86
|
+
for kv in "$@"; do
|
|
87
|
+
local key="${kv%%=*}"
|
|
88
|
+
local val="${kv#*=}"
|
|
89
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
90
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
91
|
+
else
|
|
92
|
+
val="${val//\"/\\\"}"
|
|
93
|
+
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
94
|
+
fi
|
|
95
|
+
done
|
|
96
|
+
mkdir -p "${HOME}/.shipwright"
|
|
97
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
# ─── Instrumentation Storage ───────────────────────────────────────────────
|
|
101
|
+
INSTRUMENT_DIR="${HOME}/.shipwright/instrumentation"
|
|
102
|
+
INSTRUMENT_ACTIVE="${INSTRUMENT_DIR}/active"
|
|
103
|
+
INSTRUMENT_COMPLETED="${HOME}/.shipwright/instrumentation.jsonl"
|
|
104
|
+
|
|
105
|
+
ensure_instrument_dirs() {
|
|
106
|
+
mkdir -p "$INSTRUMENT_ACTIVE" "${HOME}/.shipwright"
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
# ─── Help ───────────────────────────────────────────────────────────────────
|
|
110
|
+
show_help() {
|
|
111
|
+
echo ""
|
|
112
|
+
echo -e "${CYAN}${BOLD} Shipwright Instrumentation${RESET} ${DIM}v${VERSION}${RESET}"
|
|
113
|
+
echo -e "${DIM} ══════════════════════════════════════════════════════════════${RESET}"
|
|
114
|
+
echo ""
|
|
115
|
+
echo -e " ${BOLD}USAGE${RESET}"
|
|
116
|
+
echo -e " shipwright instrument <command> [options]"
|
|
117
|
+
echo ""
|
|
118
|
+
echo -e " ${BOLD}COMMANDS${RESET}"
|
|
119
|
+
echo -e " ${CYAN}start${RESET} Begin instrumenting a pipeline run"
|
|
120
|
+
echo -e " ${CYAN}record${RESET} Record a metric during execution"
|
|
121
|
+
echo -e " ${CYAN}stage-start${RESET} Mark the start of a stage"
|
|
122
|
+
echo -e " ${CYAN}stage-end${RESET} Mark the end of a stage with result"
|
|
123
|
+
echo -e " ${CYAN}finish${RESET} Complete a pipeline run record"
|
|
124
|
+
echo -e " ${CYAN}summary${RESET} Show run summary (predicted vs actual)"
|
|
125
|
+
echo -e " ${CYAN}trends${RESET} Show prediction accuracy over time"
|
|
126
|
+
echo -e " ${CYAN}export${RESET} Export instrumentation data"
|
|
127
|
+
echo -e " ${CYAN}help${RESET} Show this help message"
|
|
128
|
+
echo ""
|
|
129
|
+
echo -e " ${BOLD}START OPTIONS${RESET}"
|
|
130
|
+
echo -e " --run-id ID Unique run identifier (required)"
|
|
131
|
+
echo -e " --issue N GitHub issue number"
|
|
132
|
+
echo -e " --repo PATH Repository path (default: current)"
|
|
133
|
+
echo -e " --predicted <json> Initial predicted values (optional)"
|
|
134
|
+
echo ""
|
|
135
|
+
echo -e " ${BOLD}RECORD OPTIONS${RESET}"
|
|
136
|
+
echo -e " --run-id ID Run identifier (required)"
|
|
137
|
+
echo -e " --stage NAME Stage name (required)"
|
|
138
|
+
echo -e " --metric NAME Metric name (required)"
|
|
139
|
+
echo -e " --value VAL Metric value (required)"
|
|
140
|
+
echo ""
|
|
141
|
+
echo -e " ${BOLD}STAGE OPTIONS${RESET}"
|
|
142
|
+
echo -e " --run-id ID Run identifier (required)"
|
|
143
|
+
echo -e " --stage NAME Stage name (required)"
|
|
144
|
+
echo -e " --result success|fail Result (stage-end only)"
|
|
145
|
+
echo ""
|
|
146
|
+
echo -e " ${BOLD}FINISH OPTIONS${RESET}"
|
|
147
|
+
echo -e " --run-id ID Run identifier (required)"
|
|
148
|
+
echo -e " --result success|fail Final pipeline result"
|
|
149
|
+
echo ""
|
|
150
|
+
echo -e " ${BOLD}SUMMARY OPTIONS${RESET}"
|
|
151
|
+
echo -e " --run-id ID Run identifier (required)"
|
|
152
|
+
echo ""
|
|
153
|
+
echo -e " ${BOLD}TRENDS OPTIONS${RESET}"
|
|
154
|
+
echo -e " --metric NAME Filter by metric (optional)"
|
|
155
|
+
echo -e " --last N Last N runs (default: 20)"
|
|
156
|
+
echo ""
|
|
157
|
+
echo -e " ${BOLD}EXPORT OPTIONS${RESET}"
|
|
158
|
+
echo -e " --format json|csv Output format (default: json)"
|
|
159
|
+
echo -e " --last N Last N runs (default: all)"
|
|
160
|
+
echo ""
|
|
161
|
+
echo -e " ${BOLD}EXAMPLES${RESET}"
|
|
162
|
+
echo -e " ${DIM}# Start instrumenting a run${RESET}"
|
|
163
|
+
echo -e " shipwright instrument start --run-id abc123 --issue 42"
|
|
164
|
+
echo ""
|
|
165
|
+
echo -e " ${DIM}# Record stage execution${RESET}"
|
|
166
|
+
echo -e " shipwright instrument stage-start --run-id abc123 --stage plan"
|
|
167
|
+
echo -e " ${DIM}# ... do work ...${RESET}"
|
|
168
|
+
echo -e " shipwright instrument stage-end --run-id abc123 --stage plan --result success"
|
|
169
|
+
echo ""
|
|
170
|
+
echo -e " ${DIM}# Record individual metric${RESET}"
|
|
171
|
+
echo -e " shipwright instrument record --run-id abc123 --stage build --metric iterations --value 7"
|
|
172
|
+
echo ""
|
|
173
|
+
echo -e " ${DIM}# Finish the run${RESET}"
|
|
174
|
+
echo -e " shipwright instrument finish --run-id abc123 --result success"
|
|
175
|
+
echo ""
|
|
176
|
+
echo -e " ${DIM}# Show what was predicted vs actual${RESET}"
|
|
177
|
+
echo -e " shipwright instrument summary --run-id abc123"
|
|
178
|
+
echo ""
|
|
179
|
+
echo -e " ${DIM}# Analyze trends across runs${RESET}"
|
|
180
|
+
echo -e " shipwright instrument trends --metric duration --last 30"
|
|
181
|
+
echo ""
|
|
182
|
+
echo -e " ${DIM}# Export data for analysis${RESET}"
|
|
183
|
+
echo -e " shipwright instrument export --format csv --last 50"
|
|
184
|
+
echo ""
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# ─── Start Instrumentation ──────────────────────────────────────────────────
|
|
188
|
+
cmd_start() {
|
|
189
|
+
local run_id="" issue="" repo="." predicted=""
|
|
190
|
+
|
|
191
|
+
while [[ $# -gt 0 ]]; do
|
|
192
|
+
case "$1" in
|
|
193
|
+
--run-id) run_id="${2:-}"; shift 2 ;;
|
|
194
|
+
--issue) issue="${2:-}"; shift 2 ;;
|
|
195
|
+
--repo) repo="${2:-}"; shift 2 ;;
|
|
196
|
+
--predicted) predicted="${2:-}"; shift 2 ;;
|
|
197
|
+
*)
|
|
198
|
+
warn "Unknown option: $1"
|
|
199
|
+
shift
|
|
200
|
+
;;
|
|
201
|
+
esac
|
|
202
|
+
done
|
|
203
|
+
|
|
204
|
+
if [[ -z "$run_id" ]]; then
|
|
205
|
+
error "Usage: shipwright instrument start --run-id ID [--issue N] [--repo PATH]"
|
|
206
|
+
return 1
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
ensure_instrument_dirs
|
|
210
|
+
|
|
211
|
+
local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
|
|
212
|
+
local tmp_file
|
|
213
|
+
tmp_file="$(mktemp "${INSTRUMENT_ACTIVE}/.tmp.XXXXXX")"
|
|
214
|
+
|
|
215
|
+
# Get repo info if not provided
|
|
216
|
+
if [[ "$repo" == "." ]]; then
|
|
217
|
+
repo="$(cd "$repo" && pwd 2>/dev/null || echo "unknown")"
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# Build initial record with jq
|
|
221
|
+
jq -n \
|
|
222
|
+
--arg run_id "$run_id" \
|
|
223
|
+
--argjson issue "$([ -n "$issue" ] && echo "$issue" || echo "null")" \
|
|
224
|
+
--arg repo "$repo" \
|
|
225
|
+
--arg started_at "$(now_iso)" \
|
|
226
|
+
--argjson started_epoch "$(now_epoch)" \
|
|
227
|
+
--arg predicted "$predicted" \
|
|
228
|
+
'{
|
|
229
|
+
run_id: $run_id,
|
|
230
|
+
issue: $issue,
|
|
231
|
+
repo: $repo,
|
|
232
|
+
started_at: $started_at,
|
|
233
|
+
started_epoch: $started_epoch,
|
|
234
|
+
finished_at: null,
|
|
235
|
+
finished_epoch: null,
|
|
236
|
+
result: null,
|
|
237
|
+
predicted: (if $predicted == "" then {} else ($predicted | fromjson) end),
|
|
238
|
+
actual: {},
|
|
239
|
+
stages: {},
|
|
240
|
+
metrics: []
|
|
241
|
+
}' > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
|
|
242
|
+
|
|
243
|
+
mv "$tmp_file" "$run_file"
|
|
244
|
+
success "Started instrumentation for run ${CYAN}${run_id}${RESET} (issue #${issue})"
|
|
245
|
+
emit_event "instrument_start" "run_id=${run_id}" "issue=${issue}" "repo=${repo}"
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# ─── Record Metric ───────────────────────────────────────────────────────────
|
|
249
|
+
cmd_record() {
|
|
250
|
+
local run_id="" stage="" metric="" value=""
|
|
251
|
+
|
|
252
|
+
while [[ $# -gt 0 ]]; do
|
|
253
|
+
case "$1" in
|
|
254
|
+
--run-id) run_id="${2:-}"; shift 2 ;;
|
|
255
|
+
--stage) stage="${2:-}"; shift 2 ;;
|
|
256
|
+
--metric) metric="${2:-}"; shift 2 ;;
|
|
257
|
+
--value) value="${2:-}"; shift 2 ;;
|
|
258
|
+
*)
|
|
259
|
+
warn "Unknown option: $1"
|
|
260
|
+
shift
|
|
261
|
+
;;
|
|
262
|
+
esac
|
|
263
|
+
done
|
|
264
|
+
|
|
265
|
+
if [[ -z "$run_id" || -z "$stage" || -z "$metric" || -z "$value" ]]; then
|
|
266
|
+
error "Usage: shipwright instrument record --run-id ID --stage NAME --metric NAME --value VAL"
|
|
267
|
+
return 1
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
ensure_instrument_dirs
|
|
271
|
+
local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
|
|
272
|
+
|
|
273
|
+
if [[ ! -f "$run_file" ]]; then
|
|
274
|
+
error "Run not found: ${run_id}"
|
|
275
|
+
return 1
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
local tmp_file
|
|
279
|
+
tmp_file="$(mktemp "${INSTRUMENT_ACTIVE}/.tmp.XXXXXX")"
|
|
280
|
+
|
|
281
|
+
# Parse value as number if it's numeric
|
|
282
|
+
local value_json
|
|
283
|
+
if [[ "$value" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
284
|
+
value_json="$value"
|
|
285
|
+
else
|
|
286
|
+
value_json="\"$value\""
|
|
287
|
+
fi
|
|
288
|
+
|
|
289
|
+
# Append metric record with jq
|
|
290
|
+
jq \
|
|
291
|
+
--arg stage "$stage" \
|
|
292
|
+
--arg metric "$metric" \
|
|
293
|
+
--argjson value "$value_json" \
|
|
294
|
+
--arg recorded_at "$(now_iso)" \
|
|
295
|
+
'.metrics += [
|
|
296
|
+
{
|
|
297
|
+
stage: $stage,
|
|
298
|
+
metric: $metric,
|
|
299
|
+
value: $value,
|
|
300
|
+
recorded_at: $recorded_at
|
|
301
|
+
}
|
|
302
|
+
] |
|
|
303
|
+
.actual[$stage] //= {} |
|
|
304
|
+
.actual[$stage][$metric] = $value' \
|
|
305
|
+
"$run_file" > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
|
|
306
|
+
|
|
307
|
+
mv "$tmp_file" "$run_file"
|
|
308
|
+
success "Recorded ${CYAN}${metric}${RESET}=${CYAN}${value}${RESET} for stage ${CYAN}${stage}${RESET}"
|
|
309
|
+
emit_event "instrument_record" "run_id=${run_id}" "stage=${stage}" "metric=${metric}" "value=${value}"
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# ─── Stage Start ─────────────────────────────────────────────────────────────
|
|
313
|
+
cmd_stage_start() {
|
|
314
|
+
local run_id="" stage=""
|
|
315
|
+
|
|
316
|
+
while [[ $# -gt 0 ]]; do
|
|
317
|
+
case "$1" in
|
|
318
|
+
--run-id) run_id="${2:-}"; shift 2 ;;
|
|
319
|
+
--stage) stage="${2:-}"; shift 2 ;;
|
|
320
|
+
*)
|
|
321
|
+
warn "Unknown option: $1"
|
|
322
|
+
shift
|
|
323
|
+
;;
|
|
324
|
+
esac
|
|
325
|
+
done
|
|
326
|
+
|
|
327
|
+
if [[ -z "$run_id" || -z "$stage" ]]; then
|
|
328
|
+
error "Usage: shipwright instrument stage-start --run-id ID --stage NAME"
|
|
329
|
+
return 1
|
|
330
|
+
fi
|
|
331
|
+
|
|
332
|
+
ensure_instrument_dirs
|
|
333
|
+
local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
|
|
334
|
+
|
|
335
|
+
if [[ ! -f "$run_file" ]]; then
|
|
336
|
+
error "Run not found: ${run_id}"
|
|
337
|
+
return 1
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
local tmp_file
|
|
341
|
+
tmp_file="$(mktemp "${INSTRUMENT_ACTIVE}/.tmp.XXXXXX")"
|
|
342
|
+
|
|
343
|
+
jq \
|
|
344
|
+
--arg stage "$stage" \
|
|
345
|
+
--arg started_at "$(now_iso)" \
|
|
346
|
+
--argjson started_epoch "$(now_epoch)" \
|
|
347
|
+
'.stages[$stage] //= {} |
|
|
348
|
+
.stages[$stage].started_at = $started_at |
|
|
349
|
+
.stages[$stage].started_epoch = $started_epoch' \
|
|
350
|
+
"$run_file" > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
|
|
351
|
+
|
|
352
|
+
mv "$tmp_file" "$run_file"
|
|
353
|
+
success "Started stage ${CYAN}${stage}${RESET}"
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# ─── Stage End ───────────────────────────────────────────────────────────────
|
|
357
|
+
cmd_stage_end() {
|
|
358
|
+
local run_id="" stage="" result=""
|
|
359
|
+
|
|
360
|
+
while [[ $# -gt 0 ]]; do
|
|
361
|
+
case "$1" in
|
|
362
|
+
--run-id) run_id="${2:-}"; shift 2 ;;
|
|
363
|
+
--stage) stage="${2:-}"; shift 2 ;;
|
|
364
|
+
--result) result="${2:-}"; shift 2 ;;
|
|
365
|
+
*)
|
|
366
|
+
warn "Unknown option: $1"
|
|
367
|
+
shift
|
|
368
|
+
;;
|
|
369
|
+
esac
|
|
370
|
+
done
|
|
371
|
+
|
|
372
|
+
if [[ -z "$run_id" || -z "$stage" ]]; then
|
|
373
|
+
error "Usage: shipwright instrument stage-end --run-id ID --stage NAME [--result success|failure|timeout]"
|
|
374
|
+
return 1
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
ensure_instrument_dirs
|
|
378
|
+
local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
|
|
379
|
+
|
|
380
|
+
if [[ ! -f "$run_file" ]]; then
|
|
381
|
+
error "Run not found: ${run_id}"
|
|
382
|
+
return 1
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
local tmp_file
|
|
386
|
+
tmp_file="$(mktemp "${INSTRUMENT_ACTIVE}/.tmp.XXXXXX")"
|
|
387
|
+
|
|
388
|
+
jq \
|
|
389
|
+
--arg stage "$stage" \
|
|
390
|
+
--arg finished_at "$(now_iso)" \
|
|
391
|
+
--argjson finished_epoch "$(now_epoch)" \
|
|
392
|
+
--arg result "$result" \
|
|
393
|
+
'.stages[$stage] //= {} |
|
|
394
|
+
.stages[$stage].finished_at = $finished_at |
|
|
395
|
+
.stages[$stage].finished_epoch = $finished_epoch |
|
|
396
|
+
(if .stages[$stage].started_epoch and .stages[$stage].finished_epoch then
|
|
397
|
+
.stages[$stage].duration_s = (.stages[$stage].finished_epoch - .stages[$stage].started_epoch)
|
|
398
|
+
else . end) |
|
|
399
|
+
(if $result != "" then .stages[$stage].result = $result else . end)' \
|
|
400
|
+
"$run_file" > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
|
|
401
|
+
|
|
402
|
+
mv "$tmp_file" "$run_file"
|
|
403
|
+
success "Finished stage ${CYAN}${stage}${RESET} (${result})"
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
# ─── Finish Run ──────────────────────────────────────────────────────────────
|
|
407
|
+
cmd_finish() {
|
|
408
|
+
local run_id="" result=""
|
|
409
|
+
|
|
410
|
+
while [[ $# -gt 0 ]]; do
|
|
411
|
+
case "$1" in
|
|
412
|
+
--run-id) run_id="${2:-}"; shift 2 ;;
|
|
413
|
+
--result) result="${2:-}"; shift 2 ;;
|
|
414
|
+
*)
|
|
415
|
+
warn "Unknown option: $1"
|
|
416
|
+
shift
|
|
417
|
+
;;
|
|
418
|
+
esac
|
|
419
|
+
done
|
|
420
|
+
|
|
421
|
+
if [[ -z "$run_id" ]]; then
|
|
422
|
+
error "Usage: shipwright instrument finish --run-id ID [--result success|failure|timeout]"
|
|
423
|
+
return 1
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
ensure_instrument_dirs
|
|
427
|
+
local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
|
|
428
|
+
|
|
429
|
+
if [[ ! -f "$run_file" ]]; then
|
|
430
|
+
error "Run not found: ${run_id}"
|
|
431
|
+
return 1
|
|
432
|
+
fi
|
|
433
|
+
|
|
434
|
+
local tmp_file
|
|
435
|
+
tmp_file="$(mktemp "${INSTRUMENT_ACTIVE}/.tmp.XXXXXX")"
|
|
436
|
+
|
|
437
|
+
# Update run record with finish data
|
|
438
|
+
jq \
|
|
439
|
+
--arg finished_at "$(now_iso)" \
|
|
440
|
+
--argjson finished_epoch "$(now_epoch)" \
|
|
441
|
+
--arg result "$result" \
|
|
442
|
+
'.finished_at = $finished_at |
|
|
443
|
+
.finished_epoch = $finished_epoch |
|
|
444
|
+
.result = $result |
|
|
445
|
+
(.finished_epoch - .started_epoch) as $total_duration |
|
|
446
|
+
.total_duration_s = $total_duration' \
|
|
447
|
+
"$run_file" > "$tmp_file" || { rm -f "$tmp_file"; return 1; }
|
|
448
|
+
|
|
449
|
+
# Compact and append to JSONL (single-line JSON)
|
|
450
|
+
jq -c '.' "$tmp_file" >> "$INSTRUMENT_COMPLETED"
|
|
451
|
+
rm -f "$tmp_file"
|
|
452
|
+
|
|
453
|
+
# Remove active file
|
|
454
|
+
rm -f "$run_file"
|
|
455
|
+
|
|
456
|
+
success "Finished instrumentation for run ${CYAN}${run_id}${RESET} (${result})"
|
|
457
|
+
emit_event "instrument_finish" "run_id=${run_id}" "result=${result}"
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
# ─── Show Summary ────────────────────────────────────────────────────────────
|
|
461
|
+
cmd_summary() {
|
|
462
|
+
local run_id=""
|
|
463
|
+
|
|
464
|
+
while [[ $# -gt 0 ]]; do
|
|
465
|
+
case "$1" in
|
|
466
|
+
--run-id) run_id="${2:-}"; shift 2 ;;
|
|
467
|
+
*)
|
|
468
|
+
warn "Unknown option: $1"
|
|
469
|
+
shift
|
|
470
|
+
;;
|
|
471
|
+
esac
|
|
472
|
+
done
|
|
473
|
+
|
|
474
|
+
if [[ -z "$run_id" ]]; then
|
|
475
|
+
error "Usage: shipwright instrument summary --run-id ID"
|
|
476
|
+
return 1
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
ensure_instrument_dirs
|
|
480
|
+
|
|
481
|
+
# Check active first, then completed
|
|
482
|
+
local run_file="${INSTRUMENT_ACTIVE}/${run_id}.json"
|
|
483
|
+
if [[ ! -f "$run_file" ]]; then
|
|
484
|
+
# Try to find in JSONL
|
|
485
|
+
run_file=$(grep -l "\"run_id\":\"${run_id}\"" "$INSTRUMENT_COMPLETED" 2>/dev/null | head -1 || true)
|
|
486
|
+
if [[ -z "$run_file" ]]; then
|
|
487
|
+
error "Run not found: ${run_id}"
|
|
488
|
+
return 1
|
|
489
|
+
fi
|
|
490
|
+
# Extract matching record from JSONL
|
|
491
|
+
local tmp_file
|
|
492
|
+
tmp_file="$(mktemp)"
|
|
493
|
+
grep "\"run_id\":\"${run_id}\"" "$INSTRUMENT_COMPLETED" | head -1 > "$tmp_file"
|
|
494
|
+
run_file="$tmp_file"
|
|
495
|
+
trap "rm -f '$tmp_file'" RETURN
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
# Extract data
|
|
499
|
+
local issue result started_at finished_at total_dur
|
|
500
|
+
local pred_dur pred_iter pred_tokens pred_cost
|
|
501
|
+
local actual_dur actual_iter actual_tokens actual_cost
|
|
502
|
+
|
|
503
|
+
issue=$(jq -r '.issue // "N/A"' "$run_file")
|
|
504
|
+
result=$(jq -r '.result // "pending"' "$run_file")
|
|
505
|
+
started_at=$(jq -r '.started_at' "$run_file")
|
|
506
|
+
finished_at=$(jq -r '.finished_at // "in progress"' "$run_file")
|
|
507
|
+
total_dur=$(jq -r '.total_duration_s // 0' "$run_file")
|
|
508
|
+
|
|
509
|
+
# Predicted values
|
|
510
|
+
pred_dur=$(jq -r '.predicted.timeout // 0' "$run_file")
|
|
511
|
+
pred_iter=$(jq -r '.predicted.iterations // 0' "$run_file")
|
|
512
|
+
pred_tokens=$(jq -r '.predicted.tokens // 0' "$run_file")
|
|
513
|
+
pred_cost=$(jq -r '.predicted.cost // 0' "$run_file")
|
|
514
|
+
|
|
515
|
+
# Aggregate actual values
|
|
516
|
+
actual_iter=$(jq '[.metrics[] | select(.metric == "iterations") | .value] | max' "$run_file" 2>/dev/null || echo "0")
|
|
517
|
+
actual_tokens=$(jq '[.metrics[] | select(.metric == "tokens_total") | .value] | max' "$run_file" 2>/dev/null || echo "0")
|
|
518
|
+
actual_cost=$(jq '[.metrics[] | select(.metric == "cost_usd") | .value] | add // 0' "$run_file" 2>/dev/null || echo "0")
|
|
519
|
+
actual_dur="$total_dur"
|
|
520
|
+
|
|
521
|
+
# Print summary
|
|
522
|
+
echo ""
|
|
523
|
+
echo -e "${CYAN}${BOLD} Pipeline Run ${run_id}${RESET} ${DIM}Issue #${issue}${RESET}"
|
|
524
|
+
echo -e "${DIM} ═════════════════════════════════════════════════════════════════${RESET}"
|
|
525
|
+
echo ""
|
|
526
|
+
echo -e " ${BOLD}Result:${RESET} ${result}"
|
|
527
|
+
echo -e " ${DIM}Started: ${started_at}${RESET}"
|
|
528
|
+
echo -e " ${DIM}Finished: ${finished_at}${RESET}"
|
|
529
|
+
echo ""
|
|
530
|
+
echo -e " ${BOLD}Predicted vs Actual${RESET}"
|
|
531
|
+
echo ""
|
|
532
|
+
|
|
533
|
+
# Print table header
|
|
534
|
+
printf " %-20s %-15s %-15s %-10s\n" "Metric" "Predicted" "Actual" "Delta"
|
|
535
|
+
echo -e " ${DIM}────────────────────────────────────────────────────────────────${RESET}"
|
|
536
|
+
|
|
537
|
+
# Duration
|
|
538
|
+
local dur_fmt_pred dur_fmt_act dur_delta
|
|
539
|
+
dur_fmt_pred=$(format_duration "$pred_dur")
|
|
540
|
+
dur_fmt_act=$(format_duration "$actual_dur")
|
|
541
|
+
dur_delta=$(percent_delta "$pred_dur" "$actual_dur")
|
|
542
|
+
printf " %-20s %-15s %-15s %-10s\n" "Duration" "$dur_fmt_pred" "$dur_fmt_act" "$dur_delta"
|
|
543
|
+
|
|
544
|
+
# Iterations
|
|
545
|
+
if [[ "$pred_iter" -gt 0 || "$actual_iter" -gt 0 ]]; then
|
|
546
|
+
local iter_delta
|
|
547
|
+
iter_delta=$(percent_delta "$pred_iter" "$actual_iter")
|
|
548
|
+
printf " %-20s %-15s %-15s %-10s\n" "Iterations" "$pred_iter" "$actual_iter" "$iter_delta"
|
|
549
|
+
fi
|
|
550
|
+
|
|
551
|
+
# Tokens
|
|
552
|
+
if [[ "$pred_tokens" -gt 0 || "$actual_tokens" -gt 0 ]]; then
|
|
553
|
+
local tok_fmt_pred tok_fmt_act tok_delta
|
|
554
|
+
tok_fmt_pred=$(format_tokens "$pred_tokens")
|
|
555
|
+
tok_fmt_act=$(format_tokens "$actual_tokens")
|
|
556
|
+
tok_delta=$(percent_delta "$pred_tokens" "$actual_tokens")
|
|
557
|
+
printf " %-20s %-15s %-15s %-10s\n" "Tokens" "$tok_fmt_pred" "$tok_fmt_act" "$tok_delta"
|
|
558
|
+
fi
|
|
559
|
+
|
|
560
|
+
# Cost
|
|
561
|
+
if [[ $(echo "$pred_cost > 0" | bc) -eq 1 || $(echo "$actual_cost > 0" | bc) -eq 1 ]]; then
|
|
562
|
+
local cost_fmt_pred cost_fmt_act cost_delta
|
|
563
|
+
cost_fmt_pred=$(format_cost "$pred_cost")
|
|
564
|
+
cost_fmt_act=$(format_cost "$actual_cost")
|
|
565
|
+
cost_delta=$(percent_delta "$(echo "$pred_cost * 100" | bc)" "$(echo "$actual_cost * 100" | bc)")
|
|
566
|
+
printf " %-20s %-15s %-15s %-10s\n" "Cost" "$cost_fmt_pred" "$cost_fmt_act" "$cost_delta"
|
|
567
|
+
fi
|
|
568
|
+
|
|
569
|
+
echo ""
|
|
570
|
+
echo -e " ${BOLD}Stages${RESET}"
|
|
571
|
+
echo ""
|
|
572
|
+
jq -r '.stages | to_entries[] | " \(.key): \(.value.result // "pending") (\(.value.duration_s // "?")s)"' "$run_file"
|
|
573
|
+
echo ""
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
# ─── Show Trends ─────────────────────────────────────────────────────────────
|
|
577
|
+
cmd_trends() {
|
|
578
|
+
local metric="" last=20
|
|
579
|
+
|
|
580
|
+
while [[ $# -gt 0 ]]; do
|
|
581
|
+
case "$1" in
|
|
582
|
+
--metric) metric="${2:-}"; shift 2 ;;
|
|
583
|
+
--last) last="${2:-20}"; shift 2 ;;
|
|
584
|
+
*)
|
|
585
|
+
warn "Unknown option: $1"
|
|
586
|
+
shift
|
|
587
|
+
;;
|
|
588
|
+
esac
|
|
589
|
+
done
|
|
590
|
+
|
|
591
|
+
if [[ ! -f "$INSTRUMENT_COMPLETED" ]]; then
|
|
592
|
+
warn "No completed runs found"
|
|
593
|
+
return
|
|
594
|
+
fi
|
|
595
|
+
|
|
596
|
+
echo ""
|
|
597
|
+
echo -e "${CYAN}${BOLD} Instrumentation Trends${RESET} ${DIM}(last ${last} runs)${RESET}"
|
|
598
|
+
echo -e "${DIM} ═══════════════════════════════════════════════════════════════════${RESET}"
|
|
599
|
+
echo ""
|
|
600
|
+
|
|
601
|
+
# Extract metrics and compute statistics
|
|
602
|
+
if [[ -n "$metric" ]]; then
|
|
603
|
+
echo -e " ${BOLD}${metric}${RESET}"
|
|
604
|
+
echo -e " ${DIM}────────────────────────────────────────${RESET}"
|
|
605
|
+
fi
|
|
606
|
+
|
|
607
|
+
# Use jq to analyze trends from JSONL file (with compact output)
|
|
608
|
+
if [[ -n "$metric" ]]; then
|
|
609
|
+
jq -c -s '[.[] | .metrics[] | select(.metric == "'$metric'")] | group_by(.metric) | map({metric: .[0].metric, count: length, avg: (map(.value | tonumber) | add / length), min: (map(.value | tonumber) | min), max: (map(.value | tonumber) | max)}) | .[]' "$INSTRUMENT_COMPLETED" | while read -r line; do
|
|
610
|
+
local m avg min max
|
|
611
|
+
m=$(echo "$line" | jq -r '.metric')
|
|
612
|
+
avg=$(echo "$line" | jq -r '.avg | round')
|
|
613
|
+
min=$(echo "$line" | jq -r '.min | round')
|
|
614
|
+
max=$(echo "$line" | jq -r '.max | round')
|
|
615
|
+
printf " %-25s avg: %-8s min: %-8s max: %-8s\n" "$m" "$avg" "$min" "$max"
|
|
616
|
+
done
|
|
617
|
+
else
|
|
618
|
+
jq -c -s '[.[] | .metrics[]] | group_by(.metric) | map({metric: .[0].metric, count: length, avg: (map(.value | tonumber) | add / length), min: (map(.value | tonumber) | min), max: (map(.value | tonumber) | max)}) | .[]' "$INSTRUMENT_COMPLETED" | while read -r line; do
|
|
619
|
+
local m avg min max
|
|
620
|
+
m=$(echo "$line" | jq -r '.metric')
|
|
621
|
+
avg=$(echo "$line" | jq -r '.avg | round')
|
|
622
|
+
min=$(echo "$line" | jq -r '.min | round')
|
|
623
|
+
max=$(echo "$line" | jq -r '.max | round')
|
|
624
|
+
printf " %-25s avg: %-8s min: %-8s max: %-8s\n" "$m" "$avg" "$min" "$max"
|
|
625
|
+
done
|
|
626
|
+
fi
|
|
627
|
+
|
|
628
|
+
echo ""
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
# ─── Export Data ─────────────────────────────────────────────────────────────
|
|
632
|
+
cmd_export() {
|
|
633
|
+
local format="json" last=""
|
|
634
|
+
|
|
635
|
+
while [[ $# -gt 0 ]]; do
|
|
636
|
+
case "$1" in
|
|
637
|
+
--format) format="${2:-}"; shift 2 ;;
|
|
638
|
+
--last) last="${2:-}"; shift 2 ;;
|
|
639
|
+
*)
|
|
640
|
+
warn "Unknown option: $1"
|
|
641
|
+
shift
|
|
642
|
+
;;
|
|
643
|
+
esac
|
|
644
|
+
done
|
|
645
|
+
|
|
646
|
+
if [[ ! -f "$INSTRUMENT_COMPLETED" ]]; then
|
|
647
|
+
warn "No completed runs found"
|
|
648
|
+
return
|
|
649
|
+
fi
|
|
650
|
+
|
|
651
|
+
case "$format" in
|
|
652
|
+
json)
|
|
653
|
+
if [[ -n "$last" ]]; then
|
|
654
|
+
tail -n "$last" "$INSTRUMENT_COMPLETED" | jq -s '.'
|
|
655
|
+
else
|
|
656
|
+
jq -s '.' "$INSTRUMENT_COMPLETED"
|
|
657
|
+
fi
|
|
658
|
+
;;
|
|
659
|
+
csv)
|
|
660
|
+
echo "run_id,issue,repo,started_at,result,duration_s,iterations,tokens,cost"
|
|
661
|
+
if [[ -n "$last" ]]; then
|
|
662
|
+
tail -n "$last" "$INSTRUMENT_COMPLETED"
|
|
663
|
+
else
|
|
664
|
+
cat "$INSTRUMENT_COMPLETED"
|
|
665
|
+
fi | jq -r '[.run_id, .issue // "", .repo, .started_at, .result // "", .total_duration_s // 0, (.metrics[] | select(.metric == "iterations") | .value) // 0, (.metrics[] | select(.metric == "tokens_total") | .value) // 0, (.metrics[] | select(.metric == "cost_usd") | .value) // 0] | @csv'
|
|
666
|
+
;;
|
|
667
|
+
*)
|
|
668
|
+
error "Unknown format: ${format}. Use 'json' or 'csv'."
|
|
669
|
+
return 1
|
|
670
|
+
;;
|
|
671
|
+
esac
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
# ─── Main Command Router ────────────────────────────────────────────────────
|
|
675
|
+
main() {
|
|
676
|
+
local cmd="${1:-help}"
|
|
677
|
+
shift 2>/dev/null || true
|
|
678
|
+
|
|
679
|
+
case "$cmd" in
|
|
680
|
+
start) cmd_start "$@" ;;
|
|
681
|
+
record) cmd_record "$@" ;;
|
|
682
|
+
stage-start) cmd_stage_start "$@" ;;
|
|
683
|
+
stage-end) cmd_stage_end "$@" ;;
|
|
684
|
+
finish) cmd_finish "$@" ;;
|
|
685
|
+
summary) cmd_summary "$@" ;;
|
|
686
|
+
trends) cmd_trends "$@" ;;
|
|
687
|
+
export) cmd_export "$@" ;;
|
|
688
|
+
help|--help|-h) show_help ;;
|
|
689
|
+
*)
|
|
690
|
+
error "Unknown command: ${cmd}"
|
|
691
|
+
show_help
|
|
692
|
+
exit 1
|
|
693
|
+
;;
|
|
694
|
+
esac
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
698
|
+
main "$@"
|
|
699
|
+
fi
|