mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-04 21:04:37 +08:00
Add pr-visual-writeup skill to .agents/skills (#1354)
Shared agent skill under the cross-client `.agents/skills/` convention so Claude Code, Codex, and other compliant agents can discover it. <!-- Make sure you've read the CONTRIBUTING.md guidelines: https://github.com/stack-auth/stack-auth/blob/dev/CONTRIBUTING.md --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added an end-to-end workflow skill for automating visual-rich PR descriptions with screenshot capture, asset processing, and upload capabilities. * **Documentation** * Added reference guides covering screenshot capture patterns, asset upload workflows, and structured PR body templates. * **Chores** * Added helper scripts for development server detection, media conversion, and GitHub gist uploads. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
6bc1836e66
commit
dad752a1cb
137
.agents/skills/pr-visual-writeup/SKILL.md
Normal file
137
.agents/skills/pr-visual-writeup/SKILL.md
Normal file
@ -0,0 +1,137 @@
|
||||
---
|
||||
name: pr-visual-writeup
|
||||
description: Generate a rich GitHub PR description with dashboard/web-UI screenshots and scrolling animations captured from a running dev server, hosted as a GitHub gist, and pushed to the PR via `gh pr edit`. Use this skill whenever the user asks to "make a PR description with screenshots", "write up a PR with visuals", "add screenshots to my PR", "PR description with GIFs / demo / scroll animations", or anything involving turning a code PR into a visual-heavy writeup. Also triggers on phrases like "ship a PR writeup", "PR body with light and dark mode screenshots", "visual PR review", "generate PR body from dev server". The core value is the parallel capture pipeline — multiple browser sessions running concurrently to produce theme/viewport matrix screenshots in roughly the wall-clock time of a single pass.
|
||||
---
|
||||
|
||||
# pr-visual-writeup
|
||||
|
||||
Turn a PR into a visual writeup: inspect the diff, capture screenshots + scroll animations from a local dev server across themes and viewports **in parallel**, host everything in a GitHub gist (PAT-only, no browser cookies), compose a rich markdown body, and set it as the PR description.
|
||||
|
||||
## When this triggers
|
||||
|
||||
- "make me a pr description with screenshots / gifs / videos"
|
||||
- "pr writeup with visuals"
|
||||
- "generate pr body from the running dev server"
|
||||
- "screenshot all the pages this PR changes and put them in the description"
|
||||
|
||||
If the user only wants a text-only PR description, don't use this skill — it's for visual-heavy writeups.
|
||||
|
||||
## The shape of the work
|
||||
|
||||
Five phases. Phases 2 and 3 are the parallel-heavy ones — lean on subagents there.
|
||||
|
||||
1. **Scope** — figure out which PR, which routes, which dev server, which auth
|
||||
2. **Capture (parallel)** — matrix of {page × theme × viewport}, plus scroll animations
|
||||
3. **Process (parallel)** — convert scroll videos → GIFs (inline-playable), prep gist
|
||||
4. **Upload** — one gist, one commit, all files; get raw URLs
|
||||
5. **Compose + set** — markdown body with tables, then `gh pr edit --body-file`
|
||||
|
||||
## Phase 1 — Scope
|
||||
|
||||
Before you capture anything, know:
|
||||
|
||||
- **PR number + repo** — `gh pr view <N> --json baseRefName,headRefName,title,url`
|
||||
- **Changed UI routes** — `gh pr diff <N> --name-only` and filter for page/route files. For Next.js look for `**/page*.tsx` / `**/*page-client.tsx`. Map route files to URL paths based on the app router convention. Ignore changes purely in backend / shared components unless they have an obvious UI surface.
|
||||
- **Dev server port** — `lsof -iTCP -sTCP:LISTEN -P -n | grep node` and `curl -s http://localhost:<port>/ | grep -oE '<title>[^<]+</title>'` to identify which port is the dashboard vs. API vs. docs vs. mock-OAuth.
|
||||
- **Auth flow** — if the app requires login, inspect the sign-in page for the OAuth provider to use, and ask the user (or infer from context) which dev account to sign in as. Mock OAuth servers typically accept any email.
|
||||
|
||||
Record these facts somewhere (a scratchpad file under `/tmp/<skill-workspace>/scope.md` is fine) — the parallel subagents in Phase 2 need them.
|
||||
|
||||
## Phase 2 — Parallel capture
|
||||
|
||||
This is the skill's core trick. You have N pages × M themes × K viewports of screenshots to take. If you do this in one browser, you navigate sequentially — 9 × 2 × 2 = 36 navigations at ~5s each = 3 minutes. If you fan out, you do it in ~45s.
|
||||
|
||||
### Fan-out plan
|
||||
|
||||
Spawn one subagent per **(theme, viewport)** combination. Each subagent owns a named `agent-browser` session, authenticates once, captures every page in its assigned theme/viewport, and returns the output directory. Typical combinations:
|
||||
|
||||
- `light-standard` (1920×1200, theme=light)
|
||||
- `dark-standard` (1920×1200, theme=dark)
|
||||
- `light-wide` (2560×1440, theme=light, a subset of pages)
|
||||
- `dark-wide` (2560×1440, theme=dark, a subset of pages)
|
||||
|
||||
Widescreen captures are usually only worth taking for the "flagship" pages (the 3-5 most important ones). Full matrix on every page is overkill.
|
||||
|
||||
**Important:** issue all Agent tool calls for capture subagents **in a single assistant message** so they run concurrently. If you spawn them one at a time across turns, you've lost the parallelism.
|
||||
|
||||
The exact subagent prompt pattern lives in `references/capture-patterns.md` — read it before spawning.
|
||||
|
||||
### Scroll animations
|
||||
|
||||
Tables, long lists, and sticky-header surfaces benefit from a short down-and-back-up scroll clip. Don't do this for every page — pick the 2-3 most representative. Record via **frame-by-frame screenshot then ffmpeg stitch**, not `agent-browser record`, because `record` creates a fresh browser context that loses dev-mode auth state. The recipe is in `references/capture-patterns.md`.
|
||||
|
||||
### When fan-out is NOT worth it
|
||||
|
||||
- Only 1-2 pages total → just run sequentially in the main conversation
|
||||
- The dev server can't handle parallel logins (rare, but some mock-OAuth servers serialize)
|
||||
- The user explicitly asks for a quick single-theme capture
|
||||
|
||||
## Phase 3 — Process (parallel)
|
||||
|
||||
After capture, you have a pile of PNGs and 2-4 WebM scroll clips. The WebMs need to become GIFs because GitHub only inline-plays `.webm` when it's hosted on `user-attachments/...` (a browser-session-only upload path we're avoiding). Gist-hosted `.webm` becomes a plain download link; gist-hosted `.gif` plays inline.
|
||||
|
||||
Run all ffmpeg conversions in parallel using shell `&`:
|
||||
|
||||
```bash
|
||||
for f in *.webm; do
|
||||
(ffmpeg -y -i "$f" -vf "fps=8,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" "${f%.webm}.gif" >/dev/null 2>&1) &
|
||||
done
|
||||
wait
|
||||
```
|
||||
|
||||
`fps=8,scale=960` keeps file sizes reasonable (100-400KB) while still looking smooth.
|
||||
|
||||
## Phase 4 — Upload via gist (no browser cookies)
|
||||
|
||||
Gist-hosting via `git push` with a PAT is the PAT-only equivalent of `user-attachments`. GitHub's `user-attachments` endpoint requires a browser session cookie (not a PAT) — **don't** use tools like `gh-image` unless the user has explicitly opted in. Gist URLs look like `https://gist.githubusercontent.com/<user>/<gist-id>/raw/<filename>` and render inline as images/GIFs in PR bodies.
|
||||
|
||||
Full recipe is in `references/gist-upload.md`. Summary: create a public gist via `gh gist create`, clone it, copy all PNGs + GIFs in, commit, `git push` with a credential-helper trick that feeds the PAT. One push, all files.
|
||||
|
||||
## Phase 5 — Compose the body, then `gh pr edit`
|
||||
|
||||
Markdown structure template is in `references/pr-body-template.md`. The load-bearing patterns:
|
||||
|
||||
- **Summary** paragraph + `Base: → Head:` + scope line (files, +lines)
|
||||
- **Screenshots** section with one subsection per "flagship" page, each using a 2-col light/dark table, then a widescreen table
|
||||
- **Other migrated surfaces** compact table for the long tail
|
||||
- **Scroll behaviour** section with a light/dark GIF table
|
||||
- Everything after the visual section is the usual PR body: What's new, Notes for reviewers, Test plan
|
||||
|
||||
Set it with:
|
||||
```bash
|
||||
gh pr edit <N> --body-file <path-to-md>
|
||||
```
|
||||
|
||||
Confirm with the user before pushing if the PR is on a public repo — this is a shared-state action. On a personal fork or draft PR, go ahead.
|
||||
|
||||
## A note on trust boundaries
|
||||
|
||||
Three distinct credentials touch this workflow. Keep them straight:
|
||||
|
||||
- **PAT** (`gh auth token`) — for gist push, `gh pr edit`, `gh pr diff`. Fine to use freely.
|
||||
- **Dev-server session cookie** — for logging into the local dashboard. Local to the machine, fine.
|
||||
- **github.com browser session cookie** — what `gh-image` and similar tools extract. **Don't** use this unless the user opts in. It has broader scope than a PAT.
|
||||
|
||||
The workflow above deliberately stays in PAT territory.
|
||||
|
||||
## Bundled scripts
|
||||
|
||||
Use these — don't reinvent them inline. They live at `scripts/` relative to this SKILL.md.
|
||||
|
||||
- **`detect_dev_server.sh [min-port] [max-port]`** — lists running node dev servers with their HTML `<title>` so you can pick the right port at a glance.
|
||||
- **`convert_clips.sh <dir>`** — converts every `.webm` in a directory to `.gif` in parallel (fps=8, 960px wide, ~400KB per clip).
|
||||
- **`upload_gist.sh <desc> <dir> [<dir> ...]`** — creates a public gist, pushes every file from the input dirs into it in one commit, prints one line per file as `<basename>\t<raw-url>`. Stashes the gist id in `./gist-id.txt`.
|
||||
|
||||
## What you bundle in the workspace
|
||||
|
||||
Create a `/tmp/pr-<N>-visuals/` workspace to hold everything. After the PR body is set, the PNGs/GIFs live permanently in the gist; the local copies are safe to delete but useful to keep around if the user wants to iterate.
|
||||
|
||||
```
|
||||
/tmp/pr-<N>-visuals/
|
||||
├── scope.md # phase 1 output
|
||||
├── shots/ # captured PNGs
|
||||
├── clips/ # webm + gif scroll animations
|
||||
├── body.md # composed PR description
|
||||
├── gist-id.txt # for re-pushing if you add more shots later
|
||||
└── urls.txt # raw URL per file, for copy-paste
|
||||
```
|
||||
@ -0,0 +1,73 @@
|
||||
# Capture patterns
|
||||
|
||||
Reconstructed from SKILL.md hints — edit freely to match your own `agent-browser` or Playwright setup.
|
||||
|
||||
## Subagent prompt pattern (per theme/viewport)
|
||||
|
||||
Each subagent owns a named browser session and handles the full capture pass for one `(theme, viewport)` combination. Name the session so it's distinct from the main conversation's session and from sibling subagents — e.g. `pr<N>-light-standard`.
|
||||
|
||||
Template:
|
||||
|
||||
```
|
||||
You are capturing screenshots for PR #<N> in the <theme> theme at <WxH> viewport.
|
||||
|
||||
Scope (from /tmp/pr-<N>-visuals/scope.md):
|
||||
- Dev server: http://localhost:<port>
|
||||
- Login: <email> via mock-OAuth at <sign-in path>
|
||||
- Pages (relative URLs):
|
||||
- /projects/<projectId>/<route-1>
|
||||
- /projects/<projectId>/<route-2>
|
||||
- ...
|
||||
|
||||
Do this:
|
||||
1. Start (or reuse) an agent-browser session named "pr<N>-<theme>-<viewport>".
|
||||
2. Set viewport to <WxH>.
|
||||
3. Navigate to the sign-in page and complete the mock-OAuth flow once.
|
||||
4. Switch the app theme to "<theme>" (usually a dropdown in user settings, or
|
||||
the `prefers-color-scheme` override in devtools — inspect the app first).
|
||||
5. For each page in the list: navigate, wait for network-idle + any late-mount
|
||||
skeletons to settle (~1s extra), then take a full-page screenshot.
|
||||
Save as /tmp/pr-<N>-visuals/shots/<route-slug>__<theme>__<viewport>.png
|
||||
where route-slug replaces slashes with dashes.
|
||||
6. Return the list of PNG paths you produced.
|
||||
|
||||
Do NOT:
|
||||
- Close the browser session at the end (other subagents may be using it, and
|
||||
re-login is wasteful).
|
||||
- Use agent-browser's `record` action — it spins up a fresh context that
|
||||
drops dev-mode auth state. For scroll clips see the frame-stitch recipe.
|
||||
```
|
||||
|
||||
Spawn all subagents (one per theme/viewport combination) in a **single assistant message** with multiple `Agent` tool calls. If you send them across turns, you serialize the work and lose the parallelism that justifies the skill.
|
||||
|
||||
## Scroll animation recipe (frame-stitch)
|
||||
|
||||
`agent-browser record` opens a new browser context, which doesn't inherit your dev-session cookies — the recording lands on the login page. Work around this by taking a burst of screenshots at the current session instead:
|
||||
|
||||
```
|
||||
In session pr<N>-<theme>-<viewport>:
|
||||
1. Navigate to the target page, wait for settle.
|
||||
2. window.scrollTo(0, 0).
|
||||
3. Loop: take screenshot -> scroll by (viewport_height * 0.8) -> sleep 120ms.
|
||||
Stop when document.scrollingElement.scrollTop stops increasing.
|
||||
4. Loop back up symmetrically (optional, nicer).
|
||||
5. Save frames as /tmp/pr-<N>-visuals/clips/<slug>__<theme>/frame-####.png
|
||||
```
|
||||
|
||||
Stitch with ffmpeg:
|
||||
|
||||
```bash
|
||||
ffmpeg -y -framerate 8 -i /tmp/.../clips/<slug>__<theme>/frame-%04d.png \
|
||||
-c:v libvpx-vp9 -b:v 0 -crf 40 \
|
||||
/tmp/.../clips/<slug>__<theme>.webm
|
||||
```
|
||||
|
||||
Then `scripts/convert_clips.sh` turns the webm into a gist-friendly GIF.
|
||||
|
||||
## Picking the page matrix
|
||||
|
||||
- **Full matrix** (every page × every theme × standard viewport): always — this is the bread-and-butter comparison reviewers actually read.
|
||||
- **Wide viewport** (2560×1440): only the 3–5 flagship pages. The point of wide is to show layout behavior under extra horizontal space, which most pages handle trivially.
|
||||
- **Scroll animations**: only pages with meaningful vertical content — tables with many rows, sticky headers, settings with lots of sections. A static screenshot of a 3-field form is fine.
|
||||
|
||||
If the PR touches 15+ pages, pick ~5 flagships for the hero section and put the rest in a compact "other migrated surfaces" table.
|
||||
56
.agents/skills/pr-visual-writeup/references/gist-upload.md
Normal file
56
.agents/skills/pr-visual-writeup/references/gist-upload.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Gist upload (PAT-only)
|
||||
|
||||
The bundled `scripts/upload_gist.sh` handles this end-to-end. This doc explains the mechanism so you can diagnose failures or deviate when you need to.
|
||||
|
||||
## Why gists and not `user-attachments`
|
||||
|
||||
GitHub renders inline images and GIFs in PR bodies from any `https://` URL. Two common hosts:
|
||||
|
||||
| Host | Auth | GIF inline? | WebM inline? |
|
||||
| --- | --- | --- | --- |
|
||||
| `user-attachments.githubusercontent.com` | Browser session cookie | yes | **yes** |
|
||||
| `gist.githubusercontent.com/.../raw/...` | PAT (via git push) | yes | no (download link) |
|
||||
|
||||
`user-attachments` is nicer (WebM plays inline, smaller files) but requires the browser session cookie, which has broader scope than a PAT and shouldn't be exfiltrated by a CLI tool without explicit user consent. Stick with gists unless the user says otherwise.
|
||||
|
||||
## The push trick
|
||||
|
||||
`gh gist create` creates the gist and can seed it with one file via stdin. After that, use git to add more files:
|
||||
|
||||
```bash
|
||||
git clone https://gist.github.com/<gist-id>.git
|
||||
cp *.png *.gif <clone>/
|
||||
cd <clone>
|
||||
git add -A
|
||||
git commit -m "Add assets"
|
||||
git push
|
||||
```
|
||||
|
||||
The catch: `git push` to a gist over HTTPS needs credentials. The local git credential helper may be configured to answer with a browser session cookie or nothing useful. Override it inline with a one-shot helper that feeds your PAT:
|
||||
|
||||
```bash
|
||||
USER=$(gh api user --jq .login)
|
||||
TOKEN=$(gh auth token)
|
||||
|
||||
git -c credential.helper= \
|
||||
-c credential.helper="!f() { echo username=$USER; echo password=$TOKEN; }; f" \
|
||||
push
|
||||
```
|
||||
|
||||
The `credential.helper=` (empty) clears any inherited helpers; the second `-c` installs a single-use function-based helper that answers with the PAT. This does NOT persist — no config is written.
|
||||
|
||||
## Flat namespace
|
||||
|
||||
Gists don't support subdirectories. If your local layout is `shots/foo.png` and `clips/bar.gif`, everything gets flattened in the gist. Make sure filenames are unique across your input dirs before pushing (`upload_gist.sh` doesn't dedupe — it just cps, so a later cp wins).
|
||||
|
||||
## Raw URL shape
|
||||
|
||||
```
|
||||
https://gist.githubusercontent.com/<user>/<gist-id>/raw/<filename>
|
||||
```
|
||||
|
||||
Note: `raw/` without a revision SHA points to HEAD. If you push new commits to the gist later, old raw URLs serve the newest version of that filename, which is usually what you want for iterative PR body updates.
|
||||
|
||||
## Re-pushing to an existing gist
|
||||
|
||||
If you saved `gist-id.txt` from a prior run, you can re-push to the same gist instead of creating a new one. This keeps the URL stable across iterations (useful if the PR body has already been approved/reviewed). Swap the "create" step for a clone of the existing gist by ID.
|
||||
@ -0,0 +1,87 @@
|
||||
# PR body template
|
||||
|
||||
Reconstructed from SKILL.md hints. Treat this as a starting point and adapt to the PR's actual content — don't force-fit sections that don't apply.
|
||||
|
||||
## Full structure
|
||||
|
||||
```markdown
|
||||
## Summary
|
||||
|
||||
<One-paragraph summary of what the PR does and why. Reviewer-oriented, not
|
||||
changelog-oriented — answer "what am I about to review and why does it exist".>
|
||||
|
||||
**Base:** `<base-branch>` → **Head:** `<head-branch>`
|
||||
**Scope:** <N> files changed · +<added> / -<removed> lines
|
||||
|
||||
---
|
||||
|
||||
## Screenshots
|
||||
|
||||
### <Flagship page 1 name>
|
||||
|
||||
<One-line description of the page and what changed.>
|
||||
|
||||
| Light | Dark |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
<Widescreen if captured:>
|
||||
|
||||
| Wide (light) | Wide (dark) |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
### <Flagship page 2 name>
|
||||
|
||||
...
|
||||
|
||||
---
|
||||
|
||||
### Other migrated surfaces
|
||||
|
||||
| Page | Light | Dark |
|
||||
| --- | --- | --- |
|
||||
| Route A |  |  |
|
||||
| Route B |  |  |
|
||||
| ... | ... | ... |
|
||||
|
||||
---
|
||||
|
||||
## Scroll behaviour
|
||||
|
||||
| Light | Dark |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
---
|
||||
|
||||
## What's new
|
||||
|
||||
- bullet 1
|
||||
- bullet 2
|
||||
|
||||
## Notes for reviewers
|
||||
|
||||
- Anything tricky, non-obvious, or worth flagging.
|
||||
- Known follow-ups or things deliberately out of scope.
|
||||
|
||||
## Test plan
|
||||
|
||||
- [ ] Check X
|
||||
- [ ] Check Y
|
||||
- [ ] Visual sanity — the screenshots above are the canonical reference.
|
||||
```
|
||||
|
||||
## Patterns that pull weight
|
||||
|
||||
- **Two-column tables for light/dark**: reviewers can scan both themes at once without scrolling vertically. A single-column list of 10 alternating light/dark shots is much harder to parse.
|
||||
- **Flagship vs. long tail**: promote 3–5 pages with their own subsection + heading. Everything else goes in a compact table. This gives reviewers a clear "start here" signal.
|
||||
- **Raw URLs, not markdown image links with titles**: keep `` minimal. Long alt text makes the source unreadable for anyone editing later.
|
||||
- **Scope line up top**: the `files changed · +x/-y` line answers "how big is this" before the reviewer scrolls.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Don't embed WebM. Gist-hosted WebM renders as a download link, not a player. Convert to GIF first.
|
||||
- Don't link to the gist itself — link directly to each raw URL so the asset renders inline.
|
||||
- Don't include a mega-wall of every page × every theme. If the PR touches 15 pages, put 5 in the hero and 10 in the compact table.
|
||||
- Don't skip the text sections (What's new / Test plan) just because you have pretty pictures. Reviewers still want the prose.
|
||||
35
.agents/skills/pr-visual-writeup/scripts/convert_clips.sh
Executable file
35
.agents/skills/pr-visual-writeup/scripts/convert_clips.sh
Executable file
@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# Convert every .webm in a directory to .gif in parallel.
|
||||
# Usage: convert_clips.sh <dir>
|
||||
#
|
||||
# Output: same basename, .gif extension, next to source.
|
||||
# Config: fps=8, scale=960 — tuned for gist-hosted PR body embeds
|
||||
# (small enough for <400KB, smooth enough to read).
|
||||
|
||||
set -euo pipefail
|
||||
DIR="${1:?usage: $0 <dir>}"
|
||||
|
||||
if ! command -v ffmpeg >/dev/null 2>&1; then
|
||||
echo "ffmpeg not found. Install with: brew install ffmpeg" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
shopt -s nullglob
|
||||
clips=("$DIR"/*.webm)
|
||||
if [ ${#clips[@]} -eq 0 ]; then
|
||||
echo "no .webm files in $DIR" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "converting ${#clips[@]} clips in parallel..."
|
||||
for f in "${clips[@]}"; do
|
||||
out="${f%.webm}.gif"
|
||||
(
|
||||
ffmpeg -y -i "$f" \
|
||||
-vf "fps=8,scale=960:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
|
||||
"$out" >/dev/null 2>&1 \
|
||||
&& echo " ✓ $(basename "$out") ($(du -h "$out" | cut -f1))"
|
||||
) &
|
||||
done
|
||||
wait
|
||||
echo "done."
|
||||
38
.agents/skills/pr-visual-writeup/scripts/detect_dev_server.sh
Executable file
38
.agents/skills/pr-visual-writeup/scripts/detect_dev_server.sh
Executable file
@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Find running local dev servers and identify each by page title.
|
||||
# Usage: detect_dev_server.sh [min-port] [max-port]
|
||||
#
|
||||
# Output (one per line): <port>\t<title>\t<url>
|
||||
# Example:
|
||||
# 8101 Stack Auth Dashboard http://localhost:8101
|
||||
# 8102 Stack Auth API http://localhost:8102
|
||||
#
|
||||
# Use the output to pick the right port for screenshotting. A common convention
|
||||
# is that the "dashboard" / "app" is the one you want; the API/docs/OAuth servers
|
||||
# are separate processes on adjacent ports.
|
||||
|
||||
set -euo pipefail
|
||||
MIN="${1:-3000}"
|
||||
MAX="${2:-9999}"
|
||||
|
||||
# Collect every node-listened TCP port in the range.
|
||||
ports=$(lsof -iTCP -sTCP:LISTEN -P -n 2>/dev/null \
|
||||
| awk '/^node/ {print $9}' \
|
||||
| grep -oE ':[0-9]+$' \
|
||||
| tr -d ':' \
|
||||
| sort -u \
|
||||
| awk -v min="$MIN" -v max="$MAX" '$1 >= min && $1 <= max')
|
||||
|
||||
if [ -z "$ports" ]; then
|
||||
echo "no listening node servers in $MIN-$MAX" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for p in $ports; do
|
||||
title=$(curl -sS --max-time 2 "http://localhost:$p/" 2>/dev/null \
|
||||
| grep -oE '<title>[^<]+</title>' \
|
||||
| sed -E 's|</?title>||g' \
|
||||
| head -1)
|
||||
[ -z "$title" ] && title="(no title)"
|
||||
printf "%s\t%s\thttp://localhost:%s\n" "$p" "$title" "$p"
|
||||
done
|
||||
73
.agents/skills/pr-visual-writeup/scripts/upload_gist.sh
Executable file
73
.agents/skills/pr-visual-writeup/scripts/upload_gist.sh
Executable file
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
# Create a public gist, push every file under one or more dirs into it, print raw URLs.
|
||||
# Usage: upload_gist.sh <desc> <dir> [<dir> ...]
|
||||
#
|
||||
# Example:
|
||||
# upload_gist.sh "PR #1338 visuals" /tmp/pr-1338-visuals/shots /tmp/pr-1338-visuals/clips
|
||||
#
|
||||
# Prints a line per uploaded file:
|
||||
# <basename>\t<raw-url>
|
||||
# …and stashes the gist id in ./gist-id.txt for later re-pushes.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DESC="${1:?usage: $0 <desc> <dir> [<dir> ...]}"
|
||||
shift
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "need at least one source directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for d in "$@"; do
|
||||
if [ ! -d "$d" ]; then
|
||||
echo "not a directory: $d" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
USER=$(gh api user --jq .login)
|
||||
TOKEN=$(gh auth token)
|
||||
|
||||
# 1. Create the gist.
|
||||
GIST_URL=$(gh gist create --public --desc "$DESC" -f README.md - <<< "$DESC assets" | tail -1)
|
||||
GIST_ID=$(basename "$GIST_URL")
|
||||
echo "gist: $GIST_URL" >&2
|
||||
|
||||
# 2. Clone into a tmp working dir.
|
||||
WORK=$(mktemp -d)
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
git clone --quiet "https://gist.github.com/$GIST_ID.git" "$WORK/gist"
|
||||
|
||||
# 3. Stage every file from every input dir. Don't recurse — gists are flat.
|
||||
count=0
|
||||
for d in "$@"; do
|
||||
for f in "$d"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
cp "$f" "$WORK/gist/"
|
||||
count=$((count+1))
|
||||
done
|
||||
done
|
||||
|
||||
if [ $count -eq 0 ]; then
|
||||
echo "no files found in input directories" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "staged $count files" >&2
|
||||
|
||||
# 4. Commit + push with PAT.
|
||||
cd "$WORK/gist"
|
||||
git add -A
|
||||
git -c user.email="noreply@github.com" -c user.name="$USER" \
|
||||
commit --quiet -m "Add $count assets"
|
||||
|
||||
git -c credential.helper= \
|
||||
-c credential.helper="!f() { echo username=$USER; echo password=$TOKEN; }; f" \
|
||||
push --quiet
|
||||
|
||||
# 5. Echo URLs.
|
||||
echo "$GIST_ID" > "$OLDPWD/gist-id.txt"
|
||||
for f in *; do
|
||||
[ "$f" = "README.md" ] && continue
|
||||
echo -e "$f\thttps://gist.githubusercontent.com/$USER/$GIST_ID/raw/$f"
|
||||
done
|
||||
1
.claude/skills
Symbolic link
1
.claude/skills
Symbolic link
@ -0,0 +1 @@
|
||||
../.agents/skills
|
||||
Loading…
Reference in New Issue
Block a user