From 9aec68142dcbf89ec3c2946de7164bf5da96ac99 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 13 Mar 2026 15:43:21 -0700 Subject: [PATCH] feat: SKILL.md template system with auto-generated command references - SKILL.md.tmpl + browse/SKILL.md.tmpl with {{COMMAND_REFERENCE}} and {{SNAPSHOT_FLAGS}} placeholders - scripts/gen-skill-docs.ts generates SKILL.md from templates (supports --dry-run) - Build pipeline runs gen:skill-docs before binary compilation - Generated files have AUTO-GENERATED header, committed to git --- SKILL.md | 102 +++++++++------- SKILL.md.tmpl | 245 ++++++++++++++++++++++++++++++++++++++ browse/SKILL.md | 123 ++++++++++++++++--- browse/SKILL.md.tmpl | 106 +++++++++++++++++ package.json | 14 ++- scripts/gen-skill-docs.ts | 165 +++++++++++++++++++++++++ 6 files changed, 690 insertions(+), 65 deletions(-) create mode 100644 SKILL.md.tmpl create mode 100644 browse/SKILL.md.tmpl create mode 100644 scripts/gen-skill-docs.ts diff --git a/SKILL.md b/SKILL.md index e561e2cc..34d8c6b4 100644 --- a/SKILL.md +++ b/SKILL.md @@ -12,6 +12,8 @@ allowed-tools: - Read --- + + # gstack browse: QA Testing & Dogfooding @@ -239,15 +241,15 @@ $B css ".button" "background-color" The snapshot is your primary tool for understanding and interacting with pages. -```bash -$B snapshot -i # Interactive elements only (buttons, links, inputs) with @e refs -$B snapshot -c # Compact (no empty structural elements) -$B snapshot -d 3 # Limit depth to 3 levels -$B snapshot -s "main" # Scope to CSS selector -$B snapshot -D # Diff against previous snapshot (what changed?) -$B snapshot -a # Annotated screenshot with ref labels -$B snapshot -o /tmp/x.png # Output path for annotated screenshot -$B snapshot -C # Cursor-interactive elements (@c refs — divs with pointer, onclick) +``` +-i Interactive elements only +-c Remove empty structural elements +-d Limit tree depth +-s Scope to CSS selector +-D Diff against previous snapshot +-a Annotated screenshot with ref labels +-o Output path for annotated screenshot +-C Scan cursor:pointer/onclick/tabindex elements ``` Combine flags: `$B snapshot -i -a -C -o /tmp/annotated.png` @@ -266,83 +268,95 @@ Refs are invalidated on navigation — run `snapshot` again after `goto`. ### Navigation | Command | Description | |---------|-------------| +| `back` | History back | +| `forward` | History forward | | `goto ` | Navigate to URL | -| `back` / `forward` | History navigation | | `reload` | Reload page | | `url` | Print current URL | ### Reading | Command | Description | |---------|-------------| -| `text` | Cleaned page text | -| `html [selector]` | innerHTML | -| `links` | All links as "text -> href" | -| `forms` | Forms + fields as JSON | | `accessibility` | Full ARIA tree | +| `forms` | Form fields as JSON | +| `html [selector]` | innerHTML | +| `links` | All links as "text → href" | +| `text` | Cleaned page text | ### Interaction | Command | Description | |---------|-------------| | `click ` | Click element | -| `fill ` | Fill input | -| `select ` | Select dropdown | -| `hover ` | Hover element | -| `type ` | Type into focused element | -| `press ` | Press key (Enter, Tab, Escape) | -| `scroll [sel]` | Scroll element into view | -| `wait ` | Wait for element (max 10s) | -| `wait --networkidle` | Wait for network to be idle | -| `wait --load` | Wait for page load event | -| `upload ` | Upload file(s) | +| `cookie` | Set cookie | | `cookie-import ` | Import cookies from JSON file | -| `cookie-import-browser [browser] [--domain ]` | Import cookies from real browser (opens picker UI, or direct import with --domain) | -| `dialog-accept [text]` | Auto-accept dialogs | -| `dialog-dismiss` | Auto-dismiss dialogs | +| `cookie-import-browser [browser] [--domain d]` | Import cookies from real browser | +| `dialog-accept [text]` | Auto-accept next dialog | +| `dialog-dismiss` | Auto-dismiss next dialog | +| `fill ` | Fill input | +| `header ` | Set custom request header | +| `hover ` | Hover element | +| `press ` | Press key | +| `scroll [sel]` | Scroll element into view | +| `select ` | Select dropdown option | +| `type ` | Type into focused element | +| `upload ` | Upload file(s) | +| `useragent ` | Set user agent | | `viewport ` | Set viewport size | +| `wait ` | Wait for element/condition | ### Inspection | Command | Description | |---------|-------------| -| `js ` | Run JavaScript | -| `eval ` | Run JS file | -| `css ` | Computed CSS | -| `attrs ` | Element attributes | -| `is ` | State check (visible/hidden/enabled/disabled/checked/editable/focused) | -| `console [--clear\|--errors]` | Console messages (--errors filters to error/warning) | -| `network [--clear]` | Network requests | +| `attrs ` | Element attributes as JSON | +| `console [--clear|--errors]` | Console messages | +| `cookies` | All cookies as JSON | +| `css ` | Computed CSS value | | `dialog [--clear]` | Dialog messages | -| `cookies` | All cookies | -| `storage` | localStorage + sessionStorage | +| `eval ` | Run JS file | +| `is ` | State check | +| `js ` | Run JavaScript | +| `network [--clear]` | Network requests | | `perf` | Page load timings | +| `storage [set k v]` | localStorage + sessionStorage | ### Visual | Command | Description | |---------|-------------| -| `screenshot [path]` | Screenshot | +| `diff ` | Text diff between pages | | `pdf [path]` | Save as PDF | | `responsive [prefix]` | Mobile/tablet/desktop screenshots | -| `diff ` | Text diff between pages | +| `screenshot [path]` | Save screenshot | + +### Snapshot +| Command | Description | +|---------|-------------| +| `snapshot [flags]` | Accessibility tree with @refs | + +### Meta +| Command | Description | +|---------|-------------| +| `chain` | Multi-command from JSON stdin | ### Tabs | Command | Description | |---------|-------------| -| `tabs` | List tabs | -| `tab ` | Switch tab | -| `newtab [url]` | Open tab | | `closetab [id]` | Close tab | +| `newtab [url]` | Open new tab | +| `tab ` | Switch to tab | +| `tabs` | List open tabs | ### Server | Command | Description | |---------|-------------| +| `restart` | Restart server | | `status` | Health check | -| `stop` | Shutdown | -| `restart` | Restart | +| `stop` | Shutdown server | ## Tips 1. **Navigate once, query many times.** `goto` loads the page; then `text`, `js`, `screenshot` all hit the loaded page instantly. 2. **Use `snapshot -i` first.** See all interactive elements, then click/fill by ref. No CSS selector guessing. -3. **Use `snapshot -D` to verify.** Baseline → action → diff. See exactly what changed. +3. **Use `snapshot -D` to verify.** Baseline -> action -> diff. See exactly what changed. 4. **Use `is` for assertions.** `is visible .modal` is faster and more reliable than parsing page text. 5. **Use `snapshot -a` for evidence.** Annotated screenshots are great for bug reports. 6. **Use `snapshot -C` for tricky UIs.** Finds clickable divs that the accessibility tree misses. diff --git a/SKILL.md.tmpl b/SKILL.md.tmpl new file mode 100644 index 00000000..5d6055c8 --- /dev/null +++ b/SKILL.md.tmpl @@ -0,0 +1,245 @@ +--- +name: gstack +version: 1.1.0 +description: | + Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with + elements, verify page state, diff before/after actions, take annotated screenshots, check + responsive layouts, test forms and uploads, handle dialogs, and assert element states. + ~100ms per command. Use when you need to test a feature, verify a deployment, dogfood a + user flow, or file a bug with evidence. +allowed-tools: + - Bash + - Read + +--- + +# gstack browse: QA Testing & Dogfooding + +Persistent headless Chromium. First call auto-starts (~3s), then ~100-200ms per command. +Auto-shuts down after 30 min idle. State persists between calls (cookies, tabs, sessions). + +## SETUP (run this check BEFORE any browse command) + +```bash +B=$(browse/bin/find-browse 2>/dev/null || ~/.claude/skills/gstack/browse/bin/find-browse 2>/dev/null) +if [ -n "$B" ]; then + echo "READY: $B" +else + echo "NEEDS_SETUP" +fi +``` + +If `NEEDS_SETUP`: +1. Tell the user: "gstack browse needs a one-time build (~10 seconds). OK to proceed?" Then STOP and wait. +2. Run: `cd && ./setup` +3. If `bun` is not installed: `curl -fsSL https://bun.sh/install | bash` + +## IMPORTANT + +- Use the compiled binary via Bash: `$B ` +- NEVER use `mcp__claude-in-chrome__*` tools. They are slow and unreliable. +- Browser persists between calls — cookies, login sessions, and tabs carry over. +- Dialogs (alert/confirm/prompt) are auto-accepted by default — no browser lockup. + +## QA Workflows + +### Test a user flow (login, signup, checkout, etc.) + +```bash +B=~/.claude/skills/gstack/browse/dist/browse + +# 1. Go to the page +$B goto https://app.example.com/login + +# 2. See what's interactive +$B snapshot -i + +# 3. Fill the form using refs +$B fill @e3 "test@example.com" +$B fill @e4 "password123" +$B click @e5 + +# 4. Verify it worked +$B snapshot -D # diff shows what changed after clicking +$B is visible ".dashboard" # assert the dashboard appeared +$B screenshot /tmp/after-login.png +``` + +### Verify a deployment / check prod + +```bash +$B goto https://yourapp.com +$B text # read the page — does it load? +$B console # any JS errors? +$B network # any failed requests? +$B js "document.title" # correct title? +$B is visible ".hero-section" # key elements present? +$B screenshot /tmp/prod-check.png +``` + +### Dogfood a feature end-to-end + +```bash +# Navigate to the feature +$B goto https://app.example.com/new-feature + +# Take annotated screenshot — shows every interactive element with labels +$B snapshot -i -a -o /tmp/feature-annotated.png + +# Find ALL clickable things (including divs with cursor:pointer) +$B snapshot -C + +# Walk through the flow +$B snapshot -i # baseline +$B click @e3 # interact +$B snapshot -D # what changed? (unified diff) + +# Check element states +$B is visible ".success-toast" +$B is enabled "#next-step-btn" +$B is checked "#agree-checkbox" + +# Check console for errors after interactions +$B console +``` + +### Test responsive layouts + +```bash +# Quick: 3 screenshots at mobile/tablet/desktop +$B goto https://yourapp.com +$B responsive /tmp/layout + +# Manual: specific viewport +$B viewport 375x812 # iPhone +$B screenshot /tmp/mobile.png +$B viewport 1440x900 # Desktop +$B screenshot /tmp/desktop.png +``` + +### Test file upload + +```bash +$B goto https://app.example.com/upload +$B snapshot -i +$B upload @e3 /path/to/test-file.pdf +$B is visible ".upload-success" +$B screenshot /tmp/upload-result.png +``` + +### Test forms with validation + +```bash +$B goto https://app.example.com/form +$B snapshot -i + +# Submit empty — check validation errors appear +$B click @e10 # submit button +$B snapshot -D # diff shows error messages appeared +$B is visible ".error-message" + +# Fill and resubmit +$B fill @e3 "valid input" +$B click @e10 +$B snapshot -D # diff shows errors gone, success state +``` + +### Test dialogs (delete confirmations, prompts) + +```bash +# Set up dialog handling BEFORE triggering +$B dialog-accept # will auto-accept next alert/confirm +$B click "#delete-button" # triggers confirmation dialog +$B dialog # see what dialog appeared +$B snapshot -D # verify the item was deleted + +# For prompts that need input +$B dialog-accept "my answer" # accept with text +$B click "#rename-button" # triggers prompt +``` + +### Test authenticated pages (import real browser cookies) + +```bash +# Import cookies from your real browser (opens interactive picker) +$B cookie-import-browser + +# Or import a specific domain directly +$B cookie-import-browser comet --domain .github.com + +# Now test authenticated pages +$B goto https://github.com/settings/profile +$B snapshot -i +$B screenshot /tmp/github-profile.png +``` + +### Compare two pages / environments + +```bash +$B diff https://staging.app.com https://prod.app.com +``` + +### Multi-step chain (efficient for long flows) + +```bash +echo '[ + ["goto","https://app.example.com"], + ["snapshot","-i"], + ["fill","@e3","test@test.com"], + ["fill","@e4","password"], + ["click","@e5"], + ["snapshot","-D"], + ["screenshot","/tmp/result.png"] +]' | $B chain +``` + +## Quick Assertion Patterns + +```bash +# Element exists and is visible +$B is visible ".modal" + +# Button is enabled/disabled +$B is enabled "#submit-btn" +$B is disabled "#submit-btn" + +# Checkbox state +$B is checked "#agree" + +# Input is editable +$B is editable "#name-field" + +# Element has focus +$B is focused "#search-input" + +# Page contains text +$B js "document.body.textContent.includes('Success')" + +# Element count +$B js "document.querySelectorAll('.list-item').length" + +# Specific attribute value +$B attrs "#logo" # returns all attributes as JSON + +# CSS property +$B css ".button" "background-color" +``` + +## Snapshot System + +{{SNAPSHOT_FLAGS}} + +## Command Reference + +{{COMMAND_REFERENCE}} + +## Tips + +1. **Navigate once, query many times.** `goto` loads the page; then `text`, `js`, `screenshot` all hit the loaded page instantly. +2. **Use `snapshot -i` first.** See all interactive elements, then click/fill by ref. No CSS selector guessing. +3. **Use `snapshot -D` to verify.** Baseline -> action -> diff. See exactly what changed. +4. **Use `is` for assertions.** `is visible .modal` is faster and more reliable than parsing page text. +5. **Use `snapshot -a` for evidence.** Annotated screenshots are great for bug reports. +6. **Use `snapshot -C` for tricky UIs.** Finds clickable divs that the accessibility tree misses. +7. **Check `console` after actions.** Catch JS errors that don't surface visually. +8. **Use `chain` for long flows.** Single command, no per-step CLI overhead. diff --git a/browse/SKILL.md b/browse/SKILL.md index 99c979c5..03088102 100644 --- a/browse/SKILL.md +++ b/browse/SKILL.md @@ -12,6 +12,8 @@ allowed-tools: - Read --- + + # browse: QA Testing & Dogfooding @@ -99,30 +101,115 @@ $B diff https://staging.app.com https://prod.app.com ## Snapshot Flags +The snapshot is your primary tool for understanding and interacting with pages. + ``` --i Interactive elements only (buttons, links, inputs) --c Compact (no empty structural nodes) --d Limit depth --s Scope to CSS selector +-i Interactive elements only +-c Remove empty structural elements +-d Limit tree depth +-s Scope to CSS selector -D Diff against previous snapshot -a Annotated screenshot with ref labels --o Output path for screenshot --C Cursor-interactive elements (@c refs) +-o Output path for annotated screenshot +-C Scan cursor:pointer/onclick/tabindex elements ``` -Combine: `$B snapshot -i -a -C -o /tmp/annotated.png` +Combine flags: `$B snapshot -i -a -C -o /tmp/annotated.png` -Use @refs after snapshot: `$B click @e3`, `$B fill @e4 "value"`, `$B click @c1` +After snapshot, use @refs everywhere: +```bash +$B click @e3 $B fill @e4 "value" $B hover @e1 +$B html @e2 $B css @e5 "color" $B attrs @e6 +$B click @c1 # cursor-interactive ref (from -C) +``` + +Refs are invalidated on navigation — run `snapshot` again after `goto`. ## Full Command List -**Navigate:** goto, back, forward, reload, url -**Read:** text, html, links, forms, accessibility -**Snapshot:** snapshot (with flags above) -**Interact:** click, fill, select, hover, type, press, scroll, wait, wait --networkidle, wait --load, viewport, upload, cookie-import, dialog-accept, dialog-dismiss -**Inspect:** js, eval, css, attrs, is, console, console --errors, network, dialog, cookies, storage, perf -**Visual:** screenshot, pdf, responsive -**Compare:** diff -**Multi-step:** chain (pipe JSON array) -**Tabs:** tabs, tab, newtab, closetab -**Server:** status, stop, restart +### Navigation +| Command | Description | +|---------|-------------| +| `back` | History back | +| `forward` | History forward | +| `goto ` | Navigate to URL | +| `reload` | Reload page | +| `url` | Print current URL | + +### Reading +| Command | Description | +|---------|-------------| +| `accessibility` | Full ARIA tree | +| `forms` | Form fields as JSON | +| `html [selector]` | innerHTML | +| `links` | All links as "text → href" | +| `text` | Cleaned page text | + +### Interaction +| Command | Description | +|---------|-------------| +| `click ` | Click element | +| `cookie` | Set cookie | +| `cookie-import ` | Import cookies from JSON file | +| `cookie-import-browser [browser] [--domain d]` | Import cookies from real browser | +| `dialog-accept [text]` | Auto-accept next dialog | +| `dialog-dismiss` | Auto-dismiss next dialog | +| `fill ` | Fill input | +| `header ` | Set custom request header | +| `hover ` | Hover element | +| `press ` | Press key | +| `scroll [sel]` | Scroll element into view | +| `select ` | Select dropdown option | +| `type ` | Type into focused element | +| `upload ` | Upload file(s) | +| `useragent ` | Set user agent | +| `viewport ` | Set viewport size | +| `wait ` | Wait for element/condition | + +### Inspection +| Command | Description | +|---------|-------------| +| `attrs ` | Element attributes as JSON | +| `console [--clear|--errors]` | Console messages | +| `cookies` | All cookies as JSON | +| `css ` | Computed CSS value | +| `dialog [--clear]` | Dialog messages | +| `eval ` | Run JS file | +| `is ` | State check | +| `js ` | Run JavaScript | +| `network [--clear]` | Network requests | +| `perf` | Page load timings | +| `storage [set k v]` | localStorage + sessionStorage | + +### Visual +| Command | Description | +|---------|-------------| +| `diff ` | Text diff between pages | +| `pdf [path]` | Save as PDF | +| `responsive [prefix]` | Mobile/tablet/desktop screenshots | +| `screenshot [path]` | Save screenshot | + +### Snapshot +| Command | Description | +|---------|-------------| +| `snapshot [flags]` | Accessibility tree with @refs | + +### Meta +| Command | Description | +|---------|-------------| +| `chain` | Multi-command from JSON stdin | + +### Tabs +| Command | Description | +|---------|-------------| +| `closetab [id]` | Close tab | +| `newtab [url]` | Open new tab | +| `tab ` | Switch to tab | +| `tabs` | List open tabs | + +### Server +| Command | Description | +|---------|-------------| +| `restart` | Restart server | +| `status` | Health check | +| `stop` | Shutdown server | diff --git a/browse/SKILL.md.tmpl b/browse/SKILL.md.tmpl new file mode 100644 index 00000000..f0fd0284 --- /dev/null +++ b/browse/SKILL.md.tmpl @@ -0,0 +1,106 @@ +--- +name: browse +version: 1.1.0 +description: | + Fast headless browser for QA testing and site dogfooding. Navigate any URL, interact with + elements, verify page state, diff before/after actions, take annotated screenshots, check + responsive layouts, test forms and uploads, handle dialogs, and assert element states. + ~100ms per command. Use when you need to test a feature, verify a deployment, dogfood a + user flow, or file a bug with evidence. +allowed-tools: + - Bash + - Read + +--- + +# browse: QA Testing & Dogfooding + +Persistent headless Chromium. First call auto-starts (~3s), then ~100ms per command. +State persists between calls (cookies, tabs, login sessions). + +## Core QA Patterns + +### 1. Verify a page loads correctly +```bash +$B goto https://yourapp.com +$B text # content loads? +$B console # JS errors? +$B network # failed requests? +$B is visible ".main-content" # key elements present? +``` + +### 2. Test a user flow +```bash +$B goto https://app.com/login +$B snapshot -i # see all interactive elements +$B fill @e3 "user@test.com" +$B fill @e4 "password" +$B click @e5 # submit +$B snapshot -D # diff: what changed after submit? +$B is visible ".dashboard" # success state present? +``` + +### 3. Verify an action worked +```bash +$B snapshot # baseline +$B click @e3 # do something +$B snapshot -D # unified diff shows exactly what changed +``` + +### 4. Visual evidence for bug reports +```bash +$B snapshot -i -a -o /tmp/annotated.png # labeled screenshot +$B screenshot /tmp/bug.png # plain screenshot +$B console # error log +``` + +### 5. Find all clickable elements (including non-ARIA) +```bash +$B snapshot -C # finds divs with cursor:pointer, onclick, tabindex +$B click @c1 # interact with them +``` + +### 6. Assert element states +```bash +$B is visible ".modal" +$B is enabled "#submit-btn" +$B is disabled "#submit-btn" +$B is checked "#agree-checkbox" +$B is editable "#name-field" +$B is focused "#search-input" +$B js "document.body.textContent.includes('Success')" +``` + +### 7. Test responsive layouts +```bash +$B responsive /tmp/layout # mobile + tablet + desktop screenshots +$B viewport 375x812 # or set specific viewport +$B screenshot /tmp/mobile.png +``` + +### 8. Test file uploads +```bash +$B upload "#file-input" /path/to/file.pdf +$B is visible ".upload-success" +``` + +### 9. Test dialogs +```bash +$B dialog-accept "yes" # set up handler +$B click "#delete-button" # trigger dialog +$B dialog # see what appeared +$B snapshot -D # verify deletion happened +``` + +### 10. Compare environments +```bash +$B diff https://staging.app.com https://prod.app.com +``` + +## Snapshot Flags + +{{SNAPSHOT_FLAGS}} + +## Full Command List + +{{COMMAND_REFERENCE}} diff --git a/package.json b/package.json index bece501c..27fe0a8c 100644 --- a/package.json +++ b/package.json @@ -8,10 +8,15 @@ "browse": "./browse/dist/browse" }, "scripts": { - "build": "bun build --compile browse/src/cli.ts --outfile browse/dist/browse && bun build --compile browse/src/find-browse.ts --outfile browse/dist/find-browse && git rev-parse HEAD > browse/dist/.version && rm -f .*.bun-build", + "build": "bun run gen:skill-docs && bun build --compile browse/src/cli.ts --outfile browse/dist/browse && bun build --compile browse/src/find-browse.ts --outfile browse/dist/find-browse && git rev-parse HEAD > browse/dist/.version && rm -f .*.bun-build", + "gen:skill-docs": "bun run scripts/gen-skill-docs.ts", "dev": "bun run browse/src/cli.ts", "server": "bun run browse/src/server.ts", - "test": "bun test", + "test": "bun test browse/test/ test/ --ignore test/skill-e2e.test.ts", + "test:e2e": "SKILL_E2E=1 bun test test/skill-e2e.test.ts", + "test:all": "bun test browse/test/ test/ --ignore test/skill-e2e.test.ts && SKILL_E2E=1 bun test test/skill-e2e.test.ts", + "skill:check": "bun run scripts/skill-check.ts", + "dev:skill": "bun run scripts/dev-skill.ts", "start": "bun run browse/src/server.ts" }, "dependencies": { @@ -30,5 +35,8 @@ "claude", "ai-agent", "devtools" - ] + ], + "devDependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.2.75" + } } diff --git a/scripts/gen-skill-docs.ts b/scripts/gen-skill-docs.ts new file mode 100644 index 00000000..63af19bb --- /dev/null +++ b/scripts/gen-skill-docs.ts @@ -0,0 +1,165 @@ +#!/usr/bin/env bun +/** + * Generate SKILL.md files from .tmpl templates. + * + * Pipeline: + * read .tmpl → find {{PLACEHOLDERS}} → resolve from source → format → write .md + * + * Supports --dry-run: generate to memory, exit 1 if different from committed file. + * Used by skill:check and CI freshness checks. + */ + +import { COMMAND_DESCRIPTIONS } from '../browse/src/commands'; +import { SNAPSHOT_FLAGS } from '../browse/src/snapshot'; +import * as fs from 'fs'; +import * as path from 'path'; + +const ROOT = path.resolve(import.meta.dir, '..'); +const DRY_RUN = process.argv.includes('--dry-run'); + +// ─── Placeholder Resolvers ────────────────────────────────── + +function generateCommandReference(): string { + // Group commands by category + const groups = new Map>(); + for (const [cmd, meta] of Object.entries(COMMAND_DESCRIPTIONS)) { + const list = groups.get(meta.category) || []; + list.push({ command: cmd, description: meta.description, usage: meta.usage }); + groups.set(meta.category, list); + } + + // Category display order + const categoryOrder = [ + 'Navigation', 'Reading', 'Interaction', 'Inspection', + 'Visual', 'Snapshot', 'Meta', 'Tabs', 'Server', + ]; + + const sections: string[] = []; + for (const category of categoryOrder) { + const commands = groups.get(category); + if (!commands || commands.length === 0) continue; + + // Sort alphabetically within category + commands.sort((a, b) => a.command.localeCompare(b.command)); + + sections.push(`### ${category}`); + sections.push('| Command | Description |'); + sections.push('|---------|-------------|'); + for (const cmd of commands) { + const display = cmd.usage ? `\`${cmd.usage}\`` : `\`${cmd.command}\``; + sections.push(`| ${display} | ${cmd.description} |`); + } + sections.push(''); + } + + return sections.join('\n').trimEnd(); +} + +function generateSnapshotFlags(): string { + const lines: string[] = [ + 'The snapshot is your primary tool for understanding and interacting with pages.', + '', + '```', + ]; + + for (const flag of SNAPSHOT_FLAGS) { + const flagStr = flag.takesValue + ? `${flag.short.padEnd(10)}${flag.description}` + : `${flag.short.padEnd(10)}${flag.description}`; + lines.push(flagStr); + } + + lines.push('```'); + lines.push(''); + lines.push('Combine flags: `$B snapshot -i -a -C -o /tmp/annotated.png`'); + lines.push(''); + lines.push('After snapshot, use @refs everywhere:'); + lines.push('```bash'); + lines.push('$B click @e3 $B fill @e4 "value" $B hover @e1'); + lines.push('$B html @e2 $B css @e5 "color" $B attrs @e6'); + lines.push('$B click @c1 # cursor-interactive ref (from -C)'); + lines.push('```'); + lines.push(''); + lines.push('Refs are invalidated on navigation — run `snapshot` again after `goto`.'); + + return lines.join('\n'); +} + +const RESOLVERS: Record string> = { + COMMAND_REFERENCE: generateCommandReference, + SNAPSHOT_FLAGS: generateSnapshotFlags, +}; + +// ─── Template Processing ──────────────────────────────────── + +const GENERATED_HEADER = `\n\n`; + +function processTemplate(tmplPath: string): { outputPath: string; content: string } { + const tmplContent = fs.readFileSync(tmplPath, 'utf-8'); + const relTmplPath = path.relative(ROOT, tmplPath); + const outputPath = tmplPath.replace(/\.tmpl$/, ''); + + // Replace placeholders + let content = tmplContent.replace(/\{\{(\w+)\}\}/g, (match, name) => { + const resolver = RESOLVERS[name]; + if (!resolver) throw new Error(`Unknown placeholder {{${name}}} in ${relTmplPath}`); + return resolver(); + }); + + // Check for any remaining unresolved placeholders + const remaining = content.match(/\{\{(\w+)\}\}/g); + if (remaining) { + throw new Error(`Unresolved placeholders in ${relTmplPath}: ${remaining.join(', ')}`); + } + + // Prepend generated header (after frontmatter) + const header = GENERATED_HEADER.replace('{{SOURCE}}', path.basename(tmplPath)); + const fmEnd = content.indexOf('---', content.indexOf('---') + 3); + if (fmEnd !== -1) { + const insertAt = content.indexOf('\n', fmEnd) + 1; + content = content.slice(0, insertAt) + header + content.slice(insertAt); + } else { + content = header + content; + } + + return { outputPath, content }; +} + +// ─── Main ─────────────────────────────────────────────────── + +function findTemplates(): string[] { + const templates: string[] = []; + const candidates = [ + path.join(ROOT, 'SKILL.md.tmpl'), + path.join(ROOT, 'browse', 'SKILL.md.tmpl'), + ]; + for (const p of candidates) { + if (fs.existsSync(p)) templates.push(p); + } + return templates; +} + +let hasChanges = false; + +for (const tmplPath of findTemplates()) { + const { outputPath, content } = processTemplate(tmplPath); + const relOutput = path.relative(ROOT, outputPath); + + if (DRY_RUN) { + const existing = fs.existsSync(outputPath) ? fs.readFileSync(outputPath, 'utf-8') : ''; + if (existing !== content) { + console.log(`STALE: ${relOutput}`); + hasChanges = true; + } else { + console.log(`FRESH: ${relOutput}`); + } + } else { + fs.writeFileSync(outputPath, content); + console.log(`GENERATED: ${relOutput}`); + } +} + +if (DRY_RUN && hasChanges) { + console.error('\nGenerated SKILL.md files are stale. Run: bun run gen:skill-docs'); + process.exit(1); +}