---
name: vitedocs:capture
description: Capture screenshots and record interaction videos for VitePress docs. Uploads videos to GitHub Releases.
allowed-tools:
  - Read
  - Write
  - Edit
  - Bash
  - Glob
  - Grep
  - AskUserQuestion
---

# vitedocs:capture

This mode reads `.vitepress/docs-manifest.json`. Run `/vitedocs:generate` first if no manifest exists yet.

Handles two capture types from the manifest:
- **screenshot** (`"type": "screenshot"`) — static PNG via Playwright
- **video** (`"type": "video"`) — recorded interaction with step captions, uploaded to GitHub Releases

---

## Step 1 — Gather capture details

**Q1 — Source:**
- header: "Capture source"
- question: "Where should I capture from?"
- options:
  - "Local dev server — I'll capture from a running server"
  - "Deployed URL — give me the base URL"

If deployed: ask as plain text — "What is the base URL?"

**Q2 — Stack type** (only if local):
- header: "Project type"
- question: "What type of project is this?"
- options:
  - "Node/npm — you can optionally start the server for me"
  - "WordPress, PHP, or other non-Node stack"
  - "Static files"

**Q3 — Server status** (only if local Node):
- header: "Dev server"
- question: "Is the dev server already running?"
- options:
  - "Already running"
  - "Not running — please start it for me"

If starting: ask as plain text — "What command starts it? And what URL/port does it run on?"

**Q4 — Authentication:**
- header: "Login required?"
- question: "Does reaching the target screens require login?"
- options:
  - "No — all target screens are public"
  - "Yes — I'll provide credentials"

If yes: ask as plain text — "Please provide the username and password. ⚠️ Credentials are used only to drive the browser in this session — they will NEVER be written to any file, the manifest, or anywhere outside this conversation."

**Q5 — Scope:**
- header: "Capture scope"
- question: "Which items should I capture?"
- options:
  - "All placeholders from the manifest (screenshots + videos)"
  - "Screenshots only"
  - "Videos only"
  - "Specific items — I'll tell you which"

If specific: ask as plain text — "Which pages or capture names?"

**Q6 — GitHub repo** (ask only if manifest contains any video entries in scope):
- header: "GitHub repo"
- question: "Videos are uploaded to GitHub Releases. What is the GitHub repo?"
- options:
  - "Same repo as the docs"
  - "Different repo — I'll type it"

If different: ask as plain text — "What is the repo? (full URL or `owner/repo`)"

Wait for all answers before proceeding.

---

## Step 2 — Find all pending captures

Read `.vitepress/docs-manifest.json`. Filter to:
- Screenshots where `"placeholder": true`
- Videos where `"videoUrl": ""`

Group by type. If no manifest exists, scan all markdown files for `VideoDemo` and image references in `public/screenshots/` or `public/videos/`.

Report what was found:
```
Found 4 pending captures:
  Screenshots (2): timer-main.png, settings-overview.png
  Videos (2):      timer-start.webm, form-submit.webm
```

---

## Step 3 — Capture screenshots

Run ALL screenshots as a single batched heredoc — one bash approval covers the entire set.

**⛔ Credential rule — NEVER write credentials to any file.** All scripts run as inline bash heredocs. Credentials are passed directly as inline values within the heredoc and exist only in memory.

**For pages requiring auth:** drive a login flow before navigating to target URLs. If no credentials were given, skip auth-gated items and list them in the summary.

Batch all screenshots into one heredoc:

```bash
node --input-type=module << 'SCRIPT'
import { chromium } from '/opt/homebrew/lib/node_modules/playwright/index.mjs';
const browser = await chromium.launch({ channel: 'chrome', args: ['--no-sandbox'] });

const captures = [
  { url: 'BASE_URL/PATH_1', path: 'OUTPUT_PATH_1', settle: SETTLE_MS },
  { url: 'BASE_URL/PATH_2', path: 'OUTPUT_PATH_2', settle: SETTLE_MS },
];

for (const cap of captures) {
  const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
  await page.goto(cap.url, { waitUntil: 'load', timeout: 60000 });
  await page.waitForTimeout(cap.settle);
  await page.evaluate(() => {
    const el = document.getElementById('devtools-indicator');
    if (el) el.style.setProperty('display', 'none', 'important');
  });
  await page.screenshot({ path: cap.path, fullPage: false });
  await page.close();
  console.log('Captured', cap.path);
}

await browser.close();
SCRIPT
```

Adjust `settle` based on stack: WordPress/non-SPA → `500`, Next.js/SPA → `2000`–`4000`.

The `devtools-indicator` hide call is safe to include for all stacks — it's a no-op when the element doesn't exist. It specifically targets the Next.js Turbopack dev tools badge (`id="devtools-indicator"`) which is injected at runtime during development and would otherwise appear in every capture.

After the batch completes, display each captured image inline with the Read tool for verification.

---

## Step 4 — Capture videos

Capture each video one at a time (each needs its own heredoc due to the unique step sequence).

For each video entry, run a heredoc that:
1. Records the screen with `recordVideo`
2. Executes the steps from the manifest, tracking elapsed time for each
3. Captures a poster PNG at the final step
4. Moves the video from the temp dir to the final output path
5. Generates the `.vtt` caption file (written to disk — goes in git)
6. Prints the step timings as JSON

```bash
node --input-type=module << 'SCRIPT'
import { chromium } from '/opt/homebrew/lib/node_modules/playwright/index.mjs';
import { writeFileSync, mkdirSync, readdirSync, renameSync } from 'fs';
import { dirname } from 'path';

const VIDEO_TMP  = 'DOCS_FOLDER/public/videos/.tmp';
const VIDEO_OUT  = 'DOCS_FOLDER/public/videos/CAPTURE_NAME.webm';
const VTT_OUT    = 'DOCS_FOLDER/public/videos/CAPTURE_NAME.vtt';
const POSTER_OUT = 'DOCS_FOLDER/public/screenshots/CAPTURE_NAME-poster.png';
const BASE_URL   = 'BASE_URL';
const CAPTURE_URL = 'CAPTURE_PATH';

mkdirSync(VIDEO_TMP, { recursive: true });
mkdirSync(dirname(VIDEO_OUT), { recursive: true });
mkdirSync(dirname(POSTER_OUT), { recursive: true });

const browser = await chromium.launch({ channel: 'chrome', args: ['--no-sandbox'] });
const context = await browser.newContext({
  viewport: { width: 1440, height: 900 },
  recordVideo: { dir: VIDEO_TMP, size: { width: 1440, height: 900 } }
});
const page = await context.newPage();

// Inject visible cursor overlay for video recordings
await page.addInitScript(() => {
  const dot = document.createElement('div')
  dot.style.cssText = [
    'position:fixed', 'width:24px', 'height:24px',
    'background:#7C3AED', 'border:12px solid rgba(196,181,253,0.55)',
    'border-radius:50%', 'pointer-events:none', 'z-index:2147483647',
    'transform:translate(-50%,-50%)', 'transition:left 0.05s,top 0.05s',
    'box-sizing:content-box'
  ].join(';')
  document.addEventListener('DOMContentLoaded', () => document.body.appendChild(dot))
  document.addEventListener('mousemove', e => {
    dot.style.left = e.clientX + 'px'
    dot.style.top  = e.clientY + 'px'
  })
});

const t0 = Date.now();
const elapsed = () => (Date.now() - t0) / 1000;
const stepTimings = [];

await page.goto(`${BASE_URL}${CAPTURE_URL}`, { waitUntil: 'load', timeout: 60000 });
await page.waitForTimeout(800);
await page.evaluate(() => {
  const el = document.getElementById('devtools-indicator');
  if (el) el.style.setProperty('display', 'none', 'important');
});

// STEPS — substituted from manifest steps array:
// stepTimings.push({ caption: 'CAPTION', time: elapsed() }); await page.click('SELECTOR'); await page.waitForTimeout(MS);
// stepTimings.push({ caption: 'CAPTION', time: elapsed() }); await page.waitForTimeout(MS);

await page.screenshot({ path: POSTER_OUT });
const duration = elapsed();

await context.close();
await browser.close();

// Move recorded video to final path
const files = readdirSync(VIDEO_TMP).filter(f => f.endsWith('.webm'));
if (files.length) renameSync(`${VIDEO_TMP}/${files[0]}`, VIDEO_OUT);

// Generate VTT
const toVTT = s => {
  const m = Math.floor(s / 60);
  const sec = (s % 60).toFixed(3).padStart(6, '0');
  return `${String(m).padStart(2,'0')}:${sec}`;
};
let vtt = 'WEBVTT\n\n';
stepTimings.forEach((step, i) => {
  const end = i < stepTimings.length - 1 ? stepTimings[i + 1].time : duration;
  vtt += `${toVTT(step.time)} --> ${toVTT(end)}\n${step.caption}\n\n`;
});
writeFileSync(VTT_OUT, vtt);

console.log(JSON.stringify({ stepTimings, duration }));
SCRIPT
```

After each video runs:
1. Display the poster image inline with the Read tool so the user can verify the final frame
2. Update the manifest entry with the step timings (replace the action-based steps from generate with timed steps)
3. The `.vtt` file is now on disk — it goes in git as-is

---

## Step 5 — GitHub Releases upload

After all videos are captured, provide upload instructions for each video file:

```
Videos are ready for upload to GitHub Releases.

1. Go to: https://github.com/OWNER/REPO/releases
2. Create a release tagged `docs-media` (or open the existing one)
   — This is a dedicated release just for doc assets, not tied to code versions.
3. Attach these files:
     public/videos/timer-start.webm
     public/videos/form-submit.webm
4. Once uploaded, each file will have a URL like:
     https://github.com/OWNER/REPO/releases/download/docs-media/timer-start.webm
```

Then ask:

- header: "Upload complete?"
- question: "Have you uploaded the videos to GitHub Releases?"
- options:
  - "Yes — update the manifest and docs"
  - "I'll do it later — skip for now"

If yes: update each video entry in the manifest with the correct `videoUrl`. Also update any `<VideoDemo src="...">` placeholders in the markdown files with the real URL.

If skip: leave `videoUrl: ""` in the manifest. The VideoDemo component will still render the step list and poster — it just won't have a playable video until the URL is filled in.

---

## Step 6 — Rewrite prose around captures

After all captures complete:
- For each screenshot: go back to the doc page and rewrite the surrounding paragraph to match what's actually in the screenshot
- For each video: verify the step captions in the doc match what was captured — update if anything looks off

Read each screenshot with the Read tool to inform the rewrite.

---

## Step 7 — Update manifest

For each successfully captured item:
- Screenshots: set `"placeholder": false`, record capture timestamp
- Videos: set step timings, `vtt` path, `poster` path, and `videoUrl` (once uploaded)

---

## Step 8 — Summary

```
Captured X screenshots, Y videos.

Screenshots:
  ✓ timer-main.png
  ✓ settings-overview.png

Videos:
  ✓ timer-start.webm — poster captured, VTT written, awaiting upload
  ✓ form-submit.webm — poster captured, VTT written, awaiting upload

  Upload to: https://github.com/OWNER/REPO/releases/tag/docs-media

Prose rewritten in X doc pages.
Run /vitedocs:sync after future code changes to keep docs current.
```
