feat(skills): import 17 skills from local-dev; fix stale host
release / tag (push) Failing after 1s

Consolidating the divergence between this repo and local-dev's embedded
~/dev/.skills/ (the two had drifted; the 19 overlapping skills were byte-identical).

- Import the 17 skills that existed only in local-dev: discovery-framing,
  stage-gate-review, web-shot, and the 14 superpowers-* skills. This repo is now
  the superset / single source of truth.
- SKILLS_INDEX.md: add rows for the 17.
- install.sh + Taskfile.yml: git.d-ma.be host (was stale gitea.d-ma.be, which
  broke the one-line curl|bash installer post-rename).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 00:10:21 +02:00
co-authored by Claude Opus 4.8
parent 5ffa397029
commit 1e29c6d1af
53 changed files with 9041 additions and 3 deletions
+67
View File
@@ -0,0 +1,67 @@
---
name: web-shot
description: Screenshot any running web UI by spawning a one-shot Playwright Job on the koala k3s cluster. Auto-detects the calling host's Tailscale name so a dev server on flamingo:8099, iguana:3000 or koala:8181 just works. Use when the user asks for a screenshot, visual verification, "what does the UI look like", or any time chromium / firefox / a browser is requested but none is installed on the host.
---
# web-shot
Screenshot a locally-running web UI without installing a browser on the agent's host.
## Mechanism
1. `shot.sh` builds a k8s Job that runs `mcr.microsoft.com/playwright:v1.49.0-jammy` on koala's k3s cluster.
2. `hostNetwork: true` is **not** used — the pod uses cluster DNS so Tailscale Magic DNS resolves `flamingo`, `iguana`, `koala` directly when the koala node has Tailscale up (it does).
3. The pod renders the URL with chromium, drops PNGs into an `emptyDir`, then `sleep`s briefly so `kubectl cp` can pull the files out.
4. PNGs land in `./screenshots/` (or `--out DIR`) on the agent's host.
## Prerequisites
- `kubectl` on the calling host with a kubeconfig pointing at koala's k3s.
- The dev server **binds 0.0.0.0** (not 127.0.0.1) so pods on koala can reach it across Tailscale. For Go: `http.ListenAndServe(":8099", ...)`. For Node: `--host 0.0.0.0`. For Vite: `--host`.
- The dev server's host (flamingo, iguana, koala, …) is reachable on Tailscale and Magic DNS resolves the short hostname.
## Usage
```bash
# auto-detect Tailscale hostname, screenshot port 8099 root
~/dev/.skills/web-shot/shot.sh --port 8099
# explicit URL
~/dev/.skills/web-shot/shot.sh --url http://flamingo:5173
# capture a search state too (types into input[name=q])
~/dev/.skills/web-shot/shot.sh --port 8181 --search agentsquad
# different output dir
~/dev/.skills/web-shot/shot.sh --port 3000 --out /tmp/shots
```
## Flags
| Flag | Default | Notes |
|-------------|-------------------------------|-----------------------------------------------------------|
| `--port N` | — | Host port. Combined with `--host` and `--path`. |
| `--path P` | `/` | URL path after the host:port. |
| `--host H` | auto via `tailscale status` | Override the hostname pods will dial. |
| `--url U` | — | Full URL. Overrides `--host`, `--port`, `--path`. |
| `--search Q`| — | Also capture a search state — types `Q` into `input[name=q]`. |
| `--out D` | `./screenshots` | Output dir (created if missing). |
| `--name N` | `web-shot` | Job name prefix. |
| `-h` | — | Show usage. |
## Output
- `<out>/home.png` — initial page.
- `<out>/search.png` — only when `--search` was passed.
## Failure modes
- `kubectl: command not found` → install kubectl + tailscale-mode kubeconfig.
- `connection refused` from the pod → dev server bound to 127.0.0.1, fix the bind.
- `No matches for selector` when `--search` was given → page lacks `input[name=q]`. Use a project-specific script or rerun without `--search`.
- Image pull slow on first run → ~1GB playwright image cached on koala after first use.
## When NOT to use
- The site needs auth / cookies / login state — extend the script or use a project-local Playwright suite.
- You need PDF, video, or interaction more elaborate than "type a query and screenshot". Promote to a real Playwright test under the project repo.
+181
View File
@@ -0,0 +1,181 @@
#!/usr/bin/env bash
# web-shot/shot.sh — screenshot a running web UI via a k3s playwright Job.
#
# Reusable across any Tailscale-connected host whose kubeconfig points at the
# koala k3s cluster. See SKILL.md alongside this file for prereqs.
set -euo pipefail
# --- defaults --------------------------------------------------------------
PORT=""
PATH_=""
HOST=""
URL=""
SEARCH=""
OUT_DIR=""
JOB_PREFIX="web-shot"
PLAYWRIGHT_TAG="v1.49.0-jammy"
PLAYWRIGHT_NPM="1.49.0"
NAMESPACE="default"
usage() {
sed -n '1,/^# ---/p' "$0" | sed 's/^# \{0,1\}//;1,/^!/d' >&2
cat >&2 <<EOF
usage: shot.sh [--port N] [--path P] [--host H] [--url U]
[--search Q] [--out DIR] [--name PREFIX]
See SKILL.md for prerequisites and examples.
EOF
exit 2
}
# --- parse args ------------------------------------------------------------
while [ $# -gt 0 ]; do
case "$1" in
--port) PORT="$2"; shift 2 ;;
--path) PATH_="$2"; shift 2 ;;
--host) HOST="$2"; shift 2 ;;
--url) URL="$2"; shift 2 ;;
--search) SEARCH="$2"; shift 2 ;;
--out) OUT_DIR="$2"; shift 2 ;;
--name) JOB_PREFIX="$2"; shift 2 ;;
-h|--help) usage ;;
*) echo "unknown flag: $1" >&2; usage ;;
esac
done
# --- resolve URL -----------------------------------------------------------
if [ -z "$URL" ]; then
if [ -z "$PORT" ]; then
echo "✗ --port or --url is required" >&2
usage
fi
if [ -z "$HOST" ]; then
if command -v tailscale >/dev/null 2>&1; then
HOST="$(tailscale status --self --json 2>/dev/null | jq -r '.Self.HostName' 2>/dev/null || true)"
fi
if [ -z "$HOST" ] && [ -r /etc/hostname ]; then
HOST="$(tr -d '\n' </etc/hostname)"
fi
if [ -z "$HOST" ]; then
echo "✗ could not auto-detect Tailscale hostname — pass --host" >&2
exit 1
fi
fi
PATH_="${PATH_:-/}"
case "$PATH_" in /*) ;; *) PATH_="/$PATH_" ;; esac
URL="http://${HOST}:${PORT}${PATH_}"
fi
OUT_DIR="${OUT_DIR:-$(pwd)/screenshots}"
mkdir -p "$OUT_DIR"
JOB_NAME="${JOB_PREFIX}-$(date +%s)"
echo "→ host=${HOST:-} url=${URL} out=${OUT_DIR} job=${JOB_NAME}"
# --- preflight: dev server must be reachable -------------------------------
# (Optional but cheap. Run from the caller, not the pod.)
if ! curl -fsS --max-time 3 "${URL%/}/healthz" >/dev/null 2>&1 \
&& ! curl -fsS --max-time 3 "$URL" >/dev/null 2>&1; then
echo "${URL} not reachable from this host. Pod will retry; if it fails, the dev server may be bound to 127.0.0.1 only." >&2
fi
# --- emit + apply Job ------------------------------------------------------
TAKE_SEARCH=""
[ -n "$SEARCH" ] && TAKE_SEARCH="yes"
cleanup() {
kubectl -n "$NAMESPACE" delete job "$JOB_NAME" --ignore-not-found=true >/dev/null 2>&1 || true
}
trap cleanup EXIT
kubectl -n "$NAMESPACE" apply -f - >/dev/null <<YAML
apiVersion: batch/v1
kind: Job
metadata:
name: ${JOB_NAME}
spec:
ttlSecondsAfterFinished: 120
backoffLimit: 0
template:
spec:
restartPolicy: Never
containers:
- name: shot
image: mcr.microsoft.com/playwright:${PLAYWRIGHT_TAG}
env:
- { name: URL, value: "${URL}" }
- { name: SEARCH, value: "${SEARCH}" }
- { name: DO_SEARCH, value: "${TAKE_SEARCH}" }
command: ["bash", "-lc"]
args:
- |
set -e
cd /tmp
npm init -y >/dev/null 2>&1
npm install playwright@${PLAYWRIGHT_NPM} --no-fund --no-audit --silent 2>&1 | tail -1
mkdir -p /shot
cat > /tmp/s.mjs <<'JS'
import { chromium } from 'playwright';
const url = process.env.URL;
const out = process.env.OUT;
const action = process.env.ACTION || '';
const b = await chromium.launch();
const ctx = await b.newContext({ viewport: { width: 1280, height: 900 }, deviceScaleFactor: 2 });
const p = await ctx.newPage();
await p.goto(url, { waitUntil: 'networkidle' });
if (action.startsWith('search:')) {
const q = action.slice('search:'.length);
await p.fill('input[name="q"]', q);
await p.waitForResponse(r => r.url().includes('/search'), { timeout: 5000 }).catch(() => {});
await p.waitForTimeout(500);
}
await p.screenshot({ path: out, fullPage: true });
await b.close();
console.log('wrote', out);
JS
URL="\$URL" OUT=/shot/home.png ACTION= node /tmp/s.mjs
if [ "\$DO_SEARCH" = "yes" ]; then
URL="\$URL" OUT=/shot/search.png ACTION="search:\$SEARCH" node /tmp/s.mjs
fi
chmod 644 /shot/*.png
echo "READY"
# Window for kubectl cp; deleted by trap after we've grabbed files.
sleep 90
YAML
# --- wait for pod + READY marker ------------------------------------------
echo "→ waiting for pod scheduling…"
for _ in $(seq 1 60); do
POD="$(kubectl -n "$NAMESPACE" get pods -l "batch.kubernetes.io/job-name=$JOB_NAME" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)"
[ -n "$POD" ] && break
sleep 1
done
if [ -z "$POD" ]; then
echo "✗ pod not scheduled" >&2; exit 1
fi
echo "→ pod $POD"
echo "→ waiting for READY (pulling image + rendering)…"
for _ in $(seq 1 60); do
if kubectl -n "$NAMESPACE" logs "$POD" 2>/dev/null | grep -q '^READY$'; then
break
fi
PHASE="$(kubectl -n "$NAMESPACE" get pod "$POD" -o jsonpath='{.status.phase}' 2>/dev/null || true)"
if [ "$PHASE" = "Failed" ]; then
echo "✗ pod Failed — logs:" >&2
kubectl -n "$NAMESPACE" logs "$POD" 2>&1 | tail -30 >&2
exit 1
fi
sleep 2
done
# --- copy files out --------------------------------------------------------
kubectl -n "$NAMESPACE" cp "${NAMESPACE}/${POD}:/shot/home.png" "$OUT_DIR/home.png" 2>/dev/null \
|| { echo "✗ kubectl cp home.png failed" >&2; exit 1; }
if [ "$TAKE_SEARCH" = "yes" ]; then
kubectl -n "$NAMESPACE" cp "${NAMESPACE}/${POD}:/shot/search.png" "$OUT_DIR/search.png" 2>/dev/null \
|| { echo "✗ kubectl cp search.png failed" >&2; exit 1; }
fi
ls -la "$OUT_DIR"/*.png
echo "✓ screenshots in $OUT_DIR"