shipwright-cli 1.7.1 → 1.10.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/agents/code-reviewer.md +90 -0
- package/.claude/agents/devops-engineer.md +142 -0
- package/.claude/agents/pipeline-agent.md +80 -0
- package/.claude/agents/shell-script-specialist.md +150 -0
- package/.claude/agents/test-specialist.md +196 -0
- package/.claude/hooks/post-tool-use.sh +45 -0
- package/.claude/hooks/pre-tool-use.sh +25 -0
- package/.claude/hooks/session-started.sh +37 -0
- package/README.md +212 -814
- package/claude-code/CLAUDE.md.shipwright +54 -0
- package/claude-code/hooks/notify-idle.sh +2 -2
- package/claude-code/hooks/session-start.sh +24 -0
- package/claude-code/hooks/task-completed.sh +6 -2
- package/claude-code/settings.json.template +12 -0
- package/dashboard/public/app.js +4422 -0
- package/dashboard/public/index.html +816 -0
- package/dashboard/public/styles.css +4755 -0
- package/dashboard/server.ts +4315 -0
- package/docs/KNOWN-ISSUES.md +18 -10
- package/docs/TIPS.md +38 -26
- package/docs/patterns/README.md +33 -23
- package/package.json +9 -5
- package/scripts/adapters/iterm2-adapter.sh +1 -1
- package/scripts/adapters/tmux-adapter.sh +52 -23
- package/scripts/adapters/wezterm-adapter.sh +26 -14
- package/scripts/lib/compat.sh +200 -0
- package/scripts/lib/helpers.sh +72 -0
- package/scripts/postinstall.mjs +72 -13
- package/scripts/{cct → sw} +118 -22
- package/scripts/sw-adversarial.sh +274 -0
- package/scripts/sw-architecture-enforcer.sh +330 -0
- package/scripts/sw-checkpoint.sh +468 -0
- package/scripts/sw-cleanup.sh +359 -0
- package/scripts/sw-connect.sh +619 -0
- package/scripts/{cct-cost.sh → sw-cost.sh} +368 -34
- package/scripts/sw-daemon.sh +5574 -0
- package/scripts/sw-dashboard.sh +477 -0
- package/scripts/sw-developer-simulation.sh +252 -0
- package/scripts/sw-docs.sh +635 -0
- package/scripts/sw-doctor.sh +907 -0
- package/scripts/{cct-fix.sh → sw-fix.sh} +10 -6
- package/scripts/{cct-fleet.sh → sw-fleet.sh} +498 -22
- package/scripts/sw-github-checks.sh +521 -0
- package/scripts/sw-github-deploy.sh +533 -0
- package/scripts/sw-github-graphql.sh +972 -0
- package/scripts/sw-heartbeat.sh +293 -0
- package/scripts/{cct-init.sh → sw-init.sh} +144 -11
- package/scripts/sw-intelligence.sh +1196 -0
- package/scripts/sw-jira.sh +643 -0
- package/scripts/sw-launchd.sh +364 -0
- package/scripts/sw-linear.sh +648 -0
- package/scripts/{cct-logs.sh → sw-logs.sh} +72 -2
- package/scripts/sw-loop.sh +2217 -0
- package/scripts/{cct-memory.sh → sw-memory.sh} +514 -36
- package/scripts/sw-patrol-meta.sh +417 -0
- package/scripts/sw-pipeline-composer.sh +455 -0
- package/scripts/sw-pipeline-vitals.sh +1096 -0
- package/scripts/sw-pipeline.sh +7593 -0
- package/scripts/sw-predictive.sh +820 -0
- package/scripts/{cct-prep.sh → sw-prep.sh} +339 -49
- package/scripts/{cct-ps.sh → sw-ps.sh} +9 -6
- package/scripts/{cct-reaper.sh → sw-reaper.sh} +10 -6
- package/scripts/sw-remote.sh +687 -0
- package/scripts/sw-self-optimize.sh +1048 -0
- package/scripts/sw-session.sh +541 -0
- package/scripts/sw-setup.sh +234 -0
- package/scripts/sw-status.sh +796 -0
- package/scripts/{cct-templates.sh → sw-templates.sh} +9 -4
- package/scripts/sw-tmux.sh +591 -0
- package/scripts/sw-tracker-jira.sh +277 -0
- package/scripts/sw-tracker-linear.sh +292 -0
- package/scripts/sw-tracker.sh +409 -0
- package/scripts/{cct-upgrade.sh → sw-upgrade.sh} +103 -46
- package/scripts/{cct-worktree.sh → sw-worktree.sh} +3 -0
- package/templates/pipelines/autonomous.json +35 -6
- 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 +28 -2
- package/templates/pipelines/hotfix.json +19 -0
- package/templates/pipelines/standard.json +31 -0
- package/tmux/{claude-teams-overlay.conf → shipwright-overlay.conf} +27 -9
- package/tmux/templates/accessibility.json +34 -0
- package/tmux/templates/api-design.json +35 -0
- package/tmux/templates/architecture.json +1 -0
- package/tmux/templates/bug-fix.json +9 -0
- package/tmux/templates/code-review.json +1 -0
- package/tmux/templates/compliance.json +36 -0
- package/tmux/templates/data-pipeline.json +36 -0
- package/tmux/templates/debt-paydown.json +34 -0
- package/tmux/templates/devops.json +1 -0
- package/tmux/templates/documentation.json +1 -0
- package/tmux/templates/exploration.json +1 -0
- package/tmux/templates/feature-dev.json +1 -0
- package/tmux/templates/full-stack.json +8 -0
- package/tmux/templates/i18n.json +34 -0
- package/tmux/templates/incident-response.json +36 -0
- package/tmux/templates/migration.json +1 -0
- package/tmux/templates/observability.json +35 -0
- package/tmux/templates/onboarding.json +33 -0
- package/tmux/templates/performance.json +35 -0
- package/tmux/templates/refactor.json +1 -0
- package/tmux/templates/release.json +35 -0
- package/tmux/templates/security-audit.json +8 -0
- package/tmux/templates/spike.json +34 -0
- package/tmux/templates/testing.json +1 -0
- package/tmux/tmux.conf +98 -9
- package/scripts/cct-cleanup.sh +0 -172
- package/scripts/cct-daemon.sh +0 -3189
- package/scripts/cct-doctor.sh +0 -414
- package/scripts/cct-loop.sh +0 -1332
- package/scripts/cct-pipeline.sh +0 -3844
- package/scripts/cct-session.sh +0 -284
- package/scripts/cct-status.sh +0 -169
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ╔═══════════════════════════════════════════════════════════════════════════╗
|
|
3
|
+
# ║ shipwright github-graphql — GitHub GraphQL API Client ║
|
|
4
|
+
# ║ Code history · Blame data · Contributors · Security alerts ║
|
|
5
|
+
# ╚═══════════════════════════════════════════════════════════════════════════╝
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
trap 'echo "ERROR: $BASH_SOURCE:$LINENO exited with status $?" >&2' ERR
|
|
8
|
+
|
|
9
|
+
VERSION="1.10.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
|
+
# ─── Structured Event Log ──────────────────────────────────────────────────
|
|
38
|
+
EVENTS_FILE="${HOME}/.shipwright/events.jsonl"
|
|
39
|
+
|
|
40
|
+
emit_event() {
|
|
41
|
+
local event_type="$1"
|
|
42
|
+
shift
|
|
43
|
+
local json_fields=""
|
|
44
|
+
for kv in "$@"; do
|
|
45
|
+
local key="${kv%%=*}"
|
|
46
|
+
local val="${kv#*=}"
|
|
47
|
+
if [[ "$val" =~ ^-?[0-9]+\.?[0-9]*$ ]]; then
|
|
48
|
+
json_fields="${json_fields},\"${key}\":${val}"
|
|
49
|
+
else
|
|
50
|
+
val="${val//\"/\\\"}"
|
|
51
|
+
json_fields="${json_fields},\"${key}\":\"${val}\""
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
mkdir -p "${HOME}/.shipwright"
|
|
55
|
+
echo "{\"ts\":\"$(now_iso)\",\"ts_epoch\":$(now_epoch),\"type\":\"${event_type}\"${json_fields}}" >> "$EVENTS_FILE"
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# ─── Cache Configuration ───────────────────────────────────────────────────
|
|
59
|
+
GH_CACHE_DIR="${HOME}/.shipwright/github-cache"
|
|
60
|
+
|
|
61
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
62
|
+
# AVAILABILITY CHECK
|
|
63
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
64
|
+
|
|
65
|
+
_gh_graphql_available() {
|
|
66
|
+
if [[ "${NO_GITHUB:-false}" == "true" ]]; then
|
|
67
|
+
return 1
|
|
68
|
+
fi
|
|
69
|
+
if ! command -v gh >/dev/null 2>&1; then
|
|
70
|
+
return 1
|
|
71
|
+
fi
|
|
72
|
+
if ! gh auth status >/dev/null 2>&1; then
|
|
73
|
+
return 1
|
|
74
|
+
fi
|
|
75
|
+
return 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
79
|
+
# CACHE LAYER
|
|
80
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
81
|
+
|
|
82
|
+
_gh_cache_init() {
|
|
83
|
+
mkdir -p "$GH_CACHE_DIR"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
_gh_cache_get() {
|
|
87
|
+
local cache_key="$1"
|
|
88
|
+
local ttl_seconds="${2:-3600}"
|
|
89
|
+
local cache_file="${GH_CACHE_DIR}/${cache_key}.json"
|
|
90
|
+
|
|
91
|
+
if [[ ! -f "$cache_file" ]]; then
|
|
92
|
+
return 1
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Check file age
|
|
96
|
+
local file_epoch now
|
|
97
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
98
|
+
file_epoch=$(stat -f '%m' "$cache_file" 2>/dev/null || echo "0")
|
|
99
|
+
else
|
|
100
|
+
file_epoch=$(stat -c '%Y' "$cache_file" 2>/dev/null || echo "0")
|
|
101
|
+
fi
|
|
102
|
+
now=$(now_epoch)
|
|
103
|
+
local age=$(( now - file_epoch ))
|
|
104
|
+
|
|
105
|
+
if [[ "$age" -gt "$ttl_seconds" ]]; then
|
|
106
|
+
return 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
cat "$cache_file"
|
|
110
|
+
return 0
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
_gh_cache_set() {
|
|
114
|
+
local cache_key="$1"
|
|
115
|
+
local content="$2"
|
|
116
|
+
_gh_cache_init
|
|
117
|
+
local cache_file="${GH_CACHE_DIR}/${cache_key}.json"
|
|
118
|
+
local tmp_file="${cache_file}.tmp.$$"
|
|
119
|
+
printf '%s\n' "$content" > "$tmp_file"
|
|
120
|
+
mv "$tmp_file" "$cache_file"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
_gh_cache_clear() {
|
|
124
|
+
if [[ -d "$GH_CACHE_DIR" ]]; then
|
|
125
|
+
rm -rf "$GH_CACHE_DIR"
|
|
126
|
+
_gh_cache_init
|
|
127
|
+
success "GitHub cache cleared"
|
|
128
|
+
else
|
|
129
|
+
info "No cache to clear"
|
|
130
|
+
fi
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
_gh_cache_stats() {
|
|
134
|
+
_gh_cache_init
|
|
135
|
+
local count=0
|
|
136
|
+
local total_size=0
|
|
137
|
+
local oldest=""
|
|
138
|
+
local newest=""
|
|
139
|
+
|
|
140
|
+
for f in "$GH_CACHE_DIR"/*.json; do
|
|
141
|
+
[[ -f "$f" ]] || continue
|
|
142
|
+
count=$((count + 1))
|
|
143
|
+
local size
|
|
144
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
145
|
+
size=$(stat -f '%z' "$f" 2>/dev/null || echo "0")
|
|
146
|
+
else
|
|
147
|
+
size=$(stat -c '%s' "$f" 2>/dev/null || echo "0")
|
|
148
|
+
fi
|
|
149
|
+
total_size=$((total_size + size))
|
|
150
|
+
done
|
|
151
|
+
|
|
152
|
+
echo -e "${CYAN}${BOLD}GitHub API Cache${RESET}"
|
|
153
|
+
echo -e " ${DIM}Directory:${RESET} $GH_CACHE_DIR"
|
|
154
|
+
echo -e " ${DIM}Entries:${RESET} $count"
|
|
155
|
+
echo -e " ${DIM}Total size:${RESET} $((total_size / 1024))KB"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
159
|
+
# REPO DETECTION
|
|
160
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
161
|
+
|
|
162
|
+
GH_OWNER=""
|
|
163
|
+
GH_REPO=""
|
|
164
|
+
|
|
165
|
+
_gh_detect_repo() {
|
|
166
|
+
if [[ -n "$GH_OWNER" && -n "$GH_REPO" ]]; then
|
|
167
|
+
return 0
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
local remote_url
|
|
171
|
+
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
|
172
|
+
|
|
173
|
+
if [[ -z "$remote_url" ]]; then
|
|
174
|
+
error "No git remote 'origin' found"
|
|
175
|
+
return 1
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
# Handle SSH: git@github.com:owner/repo.git
|
|
179
|
+
if [[ "$remote_url" =~ git@github\.com:([^/]+)/([^/.]+)(\.git)?$ ]]; then
|
|
180
|
+
GH_OWNER="${BASH_REMATCH[1]}"
|
|
181
|
+
GH_REPO="${BASH_REMATCH[2]}"
|
|
182
|
+
return 0
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# Handle HTTPS: https://github.com/owner/repo.git
|
|
186
|
+
if [[ "$remote_url" =~ github\.com/([^/]+)/([^/.]+)(\.git)?$ ]]; then
|
|
187
|
+
GH_OWNER="${BASH_REMATCH[1]}"
|
|
188
|
+
GH_REPO="${BASH_REMATCH[2]}"
|
|
189
|
+
return 0
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
error "Could not parse owner/repo from remote: $remote_url"
|
|
193
|
+
return 1
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
197
|
+
# CORE GRAPHQL EXECUTION
|
|
198
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
199
|
+
|
|
200
|
+
gh_graphql() {
|
|
201
|
+
local query="$1"
|
|
202
|
+
local variables="${2:-{\}}"
|
|
203
|
+
|
|
204
|
+
if ! _gh_graphql_available; then
|
|
205
|
+
echo '{"error": "GitHub API not available"}'
|
|
206
|
+
return 1
|
|
207
|
+
fi
|
|
208
|
+
|
|
209
|
+
local result
|
|
210
|
+
result=$(gh api graphql -f query="$query" --input - <<< "$variables" 2>/dev/null) || {
|
|
211
|
+
error "GraphQL query failed"
|
|
212
|
+
echo '{"error": "query failed"}'
|
|
213
|
+
return 1
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Check for GraphQL errors
|
|
217
|
+
local has_errors
|
|
218
|
+
has_errors=$(echo "$result" | jq 'has("errors")' 2>/dev/null || echo "false")
|
|
219
|
+
if [[ "$has_errors" == "true" ]]; then
|
|
220
|
+
local err_msg
|
|
221
|
+
err_msg=$(echo "$result" | jq -r '.errors[0].message // "unknown error"' 2>/dev/null)
|
|
222
|
+
warn "GraphQL error: $err_msg"
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
echo "$result"
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
gh_graphql_cached() {
|
|
229
|
+
local cache_key="$1"
|
|
230
|
+
local ttl_seconds="$2"
|
|
231
|
+
local query="$3"
|
|
232
|
+
local variables="${4:-{\}}"
|
|
233
|
+
|
|
234
|
+
# Try cache first
|
|
235
|
+
local cached
|
|
236
|
+
cached=$(_gh_cache_get "$cache_key" "$ttl_seconds" 2>/dev/null) && {
|
|
237
|
+
emit_event "github.cache_hit" "key=$cache_key"
|
|
238
|
+
echo "$cached"
|
|
239
|
+
return 0
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# Cache miss — execute query
|
|
243
|
+
local result
|
|
244
|
+
result=$(gh_graphql "$query" "$variables") || return $?
|
|
245
|
+
|
|
246
|
+
# Cache the result
|
|
247
|
+
_gh_cache_set "$cache_key" "$result"
|
|
248
|
+
emit_event "github.cache_miss" "key=$cache_key"
|
|
249
|
+
|
|
250
|
+
echo "$result"
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
254
|
+
# DATA QUERIES
|
|
255
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
256
|
+
|
|
257
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
258
|
+
# File change frequency — commit count for a path in last N days
|
|
259
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
260
|
+
gh_file_change_frequency() {
|
|
261
|
+
local owner="$1"
|
|
262
|
+
local repo="$2"
|
|
263
|
+
local path="$3"
|
|
264
|
+
local days="${4:-30}"
|
|
265
|
+
|
|
266
|
+
if ! _gh_graphql_available; then
|
|
267
|
+
echo "0"
|
|
268
|
+
return 0
|
|
269
|
+
fi
|
|
270
|
+
|
|
271
|
+
local since
|
|
272
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
273
|
+
since=$(date -u -v-"${days}d" +"%Y-%m-%dT%H:%M:%SZ")
|
|
274
|
+
else
|
|
275
|
+
since=$(date -u -d "${days} days ago" +"%Y-%m-%dT%H:%M:%SZ")
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
local cache_key="freq_${owner}_${repo}_$(echo "$path" | tr '/' '_')_${days}d"
|
|
279
|
+
|
|
280
|
+
local query='query($owner: String!, $repo: String!, $since: GitTimestamp!) {
|
|
281
|
+
repository(owner: $owner, name: $repo) {
|
|
282
|
+
defaultBranchRef {
|
|
283
|
+
target {
|
|
284
|
+
... on Commit {
|
|
285
|
+
history(since: $since) {
|
|
286
|
+
totalCount
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}'
|
|
293
|
+
|
|
294
|
+
local variables
|
|
295
|
+
variables=$(jq -n --arg owner "$owner" --arg repo "$repo" --arg since "$since" \
|
|
296
|
+
'{owner: $owner, repo: $repo, since: $since}')
|
|
297
|
+
|
|
298
|
+
local result
|
|
299
|
+
result=$(gh_graphql_cached "$cache_key" "3600" "$query" "$variables") || {
|
|
300
|
+
echo "0"
|
|
301
|
+
return 0
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
local count
|
|
305
|
+
count=$(echo "$result" | jq -r '.data.repository.defaultBranchRef.target.history.totalCount // 0' 2>/dev/null || echo "0")
|
|
306
|
+
echo "$count"
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
310
|
+
# Blame data — commit authors and counts for a file
|
|
311
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
312
|
+
gh_blame_data() {
|
|
313
|
+
local owner="$1"
|
|
314
|
+
local repo="$2"
|
|
315
|
+
local path="$3"
|
|
316
|
+
|
|
317
|
+
if ! _gh_graphql_available; then
|
|
318
|
+
echo "[]"
|
|
319
|
+
return 0
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
local cache_key="blame_${owner}_${repo}_$(echo "$path" | tr '/' '_')"
|
|
323
|
+
local cached
|
|
324
|
+
cached=$(_gh_cache_get "$cache_key" "14400" 2>/dev/null) && {
|
|
325
|
+
emit_event "github.cache_hit" "key=$cache_key"
|
|
326
|
+
echo "$cached"
|
|
327
|
+
return 0
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Use REST API for commit history on path
|
|
331
|
+
local result
|
|
332
|
+
result=$(gh api "repos/${owner}/${repo}/commits?path=${path}&per_page=100" 2>/dev/null) || {
|
|
333
|
+
echo "[]"
|
|
334
|
+
return 0
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
# Parse commit authors and aggregate
|
|
338
|
+
local parsed
|
|
339
|
+
parsed=$(echo "$result" | jq '[group_by(.commit.author.name) | .[] |
|
|
340
|
+
{
|
|
341
|
+
author: .[0].commit.author.name,
|
|
342
|
+
commits: length,
|
|
343
|
+
last_commit: (sort_by(.commit.author.date) | last | .commit.author.date)
|
|
344
|
+
}] | sort_by(-.commits)' 2>/dev/null || echo "[]")
|
|
345
|
+
|
|
346
|
+
_gh_cache_set "$cache_key" "$parsed"
|
|
347
|
+
emit_event "github.cache_miss" "key=$cache_key"
|
|
348
|
+
|
|
349
|
+
echo "$parsed"
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
353
|
+
# Contributors — repo contributor list with commit counts
|
|
354
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
355
|
+
gh_contributors() {
|
|
356
|
+
local owner="$1"
|
|
357
|
+
local repo="$2"
|
|
358
|
+
|
|
359
|
+
if ! _gh_graphql_available; then
|
|
360
|
+
echo "[]"
|
|
361
|
+
return 0
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
local cache_key="contrib_${owner}_${repo}"
|
|
365
|
+
local cached
|
|
366
|
+
cached=$(_gh_cache_get "$cache_key" "86400" 2>/dev/null) && {
|
|
367
|
+
emit_event "github.cache_hit" "key=$cache_key"
|
|
368
|
+
echo "$cached"
|
|
369
|
+
return 0
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
local result
|
|
373
|
+
result=$(gh api "repos/${owner}/${repo}/contributors?per_page=100" 2>/dev/null) || {
|
|
374
|
+
echo "[]"
|
|
375
|
+
return 0
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
local parsed
|
|
379
|
+
parsed=$(echo "$result" | jq '[.[] | {login: .login, contributions: .contributions}]' 2>/dev/null || echo "[]")
|
|
380
|
+
|
|
381
|
+
_gh_cache_set "$cache_key" "$parsed"
|
|
382
|
+
emit_event "github.cache_miss" "key=$cache_key"
|
|
383
|
+
|
|
384
|
+
echo "$parsed"
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
388
|
+
# Similar issues — closed issues matching search text
|
|
389
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
390
|
+
gh_similar_issues() {
|
|
391
|
+
local owner="$1"
|
|
392
|
+
local repo="$2"
|
|
393
|
+
local search_text="$3"
|
|
394
|
+
local limit="${4:-5}"
|
|
395
|
+
|
|
396
|
+
if ! _gh_graphql_available; then
|
|
397
|
+
echo "[]"
|
|
398
|
+
return 0
|
|
399
|
+
fi
|
|
400
|
+
|
|
401
|
+
# Truncate search text to 100 chars (API limit)
|
|
402
|
+
if [[ ${#search_text} -gt 100 ]]; then
|
|
403
|
+
search_text="${search_text:0:100}"
|
|
404
|
+
fi
|
|
405
|
+
|
|
406
|
+
local search_query="repo:${owner}/${repo} is:issue is:closed ${search_text}"
|
|
407
|
+
local cache_key="similar_${owner}_${repo}_$(echo "$search_text" | tr ' /' '__' | head -c 50)"
|
|
408
|
+
|
|
409
|
+
local query='query($q: String!, $limit: Int!) {
|
|
410
|
+
search(query: $q, type: ISSUE, first: $limit) {
|
|
411
|
+
nodes {
|
|
412
|
+
... on Issue {
|
|
413
|
+
number
|
|
414
|
+
title
|
|
415
|
+
closedAt
|
|
416
|
+
labels(first: 10) {
|
|
417
|
+
nodes { name }
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}'
|
|
423
|
+
|
|
424
|
+
local variables
|
|
425
|
+
variables=$(jq -n --arg q "$search_query" --argjson limit "$limit" \
|
|
426
|
+
'{q: $q, limit: $limit}')
|
|
427
|
+
|
|
428
|
+
local result
|
|
429
|
+
result=$(gh_graphql_cached "$cache_key" "3600" "$query" "$variables") || {
|
|
430
|
+
echo "[]"
|
|
431
|
+
return 0
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
echo "$result" | jq '[.data.search.nodes[] |
|
|
435
|
+
{
|
|
436
|
+
number: .number,
|
|
437
|
+
title: .title,
|
|
438
|
+
labels: [.labels.nodes[].name],
|
|
439
|
+
closedAt: .closedAt
|
|
440
|
+
}]' 2>/dev/null || echo "[]"
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
444
|
+
# Commit history — recent commits touching a path
|
|
445
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
446
|
+
gh_commit_history() {
|
|
447
|
+
local owner="$1"
|
|
448
|
+
local repo="$2"
|
|
449
|
+
local path="$3"
|
|
450
|
+
local limit="${4:-10}"
|
|
451
|
+
|
|
452
|
+
if ! _gh_graphql_available; then
|
|
453
|
+
echo "[]"
|
|
454
|
+
return 0
|
|
455
|
+
fi
|
|
456
|
+
|
|
457
|
+
local cache_key="history_${owner}_${repo}_$(echo "$path" | tr '/' '_')_${limit}"
|
|
458
|
+
local cached
|
|
459
|
+
cached=$(_gh_cache_get "$cache_key" "3600" 2>/dev/null) && {
|
|
460
|
+
emit_event "github.cache_hit" "key=$cache_key"
|
|
461
|
+
echo "$cached"
|
|
462
|
+
return 0
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
local result
|
|
466
|
+
result=$(gh api "repos/${owner}/${repo}/commits?path=${path}&per_page=${limit}" 2>/dev/null) || {
|
|
467
|
+
echo "[]"
|
|
468
|
+
return 0
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
local parsed
|
|
472
|
+
parsed=$(echo "$result" | jq '[.[] | {
|
|
473
|
+
sha: .sha[0:7],
|
|
474
|
+
message: (.commit.message | split("\n")[0]),
|
|
475
|
+
author: .commit.author.name,
|
|
476
|
+
date: .commit.author.date
|
|
477
|
+
}]' 2>/dev/null || echo "[]")
|
|
478
|
+
|
|
479
|
+
_gh_cache_set "$cache_key" "$parsed"
|
|
480
|
+
emit_event "github.cache_miss" "key=$cache_key"
|
|
481
|
+
|
|
482
|
+
echo "$parsed"
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
486
|
+
# Branch protection — protection rules for a branch
|
|
487
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
488
|
+
gh_branch_protection() {
|
|
489
|
+
local owner="$1"
|
|
490
|
+
local repo="$2"
|
|
491
|
+
local branch="${3:-main}"
|
|
492
|
+
|
|
493
|
+
if ! _gh_graphql_available; then
|
|
494
|
+
echo '{"protected": false}'
|
|
495
|
+
return 0
|
|
496
|
+
fi
|
|
497
|
+
|
|
498
|
+
local cache_key="protection_${owner}_${repo}_${branch}"
|
|
499
|
+
local cached
|
|
500
|
+
cached=$(_gh_cache_get "$cache_key" "3600" 2>/dev/null) && {
|
|
501
|
+
emit_event "github.cache_hit" "key=$cache_key"
|
|
502
|
+
echo "$cached"
|
|
503
|
+
return 0
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
local result
|
|
507
|
+
result=$(gh api "repos/${owner}/${repo}/branches/${branch}/protection" 2>/dev/null) || {
|
|
508
|
+
# 404 = no protection rules
|
|
509
|
+
local parsed='{"protected": false}'
|
|
510
|
+
_gh_cache_set "$cache_key" "$parsed"
|
|
511
|
+
echo "$parsed"
|
|
512
|
+
return 0
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
local parsed
|
|
516
|
+
parsed=$(echo "$result" | jq '{
|
|
517
|
+
protected: true,
|
|
518
|
+
required_reviewers: (.required_pull_request_reviews.required_approving_review_count // 0),
|
|
519
|
+
dismiss_stale_reviews: (.required_pull_request_reviews.dismiss_stale_reviews // false),
|
|
520
|
+
require_code_owner_reviews: (.required_pull_request_reviews.require_code_owner_reviews // false),
|
|
521
|
+
required_checks: [(.required_status_checks.contexts // [])[]],
|
|
522
|
+
enforce_admins: (.enforce_admins.enabled // false),
|
|
523
|
+
linear_history: (.required_linear_history.enabled // false)
|
|
524
|
+
}' 2>/dev/null || echo '{"protected": false}')
|
|
525
|
+
|
|
526
|
+
_gh_cache_set "$cache_key" "$parsed"
|
|
527
|
+
emit_event "github.cache_miss" "key=$cache_key"
|
|
528
|
+
|
|
529
|
+
echo "$parsed"
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
533
|
+
# CODEOWNERS — parsed CODEOWNERS file
|
|
534
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
535
|
+
gh_codeowners() {
|
|
536
|
+
local owner="$1"
|
|
537
|
+
local repo="$2"
|
|
538
|
+
|
|
539
|
+
if ! _gh_graphql_available; then
|
|
540
|
+
echo "[]"
|
|
541
|
+
return 0
|
|
542
|
+
fi
|
|
543
|
+
|
|
544
|
+
local cache_key="codeowners_${owner}_${repo}"
|
|
545
|
+
local cached
|
|
546
|
+
cached=$(_gh_cache_get "$cache_key" "86400" 2>/dev/null) && {
|
|
547
|
+
emit_event "github.cache_hit" "key=$cache_key"
|
|
548
|
+
echo "$cached"
|
|
549
|
+
return 0
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
# Try common CODEOWNERS locations
|
|
553
|
+
local content=""
|
|
554
|
+
local locations=("CODEOWNERS" ".github/CODEOWNERS" "docs/CODEOWNERS")
|
|
555
|
+
for loc in "${locations[@]}"; do
|
|
556
|
+
content=$(gh api "repos/${owner}/${repo}/contents/${loc}" --jq '.content' 2>/dev/null || true)
|
|
557
|
+
if [[ -n "$content" ]]; then
|
|
558
|
+
break
|
|
559
|
+
fi
|
|
560
|
+
done
|
|
561
|
+
|
|
562
|
+
if [[ -z "$content" ]]; then
|
|
563
|
+
_gh_cache_set "$cache_key" "[]"
|
|
564
|
+
echo "[]"
|
|
565
|
+
return 0
|
|
566
|
+
fi
|
|
567
|
+
|
|
568
|
+
# Decode base64 and parse
|
|
569
|
+
local decoded
|
|
570
|
+
decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "")
|
|
571
|
+
|
|
572
|
+
if [[ -z "$decoded" ]]; then
|
|
573
|
+
_gh_cache_set "$cache_key" "[]"
|
|
574
|
+
echo "[]"
|
|
575
|
+
return 0
|
|
576
|
+
fi
|
|
577
|
+
|
|
578
|
+
# Parse CODEOWNERS format: pattern owners...
|
|
579
|
+
local parsed="["
|
|
580
|
+
local first=true
|
|
581
|
+
while IFS= read -r line; do
|
|
582
|
+
# Skip empty lines and comments
|
|
583
|
+
[[ -z "$line" ]] && continue
|
|
584
|
+
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
|
585
|
+
|
|
586
|
+
local pattern
|
|
587
|
+
pattern=$(echo "$line" | awk '{print $1}')
|
|
588
|
+
local owners_str
|
|
589
|
+
owners_str=$(echo "$line" | awk '{$1=""; print $0}' | xargs)
|
|
590
|
+
|
|
591
|
+
# Build JSON owners array
|
|
592
|
+
local owners_json="["
|
|
593
|
+
local ofirst=true
|
|
594
|
+
for o in $owners_str; do
|
|
595
|
+
if [[ "$ofirst" == "true" ]]; then
|
|
596
|
+
ofirst=false
|
|
597
|
+
else
|
|
598
|
+
owners_json="${owners_json},"
|
|
599
|
+
fi
|
|
600
|
+
owners_json="${owners_json}$(jq -n --arg v "$o" '$v')"
|
|
601
|
+
done
|
|
602
|
+
owners_json="${owners_json}]"
|
|
603
|
+
|
|
604
|
+
if [[ "$first" == "true" ]]; then
|
|
605
|
+
first=false
|
|
606
|
+
else
|
|
607
|
+
parsed="${parsed},"
|
|
608
|
+
fi
|
|
609
|
+
parsed="${parsed}$(jq -n --arg p "$pattern" --argjson o "$owners_json" '{pattern: $p, owners: $o}')"
|
|
610
|
+
done <<< "$decoded"
|
|
611
|
+
parsed="${parsed}]"
|
|
612
|
+
|
|
613
|
+
_gh_cache_set "$cache_key" "$parsed"
|
|
614
|
+
emit_event "github.cache_miss" "key=$cache_key"
|
|
615
|
+
|
|
616
|
+
echo "$parsed"
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
620
|
+
# Security alerts — CodeQL code scanning alerts
|
|
621
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
622
|
+
gh_security_alerts() {
|
|
623
|
+
local owner="$1"
|
|
624
|
+
local repo="$2"
|
|
625
|
+
|
|
626
|
+
if ! _gh_graphql_available; then
|
|
627
|
+
echo "[]"
|
|
628
|
+
return 0
|
|
629
|
+
fi
|
|
630
|
+
|
|
631
|
+
local cache_key="security_${owner}_${repo}"
|
|
632
|
+
local cached
|
|
633
|
+
cached=$(_gh_cache_get "$cache_key" "1800" 2>/dev/null) && {
|
|
634
|
+
emit_event "github.cache_hit" "key=$cache_key"
|
|
635
|
+
echo "$cached"
|
|
636
|
+
return 0
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
local result
|
|
640
|
+
result=$(gh api "repos/${owner}/${repo}/code-scanning/alerts?state=open&per_page=50" 2>/dev/null) || {
|
|
641
|
+
# 403 = feature not enabled, 404 = not found
|
|
642
|
+
_gh_cache_set "$cache_key" "[]"
|
|
643
|
+
echo "[]"
|
|
644
|
+
return 0
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
local parsed
|
|
648
|
+
parsed=$(echo "$result" | jq '[.[] | {
|
|
649
|
+
number: .number,
|
|
650
|
+
severity: .rule.severity,
|
|
651
|
+
rule: .rule.id,
|
|
652
|
+
description: .rule.description,
|
|
653
|
+
path: .most_recent_instance.location.path,
|
|
654
|
+
state: .state
|
|
655
|
+
}]' 2>/dev/null || echo "[]")
|
|
656
|
+
|
|
657
|
+
_gh_cache_set "$cache_key" "$parsed"
|
|
658
|
+
emit_event "github.cache_miss" "key=$cache_key"
|
|
659
|
+
|
|
660
|
+
echo "$parsed"
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
664
|
+
# Dependabot alerts — vulnerability alerts
|
|
665
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
666
|
+
gh_dependabot_alerts() {
|
|
667
|
+
local owner="$1"
|
|
668
|
+
local repo="$2"
|
|
669
|
+
|
|
670
|
+
if ! _gh_graphql_available; then
|
|
671
|
+
echo "[]"
|
|
672
|
+
return 0
|
|
673
|
+
fi
|
|
674
|
+
|
|
675
|
+
local cache_key="dependabot_${owner}_${repo}"
|
|
676
|
+
local cached
|
|
677
|
+
cached=$(_gh_cache_get "$cache_key" "1800" 2>/dev/null) && {
|
|
678
|
+
emit_event "github.cache_hit" "key=$cache_key"
|
|
679
|
+
echo "$cached"
|
|
680
|
+
return 0
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
local result
|
|
684
|
+
result=$(gh api "repos/${owner}/${repo}/dependabot/alerts?state=open&per_page=50" 2>/dev/null) || {
|
|
685
|
+
# 403 = feature not enabled
|
|
686
|
+
_gh_cache_set "$cache_key" "[]"
|
|
687
|
+
echo "[]"
|
|
688
|
+
return 0
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
local parsed
|
|
692
|
+
parsed=$(echo "$result" | jq '[.[] | {
|
|
693
|
+
number: .number,
|
|
694
|
+
severity: .security_advisory.severity,
|
|
695
|
+
package: .dependency.package.name,
|
|
696
|
+
ecosystem: .dependency.package.ecosystem,
|
|
697
|
+
vulnerable_range: .security_vulnerability.vulnerable_version_range,
|
|
698
|
+
summary: .security_advisory.summary,
|
|
699
|
+
state: .state
|
|
700
|
+
}]' 2>/dev/null || echo "[]")
|
|
701
|
+
|
|
702
|
+
_gh_cache_set "$cache_key" "$parsed"
|
|
703
|
+
emit_event "github.cache_miss" "key=$cache_key"
|
|
704
|
+
|
|
705
|
+
echo "$parsed"
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
709
|
+
# Actions runs — recent workflow run durations
|
|
710
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
711
|
+
gh_actions_runs() {
|
|
712
|
+
local owner="$1"
|
|
713
|
+
local repo="$2"
|
|
714
|
+
local workflow="$3"
|
|
715
|
+
local limit="${4:-10}"
|
|
716
|
+
|
|
717
|
+
if ! _gh_graphql_available; then
|
|
718
|
+
echo "[]"
|
|
719
|
+
return 0
|
|
720
|
+
fi
|
|
721
|
+
|
|
722
|
+
local cache_key="actions_${owner}_${repo}_$(echo "$workflow" | tr '/' '_')_${limit}"
|
|
723
|
+
local cached
|
|
724
|
+
cached=$(_gh_cache_get "$cache_key" "900" 2>/dev/null) && {
|
|
725
|
+
emit_event "github.cache_hit" "key=$cache_key"
|
|
726
|
+
echo "$cached"
|
|
727
|
+
return 0
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
local result
|
|
731
|
+
result=$(gh api "repos/${owner}/${repo}/actions/workflows/${workflow}/runs?per_page=${limit}" 2>/dev/null) || {
|
|
732
|
+
echo "[]"
|
|
733
|
+
return 0
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
local parsed
|
|
737
|
+
parsed=$(echo "$result" | jq '[.workflow_runs[] | {
|
|
738
|
+
id: .id,
|
|
739
|
+
conclusion: .conclusion,
|
|
740
|
+
created_at: .created_at,
|
|
741
|
+
updated_at: .updated_at,
|
|
742
|
+
duration_seconds: (
|
|
743
|
+
((.updated_at | fromdateiso8601) - (.created_at | fromdateiso8601))
|
|
744
|
+
)
|
|
745
|
+
}]' 2>/dev/null || echo "[]")
|
|
746
|
+
|
|
747
|
+
_gh_cache_set "$cache_key" "$parsed"
|
|
748
|
+
emit_event "github.cache_miss" "key=$cache_key"
|
|
749
|
+
|
|
750
|
+
echo "$parsed"
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
754
|
+
# AGGREGATED REPO CONTEXT
|
|
755
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
756
|
+
|
|
757
|
+
gh_repo_context() {
|
|
758
|
+
local owner="$1"
|
|
759
|
+
local repo="$2"
|
|
760
|
+
|
|
761
|
+
if ! _gh_graphql_available; then
|
|
762
|
+
echo '{"error": "GitHub API not available", "contributors": [], "security_alerts": [], "dependabot_alerts": []}'
|
|
763
|
+
return 0
|
|
764
|
+
fi
|
|
765
|
+
|
|
766
|
+
local cache_key="context_${owner}_${repo}"
|
|
767
|
+
local cached
|
|
768
|
+
cached=$(_gh_cache_get "$cache_key" "3600" 2>/dev/null) && {
|
|
769
|
+
emit_event "github.cache_hit" "key=$cache_key"
|
|
770
|
+
echo "$cached"
|
|
771
|
+
return 0
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
info "Fetching repo context for ${owner}/${repo}..." >&2
|
|
775
|
+
|
|
776
|
+
# Gather data (each function handles its own caching/errors)
|
|
777
|
+
local contributors security dependabot protection
|
|
778
|
+
|
|
779
|
+
contributors=$(gh_contributors "$owner" "$repo")
|
|
780
|
+
security=$(gh_security_alerts "$owner" "$repo")
|
|
781
|
+
dependabot=$(gh_dependabot_alerts "$owner" "$repo")
|
|
782
|
+
protection=$(gh_branch_protection "$owner" "$repo" "main")
|
|
783
|
+
|
|
784
|
+
# Get basic repo info
|
|
785
|
+
local repo_info
|
|
786
|
+
repo_info=$(gh api "repos/${owner}/${repo}" 2>/dev/null || echo '{}')
|
|
787
|
+
|
|
788
|
+
local primary_language
|
|
789
|
+
primary_language=$(echo "$repo_info" | jq -r '.language // "unknown"' 2>/dev/null || echo "unknown")
|
|
790
|
+
|
|
791
|
+
local contributor_count
|
|
792
|
+
contributor_count=$(echo "$contributors" | jq 'length' 2>/dev/null || echo "0")
|
|
793
|
+
|
|
794
|
+
local top_contributors
|
|
795
|
+
top_contributors=$(echo "$contributors" | jq '.[0:5]' 2>/dev/null || echo "[]")
|
|
796
|
+
|
|
797
|
+
local security_count
|
|
798
|
+
security_count=$(echo "$security" | jq 'length' 2>/dev/null || echo "0")
|
|
799
|
+
|
|
800
|
+
local dependabot_count
|
|
801
|
+
dependabot_count=$(echo "$dependabot" | jq 'length' 2>/dev/null || echo "0")
|
|
802
|
+
|
|
803
|
+
local context
|
|
804
|
+
context=$(jq -n \
|
|
805
|
+
--arg owner "$owner" \
|
|
806
|
+
--arg repo "$repo" \
|
|
807
|
+
--arg language "$primary_language" \
|
|
808
|
+
--argjson contributor_count "$contributor_count" \
|
|
809
|
+
--argjson top_contributors "$top_contributors" \
|
|
810
|
+
--argjson security_count "$security_count" \
|
|
811
|
+
--argjson dependabot_count "$dependabot_count" \
|
|
812
|
+
--argjson protection "$protection" \
|
|
813
|
+
--argjson security_alerts "$security" \
|
|
814
|
+
--argjson dependabot_alerts "$dependabot" \
|
|
815
|
+
'{
|
|
816
|
+
owner: $owner,
|
|
817
|
+
repo: $repo,
|
|
818
|
+
primary_language: $language,
|
|
819
|
+
contributor_count: $contributor_count,
|
|
820
|
+
top_contributors: $top_contributors,
|
|
821
|
+
security_alert_count: $security_count,
|
|
822
|
+
dependabot_alert_count: $dependabot_count,
|
|
823
|
+
branch_protection: $protection,
|
|
824
|
+
security_alerts: $security_alerts,
|
|
825
|
+
dependabot_alerts: $dependabot_alerts,
|
|
826
|
+
fetched_at: now | todate
|
|
827
|
+
}')
|
|
828
|
+
|
|
829
|
+
_gh_cache_set "$cache_key" "$context"
|
|
830
|
+
emit_event "github.repo_context" "owner=$owner" "repo=$repo"
|
|
831
|
+
|
|
832
|
+
echo "$context"
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
836
|
+
# CLI COMMANDS
|
|
837
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
838
|
+
|
|
839
|
+
gh_repo_context_cli() {
|
|
840
|
+
local owner="${1:-}"
|
|
841
|
+
local repo="${2:-}"
|
|
842
|
+
|
|
843
|
+
if [[ -z "$owner" || -z "$repo" ]]; then
|
|
844
|
+
_gh_detect_repo || { error "Usage: sw github-graphql context <owner> <repo>"; exit 1; }
|
|
845
|
+
owner="$GH_OWNER"
|
|
846
|
+
repo="$GH_REPO"
|
|
847
|
+
fi
|
|
848
|
+
|
|
849
|
+
local result
|
|
850
|
+
result=$(gh_repo_context "$owner" "$repo")
|
|
851
|
+
echo "$result" | jq .
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
gh_security_cli() {
|
|
855
|
+
local owner="${1:-}"
|
|
856
|
+
local repo="${2:-}"
|
|
857
|
+
|
|
858
|
+
if [[ -z "$owner" || -z "$repo" ]]; then
|
|
859
|
+
_gh_detect_repo || { error "Usage: sw github-graphql security <owner> <repo>"; exit 1; }
|
|
860
|
+
owner="$GH_OWNER"
|
|
861
|
+
repo="$GH_REPO"
|
|
862
|
+
fi
|
|
863
|
+
|
|
864
|
+
echo -e "${CYAN}${BOLD}Security Overview: ${owner}/${repo}${RESET}"
|
|
865
|
+
echo ""
|
|
866
|
+
|
|
867
|
+
echo -e "${BOLD}Code Scanning Alerts${RESET}"
|
|
868
|
+
local security
|
|
869
|
+
security=$(gh_security_alerts "$owner" "$repo")
|
|
870
|
+
local sec_count
|
|
871
|
+
sec_count=$(echo "$security" | jq 'length' 2>/dev/null || echo "0")
|
|
872
|
+
if [[ "$sec_count" -gt 0 ]]; then
|
|
873
|
+
echo "$security" | jq -r '.[] | " \(.severity)\t\(.rule)\t\(.path // "n/a")"'
|
|
874
|
+
else
|
|
875
|
+
echo -e " ${GREEN}No open alerts${RESET}"
|
|
876
|
+
fi
|
|
877
|
+
echo ""
|
|
878
|
+
|
|
879
|
+
echo -e "${BOLD}Dependabot Alerts${RESET}"
|
|
880
|
+
local dependabot
|
|
881
|
+
dependabot=$(gh_dependabot_alerts "$owner" "$repo")
|
|
882
|
+
local dep_count
|
|
883
|
+
dep_count=$(echo "$dependabot" | jq 'length' 2>/dev/null || echo "0")
|
|
884
|
+
if [[ "$dep_count" -gt 0 ]]; then
|
|
885
|
+
echo "$dependabot" | jq -r '.[] | " \(.severity)\t\(.package)\t\(.summary)"'
|
|
886
|
+
else
|
|
887
|
+
echo -e " ${GREEN}No open alerts${RESET}"
|
|
888
|
+
fi
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
gh_blame_data_cli() {
|
|
892
|
+
local owner="${1:-}"
|
|
893
|
+
local repo="${2:-}"
|
|
894
|
+
local path="${3:-}"
|
|
895
|
+
|
|
896
|
+
if [[ -z "$path" ]]; then
|
|
897
|
+
error "Usage: sw github-graphql blame <owner> <repo> <path>"
|
|
898
|
+
exit 1
|
|
899
|
+
fi
|
|
900
|
+
|
|
901
|
+
local result
|
|
902
|
+
result=$(gh_blame_data "$owner" "$repo" "$path")
|
|
903
|
+
echo "$result" | jq .
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
gh_commit_history_cli() {
|
|
907
|
+
local owner="${1:-}"
|
|
908
|
+
local repo="${2:-}"
|
|
909
|
+
local path="${3:-}"
|
|
910
|
+
local limit="${4:-10}"
|
|
911
|
+
|
|
912
|
+
if [[ -z "$path" ]]; then
|
|
913
|
+
error "Usage: sw github-graphql history <owner> <repo> <path> [limit]"
|
|
914
|
+
exit 1
|
|
915
|
+
fi
|
|
916
|
+
|
|
917
|
+
local result
|
|
918
|
+
result=$(gh_commit_history "$owner" "$repo" "$path" "$limit")
|
|
919
|
+
echo "$result" | jq .
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
gh_cache_cli() {
|
|
923
|
+
local subcmd="${1:-stats}"
|
|
924
|
+
case "$subcmd" in
|
|
925
|
+
stats) _gh_cache_stats ;;
|
|
926
|
+
clear) _gh_cache_clear ;;
|
|
927
|
+
*) error "Usage: sw github-graphql cache [stats|clear]"; exit 1 ;;
|
|
928
|
+
esac
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
show_help() {
|
|
932
|
+
echo -e "${CYAN}${BOLD}shipwright github-graphql${RESET} — GitHub GraphQL API Client"
|
|
933
|
+
echo ""
|
|
934
|
+
echo -e "${BOLD}Usage:${RESET}"
|
|
935
|
+
echo " sw github-graphql <command> [args]"
|
|
936
|
+
echo ""
|
|
937
|
+
echo -e "${BOLD}Commands:${RESET}"
|
|
938
|
+
echo " context [owner] [repo] Aggregated repo context for intelligence"
|
|
939
|
+
echo " security [owner] [repo] Security alert overview"
|
|
940
|
+
echo " blame <owner> <repo> <path> File blame/contributor data"
|
|
941
|
+
echo " history <owner> <repo> <path> [limit] Commit history for file"
|
|
942
|
+
echo " cache [stats|clear] Manage API cache"
|
|
943
|
+
echo " help Show this help"
|
|
944
|
+
echo ""
|
|
945
|
+
echo -e "${DIM}If owner/repo omitted, auto-detects from git remote.${RESET}"
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
949
|
+
# MAIN
|
|
950
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
951
|
+
|
|
952
|
+
main() {
|
|
953
|
+
local cmd="${1:-help}"
|
|
954
|
+
case "$cmd" in
|
|
955
|
+
context) shift; gh_repo_context_cli "$@" ;;
|
|
956
|
+
security) shift; gh_security_cli "$@" ;;
|
|
957
|
+
blame) shift; gh_blame_data_cli "$@" ;;
|
|
958
|
+
history) shift; gh_commit_history_cli "$@" ;;
|
|
959
|
+
cache) shift; gh_cache_cli "$@" ;;
|
|
960
|
+
help|--help|-h) show_help ;;
|
|
961
|
+
*)
|
|
962
|
+
error "Unknown command: $cmd"
|
|
963
|
+
show_help
|
|
964
|
+
exit 1
|
|
965
|
+
;;
|
|
966
|
+
esac
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
# Only run main if executed directly (not sourced)
|
|
970
|
+
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
971
|
+
main "$@"
|
|
972
|
+
fi
|