@northbridge-security/secureai 0.1.13

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.
Files changed (50) hide show
  1. package/.claude/README.md +122 -0
  2. package/.claude/commands/architect/clean.md +978 -0
  3. package/.claude/commands/architect/kiss.md +762 -0
  4. package/.claude/commands/architect/review.md +704 -0
  5. package/.claude/commands/catchup.md +90 -0
  6. package/.claude/commands/code.md +115 -0
  7. package/.claude/commands/commit.md +1218 -0
  8. package/.claude/commands/cover.md +1298 -0
  9. package/.claude/commands/fmea.md +275 -0
  10. package/.claude/commands/kaizen.md +312 -0
  11. package/.claude/commands/pr.md +503 -0
  12. package/.claude/commands/todo.md +99 -0
  13. package/.claude/commands/worktree.md +738 -0
  14. package/.claude/commands/wrapup.md +103 -0
  15. package/LICENSE +183 -0
  16. package/README.md +108 -0
  17. package/dist/cli.js +75634 -0
  18. package/docs/agents/devops-reviewer.md +889 -0
  19. package/docs/agents/kiss-simplifier.md +1088 -0
  20. package/docs/agents/typescript.md +8 -0
  21. package/docs/guides/README.md +109 -0
  22. package/docs/guides/agents.clean.arch.md +244 -0
  23. package/docs/guides/agents.clean.arch.ts.md +1314 -0
  24. package/docs/guides/agents.gotask.md +1037 -0
  25. package/docs/guides/agents.markdown.md +1209 -0
  26. package/docs/guides/agents.onepassword.md +285 -0
  27. package/docs/guides/agents.sonar.md +857 -0
  28. package/docs/guides/agents.tdd.md +838 -0
  29. package/docs/guides/agents.tdd.ts.md +1062 -0
  30. package/docs/guides/agents.typesript.md +1389 -0
  31. package/docs/guides/github-mcp.md +1075 -0
  32. package/package.json +130 -0
  33. package/packages/secureai-cli/src/cli.ts +21 -0
  34. package/tasks/README.md +880 -0
  35. package/tasks/aws.yml +64 -0
  36. package/tasks/bash.yml +118 -0
  37. package/tasks/bun.yml +738 -0
  38. package/tasks/claude.yml +183 -0
  39. package/tasks/docker.yml +420 -0
  40. package/tasks/docs.yml +127 -0
  41. package/tasks/git.yml +1336 -0
  42. package/tasks/gotask.yml +132 -0
  43. package/tasks/json.yml +77 -0
  44. package/tasks/markdown.yml +95 -0
  45. package/tasks/onepassword.yml +350 -0
  46. package/tasks/security.yml +102 -0
  47. package/tasks/sonar.yml +437 -0
  48. package/tasks/template.yml +74 -0
  49. package/tasks/vscode.yml +103 -0
  50. package/tasks/yaml.yml +121 -0
package/tasks/git.yml ADDED
@@ -0,0 +1,1336 @@
1
+ # Git & GitHub Operations Tasks
2
+ # Tasks for Git operations and GitHub PR management
3
+
4
+ version: "3"
5
+
6
+ tasks:
7
+ default:
8
+ desc: "Show available Git tasks"
9
+ aliases: [help, h]
10
+ silent: true
11
+ cmds:
12
+ - |
13
+ # Color codes
14
+ GREEN='\033[0;32m'
15
+ YELLOW='\033[0;33m'
16
+ BOLD='\033[1m'
17
+ NC='\033[0m'
18
+
19
+ echo -e "${BOLD}Git & GitHub Operations${NC}"
20
+ echo ""
21
+ echo "Command Alias Description Examples"
22
+ echo "───────────────────────────────────────────────────────────────────────────────────────────────────"
23
+ echo -e "${BOLD}Repository Utilities:${NC}"
24
+ echo -e " ${GREEN}task git:repo:root${NC} ${YELLOW}root${NC} Get repository root path"
25
+ echo -e " ${GREEN}task git:repo:url${NC} ${YELLOW}url${NC} Get repository HTTPS URL"
26
+ echo -e " ${GREEN}task git:branch:current${NC} ${YELLOW}branch${NC} Get current branch name"
27
+ echo -e " ${GREEN}task git:branch:default${NC} ${YELLOW}main${NC} Get default branch (main/master)"
28
+ echo -e " ${GREEN}task git:branch:list${NC} ${YELLOW}bl${NC} List branches with local/remote status"
29
+ echo -e " ${GREEN}task git:branch:prune${NC} ${YELLOW}prune${NC} Delete local branches gone on remote"
30
+ echo -e " ${GREEN}task git:token${NC} ${YELLOW}token${NC} Get GitHub authentication token"
31
+ echo ""
32
+ echo -e "${BOLD}Pull Request Management:${NC}"
33
+ echo -e " ${GREEN}task git:pr:create${NC} ${YELLOW}pr${NC} Create/update draft PR from file FILE=\".pr.local.md\""
34
+ echo -e " ${GREEN}task git:pr:open${NC} ${YELLOW}pro${NC} Open current PR in browser"
35
+ echo -e " ${GREEN}task git:pr:list${NC} ${YELLOW}prl${NC} List pull requests LIMIT=10 STATE=open"
36
+ echo -e " ${GREEN}task git:pr:comments${NC} ${YELLOW}com${NC} Get all comments from current PR"
37
+ echo ""
38
+ echo -e "${BOLD}GitHub Actions:${NC}"
39
+ echo -e " ${GREEN}task git:runs:log${NC} ${YELLOW}logs${NC} Download workflow run logs STATE=all ALL=true"
40
+ echo -e " ${GREEN}task git:actions:pin${NC} ${YELLOW}pin${NC} Pin actions to SHAs UPGRADE=1 CHECK=1"
41
+ echo ""
42
+ echo -e "${BOLD}Security:${NC}"
43
+ echo -e " ${GREEN}task git:leaks${NC} ${YELLOW}leaks${NC} Scan for secrets with gitleaks"
44
+ echo -e " ${GREEN}task git:cve${NC} ${YELLOW}cve${NC} Download CVEs for repository MIN_SEVERITY=medium"
45
+ echo ""
46
+ echo -e "${BOLD}Usage Examples:${NC}"
47
+ echo -e " task git:pr:list LIMIT=20 STATE=open List 20 open PRs"
48
+ echo -e " task git:pr:create FILE=\"custom.md\" Create PR from custom file"
49
+ echo -e " task git:runs:log Download latest failed run"
50
+ echo -e " task git:runs:log STATE=all ALL=true Download all runs (any state)"
51
+ echo -e " task git:runs:log RUN_ID=12345678 Download specific run"
52
+ echo -e " task git:runs:log WORKFLOW=\"Tests\" STATE=all Download all test runs"
53
+ echo ""
54
+
55
+ cli:
56
+ internal: true
57
+ silent: true
58
+ desc: Verify that gh CLI is installed, and install if not found
59
+ cmds:
60
+ - |
61
+ # Check if gh is already installed
62
+ if command -v gh &> /dev/null; then
63
+ exit 0
64
+ fi
65
+
66
+ # gh not found, install it silently
67
+ echo "Installing GitHub CLI..."
68
+
69
+ # Detect OS
70
+ OS="$(uname -s)"
71
+ case "$OS" in
72
+ Darwin)
73
+ # macOS - use Homebrew
74
+ if command -v brew &> /dev/null; then
75
+ brew install gh --quiet
76
+ else
77
+ echo "Error: Homebrew not found. Install from https://brew.sh"
78
+ exit 1
79
+ fi
80
+ ;;
81
+
82
+ Linux)
83
+ # Linux - try different package managers
84
+ if command -v apt-get &> /dev/null; then
85
+ # Debian/Ubuntu
86
+ sudo mkdir -p -m 755 /etc/apt/keyrings
87
+ wget -qO- https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo tee /etc/apt/keyrings/githubcli-archive-keyring.gpg > /dev/null
88
+ sudo chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg
89
+ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
90
+ sudo apt-get update -qq
91
+ sudo apt-get install -qq -y gh
92
+ elif command -v dnf &> /dev/null; then
93
+ # Fedora/RHEL 9+
94
+ sudo dnf install -q -y 'dnf-command(config-manager)'
95
+ sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
96
+ sudo dnf install -q -y gh
97
+ elif command -v yum &> /dev/null; then
98
+ # RHEL/CentOS 8
99
+ sudo yum install -q -y 'dnf-command(config-manager)'
100
+ sudo yum-config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
101
+ sudo yum install -q -y gh
102
+ else
103
+ echo "Error: No supported package manager found (apt-get, dnf, yum)"
104
+ echo "Install manually from https://github.com/cli/cli/releases"
105
+ exit 1
106
+ fi
107
+ ;;
108
+
109
+ MINGW*|MSYS*|CYGWIN*)
110
+ # Windows - try winget, then choco, then scoop
111
+ if command -v winget.exe &> /dev/null; then
112
+ winget.exe install --id GitHub.cli --silent --accept-package-agreements --accept-source-agreements
113
+ elif command -v choco &> /dev/null; then
114
+ choco install gh -y
115
+ elif command -v scoop &> /dev/null; then
116
+ scoop install gh
117
+ else
118
+ echo "Error: No supported package manager found (winget, chocolatey, scoop)"
119
+ echo "Install manually from https://github.com/cli/cli/releases"
120
+ exit 1
121
+ fi
122
+ ;;
123
+
124
+ *)
125
+ echo "Error: Unsupported operating system: $OS"
126
+ echo "Install manually from https://github.com/cli/cli/releases"
127
+ exit 1
128
+ ;;
129
+ esac
130
+
131
+ # Verify installation
132
+ if command -v gh &> /dev/null; then
133
+ echo "✓ GitHub CLI installed successfully"
134
+ else
135
+ echo "Error: Installation completed but gh command not found"
136
+ echo "Try closing and reopening your terminal"
137
+ exit 1
138
+ fi
139
+
140
+ token:
141
+ desc: Get the authentication token
142
+ aliases: [token]
143
+ silent: true
144
+ cmds:
145
+ - |
146
+ # Source secrets if .env has 1Password references
147
+ source $(task op:export)
148
+
149
+ # Check if token is still encrypted (force a refresh)
150
+ if [[ "${GITHUB_TOKEN:-}" == *"op://"* ]]; then
151
+ source $(task op:export:force)
152
+ fi
153
+
154
+ # Check if token is STILL encrypted (1Password reference not resolved)
155
+ if [[ "${GITHUB_TOKEN:-}" == *"op://"* ]]; then
156
+ echo -e "${RED}❌ Unable to decrypt authentication token${NC}" >&2
157
+ echo "1Password CLI may not be installed or configured" >&2
158
+ exit 1
159
+ fi
160
+
161
+ # Validate GitHub token is available
162
+ if [ -z "${GITHUB_TOKEN:-}" ]; then
163
+ echo -e "${RED}❌ GitHub token not found${NC}" >&2
164
+ echo "Set GITHUB_TOKEN in .env" >&2
165
+ exit 1
166
+ fi
167
+
168
+ # Output token for capture by calling task
169
+ echo "$GITHUB_TOKEN"
170
+
171
+ repo:root:
172
+ desc: Get the absolute path to the repository root
173
+ silent: true
174
+ aliases: [root]
175
+ cmds:
176
+ - |
177
+ ROOT_DIR=$(git rev-parse --show-toplevel)
178
+ if [ -z "$ROOT_DIR" ]; then
179
+ echo "Error: Not in a git repository"
180
+ exit 1
181
+ fi
182
+ echo "$ROOT_DIR"
183
+
184
+ repo:url:
185
+ silent: true
186
+ aliases: [url]
187
+ desc: Get repository HTTPS URL from git remote (converts SSH to HTTPS, git@git alias to github.com)
188
+ cmds:
189
+ - |
190
+ # Get remote URL
191
+ REMOTE_URL=$(git remote get-url origin 2>/dev/null)
192
+
193
+ if [ -z "$REMOTE_URL" ]; then
194
+ echo "Error: No git remote 'origin' found" >&2
195
+ exit 1
196
+ fi
197
+
198
+ # Convert SSH URL to HTTPS URL using sed (more reliable than bash regex)
199
+ # SSH formats:
200
+ # git@github.com:owner/repo.git
201
+ # git@git:owner/repo.git (alias for github.com)
202
+ # HTTPS formats:
203
+ # https://github.com/owner/repo.git
204
+
205
+ if echo "$REMOTE_URL" | grep -q '^git@'; then
206
+ # SSH format: git@host:owner/repo.git
207
+ # Extract host and path using sed
208
+ HOST=$(echo "$REMOTE_URL" | sed -E 's/^git@([^:]+):.*/\1/')
209
+ PATH=$(echo "$REMOTE_URL" | sed -E 's/^git@[^:]+:(.*)/\1/')
210
+
211
+ # Remove .git suffix if present
212
+ PATH="${PATH%.git}"
213
+
214
+ # Convert git@git alias to github.com
215
+ if [ "$HOST" = "git" ]; then
216
+ HOST="github.com"
217
+ fi
218
+
219
+ HTTPS_URL="https://${HOST}/${PATH}"
220
+ elif echo "$REMOTE_URL" | grep -q '^https\?://'; then
221
+ # Already HTTPS format
222
+ HTTPS_URL="${REMOTE_URL%.git}"
223
+ else
224
+ echo "Error: Unrecognized git remote URL format: $REMOTE_URL" >&2
225
+ exit 1
226
+ fi
227
+
228
+ echo "$HTTPS_URL"
229
+
230
+ branch:current:
231
+ desc: Get the current branch name
232
+ aliases: [branch]
233
+ silent: true
234
+ cmds:
235
+ - |
236
+ CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null)
237
+ if [ -z "$CURRENT_BRANCH" ]; then
238
+ echo "Error: Not in a git repository" >&2
239
+ exit 1
240
+ fi
241
+ echo "$CURRENT_BRANCH"
242
+
243
+ branch:default:
244
+ desc: Get the default branch (main/master)
245
+ silent: true
246
+ aliases: [main]
247
+ cmds:
248
+ - |
249
+ # Get default branch (local-only, no network access needed)
250
+ DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@')
251
+
252
+ # Fallback: try to detect from remote branches
253
+ if [ -z "$DEFAULT_BRANCH" ]; then
254
+ if git show-ref --verify --quiet refs/remotes/origin/main; then
255
+ DEFAULT_BRANCH="main"
256
+ elif git show-ref --verify --quiet refs/remotes/origin/master; then
257
+ DEFAULT_BRANCH="master"
258
+ else
259
+ # Last resort: use gh CLI
260
+ DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null || echo "main")
261
+ fi
262
+ fi
263
+
264
+ if [ -z "$DEFAULT_BRANCH" ]; then
265
+ echo "Error: Unable to determine default branch" >&2
266
+ exit 1
267
+ fi
268
+
269
+ echo "$DEFAULT_BRANCH"
270
+
271
+ branch:prune:
272
+ desc: Delete local branches that no longer exist on remote
273
+ silent: true
274
+ aliases: [prune, clean-branches]
275
+ cmds:
276
+ - bun run ./src/tasks/git/branch-prune.ts
277
+
278
+ branch:list:
279
+ desc: List all branches with local/remote status
280
+ silent: true
281
+ aliases: [branches, bl]
282
+ cmds:
283
+ - |
284
+ # Color codes
285
+ GREEN='\033[0;32m'
286
+ YELLOW='\033[0;33m'
287
+ RED='\033[0;31m'
288
+ CYAN='\033[0;36m'
289
+ BOLD='\033[1m'
290
+ NC='\033[0m'
291
+
292
+ # Fetch latest remote info (silent)
293
+ git fetch --prune &>/dev/null || true
294
+
295
+ # Get current branch
296
+ CURRENT=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
297
+
298
+ # Get default branch
299
+ DEFAULT=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "main")
300
+
301
+ # Collect local branches (strip * for current, + for worktree)
302
+ LOCAL_BRANCHES=$(git branch 2>/dev/null | sed 's/^[\*+] //' | sed 's/^ *//' | sort -u)
303
+
304
+ # Collect remote branches (strip origin/ prefix)
305
+ REMOTE_BRANCHES=$(git branch -r 2>/dev/null | grep -v '\->' | sed 's/^ *//' | sed 's|^origin/||' | sort -u)
306
+
307
+ # Combine and dedupe all branches
308
+ ALL_BRANCHES=$(printf "%s\n%s" "$LOCAL_BRANCHES" "$REMOTE_BRANCHES" | sort -u | grep -v '^$')
309
+
310
+ # Print header
311
+ printf "${BOLD}Branches${NC}\n\n"
312
+ printf "%-44s %-10s %-10s %s\n" "Branch" "Local" "Remote" "Status"
313
+ printf "────────────────────────────────────────────────────────────────────────────────\n"
314
+
315
+ # Counters
316
+ TOTAL=0
317
+ SYNCED=0
318
+ LOCAL_ONLY=0
319
+ REMOTE_ONLY=0
320
+
321
+ # Process each branch
322
+ echo "$ALL_BRANCHES" | while read -r branch; do
323
+ [ -z "$branch" ] && continue
324
+
325
+ # Check if exists locally
326
+ IS_LOCAL=$(echo "$LOCAL_BRANCHES" | grep -Fx "$branch" > /dev/null && echo "yes" || echo "no")
327
+
328
+ # Check if exists remotely
329
+ IS_REMOTE=$(echo "$REMOTE_BRANCHES" | grep -Fx "$branch" > /dev/null && echo "yes" || echo "no")
330
+
331
+ # Determine indicators and status
332
+ if [ "$IS_LOCAL" = "yes" ] && [ "$IS_REMOTE" = "yes" ]; then
333
+ LOCAL_IND="${GREEN}✓${NC}"
334
+ REMOTE_IND="${GREEN}✓${NC}"
335
+ STATUS="${GREEN}synced${NC}"
336
+ elif [ "$IS_LOCAL" = "yes" ]; then
337
+ LOCAL_IND="${GREEN}✓${NC}"
338
+ REMOTE_IND="${RED}✗${NC}"
339
+ STATUS="${YELLOW}local only${NC}"
340
+ else
341
+ LOCAL_IND="${RED}✗${NC}"
342
+ REMOTE_IND="${GREEN}✓${NC}"
343
+ STATUS="${CYAN}remote only${NC}"
344
+ fi
345
+
346
+ # Mark current and default branches
347
+ MARKER=""
348
+ if [ "$branch" = "$CURRENT" ]; then
349
+ MARKER=" ${CYAN}← current${NC}"
350
+ elif [ "$branch" = "$DEFAULT" ]; then
351
+ MARKER=" ${YELLOW}← default${NC}"
352
+ fi
353
+
354
+ # Print row
355
+ printf "%-44s ${LOCAL_IND} ${REMOTE_IND} ${STATUS}${MARKER}\n" "$branch"
356
+ done
357
+
358
+ # Calculate summary (outside subshell)
359
+ TOTAL=$(echo "$ALL_BRANCHES" | grep -c '.' || echo 0)
360
+ SYNCED=0
361
+ LOCAL_ONLY=0
362
+ REMOTE_ONLY=0
363
+
364
+ echo "$ALL_BRANCHES" | while read -r branch; do
365
+ [ -z "$branch" ] && continue
366
+ IS_LOCAL=$(echo "$LOCAL_BRANCHES" | grep -Fx "$branch" > /dev/null && echo "yes" || echo "no")
367
+ IS_REMOTE=$(echo "$REMOTE_BRANCHES" | grep -Fx "$branch" > /dev/null && echo "yes" || echo "no")
368
+ if [ "$IS_LOCAL" = "yes" ] && [ "$IS_REMOTE" = "yes" ]; then
369
+ SYNCED=$((SYNCED + 1))
370
+ elif [ "$IS_LOCAL" = "yes" ]; then
371
+ LOCAL_ONLY=$((LOCAL_ONLY + 1))
372
+ else
373
+ REMOTE_ONLY=$((REMOTE_ONLY + 1))
374
+ fi
375
+ echo "$SYNCED $LOCAL_ONLY $REMOTE_ONLY"
376
+ done | tail -1 | read SYNCED LOCAL_ONLY REMOTE_ONLY 2>/dev/null || true
377
+
378
+ # Recalculate to avoid subshell issue
379
+ SYNCED=$(echo "$ALL_BRANCHES" | while read -r b; do
380
+ [ -z "$b" ] && continue
381
+ IL=$(echo "$LOCAL_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
382
+ IR=$(echo "$REMOTE_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
383
+ [ "$IL" = "y" ] && [ "$IR" = "y" ] && echo "$b"
384
+ done | grep -c '.' || echo 0)
385
+
386
+ LOCAL_ONLY=$(echo "$ALL_BRANCHES" | while read -r b; do
387
+ [ -z "$b" ] && continue
388
+ IL=$(echo "$LOCAL_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
389
+ IR=$(echo "$REMOTE_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
390
+ [ "$IL" = "y" ] && [ "$IR" = "n" ] && echo "$b"
391
+ done | grep -c '.' || echo 0)
392
+
393
+ REMOTE_ONLY=$(echo "$ALL_BRANCHES" | while read -r b; do
394
+ [ -z "$b" ] && continue
395
+ IL=$(echo "$LOCAL_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
396
+ IR=$(echo "$REMOTE_BRANCHES" | grep -Fx "$b" > /dev/null && echo "y" || echo "n")
397
+ [ "$IL" = "n" ] && [ "$IR" = "y" ] && echo "$b"
398
+ done | grep -c '.' || echo 0)
399
+
400
+ printf "\n${BOLD}Summary:${NC} $TOTAL branches ($SYNCED synced, $LOCAL_ONLY local only, $REMOTE_ONLY remote only)\n"
401
+
402
+ pr:list:
403
+ desc: "List pull requests (use LIMIT=n to change number shown, default 10)"
404
+ silent: true
405
+ deps: [cli]
406
+ dotenv: [".env"]
407
+ vars:
408
+ LIMIT: '{{.LIMIT | default "10"}}'
409
+ STATE: '{{.STATE | default "all"}}'
410
+ cmds:
411
+ - |
412
+ # Get and validate GitHub token (handles 1Password decryption)
413
+ export GH_TOKEN=$(task git:token)
414
+
415
+ # Get repository info
416
+ REPO=$(task git:repo:url)
417
+
418
+ # If that fails, extract from git remote
419
+ if [ -z "$REPO" ]; then
420
+ REPO=$(git remote get-url origin | sed -E 's|^(https://github.com/|git@github.com:)||' | sed 's|\.git$||')
421
+ fi
422
+
423
+ echo -e "{{.CYAN}}Pull Requests for: $REPO{{.NC}}"
424
+ echo ""
425
+
426
+ # List PRs with state filter
427
+ STATE="{{.STATE}}"
428
+ STATE_FILTER=""
429
+ if [ "$STATE" != "all" ]; then
430
+ STATE_FILTER="--state $STATE"
431
+ fi
432
+
433
+ # Show PRs in a nice table format
434
+ gh pr list $STATE_FILTER --limit {{.LIMIT}} --json number,title,state,author,createdAt,isDraft,headRefName,baseRefName --jq '.[] | "#\(.number) | " + (if .isDraft then "📝 " else "" end) + (.title | .[0:60]) + (if (.title | length) > 60 then "..." else "" end) + " | " + (.state | ascii_upcase) + " | " + .author.login + " | " + (.headRefName + " → " + .baseRefName) + " | " + (.createdAt | fromdateiso8601 | strftime("%Y-%m-%d"))' | column -t -s '|'
435
+
436
+ echo ""
437
+ echo -e "{{.YELLOW}}Total shown: {{.LIMIT}} (use LIMIT=n to change){{.NC}}"
438
+ echo -e "{{.YELLOW}}State filter: {{.STATE}} (use STATE=open|closed|merged|all){{.NC}}"
439
+
440
+ aliases: [prl]
441
+
442
+ pr:create:
443
+ desc: Create draft GitHub pull request from markdown file
444
+ aliases: [pr]
445
+ deps: [cli]
446
+ silent: true
447
+ vars:
448
+ PR_FILE: '{{.FILE | default ".pr.local.md"}}'
449
+ cmds:
450
+ - |
451
+ set -euo pipefail
452
+
453
+ # Color codes
454
+ GREEN='\033[0;32m'
455
+ YELLOW='\033[0;33m'
456
+ RED='\033[0;31m'
457
+ BOLD='\033[1m'
458
+ NC='\033[0m'
459
+
460
+ PR_FILE="{{.PR_FILE}}"
461
+ PR_URL=""
462
+ PR_NUMBER=""
463
+ PR_ACTION=""
464
+
465
+ # Validate PR file exists
466
+ if [ ! -f "$PR_FILE" ]; then
467
+ echo -e "${RED}❌ PR file not found: $PR_FILE${NC}" >&2
468
+ echo "Run /pr command first to generate PR content" >&2
469
+ exit 1
470
+ fi
471
+
472
+ # Get and validate GitHub token (handles 1Password decryption)
473
+ export GH_TOKEN=$(task git:token)
474
+
475
+ # Ensure we're in a git repository
476
+ if ! git rev-parse --git-dir > /dev/null 2>&1; then
477
+ echo -e "${RED}❌ Not in a git repository${NC}" >&2
478
+ exit 1
479
+ fi
480
+
481
+ # Check if current branch has commits
482
+ if ! git log -1 > /dev/null 2>&1; then
483
+ echo -e "${RED}❌ No commits on current branch${NC}" >&2
484
+ echo "Make at least one commit before creating PR" >&2
485
+ exit 1
486
+ fi
487
+
488
+ # Extract title (first line starting with # )
489
+ TITLE=$(grep "^# " "$PR_FILE" | head -n 1 | sed 's/^# //')
490
+
491
+ if [ -z "$TITLE" ]; then
492
+ echo -e "${RED}❌ No title found in PR file${NC}" >&2
493
+ echo "PR file must start with '# Title'" >&2
494
+ exit 1
495
+ fi
496
+
497
+ # Extract body (everything after first line, skip empty lines at start)
498
+ BODY=$(sed '1d' "$PR_FILE" | sed -e :a -e '/./,$!d;/^\n*$/{$d;N;};/\n$/ba')
499
+
500
+ if [ -z "$BODY" ]; then
501
+ echo -e "${RED}❌ No body content found in PR file${NC}" >&2
502
+ exit 1
503
+ fi
504
+
505
+ echo -e "${GREEN}✓${NC} Title: $TITLE"
506
+
507
+ # Get current and default branches
508
+ CURRENT_BRANCH=$(task git:branch:current)
509
+ DEFAULT_BRANCH=$(task git:branch:default)
510
+
511
+ # Push branch to remote (gh pr create will handle this if needed)
512
+ # Try to push, but don't fail if SSH isn't configured - gh CLI will use HTTPS
513
+
514
+ if git push -u origin "$CURRENT_BRANCH" &>/dev/null; then
515
+ echo -e "${GREEN}✓${NC} Branch pushed to origin/$CURRENT_BRANCH"
516
+ else
517
+ echo -e "${YELLOW}⚠${NC} Git push failed (possibly SSH not configured)"
518
+ echo -e "${YELLOW}→${NC} Will rely on gh CLI to push via HTTPS"
519
+ fi
520
+
521
+ # Check if PR already exists for this branch
522
+ EXISTING_PR=$(gh pr list --head "$CURRENT_BRANCH" --base "$DEFAULT_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
523
+
524
+ if [ -n "$EXISTING_PR" ]; then
525
+ # Update PR title and body
526
+ gh pr edit "$EXISTING_PR" \
527
+ --title "$TITLE" \
528
+ --body "$BODY" &>/dev/null
529
+
530
+ PR_NUMBER="$EXISTING_PR"
531
+ PR_ACTION="Updated"
532
+ else
533
+ # Create draft PR and capture output
534
+ PR_OUTPUT=$(gh pr create \
535
+ --base "$DEFAULT_BRANCH" \
536
+ --head "$CURRENT_BRANCH" \
537
+ --draft \
538
+ --title "$TITLE" \
539
+ --body "$BODY" 2>&1)
540
+
541
+ if [ $? -ne 0 ]; then
542
+ echo -e "${RED}❌ Failed to create PR${NC}" >&2
543
+ echo "$PR_OUTPUT" >&2
544
+ exit 1
545
+ fi
546
+
547
+ # Extract PR number from output URL
548
+ PR_URL=$(echo "$PR_OUTPUT" | grep -o 'https://github.com/[^/]*/[^/]*/pull/[0-9]*' | head -1)
549
+ PR_NUMBER=$(echo "$PR_URL" | grep -o '[0-9]*$')
550
+ PR_ACTION="Created"
551
+ fi
552
+
553
+ # Get PR URL for opening in browser
554
+ if [ -z "$PR_URL" ] && [ -n "$PR_NUMBER" ]; then
555
+ # Get URL from gh CLI
556
+ PR_URL=$(gh pr view "$PR_NUMBER" --json url --jq '.url' 2>/dev/null || echo "")
557
+ fi
558
+
559
+ # Display success message with URL
560
+ if [ -n "$PR_URL" ]; then
561
+ echo -e "${GREEN}✓${NC} ${PR_ACTION}: $PR_URL"
562
+
563
+ # Open PR in browser (cross-platform)
564
+ if [ "$(uname)" = "Darwin" ]; then
565
+ # macOS
566
+ open "$PR_URL"
567
+ elif [ "$(uname)" = "Linux" ]; then
568
+ # Linux
569
+ if command -v xdg-open &> /dev/null; then
570
+ xdg-open "$PR_URL"
571
+ fi
572
+ elif [ "$(uname -o 2>/dev/null)" = "Msys" ] || [ "$(uname -o 2>/dev/null)" = "Cygwin" ]; then
573
+ # Windows (Git Bash or Cygwin)
574
+ start "$PR_URL"
575
+ fi
576
+ fi
577
+
578
+ pr:open:
579
+ desc: Open current pull request in browser
580
+ aliases: [pro]
581
+ deps: [cli]
582
+ silent: true
583
+ cmds:
584
+ - |
585
+ set -euo pipefail
586
+
587
+ # Color codes
588
+ GREEN='\033[0;32m'
589
+ YELLOW='\033[0;33m'
590
+ RED='\033[0;31m'
591
+ BOLD='\033[1m'
592
+ NC='\033[0m'
593
+
594
+ # Get and validate GitHub token
595
+ export GH_TOKEN=$(task git:token)
596
+
597
+ # Get current and default branches
598
+ CURRENT_BRANCH=$(task git:branch:current)
599
+ DEFAULT_BRANCH=$(task git:branch:default)
600
+
601
+ # Check if PR exists for this branch
602
+ PR_NUMBER=$(gh pr list --head "$CURRENT_BRANCH" --base "$DEFAULT_BRANCH" --json number --jq '.[0].number' 2>/dev/null || echo "")
603
+
604
+ if [ -z "$PR_NUMBER" ]; then
605
+ echo -e "${RED}❌ No pull request found for branch: $CURRENT_BRANCH${NC}" >&2
606
+ echo "" >&2
607
+ echo "Create a PR first:" >&2
608
+ echo -e " ${GREEN}task git:pr:create${NC}" >&2
609
+ echo "" >&2
610
+ exit 1
611
+ fi
612
+
613
+ # Get PR URL
614
+ PR_URL=$(gh pr view "$PR_NUMBER" --json url --jq '.url' 2>/dev/null || echo "")
615
+
616
+ if [ -z "$PR_URL" ]; then
617
+ echo -e "${RED}❌ Failed to get PR URL${NC}" >&2
618
+ exit 1
619
+ fi
620
+
621
+ echo -e "${GREEN}✓${NC} Opening: $PR_URL"
622
+
623
+ # Detect OS and open browser
624
+ if [ "$(uname)" = "Darwin" ]; then
625
+ open "$PR_URL"
626
+ elif [ "$(uname)" = "Linux" ]; then
627
+ if command -v xdg-open &> /dev/null; then
628
+ xdg-open "$PR_URL"
629
+ fi
630
+ elif [ "$(uname -o 2>/dev/null)" = "Msys" ] || [ "$(uname -o 2>/dev/null)" = "Cygwin" ]; then
631
+ start "$PR_URL"
632
+ fi
633
+
634
+ pr:comments:
635
+ desc: Retrieve all comments of the current pull request
636
+ aliases: [com]
637
+ silent: true
638
+ deps: [cli]
639
+ dotenv: [".env"]
640
+ vars:
641
+ REPO_ROOT:
642
+ sh: git rev-parse --show-toplevel 2>/dev/null || pwd
643
+ LOG_DIR:
644
+ sh: echo "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.logs/github/comments"
645
+ cmds:
646
+ - |
647
+ # Color codes
648
+ GREEN='\033[0;32m'
649
+ RED='\033[0;31m'
650
+ NC='\033[0m'
651
+
652
+ # Get and validate GitHub token
653
+ export GH_TOKEN=$(task git:token)
654
+
655
+ # Get current branch and repository
656
+ CURRENT_BRANCH=$(task git:branch:current)
657
+ REPO=$(task git:repo:url | sed -E 's|^https://github.com/||')
658
+
659
+ # Find PR for current branch (gh pr view auto-detects PR for current branch)
660
+ PR_NUMBER=$(gh pr view --json number --jq '.number' 2>/dev/null || echo "")
661
+
662
+ if [ -z "$PR_NUMBER" ]; then
663
+ echo -e "${RED}❌ No pull request found for branch: $CURRENT_BRANCH${NC}" >&2
664
+ exit 1
665
+ fi
666
+
667
+ # Get PR details
668
+ PR_DATA=$(gh pr view "$PR_NUMBER" --json number,title,author,state)
669
+ PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
670
+ PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
671
+ PR_STATE=$(echo "$PR_DATA" | jq -r '.state')
672
+
673
+ # Create log directory
674
+ mkdir -p "{{.LOG_DIR}}"
675
+
676
+ # Generate timestamp
677
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
678
+ LOG_FILE="{{.LOG_DIR}}/${TIMESTAMP}.log"
679
+
680
+ # Fetch all comments (silent)
681
+ {
682
+ echo "=========================================="
683
+ echo "PR Comments Report"
684
+ echo "=========================================="
685
+ echo "Generated: $(date)"
686
+ echo "Repository: $REPO"
687
+ echo "PR #$PR_NUMBER: $PR_TITLE"
688
+ echo "Author: $PR_AUTHOR"
689
+ echo "State: $PR_STATE"
690
+ echo "Branch: $CURRENT_BRANCH"
691
+ echo ""
692
+ echo "=========================================="
693
+ echo "ISSUE COMMENTS (General PR Discussion)"
694
+ echo "=========================================="
695
+ echo ""
696
+
697
+ # Get issue comments
698
+ ISSUE_COMMENTS=$(gh api "/repos/$REPO/issues/$PR_NUMBER/comments" --paginate 2>/dev/null | jq -s 'add // []')
699
+ ISSUE_COUNT=$(echo "$ISSUE_COMMENTS" | jq 'length')
700
+
701
+ if [ "$ISSUE_COUNT" -gt 0 ]; then
702
+ echo "$ISSUE_COMMENTS" | jq -r '.[] | "[@\(.user.login)] \(.created_at)\n\(.body)\n\n---\n"'
703
+ else
704
+ echo "No general comments found."
705
+ echo ""
706
+ fi
707
+
708
+ echo ""
709
+ echo "=========================================="
710
+ echo "REVIEW COMMENTS (Inline Code Comments)"
711
+ echo "=========================================="
712
+ echo ""
713
+
714
+ # Get review comments
715
+ REVIEW_COMMENTS=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" --paginate 2>/dev/null | jq -s 'add // []')
716
+ REVIEW_COUNT=$(echo "$REVIEW_COMMENTS" | jq 'length')
717
+
718
+ if [ "$REVIEW_COUNT" -gt 0 ]; then
719
+ echo "$REVIEW_COMMENTS" | jq -r '.[] | "[@\(.user.login)] \(.path):\(.line // .original_line // "??") - \(.created_at)\n\(.body)\n\n---\n"'
720
+ else
721
+ echo "No code review comments found."
722
+ echo ""
723
+ fi
724
+
725
+ echo ""
726
+ echo "=========================================="
727
+ echo "REVIEWS (Approve/Request Changes/Comment)"
728
+ echo "=========================================="
729
+ echo ""
730
+
731
+ # Get reviews
732
+ REVIEWS=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" --paginate 2>/dev/null | jq -s 'add // []')
733
+ REVIEWS_COUNT=$(echo "$REVIEWS" | jq 'length')
734
+
735
+ if [ "$REVIEWS_COUNT" -gt 0 ]; then
736
+ echo "$REVIEWS" | jq -r '.[] | "[@\(.user.login)] \(.state) - \(.submitted_at)\n\(.body // "No comment")\n\n---\n"'
737
+ else
738
+ echo "No reviews found."
739
+ echo ""
740
+ fi
741
+
742
+ echo ""
743
+ echo "=========================================="
744
+ echo "SUMMARY"
745
+ echo "=========================================="
746
+ echo "General comments: $ISSUE_COUNT"
747
+ echo "Code review comments: $REVIEW_COUNT"
748
+ echo "Reviews: $REVIEWS_COUNT"
749
+ echo "Total: $((ISSUE_COUNT + REVIEW_COUNT + REVIEWS_COUNT))"
750
+ } > "$LOG_FILE"
751
+
752
+ # Display summary - calculate counts from already-fetched data
753
+ ISSUE_COUNT=$(gh api "/repos/$REPO/issues/$PR_NUMBER/comments" --paginate 2>/dev/null | jq -s 'add // [] | length')
754
+ REVIEW_COUNT=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/comments" --paginate 2>/dev/null | jq -s 'add // [] | length')
755
+ REVIEWS_COUNT=$(gh api "/repos/$REPO/pulls/$PR_NUMBER/reviews" --paginate 2>/dev/null | jq -s 'add // [] | length')
756
+ TOTAL=$((ISSUE_COUNT + REVIEW_COUNT + REVIEWS_COUNT))
757
+
758
+ echo -e "${GREEN}✓${NC} Retrieved $TOTAL comments for PR #$PR_NUMBER → $LOG_FILE"
759
+
760
+ # Open in VSCode if available (silent if not installed)
761
+ if command -v code &> /dev/null; then
762
+ code "$LOG_FILE" 2>/dev/null || true
763
+ elif command -v code-insiders &> /dev/null; then
764
+ code-insiders "$LOG_FILE" 2>/dev/null || true
765
+ fi
766
+
767
+ runs:log:
768
+ desc: Download GitHub Actions workflow run logs
769
+ aliases: [logs, rl]
770
+ silent: true
771
+ deps: [cli]
772
+ vars:
773
+ RUN_ID: '{{.RUN_ID | default ""}}'
774
+ WORKFLOW: '{{.WORKFLOW | default ""}}'
775
+ STATE: '{{.STATE | default "failure"}}'
776
+ ALL: '{{.ALL | default "false"}}'
777
+ REPO_ROOT:
778
+ sh: git rev-parse --show-toplevel 2>/dev/null || pwd
779
+ BASE_LOG_DIR:
780
+ sh: echo "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.logs/github/runs"
781
+ cmds:
782
+ - |
783
+ set -euo pipefail
784
+
785
+ # Color codes
786
+ GREEN='\033[0;32m'
787
+ RED='\033[0;31m'
788
+ YELLOW='\033[0;33m'
789
+ NC='\033[0m'
790
+
791
+ # Get and validate GitHub token
792
+ export GH_TOKEN=$(task git:token)
793
+
794
+ # Get repository info
795
+ REPO=$(task git:repo:url | sed -E 's|^https://github.com/||')
796
+
797
+ STATE="{{.STATE}}"
798
+ DOWNLOAD_ALL="{{.ALL}}"
799
+
800
+ # Create timestamped directory for this download session
801
+ TIMESTAMP=$(date +"%Y%m%d-%H%M%S")
802
+ SESSION_DIR="{{.BASE_LOG_DIR}}/${TIMESTAMP}"
803
+ mkdir -p "$SESSION_DIR"
804
+
805
+ # Get current branch
806
+ CURRENT_BRANCH=$(task git:branch:current)
807
+
808
+ # If RUN_ID specified, download that specific run (ignore STATE and ALL)
809
+ if [ -n "{{.RUN_ID}}" ]; then
810
+ RUN_IDS=("{{.RUN_ID}}")
811
+ elif [ "$DOWNLOAD_ALL" = "true" ]; then
812
+ # Download all historical runs matching STATE (up to 100)
813
+ WORKFLOW_FILTER=""
814
+ if [ -n "{{.WORKFLOW}}" ]; then
815
+ WORKFLOW_FILTER="--workflow {{.WORKFLOW}}"
816
+ fi
817
+
818
+ RUN_DATA=$(gh run list \
819
+ -b "$CURRENT_BRANCH" \
820
+ $WORKFLOW_FILTER \
821
+ --status completed \
822
+ --limit 100 \
823
+ --json databaseId,name,conclusion 2>/dev/null || echo "[]")
824
+
825
+ if [ "$RUN_DATA" = "[]" ] || [ -z "$RUN_DATA" ]; then
826
+ echo -e "${RED}❌ No workflow runs found${NC}" >&2
827
+ exit 1
828
+ fi
829
+
830
+ # Filter by conclusion/state
831
+ if [ "$STATE" != "all" ]; then
832
+ RUN_IDS=($(echo "$RUN_DATA" | jq -r ".[] | select(.conclusion == \"$STATE\") | .databaseId"))
833
+ else
834
+ RUN_IDS=($(echo "$RUN_DATA" | jq -r '.[].databaseId'))
835
+ fi
836
+
837
+ if [ ${#RUN_IDS[@]} -eq 0 ]; then
838
+ echo -e "${YELLOW}⚠${NC} No runs found with STATE=$STATE, switching to STATE=all"
839
+ STATE="all"
840
+ RUN_IDS=($(echo "$RUN_DATA" | jq -r '.[].databaseId'))
841
+ fi
842
+ else
843
+ # Download latest run per workflow matching STATE
844
+ WORKFLOW_FILTER=""
845
+ if [ -n "{{.WORKFLOW}}" ]; then
846
+ WORKFLOW_FILTER="--workflow {{.WORKFLOW}}"
847
+ fi
848
+
849
+ # Get all workflows in repo and latest run for each
850
+ RUN_IDS=()
851
+
852
+ if [ -n "{{.WORKFLOW}}" ]; then
853
+ # Single workflow specified
854
+ WORKFLOW_NAME="{{.WORKFLOW}}"
855
+
856
+ if [ "$STATE" != "all" ]; then
857
+ LATEST_RUN=$(gh run list \
858
+ --workflow "$WORKFLOW_NAME" \
859
+ -b "$CURRENT_BRANCH" \
860
+ --status completed \
861
+ --limit 10 \
862
+ --json databaseId,conclusion 2>/dev/null | \
863
+ jq -r ".[] | select(.conclusion == \"$STATE\") | .databaseId" | head -1)
864
+ else
865
+ LATEST_RUN=$(gh run list \
866
+ --workflow "$WORKFLOW_NAME" \
867
+ -b "$CURRENT_BRANCH" \
868
+ --status completed \
869
+ --limit 1 \
870
+ --json databaseId 2>/dev/null | \
871
+ jq -r '.[].databaseId')
872
+ fi
873
+
874
+ if [ -n "$LATEST_RUN" ]; then
875
+ RUN_IDS+=("$LATEST_RUN")
876
+ fi
877
+ else
878
+ # Get all workflows and process with while loop
879
+ while IFS= read -r WORKFLOW_NAME; do
880
+ [ -z "$WORKFLOW_NAME" ] && continue
881
+
882
+ if [ "$STATE" != "all" ]; then
883
+ LATEST_RUN=$(gh run list \
884
+ --workflow "$WORKFLOW_NAME" \
885
+ -b "$CURRENT_BRANCH" \
886
+ --status completed \
887
+ --limit 10 \
888
+ --json databaseId,conclusion 2>/dev/null | \
889
+ jq -r ".[] | select(.conclusion == \"$STATE\") | .databaseId" | head -1)
890
+ else
891
+ LATEST_RUN=$(gh run list \
892
+ --workflow "$WORKFLOW_NAME" \
893
+ -b "$CURRENT_BRANCH" \
894
+ --status completed \
895
+ --limit 1 \
896
+ --json databaseId 2>/dev/null | \
897
+ jq -r '.[].databaseId')
898
+ fi
899
+
900
+ if [ -n "$LATEST_RUN" ]; then
901
+ RUN_IDS+=("$LATEST_RUN")
902
+ fi
903
+ done < <(gh api "/repos/$REPO/actions/workflows" --jq '.workflows[].name' 2>/dev/null)
904
+ fi
905
+
906
+ # Auto-fallback if no runs found
907
+ if [ ${#RUN_IDS[@]} -eq 0 ]; then
908
+ if [ "$STATE" != "all" ]; then
909
+ echo -e "${YELLOW}⚠${NC} No runs found with STATE=$STATE, retrying with STATE=all"
910
+ STATE="all"
911
+ RUN_IDS=()
912
+
913
+ if [ -n "{{.WORKFLOW}}" ]; then
914
+ WORKFLOW_NAME="{{.WORKFLOW}}"
915
+ LATEST_RUN=$(gh run list \
916
+ --workflow "$WORKFLOW_NAME" \
917
+ -b "$CURRENT_BRANCH" \
918
+ --status completed \
919
+ --limit 1 \
920
+ --json databaseId 2>/dev/null | \
921
+ jq -r '.[].databaseId')
922
+
923
+ if [ -n "$LATEST_RUN" ]; then
924
+ RUN_IDS+=("$LATEST_RUN")
925
+ fi
926
+ else
927
+ while IFS= read -r WORKFLOW_NAME; do
928
+ [ -z "$WORKFLOW_NAME" ] && continue
929
+
930
+ LATEST_RUN=$(gh run list \
931
+ --workflow "$WORKFLOW_NAME" \
932
+ -b "$CURRENT_BRANCH" \
933
+ --status completed \
934
+ --limit 1 \
935
+ --json databaseId 2>/dev/null | \
936
+ jq -r '.[].databaseId')
937
+
938
+ if [ -n "$LATEST_RUN" ]; then
939
+ RUN_IDS+=("$LATEST_RUN")
940
+ fi
941
+ done < <(gh api "/repos/$REPO/actions/workflows" --jq '.workflows[].name' 2>/dev/null)
942
+ fi
943
+ fi
944
+
945
+ if [ ${#RUN_IDS[@]} -eq 0 ]; then
946
+ echo -e "${RED}❌ No completed runs found on branch: $CURRENT_BRANCH${NC}" >&2
947
+ exit 1
948
+ fi
949
+ fi
950
+ fi
951
+
952
+ # Download each run
953
+ DOWNLOADED_COUNT=0
954
+ TOTAL_LOGS=0
955
+ LAST_LOG_DIR=""
956
+
957
+ for RUN_ID in "${RUN_IDS[@]}"; do
958
+ # Get run details (silent)
959
+ RUN_INFO=$(gh api "/repos/$REPO/actions/runs/$RUN_ID" 2>/dev/null)
960
+
961
+ if [ -z "$RUN_INFO" ]; then
962
+ continue
963
+ fi
964
+
965
+ WORKFLOW_NAME=$(echo "$RUN_INFO" | jq -r '.name')
966
+ WORKFLOW_SLUG=$(echo "$WORKFLOW_NAME" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
967
+ RUN_NUMBER=$(echo "$RUN_INFO" | jq -r '.run_number')
968
+ CONCLUSION=$(echo "$RUN_INFO" | jq -r '.conclusion // "in_progress"')
969
+
970
+ # Create log directory within session
971
+ LOG_DIR="$SESSION_DIR/${WORKFLOW_SLUG}-${RUN_ID}"
972
+ mkdir -p "$LOG_DIR"
973
+
974
+ # Download and extract logs (silent)
975
+ if gh api "/repos/$REPO/actions/runs/$RUN_ID/logs" > "$LOG_DIR/logs.zip" 2>/dev/null; then
976
+ # Extract logs (silent)
977
+ cd "$LOG_DIR"
978
+ if unzip -o -q logs.zip 2>/dev/null; then
979
+ rm logs.zip
980
+
981
+ # Save run metadata (silent)
982
+ echo "$RUN_INFO" | jq '.' > "run-metadata.json" 2>/dev/null
983
+
984
+ # Count log files
985
+ LOG_COUNT=$(find . -name "*.txt" -type f | wc -l | tr -d ' ')
986
+ TOTAL_LOGS=$((TOTAL_LOGS + LOG_COUNT))
987
+ DOWNLOADED_COUNT=$((DOWNLOADED_COUNT + 1))
988
+ LAST_LOG_DIR="$LOG_DIR"
989
+
990
+ # Show individual success if downloading multiple
991
+ if [ "$DOWNLOAD_ALL" = "true" ]; then
992
+ echo -e "${GREEN}✓${NC} Run #$RUN_NUMBER ($CONCLUSION): $LOG_COUNT logs"
993
+ fi
994
+ fi
995
+ cd - > /dev/null
996
+ fi
997
+ done
998
+
999
+ # Display summary and generate README
1000
+ if [ $DOWNLOADED_COUNT -eq 0 ]; then
1001
+ echo -e "${RED}❌ Failed to download any logs${NC}" >&2
1002
+ exit 1
1003
+ fi
1004
+
1005
+ # Always create README (for both single and multiple downloads)
1006
+ README_FILE="$SESSION_DIR/README.md"
1007
+
1008
+ {
1009
+ echo "# GitHub Actions Workflow Runs"
1010
+ echo ""
1011
+ echo "**Downloaded:** $(date)"
1012
+ echo "**Repository:** $REPO"
1013
+ echo "**Branch:** $(task git:branch:current)"
1014
+ echo "**Filter:** STATE=$STATE"
1015
+ echo ""
1016
+ echo "**Total Runs:** $DOWNLOADED_COUNT"
1017
+ echo "**Total Log Files:** $TOTAL_LOGS"
1018
+ echo ""
1019
+ echo "## Run Details"
1020
+ echo ""
1021
+ echo "| Run # | Workflow | Conclusion | Logs | Directory |"
1022
+ echo "|-------|----------|------------|------|-----------|"
1023
+
1024
+ # Generate table rows for each downloaded run
1025
+ for RUN_ID in "${RUN_IDS[@]}"; do
1026
+ RUN_INFO=$(gh api "/repos/$REPO/actions/runs/$RUN_ID" 2>/dev/null)
1027
+
1028
+ if [ -z "$RUN_INFO" ]; then
1029
+ continue
1030
+ fi
1031
+
1032
+ WORKFLOW_NAME=$(echo "$RUN_INFO" | jq -r '.name')
1033
+ WORKFLOW_SLUG=$(echo "$WORKFLOW_NAME" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
1034
+ RUN_NUMBER=$(echo "$RUN_INFO" | jq -r '.run_number')
1035
+ CONCLUSION=$(echo "$RUN_INFO" | jq -r '.conclusion // "in_progress"')
1036
+ RUN_DIR="${WORKFLOW_SLUG}-${RUN_ID}"
1037
+
1038
+ # Count logs for this run
1039
+ RUN_LOG_COUNT=$(find "$SESSION_DIR/${RUN_DIR}" -name "*.txt" -type f 2>/dev/null | wc -l | tr -d ' ')
1040
+
1041
+ # Format conclusion with emoji
1042
+ case "$CONCLUSION" in
1043
+ success) CONCLUSION_DISPLAY="✅ success" ;;
1044
+ failure) CONCLUSION_DISPLAY="❌ failure" ;;
1045
+ cancelled) CONCLUSION_DISPLAY="🚫 cancelled" ;;
1046
+ skipped) CONCLUSION_DISPLAY="⏭️ skipped" ;;
1047
+ *) CONCLUSION_DISPLAY="$CONCLUSION" ;;
1048
+ esac
1049
+
1050
+ echo "| #$RUN_NUMBER | $WORKFLOW_NAME | $CONCLUSION_DISPLAY | $RUN_LOG_COUNT | [\`$RUN_DIR\`](./$RUN_DIR) |"
1051
+ done
1052
+
1053
+ echo ""
1054
+ echo "## Quick Links"
1055
+ echo ""
1056
+
1057
+ # Add links to each workflow directory
1058
+ for RUN_ID in "${RUN_IDS[@]}"; do
1059
+ RUN_INFO=$(gh api "/repos/$REPO/actions/runs/$RUN_ID" 2>/dev/null)
1060
+
1061
+ if [ -z "$RUN_INFO" ]; then
1062
+ continue
1063
+ fi
1064
+
1065
+ WORKFLOW_NAME=$(echo "$RUN_INFO" | jq -r '.name')
1066
+ WORKFLOW_SLUG=$(echo "$WORKFLOW_NAME" | tr ' ' '-' | tr '[:upper:]' '[:lower:]')
1067
+ RUN_NUMBER=$(echo "$RUN_INFO" | jq -r '.run_number')
1068
+ RUN_URL=$(echo "$RUN_INFO" | jq -r '.html_url')
1069
+
1070
+ echo "- **Run #$RUN_NUMBER** ($WORKFLOW_NAME): [GitHub]($RUN_URL) • [Logs](./${WORKFLOW_SLUG}-${RUN_ID})"
1071
+ done
1072
+ } > "$README_FILE"
1073
+
1074
+ # Display success message
1075
+ if [ $DOWNLOADED_COUNT -eq 1 ]; then
1076
+ RUN_ID="${RUN_IDS[0]}"
1077
+ RUN_INFO=$(gh api "/repos/$REPO/actions/runs/$RUN_ID" 2>/dev/null)
1078
+ RUN_NUMBER=$(echo "$RUN_INFO" | jq -r '.run_number')
1079
+ CONCLUSION=$(echo "$RUN_INFO" | jq -r '.conclusion // "in_progress"')
1080
+
1081
+ echo -e "${GREEN}✓${NC} Downloaded $TOTAL_LOGS log files for run #$RUN_NUMBER ($CONCLUSION) → $SESSION_DIR"
1082
+ else
1083
+ echo -e "${GREEN}✓${NC} Downloaded $DOWNLOADED_COUNT runs with $TOTAL_LOGS total log files → $SESSION_DIR"
1084
+ fi
1085
+
1086
+ # Open README in VSCode
1087
+ if command -v code &> /dev/null; then
1088
+ code "$README_FILE" 2>/dev/null || true
1089
+ elif command -v code-insiders &> /dev/null; then
1090
+ code-insiders "$README_FILE" 2>/dev/null || true
1091
+ fi
1092
+
1093
+ actions:pin:
1094
+ desc: Pin GitHub Actions to specific commit SHAs with version comments
1095
+ silent: true
1096
+ aliases: [pin]
1097
+ vars:
1098
+ UPGRADE_FLAG: "{{if .UPGRADE}}--upgrade{{end}}"
1099
+ CHECK_FLAG: "{{if .CHECK}}--check{{end}}"
1100
+ DRY_RUN_FLAG: "{{if .DRY_RUN}}--dry-run{{end}}"
1101
+ TARGET_PATH: '{{.TARGET_PATH | default ".github/workflows"}}'
1102
+ cmds:
1103
+ - |
1104
+ # Get and validate GitHub token
1105
+ export GITHUB_TOKEN=$(task git:token)
1106
+
1107
+ # Run pin-actions with authenticated API access
1108
+ ai-toolkit pin-actions {{.UPGRADE_FLAG}} {{.CHECK_FLAG}} {{.DRY_RUN_FLAG}} --target-path={{.TARGET_PATH}}
1109
+
1110
+ leaks:
1111
+ desc: Scan for secrets with gitleaks
1112
+ silent: true
1113
+ aliases: [leaks]
1114
+ cmds:
1115
+ - |
1116
+ GREEN='\033[0;32m'
1117
+ YELLOW='\033[0;33m'
1118
+ RED='\033[0;31m'
1119
+ NC='\033[0m'
1120
+
1121
+ echo -e "${YELLOW}🔍${NC} Running gitleaks secret scanner..."
1122
+
1123
+ # Check if gitleaks is installed, install if not
1124
+ if ! command -v gitleaks >/dev/null 2>&1; then
1125
+ echo -e "${YELLOW}⚠️${NC} gitleaks not installed, installing..."
1126
+ brew install gitleaks
1127
+ fi
1128
+
1129
+ # Temporarily set git remote to HTTPS format for gitleaks platform detection
1130
+ REMOTE_URL=$(git remote get-url origin 2>/dev/null)
1131
+ RESTORED=false
1132
+
1133
+ if [ -n "$REMOTE_URL" ] && echo "$REMOTE_URL" | grep -q '^git@'; then
1134
+ # SSH format: convert to HTTPS temporarily
1135
+ HOST=$(echo "$REMOTE_URL" | sed -E 's/^git@([^:]+):.*/\1/')
1136
+ REPO_PATH=$(echo "$REMOTE_URL" | sed -E 's/^git@[^:]+:(.*)/\1/')
1137
+ REPO_PATH="${REPO_PATH%.git}"
1138
+ # Convert git@git alias to github.com
1139
+ if [ "$HOST" = "git" ]; then
1140
+ HOST="github.com"
1141
+ fi
1142
+ HTTPS_URL="https://${HOST}/${REPO_PATH}"
1143
+
1144
+ # Temporarily update remote for platform detection
1145
+ git remote set-url origin "$HTTPS_URL" 2>/dev/null && RESTORED=true
1146
+ fi
1147
+
1148
+ # Run gitleaks (will auto-detect platform from git remote)
1149
+ if gitleaks detect --verbose --redact; then
1150
+ echo -e "${GREEN}✓${NC} No secrets found by gitleaks"
1151
+ EXIT_CODE=0
1152
+ else
1153
+ echo -e "${RED}❌${NC} gitleaks found secrets!"
1154
+ EXIT_CODE=1
1155
+ fi
1156
+
1157
+ # Restore original remote URL
1158
+ if [ "$RESTORED" = "true" ]; then
1159
+ git remote set-url origin "$REMOTE_URL" 2>/dev/null
1160
+ fi
1161
+
1162
+ exit $EXIT_CODE
1163
+
1164
+ cve:
1165
+ desc: Download known CVEs for repository from Dependabot API
1166
+ aliases: [cve]
1167
+ silent: true
1168
+ deps: [cli]
1169
+ dotenv: [".env"]
1170
+ vars:
1171
+ MIN_SEVERITY: '{{.MIN_SEVERITY | default "medium"}}'
1172
+ REPO_ROOT:
1173
+ sh: git rev-parse --show-toplevel 2>/dev/null || pwd
1174
+ LOG_DIR:
1175
+ sh: echo "$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.logs/github/cve"
1176
+ cmds:
1177
+ - |
1178
+ # Color codes
1179
+ GREEN='\033[0;32m'
1180
+ RED='\033[0;31m'
1181
+ YELLOW='\033[0;33m'
1182
+ NC='\033[0m'
1183
+
1184
+ # Get and validate GitHub token
1185
+ export GH_TOKEN=$(task git:token)
1186
+
1187
+ # Validate severity level
1188
+ MIN_SEVERITY="{{.MIN_SEVERITY}}"
1189
+ case "$MIN_SEVERITY" in
1190
+ critical|high|medium|low) ;;
1191
+ *)
1192
+ echo -e "${RED}❌ Invalid MIN_SEVERITY: $MIN_SEVERITY${NC}" >&2
1193
+ echo "Valid values: critical, high, medium, low" >&2
1194
+ exit 1
1195
+ ;;
1196
+ esac
1197
+
1198
+ # Get repository info
1199
+ REPO=$(task git:repo:url | sed -E 's|^https://github.com/||')
1200
+
1201
+ # Clean up old CVE logs
1202
+ if [ -d "{{.LOG_DIR}}" ]; then
1203
+ find "{{.LOG_DIR}}" -type f -name 'cve-*.*' -exec rm -f {} \; 2>/dev/null
1204
+ fi
1205
+
1206
+ # Create log directory
1207
+ mkdir -p "{{.LOG_DIR}}"
1208
+ TIMESTAMP=$(date +%Y%m%d-%H%M%S)
1209
+ RAW_FILE="{{.LOG_DIR}}/cve-${TIMESTAMP}-raw.json"
1210
+ LOG_FILE="{{.LOG_DIR}}/cve-${TIMESTAMP}.json"
1211
+ SUMMARY_FILE="{{.LOG_DIR}}/cve-${TIMESTAMP}-summary.txt"
1212
+
1213
+ # Fetch vulnerabilities from Dependabot API (silent)
1214
+ VULNS_RAW=$(gh api "/repos/$REPO/dependabot/alerts" --paginate 2>&1)
1215
+
1216
+ if [ $? -ne 0 ]; then
1217
+ echo -e "${RED}❌ Failed to fetch CVEs from GitHub${NC}" >&2
1218
+ echo "$VULNS_RAW" >&2
1219
+ exit 1
1220
+ fi
1221
+
1222
+ # Save raw response
1223
+ echo "$VULNS_RAW" > "$RAW_FILE"
1224
+
1225
+ # Clean and process JSON
1226
+ VULNS_CLEAN=$(echo "$VULNS_RAW" | tr -d '\000-\010\013-\037')
1227
+
1228
+ # Filter and deduplicate vulnerabilities
1229
+ VULNS=$(echo "$VULNS_CLEAN" | jq --arg min_sev "$MIN_SEVERITY" '
1230
+ [.[] | select(.state=="open") | {
1231
+ cve: (.security_advisory.cve_id // null),
1232
+ pkg: (.security_vulnerability.package.name // "unknown"),
1233
+ ecosystem: (.security_vulnerability.package.ecosystem // "unknown"),
1234
+ sev: (.security_vulnerability.severity // "unknown"),
1235
+ current_version: (.security_vulnerability.vulnerable_version_range // null),
1236
+ fixed_version: (.security_vulnerability.first_patched_version.identifier // null),
1237
+ manifest_path: (.dependency.manifest_path // null),
1238
+ summary: (.security_advisory.summary // null),
1239
+ ghsa_id: (.security_advisory.ghsa_id // null),
1240
+ cvss_score: (.security_advisory.cvss.score // null),
1241
+ created_at: (.created_at // null)
1242
+ } | select(
1243
+ if $min_sev == "low" then true
1244
+ elif $min_sev == "medium" then (.sev == "medium" or .sev == "high" or .sev == "critical")
1245
+ elif $min_sev == "high" then (.sev == "high" or .sev == "critical")
1246
+ elif $min_sev == "critical" then .sev == "critical"
1247
+ else true end
1248
+ )] |
1249
+ group_by((.cve // .ghsa_id // "NO-ID") + ":" + .pkg) |
1250
+ map({
1251
+ cve: .[0].cve,
1252
+ pkg: .[0].pkg,
1253
+ ecosystem: .[0].ecosystem,
1254
+ sev: .[0].sev,
1255
+ current_version: .[0].current_version,
1256
+ fixed_version: .[0].fixed_version,
1257
+ summary: .[0].summary,
1258
+ ghsa_id: .[0].ghsa_id,
1259
+ cvss_score: .[0].cvss_score,
1260
+ manifest_paths: [.[] | .manifest_path] | unique | sort,
1261
+ count: length
1262
+ }) | sort_by(
1263
+ if .sev == "critical" then 0
1264
+ elif .sev == "high" then 1
1265
+ elif .sev == "medium" then 2
1266
+ elif .sev == "low" then 3
1267
+ else 4 end
1268
+ )' 2>/dev/null || echo "[]")
1269
+
1270
+ # Save filtered results
1271
+ echo "$VULNS" > "$LOG_FILE"
1272
+
1273
+ # Count vulnerabilities
1274
+ VULN_COUNT=$(echo "$VULNS" | jq 'length' 2>/dev/null || echo "0")
1275
+ CRITICAL_COUNT=$(echo "$VULNS" | jq '[.[] | select(.sev == "critical")] | length' 2>/dev/null || echo "0")
1276
+ HIGH_COUNT=$(echo "$VULNS" | jq '[.[] | select(.sev == "high")] | length' 2>/dev/null || echo "0")
1277
+ MEDIUM_COUNT=$(echo "$VULNS" | jq '[.[] | select(.sev == "medium")] | length' 2>/dev/null || echo "0")
1278
+ LOW_COUNT=$(echo "$VULNS" | jq '[.[] | select(.sev == "low")] | length' 2>/dev/null || echo "0")
1279
+
1280
+ # Create summary report
1281
+ {
1282
+ echo "CVE Security Report"
1283
+ echo "==================="
1284
+ echo "Repository: $REPO"
1285
+ echo "Date: $(date)"
1286
+ echo "Minimum severity: $MIN_SEVERITY and above"
1287
+ echo "Total vulnerabilities: $VULN_COUNT"
1288
+ echo ""
1289
+
1290
+ if [ "$VULN_COUNT" -gt "0" ]; then
1291
+ echo "Severity breakdown:"
1292
+ [ "$CRITICAL_COUNT" -gt 0 ] && echo " Critical: $CRITICAL_COUNT"
1293
+ [ "$HIGH_COUNT" -gt 0 ] && echo " High: $HIGH_COUNT"
1294
+ [ "$MEDIUM_COUNT" -gt 0 ] && echo " Medium: $MEDIUM_COUNT"
1295
+ [ "$LOW_COUNT" -gt 0 ] && echo " Low: $LOW_COUNT"
1296
+ echo ""
1297
+ echo "Vulnerabilities:"
1298
+ echo "───────────────────────────────────────────────────────────────"
1299
+ echo ""
1300
+
1301
+ echo "$VULNS" | jq -r '.[] |
1302
+ "CVE: \(.cve // .ghsa_id // "NO-ID")\n" +
1303
+ "Severity: \(.sev | ascii_upcase) (CVSS: \(.cvss_score // "N/A"))\n" +
1304
+ "Package: \(.ecosystem)/\(.pkg)\n" +
1305
+ "Locations (\(.count)):\n" +
1306
+ (.manifest_paths | map(" - " + .) | join("\n")) + "\n" +
1307
+ "Current: \(.current_version // "Unknown")\n" +
1308
+ "Fixed in: \(.fixed_version // "No fix available")\n" +
1309
+ "Summary: \(.summary // "No description available" | .[0:200])\n" +
1310
+ "───────────────────────────────────────────────────────────────\n"'
1311
+ else
1312
+ echo "No vulnerabilities found."
1313
+ fi
1314
+ } > "$SUMMARY_FILE"
1315
+
1316
+ # Display results
1317
+ if [ "$VULN_COUNT" -gt "0" ]; then
1318
+ echo -e "${RED}❌ Found $VULN_COUNT CVEs ($MIN_SEVERITY and above)${NC}"
1319
+ [ "$CRITICAL_COUNT" -gt 0 ] && echo " Critical: $CRITICAL_COUNT"
1320
+ [ "$HIGH_COUNT" -gt 0 ] && echo " High: $HIGH_COUNT"
1321
+ [ "$MEDIUM_COUNT" -gt 0 ] && echo " Medium: $MEDIUM_COUNT"
1322
+ [ "$LOW_COUNT" -gt 0 ] && echo " Low: $LOW_COUNT"
1323
+ echo ""
1324
+ echo -e "${GREEN}✓${NC} Summary: $SUMMARY_FILE"
1325
+ echo -e "${GREEN}✓${NC} Filtered data: $LOG_FILE"
1326
+ echo -e "${GREEN}✓${NC} Raw API response: $RAW_FILE"
1327
+ else
1328
+ echo -e "${GREEN}✓${NC} No CVEs found ($MIN_SEVERITY severity and above) → $SUMMARY_FILE"
1329
+ fi
1330
+
1331
+ # Open summary in VSCode
1332
+ if command -v code &> /dev/null; then
1333
+ code "$SUMMARY_FILE" 2>/dev/null || true
1334
+ elif command -v code-insiders &> /dev/null; then
1335
+ code-insiders "$SUMMARY_FILE" 2>/dev/null || true
1336
+ fi