From f06e28e110c2f87987e5ccf33663583d7c80a88d Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Tue, 7 Apr 2026 09:32:20 -0700 Subject: [PATCH 1/6] Flatten README template list for agent discoverability Add a single searchable table of all 80+ templates with name, language, and one-line description so agents and humans can quickly find relevant templates without navigating subdirectories. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 125 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 95 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 56fc549f..a053aa0f 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,108 @@ # Stagehand + Browserbase Templates -A comprehensive collection of ready-to-use automation templates demonstrating the power of Stagehand and Browserbase for web automation, data extraction, and AI-powered browser interactions. +Ready-to-use automation templates for Stagehand and Browserbase. Each template has its own README with setup instructions. > All templates also live on [browserbase.com/templates](https://www.browserbase.com/templates) -## 🚀 Quick Start +## All Templates + +| Template | Lang | Description | +|----------|------|-------------| +| [agent-with-human-in-loop](typescript/agent-with-human-in-loop) | TS | Build an AI agent that can pause and ask a human for input mid-task | +| [amazon-global-price-comparison](typescript/amazon-global-price-comparison) | TS | Compare Amazon product prices across multiple countries using geolocation proxies | +| [amazon-global-price-comparison](python/amazon-global-price-comparison) | PY | Compare Amazon product prices across multiple countries using geolocation proxies | +| [amazon-product-scraping](typescript/amazon-product-scraping) | TS | Scrape the first 3 Amazon search results for a given query and return structured product data | +| [amazon-product-scraping](python/amazon-product-scraping) | PY | Scrape the first 3 Amazon search results for a given query and return structured product data | +| [basic-caching](typescript/basic-caching) | TS | Demonstrate how Stagehand's caching feature reduces cost and latency by reusing previously computed actions | +| [basic-caching](python/basic-caching) | PY | Demonstrate how Stagehand's caching feature reduces cost and latency by reusing previously computed actions | +| [basic-recaptcha](typescript/basic-recaptcha) | TS | Automatic reCAPTCHA solving using Browserbase's built-in captcha solving capabilities | +| [basic-recaptcha](python/basic-recaptcha) | PY | Automatic reCAPTCHA solving using Browserbase's built-in captcha solving capabilities | +| [browser-agent-demo](typescript/browser-agent-demo) | TS | Browser agent that searches the web, fetches page content, and autonomously extracts information | +| [browserbase-reducto](typescript/browserbase-reducto) | TS | Download financial PDFs from websites and extract structured data using AI-powered document parsing | +| [browserbase-reducto](python/browserbase-reducto) | PY | Download financial PDFs from websites and extract structured data using AI-powered document parsing | +| [business-lookup](typescript/business-lookup) | TS | Automate business registry searches using an autonomous AI agent with computer-use capabilities | +| [business-lookup](python/business-lookup) | PY | Automate business registry searches using an autonomous AI agent with computer-use capabilities | +| [cartesia-form-filling](python/cartesia-form-filling) | PY | Voice agent that conducts phone questionnaires while automatically filling out web forms | +| [cerebras-docs-checker](python/cerebras-docs-checker) | PY | Crawl documentation sites, discover source repos, and verify docs accuracy against actual codebase | +| [company-address-finder](typescript/company-address-finder) | TS | Discover company legal information and physical addresses from Terms of Service and Privacy Policy pages | +| [company-address-finder](python/company-address-finder) | PY | Discover company legal information and physical addresses from Terms of Service and Privacy Policy pages | +| [company-value-prop-generator](typescript/company-value-prop-generator) | TS | Extract and format website value propositions into concise one-liners for email personalization | +| [company-value-prop-generator](python/company-value-prop-generator) | PY | Extract and format website value propositions into concise one-liners for email personalization | +| [context](typescript/context) | TS | Persistent authentication using Browserbase contexts that survive across sessions | +| [context](python/context) | PY | Persistent authentication using Browserbase contexts that survive across sessions | +| [council-events](typescript/council-events) | TS | Automate event information extraction from Philadelphia Council | +| [council-events](python/council-events) | PY | Automate extraction of Philadelphia Council events for 2025 from the official calendar | +| [download-financial-statements](typescript/download-financial-statements) | TS | Download Apple's quarterly financial statements (PDFs) from their investor relations site | +| [download-financial-statements](python/download-financial-statements) | PY | Download Apple's quarterly financial statements (PDFs) from their investor relations site | +| [dynamic-form-filling](typescript/dynamic-form-filling) | TS | Intelligent form filling using a Stagehand AI agent that understands form context and uses semantic matching | +| [exa-browserbase](typescript/exa-browserbase) | TS | Automate job applications with AI that writes smart, tailored responses for each role | +| [exa-browserbase](python/exa-browserbase) | PY | Automate job applications with AI that writes smart, tailored responses for each role | +| [extend-browserbase](typescript/extend-browserbase) | TS | Download receipts from an expense portal and extract structured receipt data using AI-powered document parsing | +| [extend-browserbase](python/extend-browserbase) | PY | Download receipts from an expense portal and extract structured receipt data using AI-powered document parsing | +| [form-filling](typescript/form-filling) | TS | Automate form filling with Stagehand and Browserbase | +| [form-filling](python/form-filling) | PY | Automate form filling with Stagehand and Browserbase | +| [gemini-3-flash](typescript/gemini-3-flash) | TS | Autonomous web browsing using Google's Gemini 3 Flash with Stagehand and Browserbase | +| [gemini-cua](typescript/gemini-cua) | TS | Autonomous web browsing using Google's Computer Use Agent with Stagehand and Browserbase | +| [gemini-cua](python/gemini-cua) | PY | Autonomous web browsing using Google's Computer Use Agent with Stagehand and Browserbase | +| [getting-started-with-browserbase](typescript/getting-started-with-browserbase) | TS | Demo all three core Browserbase capabilities: Search API, Fetch API, and Browser Sessions | +| [getting-started-with-browserbase](python/getting-started-with-browserbase) | PY | Demo all three core Browserbase capabilities: Search API, Fetch API, and Browser Sessions | +| [gift-finder](typescript/gift-finder) | TS | Find personalized gift recommendations using AI-generated search queries and intelligent product scoring | +| [gift-finder](python/gift-finder) | PY | Find personalized gift recommendations using AI-generated search queries and intelligent product scoring | +| [google-trends](typescript/google-trends) | TS | Extract trending search keywords from Google Trends for any country with structured JSON output | +| [google-trends](python/google-trends) | PY | Extract trending search keywords from Google Trends for any country with structured JSON output | +| [hackernews](go/hackernews) | GO | Demonstrate Stagehand's core browser automation features through a complete Hacker News workflow | +| [image-url-download](typescript/image-url-download) | TS | Extract all image URLs from a page and download each image through the browser's direct connection | +| [image-url-download](python/image-url-download) | PY | Extract all image URLs from a page and download each image through the browser's direct connection | +| [job-application](typescript/job-application) | TS | Automate job applications by discovering job listings and submitting applications | +| [job-application](python/job-application) | PY | Automate job applications by discovering job listings and submitting applications | +| [license-verification](typescript/license-verification) | TS | Extract structured, validated data from websites using Stagehand + Zod | +| [license-verification](python/license-verification) | PY | Extract structured, validated data from websites using Stagehand + Pydantic | +| [manual-mfa-with-contexts](typescript/manual-mfa-with-contexts) | TS | Persist authentication across sessions using Browserbase Contexts, eliminating MFA friction | +| [manual-mfa-with-contexts](python/manual-mfa-with-contexts) | PY | Persist authentication across sessions using Browserbase Contexts, eliminating MFA friction | +| [mfa-handling](typescript/mfa-handling) | TS | Automate MFA completion using TOTP (Time-based One-Time Password) code generation | +| [mfa-handling](python/mfa-handling) | PY | Automate MFA completion using TOTP (Time-based One-Time Password) code generation | +| [microsoft-cua](typescript/microsoft-cua) | TS | Autonomous web browsing using Microsoft's Computer Use Agent with Stagehand and Browserbase | +| [nurse-verification](typescript/nurse-verification) | TS | Automate verification of nurse licenses by filling forms and extracting structured results | +| [nurse-verification](python/nurse-verification) | PY | Automate verification of nurse licenses by filling forms and extracting structured results | +| [pickleball](typescript/pickleball) | TS | Automate tennis and pickleball court bookings in San Francisco Recreation & Parks system | +| [pickleball](python/pickleball) | PY | Automate tennis and pickleball court bookings in San Francisco Recreation & Parks system | +| [playwright](typescript/playwright) | TS | Raw Playwright usage with Browserbase (no Stagehand) | +| [playwright](python/playwright) | PY | Raw Playwright usage with Browserbase (no Stagehand) | +| [playwright-mfa-handling](typescript/playwright-mfa-handling) | TS | Automate MFA completion using TOTP with raw Playwright and Browserbase | +| [playwright-mfa-handling](python/playwright-mfa-handling) | PY | Automate MFA completion using TOTP with raw Playwright and Browserbase | +| [polymarket-research](typescript/polymarket-research) | TS | Automate market research on prediction markets using Stagehand | +| [polymarket-research](python/polymarket-research) | PY | Automate research of prediction markets on Polymarket to extract current odds, pricing, and volume data | +| [proxies](typescript/proxies) | TS | Demonstrate different proxy configurations with Browserbase sessions | +| [proxies](python/proxies) | PY | Demonstrate different proxy configurations with Browserbase sessions | +| [proxies-weather](typescript/proxies-weather) | TS | Geolocation proxies fetching location-specific weather data from multiple cities | +| [proxies-weather](python/proxies-weather) | PY | Geolocation proxies fetching location-specific weather data from multiple cities | +| [puppeteer](typescript/puppeteer) | TS | Raw Puppeteer usage with Browserbase | +| [resilient-payment-agent](typescript/resilient-payment-agent) | TS | Robust payment-form automation patterns for reliable checkout flows | +| [sec-filing-research](typescript/sec-filing-research) | TS | Search SEC EDGAR for a company and extract recent filing metadata | +| [sec-filing-research](python/sec-filing-research) | PY | Search SEC EDGAR for a company and extract recent filing metadata | +| [selenium](typescript/selenium) | TS | Raw Selenium usage with Browserbase | +| [selenium](python/selenium) | PY | Raw Selenium usage with Browserbase | +| [smart-fetch-scraper](typescript/smart-fetch-scraper) | TS | Scrape a webpage using the fastest method available -- Fetch API first, full browser session as fallback | +| [smart-fetch-scraper](python/smart-fetch-scraper) | PY | Scrape a webpage using the fastest method available -- Fetch API first, full browser session as fallback | +| [website-link-tester](typescript/website-link-tester) | TS | Crawl a website's homepage, collect all links, and verify each link loads successfully | +| [website-link-tester](python/website-link-tester) | PY | Crawl a website's homepage, collect all links, and verify each link loads successfully | -1. **Choose your language**: TypeScript or Python -2. **Browse available templates** in the respective language folder -3. **Read the template's README** for detailed setup instructions and use cases -4. **Start automating!** - -## 🔧 Getting Started +## Model Gateway -1. **Choose a template** from the TypeScript or Python folders -2. **Read the template's README** for specific setup instructions -3. **Set up your environment** with the required API keys and dependencies -4. **Run the template** and start automating! +Templates use the Model Gateway to route LLM requests -- you only need your `BROWSERBASE_API_KEY`. No separate OpenAI, Anthropic, or Google API keys required. Supported models include OpenAI, Anthropic, and Google (Gemini). -> **💡 Pro Tip**: Each template's README contains detailed installation steps, environment variable requirements, and troubleshooting guides specific to that template. +> **Note**: CUA (Computer Use Agent) models are not yet supported through the Model Gateway. Templates using CUA models still require a separate model provider API key. -## Model Gateway +## Getting Started -Templates use the Model Gateway to route LLM requests — you only need your `BROWSERBASE_API_KEY`. No separate OpenAI, Anthropic, or Google API keys required. Supported models include OpenAI, Anthropic, and Google (Gemini). +1. **Choose a template** from the table above +2. **Read the template's README** for specific setup instructions +3. **Set up your environment** with the required API keys and dependencies +4. **Run the template** and start automating -> **Note**: CUA (Computer Use Agent) models are not yet supported through the Model Gateway. Templates using CUA models still require a separate model provider API key. +Each template's README contains detailed installation steps, environment variable requirements, and troubleshooting guides. -## 📚 Resources +## Resources ### Documentation @@ -35,18 +111,11 @@ Templates use the Model Gateway to route LLM requests — you only need your `BR ### Support -- **Community**: Join our Discord community - **Discord**: http://stagehand.dev/discord - **Email Support**: support@browserbase.com - **GitHub Issues**: Report bugs and request features -### Examples & Tutorials - -- **Getting Started Guide**: Learn the basics of Stagehand -- **Advanced Patterns**: Complex automation workflows -- **Best Practices**: Tips for reliable automation - -## 🤝 Contributing +## Contributing We welcome contributions! Here's how you can help: @@ -62,10 +131,6 @@ We welcome contributions! Here's how you can help: - Add proper error handling and logging - Test templates thoroughly before submitting -## 📄 License +## License This project is licensed under the MIT License - see the LICENSE file for details. - ---- - -**Ready to start automating?** Pick a template and follow its README to get started! 🚀 From 2d858cf61406f5d069040f996653c3b94e2ee6d2 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Wed, 8 Apr 2026 11:39:53 -0700 Subject: [PATCH 2/6] ci: validate README template index --- .github/workflows/readme-template-index.yml | 42 ++++ package.json | 3 +- scripts/check-readme-template-index.mjs | 214 ++++++++++++++++++++ 3 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/readme-template-index.yml create mode 100644 scripts/check-readme-template-index.mjs diff --git a/.github/workflows/readme-template-index.yml b/.github/workflows/readme-template-index.yml new file mode 100644 index 00000000..58338a35 --- /dev/null +++ b/.github/workflows/readme-template-index.yml @@ -0,0 +1,42 @@ +name: README Template Index + +on: + pull_request: + paths: + - "README.md" + - "go/**" + - "python/**" + - "typescript/**" + - "package.json" + - "scripts/check-readme-template-index.mjs" + - ".github/workflows/readme-template-index.yml" + push: + branches: + - dev + paths: + - "README.md" + - "go/**" + - "python/**" + - "typescript/**" + - "package.json" + - "scripts/check-readme-template-index.mjs" + - ".github/workflows/readme-template-index.yml" + +permissions: + contents: read + +jobs: + validate-readme-template-index: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Validate README template index + run: npm run check:readme-template-index diff --git a/package.json b/package.json index 45ed7ac4..2be8b400 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "private": true, "type": "module", "scripts": { + "check:readme-template-index": "node scripts/check-readme-template-index.mjs", "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,json,md,yml,yaml}\"", "lint": "eslint \"**/*.{ts,tsx,js,jsx}\"", @@ -13,7 +14,7 @@ "lint:python:fix": "uvx ruff check --fix python/", "format:python": "uvx ruff format python/", "format:python:check": "uvx ruff format --check python/", - "check": "npm run format:check && npm run lint && npm run lint:python && npm run format:python:check", + "check": "npm run check:readme-template-index && npm run format:check && npm run lint && npm run lint:python && npm run format:python:check", "prepare": "husky" }, "devDependencies": { diff --git a/scripts/check-readme-template-index.mjs b/scripts/check-readme-template-index.mjs new file mode 100644 index 00000000..08de4ca6 --- /dev/null +++ b/scripts/check-readme-template-index.mjs @@ -0,0 +1,214 @@ +import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import path from "node:path"; + +const REPO_ROOT = process.cwd(); +const README_PATH = path.join(REPO_ROOT, "README.md"); +const TEMPLATE_ROOTS = { + typescript: "TS", + python: "PY", + go: "GO", +}; +const LANGUAGE_ORDER = { + TS: 0, + PY: 1, + GO: 2, +}; +const SECTION_PATTERN = /^## All Templates\r?\n\r?\n([\s\S]*?)(?=^##\s)/m; +const ROW_PATTERN = /^\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(TS|PY|GO)\s*\|\s*(.+?)\s*\|$/; + +function fail(message) { + console.error(message); + process.exit(1); +} + +function getExpectedEntries() { + return Object.entries(TEMPLATE_ROOTS) + .flatMap(([root, language]) => { + const rootPath = path.join(REPO_ROOT, root); + return readdirSync(rootPath, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => ({ + name: entry.name, + path: `${root}/${entry.name}`, + language, + })); + }) + .sort( + (left, right) => + left.name.localeCompare(right.name) || + LANGUAGE_ORDER[left.language] - LANGUAGE_ORDER[right.language] || + left.path.localeCompare(right.path), + ); +} + +function getReadmeSection(readmeContents) { + const match = readmeContents.match(SECTION_PATTERN); + if (!match) { + fail("Could not find the `## All Templates` section in README.md."); + } + + return match[1]; +} + +function parseReadmeEntries(section) { + const lines = section + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line.startsWith("|")); + + if (lines.length < 3) { + fail("README template table is missing rows."); + } + + const dataLines = lines.slice(2); + + return dataLines.map((line) => { + const match = line.match(ROW_PATTERN); + if (!match) { + fail(`Invalid template row format in README.md:\n${line}`); + } + + const [, name, templatePath, language, description] = match; + return { + name, + path: templatePath, + language, + description: description.trim(), + }; + }); +} + +function findDuplicatePaths(entries) { + const counts = new Map(); + + for (const entry of entries) { + counts.set(entry.path, (counts.get(entry.path) ?? 0) + 1); + } + + return [...counts.entries()].filter(([, count]) => count > 1).map(([entryPath]) => entryPath); +} + +function validateReadmeEntries(entries) { + const problems = []; + + for (const entry of entries) { + const segments = entry.path.split("/"); + if (segments.length !== 2) { + problems.push( + `README entry \`${entry.path}\` must point to a first-level template directory.`, + ); + continue; + } + + const [root] = segments; + const expectedLanguage = TEMPLATE_ROOTS[root]; + if (!expectedLanguage) { + problems.push( + `README entry \`${entry.path}\` must live under one of: ${Object.keys(TEMPLATE_ROOTS).join(", ")}.`, + ); + continue; + } + + if (entry.language !== expectedLanguage) { + problems.push( + `README entry \`${entry.path}\` is labeled ${entry.language}, but ${root}/ templates must use ${expectedLanguage}.`, + ); + } + + const directoryName = path.posix.basename(entry.path); + if (entry.name !== directoryName) { + problems.push( + `README entry text \`${entry.name}\` must match directory name \`${directoryName}\`.`, + ); + } + + const fullPath = path.join(REPO_ROOT, entry.path); + if (!existsSync(fullPath) || !statSync(fullPath).isDirectory()) { + problems.push(`README entry \`${entry.path}\` does not exist on disk.`); + } + + if (!entry.description) { + problems.push(`README entry \`${entry.path}\` is missing a description.`); + } + } + + return problems; +} + +function getOrderMessage(expectedEntries, readmeEntries) { + const expectedPaths = expectedEntries.map((entry) => entry.path); + const actualPaths = readmeEntries.map((entry) => entry.path); + const mismatchIndex = expectedPaths.findIndex( + (expectedPath, index) => expectedPath !== actualPaths[index], + ); + + if (mismatchIndex === -1) { + return null; + } + + return [ + "README template rows are out of order.", + "Sort rows by template name, then by language in TS/PY/GO order.", + `First mismatch at row ${mismatchIndex + 1}: expected \`${expectedPaths[mismatchIndex]}\`, found \`${actualPaths[mismatchIndex]}\`.`, + ].join("\n"); +} + +function main() { + const readmeContents = readFileSync(README_PATH, "utf8"); + const expectedEntries = getExpectedEntries(); + const readmeEntries = parseReadmeEntries(getReadmeSection(readmeContents)); + + const duplicatePaths = findDuplicatePaths(readmeEntries); + const validationProblems = validateReadmeEntries(readmeEntries); + const expectedPaths = new Set(expectedEntries.map((entry) => entry.path)); + const readmePaths = new Set(readmeEntries.map((entry) => entry.path)); + const missingEntries = expectedEntries.filter((entry) => !readmePaths.has(entry.path)); + const unexpectedEntries = readmeEntries.filter((entry) => !expectedPaths.has(entry.path)); + const orderProblem = getOrderMessage(expectedEntries, readmeEntries); + + const problems = []; + + if (duplicatePaths.length > 0) { + problems.push( + `Duplicate README entries found:\n${duplicatePaths.map((entryPath) => `- ${entryPath}`).join("\n")}`, + ); + } + + if (validationProblems.length > 0) { + problems.push(validationProblems.map((problem) => `- ${problem}`).join("\n")); + } + + if (missingEntries.length > 0) { + problems.push( + `Template directories missing from README:\n${missingEntries + .map((entry) => `- ${entry.path}`) + .join("\n")}`, + ); + } + + if (unexpectedEntries.length > 0) { + problems.push( + `README entries that do not match any template directory:\n${unexpectedEntries + .map((entry) => `- ${entry.path}`) + .join("\n")}`, + ); + } + + if (orderProblem) { + problems.push(orderProblem); + } + + if (problems.length > 0) { + fail( + ["README template index is out of sync with the template directory tree.", ...problems].join( + "\n\n", + ), + ); + } + + console.log( + `README template index matches ${expectedEntries.length} first-level template directories.`, + ); +} + +main(); From 9cd98b17ecbfd109dca1cc2612fbf9d9602085f5 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Wed, 8 Apr 2026 11:41:50 -0700 Subject: [PATCH 3/6] fix: align README template index with tracked templates --- README.md | 1 - scripts/check-readme-template-index.mjs | 58 +++++++++++++++++-------- 2 files changed, 41 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index a053aa0f..7fe379cd 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,6 @@ Ready-to-use automation templates for Stagehand and Browserbase. Each template h | [proxies-weather](typescript/proxies-weather) | TS | Geolocation proxies fetching location-specific weather data from multiple cities | | [proxies-weather](python/proxies-weather) | PY | Geolocation proxies fetching location-specific weather data from multiple cities | | [puppeteer](typescript/puppeteer) | TS | Raw Puppeteer usage with Browserbase | -| [resilient-payment-agent](typescript/resilient-payment-agent) | TS | Robust payment-form automation patterns for reliable checkout flows | | [sec-filing-research](typescript/sec-filing-research) | TS | Search SEC EDGAR for a company and extract recent filing metadata | | [sec-filing-research](python/sec-filing-research) | PY | Search SEC EDGAR for a company and extract recent filing metadata | | [selenium](typescript/selenium) | TS | Raw Selenium usage with Browserbase | diff --git a/scripts/check-readme-template-index.mjs b/scripts/check-readme-template-index.mjs index 08de4ca6..1e5db84e 100644 --- a/scripts/check-readme-template-index.mjs +++ b/scripts/check-readme-template-index.mjs @@ -1,4 +1,5 @@ -import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; import path from "node:path"; const REPO_ROOT = process.cwd(); @@ -21,17 +22,46 @@ function fail(message) { process.exit(1); } +function getTrackedTemplatePaths() { + try { + const gitOutput = execFileSync( + "git", + ["ls-files", "-co", "--exclude-standard", "--", ...Object.keys(TEMPLATE_ROOTS)], + { + cwd: REPO_ROOT, + encoding: "utf8", + }, + ); + + return new Set( + gitOutput + .split(/\r?\n/) + .filter(Boolean) + .map((filePath) => { + const [root, templateName] = filePath.split("/"); + if (!TEMPLATE_ROOTS[root] || !templateName) { + return null; + } + + return `${root}/${templateName}`; + }) + .filter(Boolean), + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + fail(`Could not determine tracked template paths from git:\n${message}`); + } +} + function getExpectedEntries() { - return Object.entries(TEMPLATE_ROOTS) - .flatMap(([root, language]) => { - const rootPath = path.join(REPO_ROOT, root); - return readdirSync(rootPath, { withFileTypes: true }) - .filter((entry) => entry.isDirectory()) - .map((entry) => ({ - name: entry.name, - path: `${root}/${entry.name}`, - language, - })); + return [...getTrackedTemplatePaths()] + .map((templatePath) => { + const [root, name] = templatePath.split("/"); + return { + name, + path: templatePath, + language: TEMPLATE_ROOTS[root], + }; }) .sort( (left, right) => @@ -121,12 +151,6 @@ function validateReadmeEntries(entries) { `README entry text \`${entry.name}\` must match directory name \`${directoryName}\`.`, ); } - - const fullPath = path.join(REPO_ROOT, entry.path); - if (!existsSync(fullPath) || !statSync(fullPath).isDirectory()) { - problems.push(`README entry \`${entry.path}\` does not exist on disk.`); - } - if (!entry.description) { problems.push(`README entry \`${entry.path}\` is missing a description.`); } From 0518117af30d4c50c7928329323dfb7acad5b5d9 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Wed, 8 Apr 2026 11:51:20 -0700 Subject: [PATCH 4/6] ci: streamline README index workflow --- .github/workflows/readme-template-index.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/readme-template-index.yml b/.github/workflows/readme-template-index.yml index 58338a35..73dc982e 100644 --- a/.github/workflows/readme-template-index.yml +++ b/.github/workflows/readme-template-index.yml @@ -7,7 +7,6 @@ on: - "go/**" - "python/**" - "typescript/**" - - "package.json" - "scripts/check-readme-template-index.mjs" - ".github/workflows/readme-template-index.yml" push: @@ -18,25 +17,29 @@ on: - "go/**" - "python/**" - "typescript/**" - - "package.json" - "scripts/check-readme-template-index.mjs" - ".github/workflows/readme-template-index.yml" +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: contents: read jobs: validate-readme-template-index: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Check out repository uses: actions/checkout@v4 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: - node-version: 20 + node-version: 20.x - name: Validate README template index - run: npm run check:readme-template-index + run: node scripts/check-readme-template-index.mjs From cd19edebae4dbbf5e37d93719897a538854730c2 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Wed, 8 Apr 2026 11:52:26 -0700 Subject: [PATCH 5/6] ci: bump checkout action --- .github/workflows/readme-template-index.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/readme-template-index.yml b/.github/workflows/readme-template-index.yml index 73dc982e..5c285543 100644 --- a/.github/workflows/readme-template-index.yml +++ b/.github/workflows/readme-template-index.yml @@ -34,7 +34,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Node.js uses: actions/setup-node@v6 From d7c256659eec4b1ff7d03088e91f2cc61e2090f4 Mon Sep 17 00:00:00 2001 From: Shrey Pandya Date: Thu, 9 Apr 2026 16:19:12 -0700 Subject: [PATCH 6/6] docs: group template index by language --- README.md | 124 +++++-------- scripts/check-readme-template-index.mjs | 227 +++++++++++++++--------- 2 files changed, 190 insertions(+), 161 deletions(-) diff --git a/README.md b/README.md index 7fe379cd..51ff915f 100644 --- a/README.md +++ b/README.md @@ -6,85 +6,51 @@ Ready-to-use automation templates for Stagehand and Browserbase. Each template h ## All Templates -| Template | Lang | Description | -|----------|------|-------------| -| [agent-with-human-in-loop](typescript/agent-with-human-in-loop) | TS | Build an AI agent that can pause and ask a human for input mid-task | -| [amazon-global-price-comparison](typescript/amazon-global-price-comparison) | TS | Compare Amazon product prices across multiple countries using geolocation proxies | -| [amazon-global-price-comparison](python/amazon-global-price-comparison) | PY | Compare Amazon product prices across multiple countries using geolocation proxies | -| [amazon-product-scraping](typescript/amazon-product-scraping) | TS | Scrape the first 3 Amazon search results for a given query and return structured product data | -| [amazon-product-scraping](python/amazon-product-scraping) | PY | Scrape the first 3 Amazon search results for a given query and return structured product data | -| [basic-caching](typescript/basic-caching) | TS | Demonstrate how Stagehand's caching feature reduces cost and latency by reusing previously computed actions | -| [basic-caching](python/basic-caching) | PY | Demonstrate how Stagehand's caching feature reduces cost and latency by reusing previously computed actions | -| [basic-recaptcha](typescript/basic-recaptcha) | TS | Automatic reCAPTCHA solving using Browserbase's built-in captcha solving capabilities | -| [basic-recaptcha](python/basic-recaptcha) | PY | Automatic reCAPTCHA solving using Browserbase's built-in captcha solving capabilities | -| [browser-agent-demo](typescript/browser-agent-demo) | TS | Browser agent that searches the web, fetches page content, and autonomously extracts information | -| [browserbase-reducto](typescript/browserbase-reducto) | TS | Download financial PDFs from websites and extract structured data using AI-powered document parsing | -| [browserbase-reducto](python/browserbase-reducto) | PY | Download financial PDFs from websites and extract structured data using AI-powered document parsing | -| [business-lookup](typescript/business-lookup) | TS | Automate business registry searches using an autonomous AI agent with computer-use capabilities | -| [business-lookup](python/business-lookup) | PY | Automate business registry searches using an autonomous AI agent with computer-use capabilities | -| [cartesia-form-filling](python/cartesia-form-filling) | PY | Voice agent that conducts phone questionnaires while automatically filling out web forms | -| [cerebras-docs-checker](python/cerebras-docs-checker) | PY | Crawl documentation sites, discover source repos, and verify docs accuracy against actual codebase | -| [company-address-finder](typescript/company-address-finder) | TS | Discover company legal information and physical addresses from Terms of Service and Privacy Policy pages | -| [company-address-finder](python/company-address-finder) | PY | Discover company legal information and physical addresses from Terms of Service and Privacy Policy pages | -| [company-value-prop-generator](typescript/company-value-prop-generator) | TS | Extract and format website value propositions into concise one-liners for email personalization | -| [company-value-prop-generator](python/company-value-prop-generator) | PY | Extract and format website value propositions into concise one-liners for email personalization | -| [context](typescript/context) | TS | Persistent authentication using Browserbase contexts that survive across sessions | -| [context](python/context) | PY | Persistent authentication using Browserbase contexts that survive across sessions | -| [council-events](typescript/council-events) | TS | Automate event information extraction from Philadelphia Council | -| [council-events](python/council-events) | PY | Automate extraction of Philadelphia Council events for 2025 from the official calendar | -| [download-financial-statements](typescript/download-financial-statements) | TS | Download Apple's quarterly financial statements (PDFs) from their investor relations site | -| [download-financial-statements](python/download-financial-statements) | PY | Download Apple's quarterly financial statements (PDFs) from their investor relations site | -| [dynamic-form-filling](typescript/dynamic-form-filling) | TS | Intelligent form filling using a Stagehand AI agent that understands form context and uses semantic matching | -| [exa-browserbase](typescript/exa-browserbase) | TS | Automate job applications with AI that writes smart, tailored responses for each role | -| [exa-browserbase](python/exa-browserbase) | PY | Automate job applications with AI that writes smart, tailored responses for each role | -| [extend-browserbase](typescript/extend-browserbase) | TS | Download receipts from an expense portal and extract structured receipt data using AI-powered document parsing | -| [extend-browserbase](python/extend-browserbase) | PY | Download receipts from an expense portal and extract structured receipt data using AI-powered document parsing | -| [form-filling](typescript/form-filling) | TS | Automate form filling with Stagehand and Browserbase | -| [form-filling](python/form-filling) | PY | Automate form filling with Stagehand and Browserbase | -| [gemini-3-flash](typescript/gemini-3-flash) | TS | Autonomous web browsing using Google's Gemini 3 Flash with Stagehand and Browserbase | -| [gemini-cua](typescript/gemini-cua) | TS | Autonomous web browsing using Google's Computer Use Agent with Stagehand and Browserbase | -| [gemini-cua](python/gemini-cua) | PY | Autonomous web browsing using Google's Computer Use Agent with Stagehand and Browserbase | -| [getting-started-with-browserbase](typescript/getting-started-with-browserbase) | TS | Demo all three core Browserbase capabilities: Search API, Fetch API, and Browser Sessions | -| [getting-started-with-browserbase](python/getting-started-with-browserbase) | PY | Demo all three core Browserbase capabilities: Search API, Fetch API, and Browser Sessions | -| [gift-finder](typescript/gift-finder) | TS | Find personalized gift recommendations using AI-generated search queries and intelligent product scoring | -| [gift-finder](python/gift-finder) | PY | Find personalized gift recommendations using AI-generated search queries and intelligent product scoring | -| [google-trends](typescript/google-trends) | TS | Extract trending search keywords from Google Trends for any country with structured JSON output | -| [google-trends](python/google-trends) | PY | Extract trending search keywords from Google Trends for any country with structured JSON output | -| [hackernews](go/hackernews) | GO | Demonstrate Stagehand's core browser automation features through a complete Hacker News workflow | -| [image-url-download](typescript/image-url-download) | TS | Extract all image URLs from a page and download each image through the browser's direct connection | -| [image-url-download](python/image-url-download) | PY | Extract all image URLs from a page and download each image through the browser's direct connection | -| [job-application](typescript/job-application) | TS | Automate job applications by discovering job listings and submitting applications | -| [job-application](python/job-application) | PY | Automate job applications by discovering job listings and submitting applications | -| [license-verification](typescript/license-verification) | TS | Extract structured, validated data from websites using Stagehand + Zod | -| [license-verification](python/license-verification) | PY | Extract structured, validated data from websites using Stagehand + Pydantic | -| [manual-mfa-with-contexts](typescript/manual-mfa-with-contexts) | TS | Persist authentication across sessions using Browserbase Contexts, eliminating MFA friction | -| [manual-mfa-with-contexts](python/manual-mfa-with-contexts) | PY | Persist authentication across sessions using Browserbase Contexts, eliminating MFA friction | -| [mfa-handling](typescript/mfa-handling) | TS | Automate MFA completion using TOTP (Time-based One-Time Password) code generation | -| [mfa-handling](python/mfa-handling) | PY | Automate MFA completion using TOTP (Time-based One-Time Password) code generation | -| [microsoft-cua](typescript/microsoft-cua) | TS | Autonomous web browsing using Microsoft's Computer Use Agent with Stagehand and Browserbase | -| [nurse-verification](typescript/nurse-verification) | TS | Automate verification of nurse licenses by filling forms and extracting structured results | -| [nurse-verification](python/nurse-verification) | PY | Automate verification of nurse licenses by filling forms and extracting structured results | -| [pickleball](typescript/pickleball) | TS | Automate tennis and pickleball court bookings in San Francisco Recreation & Parks system | -| [pickleball](python/pickleball) | PY | Automate tennis and pickleball court bookings in San Francisco Recreation & Parks system | -| [playwright](typescript/playwright) | TS | Raw Playwright usage with Browserbase (no Stagehand) | -| [playwright](python/playwright) | PY | Raw Playwright usage with Browserbase (no Stagehand) | -| [playwright-mfa-handling](typescript/playwright-mfa-handling) | TS | Automate MFA completion using TOTP with raw Playwright and Browserbase | -| [playwright-mfa-handling](python/playwright-mfa-handling) | PY | Automate MFA completion using TOTP with raw Playwright and Browserbase | -| [polymarket-research](typescript/polymarket-research) | TS | Automate market research on prediction markets using Stagehand | -| [polymarket-research](python/polymarket-research) | PY | Automate research of prediction markets on Polymarket to extract current odds, pricing, and volume data | -| [proxies](typescript/proxies) | TS | Demonstrate different proxy configurations with Browserbase sessions | -| [proxies](python/proxies) | PY | Demonstrate different proxy configurations with Browserbase sessions | -| [proxies-weather](typescript/proxies-weather) | TS | Geolocation proxies fetching location-specific weather data from multiple cities | -| [proxies-weather](python/proxies-weather) | PY | Geolocation proxies fetching location-specific weather data from multiple cities | -| [puppeteer](typescript/puppeteer) | TS | Raw Puppeteer usage with Browserbase | -| [sec-filing-research](typescript/sec-filing-research) | TS | Search SEC EDGAR for a company and extract recent filing metadata | -| [sec-filing-research](python/sec-filing-research) | PY | Search SEC EDGAR for a company and extract recent filing metadata | -| [selenium](typescript/selenium) | TS | Raw Selenium usage with Browserbase | -| [selenium](python/selenium) | PY | Raw Selenium usage with Browserbase | -| [smart-fetch-scraper](typescript/smart-fetch-scraper) | TS | Scrape a webpage using the fastest method available -- Fetch API first, full browser session as fallback | -| [smart-fetch-scraper](python/smart-fetch-scraper) | PY | Scrape a webpage using the fastest method available -- Fetch API first, full browser session as fallback | -| [website-link-tester](typescript/website-link-tester) | TS | Crawl a website's homepage, collect all links, and verify each link loads successfully | -| [website-link-tester](python/website-link-tester) | PY | Crawl a website's homepage, collect all links, and verify each link loads successfully | +| Template | TS | PY | GO | Description | +| -------------------------------- | ------------------------------------------------- | --------------------------------------------- | ------------------- | -------------------------------------------------------------------------------------------------------------- | +| agent-with-human-in-loop | [TS](typescript/agent-with-human-in-loop) | - | - | Build an AI agent that can pause and ask a human for input mid-task | +| amazon-global-price-comparison | [TS](typescript/amazon-global-price-comparison) | [PY](python/amazon-global-price-comparison) | - | Compare Amazon product prices across multiple countries using geolocation proxies | +| amazon-product-scraping | [TS](typescript/amazon-product-scraping) | [PY](python/amazon-product-scraping) | - | Scrape the first 3 Amazon search results for a given query and return structured product data | +| basic-caching | [TS](typescript/basic-caching) | [PY](python/basic-caching) | - | Demonstrate how Stagehand's caching feature reduces cost and latency by reusing previously computed actions | +| basic-recaptcha | [TS](typescript/basic-recaptcha) | [PY](python/basic-recaptcha) | - | Automatic reCAPTCHA solving using Browserbase's built-in captcha solving capabilities | +| browser-agent-demo | [TS](typescript/browser-agent-demo) | - | - | Browser agent that searches the web, fetches page content, and autonomously extracts information | +| browserbase-reducto | [TS](typescript/browserbase-reducto) | [PY](python/browserbase-reducto) | - | Download financial PDFs from websites and extract structured data using AI-powered document parsing | +| business-lookup | [TS](typescript/business-lookup) | [PY](python/business-lookup) | - | Automate business registry searches using an autonomous AI agent with computer-use capabilities | +| cartesia-form-filling | - | [PY](python/cartesia-form-filling) | - | Voice agent that conducts phone questionnaires while automatically filling out web forms | +| cerebras-docs-checker | - | [PY](python/cerebras-docs-checker) | - | Crawl documentation sites, discover source repos, and verify docs accuracy against actual codebase | +| company-address-finder | [TS](typescript/company-address-finder) | [PY](python/company-address-finder) | - | Discover company legal information and physical addresses from Terms of Service and Privacy Policy pages | +| company-value-prop-generator | [TS](typescript/company-value-prop-generator) | [PY](python/company-value-prop-generator) | - | Extract and format website value propositions into concise one-liners for email personalization | +| context | [TS](typescript/context) | [PY](python/context) | - | Persistent authentication using Browserbase contexts that survive across sessions | +| council-events | [TS](typescript/council-events) | [PY](python/council-events) | - | Automate event information extraction from Philadelphia Council | +| download-financial-statements | [TS](typescript/download-financial-statements) | [PY](python/download-financial-statements) | - | Download Apple's quarterly financial statements (PDFs) from their investor relations site | +| dynamic-form-filling | [TS](typescript/dynamic-form-filling) | - | - | Intelligent form filling using a Stagehand AI agent that understands form context and uses semantic matching | +| exa-browserbase | [TS](typescript/exa-browserbase) | [PY](python/exa-browserbase) | - | Automate job applications with AI that writes smart, tailored responses for each role | +| extend-browserbase | [TS](typescript/extend-browserbase) | [PY](python/extend-browserbase) | - | Download receipts from an expense portal and extract structured receipt data using AI-powered document parsing | +| form-filling | [TS](typescript/form-filling) | [PY](python/form-filling) | - | Automate form filling with Stagehand and Browserbase | +| gemini-3-flash | [TS](typescript/gemini-3-flash) | - | - | Autonomous web browsing using Google's Gemini 3 Flash with Stagehand and Browserbase | +| gemini-cua | [TS](typescript/gemini-cua) | [PY](python/gemini-cua) | - | Autonomous web browsing using Google's Computer Use Agent with Stagehand and Browserbase | +| getting-started-with-browserbase | [TS](typescript/getting-started-with-browserbase) | [PY](python/getting-started-with-browserbase) | - | Demo all three core Browserbase capabilities: Search API, Fetch API, and Browser Sessions | +| gift-finder | [TS](typescript/gift-finder) | [PY](python/gift-finder) | - | Find personalized gift recommendations using AI-generated search queries and intelligent product scoring | +| google-trends | [TS](typescript/google-trends) | [PY](python/google-trends) | - | Extract trending search keywords from Google Trends for any country with structured JSON output | +| hackernews | - | - | [GO](go/hackernews) | Demonstrate Stagehand's core browser automation features through a complete Hacker News workflow | +| image-url-download | [TS](typescript/image-url-download) | [PY](python/image-url-download) | - | Extract all image URLs from a page and download each image through the browser's direct connection | +| job-application | [TS](typescript/job-application) | [PY](python/job-application) | - | Automate job applications by discovering job listings and submitting applications | +| license-verification | [TS](typescript/license-verification) | [PY](python/license-verification) | - | Extract structured, validated data from websites using Stagehand + Zod | +| manual-mfa-with-contexts | [TS](typescript/manual-mfa-with-contexts) | [PY](python/manual-mfa-with-contexts) | - | Persist authentication across sessions using Browserbase Contexts, eliminating MFA friction | +| mfa-handling | [TS](typescript/mfa-handling) | [PY](python/mfa-handling) | - | Automate MFA completion using TOTP (Time-based One-Time Password) code generation | +| microsoft-cua | [TS](typescript/microsoft-cua) | - | - | Autonomous web browsing using Microsoft's Computer Use Agent with Stagehand and Browserbase | +| nurse-verification | [TS](typescript/nurse-verification) | [PY](python/nurse-verification) | - | Automate verification of nurse licenses by filling forms and extracting structured results | +| pickleball | [TS](typescript/pickleball) | [PY](python/pickleball) | - | Automate tennis and pickleball court bookings in San Francisco Recreation & Parks system | +| playwright | [TS](typescript/playwright) | [PY](python/playwright) | - | Raw Playwright usage with Browserbase (no Stagehand) | +| playwright-mfa-handling | [TS](typescript/playwright-mfa-handling) | [PY](python/playwright-mfa-handling) | - | Automate MFA completion using TOTP with raw Playwright and Browserbase | +| polymarket-research | [TS](typescript/polymarket-research) | [PY](python/polymarket-research) | - | Automate market research on prediction markets using Stagehand | +| proxies | [TS](typescript/proxies) | [PY](python/proxies) | - | Demonstrate different proxy configurations with Browserbase sessions | +| proxies-weather | [TS](typescript/proxies-weather) | [PY](python/proxies-weather) | - | Geolocation proxies fetching location-specific weather data from multiple cities | +| puppeteer | [TS](typescript/puppeteer) | - | - | Raw Puppeteer usage with Browserbase | +| sec-filing-research | [TS](typescript/sec-filing-research) | [PY](python/sec-filing-research) | - | Search SEC EDGAR for a company and extract recent filing metadata | +| selenium | [TS](typescript/selenium) | [PY](python/selenium) | - | Raw Selenium usage with Browserbase | +| smart-fetch-scraper | [TS](typescript/smart-fetch-scraper) | [PY](python/smart-fetch-scraper) | - | Scrape a webpage using the fastest method available -- Fetch API first, full browser session as fallback | +| website-link-tester | [TS](typescript/website-link-tester) | [PY](python/website-link-tester) | - | Crawl a website's homepage, collect all links, and verify each link loads successfully | ## Model Gateway diff --git a/scripts/check-readme-template-index.mjs b/scripts/check-readme-template-index.mjs index 1e5db84e..c7207ef0 100644 --- a/scripts/check-readme-template-index.mjs +++ b/scripts/check-readme-template-index.mjs @@ -9,13 +9,11 @@ const TEMPLATE_ROOTS = { python: "PY", go: "GO", }; -const LANGUAGE_ORDER = { - TS: 0, - PY: 1, - GO: 2, -}; +const LANGUAGE_COLUMNS = ["TS", "PY", "GO"]; +const LANGUAGE_TO_ROOT = Object.fromEntries( + Object.entries(TEMPLATE_ROOTS).map(([root, language]) => [language, root]), +); const SECTION_PATTERN = /^## All Templates\r?\n\r?\n([\s\S]*?)(?=^##\s)/m; -const ROW_PATTERN = /^\|\s*\[([^\]]+)\]\(([^)]+)\)\s*\|\s*(TS|PY|GO)\s*\|\s*(.+?)\s*\|$/; function fail(message) { console.error(message); @@ -53,22 +51,18 @@ function getTrackedTemplatePaths() { } } -function getExpectedEntries() { - return [...getTrackedTemplatePaths()] - .map((templatePath) => { - const [root, name] = templatePath.split("/"); - return { - name, - path: templatePath, - language: TEMPLATE_ROOTS[root], - }; - }) - .sort( - (left, right) => - left.name.localeCompare(right.name) || - LANGUAGE_ORDER[left.language] - LANGUAGE_ORDER[right.language] || - left.path.localeCompare(right.path), - ); +function getExpectedRows() { + const rows = new Map(); + + for (const templatePath of [...getTrackedTemplatePaths()].sort()) { + const [root, name] = templatePath.split("/"); + const language = TEMPLATE_ROOTS[root]; + const row = rows.get(name) ?? { name, languages: {} }; + row.languages[language] = { label: language, path: templatePath }; + rows.set(name, row); + } + + return [...rows.values()].sort((left, right) => left.name.localeCompare(right.name)); } function getReadmeSection(readmeContents) { @@ -80,7 +74,23 @@ function getReadmeSection(readmeContents) { return match[1]; } -function parseReadmeEntries(section) { +function parseLanguageCell(cell, expectedLanguage, rowName, line) { + if (cell === "-") { + return null; + } + + const match = cell.match(/^\[([A-Z]{2})\]\(([^)]+)\)$/); + if (!match) { + fail( + `Invalid ${expectedLanguage} cell format for template \`${rowName}\` in README.md:\n${line}`, + ); + } + + const [, label, templatePath] = match; + return { label, path: templatePath }; +} + +function parseReadmeRows(section) { const lines = section .split(/\r?\n/) .map((line) => line.trim()) @@ -93,77 +103,127 @@ function parseReadmeEntries(section) { const dataLines = lines.slice(2); return dataLines.map((line) => { - const match = line.match(ROW_PATTERN); - if (!match) { + const cells = line + .split("|") + .slice(1, -1) + .map((cell) => cell.trim()); + + if (cells.length !== 5) { fail(`Invalid template row format in README.md:\n${line}`); } - const [, name, templatePath, language, description] = match; + const [name, tsCell, pyCell, goCell, description] = cells; + return { name, - path: templatePath, - language, + languages: { + TS: parseLanguageCell(tsCell, "TS", name, line), + PY: parseLanguageCell(pyCell, "PY", name, line), + GO: parseLanguageCell(goCell, "GO", name, line), + }, description: description.trim(), }; }); } -function findDuplicatePaths(entries) { +function findDuplicateNames(rows) { const counts = new Map(); - for (const entry of entries) { - counts.set(entry.path, (counts.get(entry.path) ?? 0) + 1); + for (const row of rows) { + counts.set(row.name, (counts.get(row.name) ?? 0) + 1); + } + + return [...counts.entries()].filter(([, count]) => count > 1).map(([name]) => name); +} + +function findDuplicatePaths(rows) { + const counts = new Map(); + + for (const row of rows) { + for (const language of LANGUAGE_COLUMNS) { + const cell = row.languages[language]; + if (!cell) { + continue; + } + + counts.set(cell.path, (counts.get(cell.path) ?? 0) + 1); + } } return [...counts.entries()].filter(([, count]) => count > 1).map(([entryPath]) => entryPath); } -function validateReadmeEntries(entries) { +function flattenRowPaths(rows) { + return rows.flatMap((row) => + LANGUAGE_COLUMNS.flatMap((language) => { + const cell = row.languages[language]; + return cell ? [cell.path] : []; + }), + ); +} + +function validateReadmeRows(rows) { const problems = []; - for (const entry of entries) { - const segments = entry.path.split("/"); - if (segments.length !== 2) { - problems.push( - `README entry \`${entry.path}\` must point to a first-level template directory.`, - ); - continue; + for (const row of rows) { + if (!row.name) { + problems.push("README has a template row with an empty name."); } - const [root] = segments; - const expectedLanguage = TEMPLATE_ROOTS[root]; - if (!expectedLanguage) { - problems.push( - `README entry \`${entry.path}\` must live under one of: ${Object.keys(TEMPLATE_ROOTS).join(", ")}.`, - ); - continue; + if (!row.description) { + problems.push(`README row \`${row.name}\` is missing a description.`); } - if (entry.language !== expectedLanguage) { - problems.push( - `README entry \`${entry.path}\` is labeled ${entry.language}, but ${root}/ templates must use ${expectedLanguage}.`, - ); + const linkedLanguages = LANGUAGE_COLUMNS.filter((language) => row.languages[language]); + if (linkedLanguages.length === 0) { + problems.push(`README row \`${row.name}\` must link at least one template.`); } - const directoryName = path.posix.basename(entry.path); - if (entry.name !== directoryName) { - problems.push( - `README entry text \`${entry.name}\` must match directory name \`${directoryName}\`.`, - ); - } - if (!entry.description) { - problems.push(`README entry \`${entry.path}\` is missing a description.`); + for (const language of LANGUAGE_COLUMNS) { + const cell = row.languages[language]; + if (!cell) { + continue; + } + + if (cell.label !== language) { + problems.push( + `README row \`${row.name}\` has a ${cell.label} link in the ${language} column.`, + ); + } + + const segments = cell.path.split("/"); + if (segments.length !== 2) { + problems.push( + `README entry \`${cell.path}\` must point to a first-level template directory.`, + ); + continue; + } + + const [root] = segments; + const expectedRoot = LANGUAGE_TO_ROOT[language]; + if (root !== expectedRoot) { + problems.push( + `README entry \`${cell.path}\` is in the ${language} column, but ${language} templates must live under \`${expectedRoot}/\`.`, + ); + } + + const directoryName = path.posix.basename(cell.path); + if (row.name !== directoryName) { + problems.push( + `README row name \`${row.name}\` must match directory name \`${directoryName}\`.`, + ); + } } } return problems; } -function getOrderMessage(expectedEntries, readmeEntries) { - const expectedPaths = expectedEntries.map((entry) => entry.path); - const actualPaths = readmeEntries.map((entry) => entry.path); - const mismatchIndex = expectedPaths.findIndex( - (expectedPath, index) => expectedPath !== actualPaths[index], +function getOrderMessage(expectedRows, readmeRows) { + const expectedNames = expectedRows.map((row) => row.name); + const actualNames = readmeRows.map((row) => row.name); + const mismatchIndex = expectedNames.findIndex( + (expectedName, index) => expectedName !== actualNames[index], ); if (mismatchIndex === -1) { @@ -172,26 +232,33 @@ function getOrderMessage(expectedEntries, readmeEntries) { return [ "README template rows are out of order.", - "Sort rows by template name, then by language in TS/PY/GO order.", - `First mismatch at row ${mismatchIndex + 1}: expected \`${expectedPaths[mismatchIndex]}\`, found \`${actualPaths[mismatchIndex]}\`.`, + "Sort rows alphabetically by template name.", + `First mismatch at row ${mismatchIndex + 1}: expected \`${expectedNames[mismatchIndex]}\`, found \`${actualNames[mismatchIndex]}\`.`, ].join("\n"); } function main() { const readmeContents = readFileSync(README_PATH, "utf8"); - const expectedEntries = getExpectedEntries(); - const readmeEntries = parseReadmeEntries(getReadmeSection(readmeContents)); - - const duplicatePaths = findDuplicatePaths(readmeEntries); - const validationProblems = validateReadmeEntries(readmeEntries); - const expectedPaths = new Set(expectedEntries.map((entry) => entry.path)); - const readmePaths = new Set(readmeEntries.map((entry) => entry.path)); - const missingEntries = expectedEntries.filter((entry) => !readmePaths.has(entry.path)); - const unexpectedEntries = readmeEntries.filter((entry) => !expectedPaths.has(entry.path)); - const orderProblem = getOrderMessage(expectedEntries, readmeEntries); + const expectedRows = getExpectedRows(); + const readmeRows = parseReadmeRows(getReadmeSection(readmeContents)); + + const duplicateNames = findDuplicateNames(readmeRows); + const duplicatePaths = findDuplicatePaths(readmeRows); + const validationProblems = validateReadmeRows(readmeRows); + const expectedPaths = new Set(flattenRowPaths(expectedRows)); + const readmePaths = new Set(flattenRowPaths(readmeRows)); + const missingEntries = [...expectedPaths].filter((entry) => !readmePaths.has(entry)); + const unexpectedEntries = [...readmePaths].filter((entry) => !expectedPaths.has(entry)); + const orderProblem = getOrderMessage(expectedRows, readmeRows); const problems = []; + if (duplicateNames.length > 0) { + problems.push( + `Duplicate README template rows found:\n${duplicateNames.map((name) => `- ${name}`).join("\n")}`, + ); + } + if (duplicatePaths.length > 0) { problems.push( `Duplicate README entries found:\n${duplicatePaths.map((entryPath) => `- ${entryPath}`).join("\n")}`, @@ -204,17 +271,13 @@ function main() { if (missingEntries.length > 0) { problems.push( - `Template directories missing from README:\n${missingEntries - .map((entry) => `- ${entry.path}`) - .join("\n")}`, + `Template directories missing from README:\n${missingEntries.map((entry) => `- ${entry}`).join("\n")}`, ); } if (unexpectedEntries.length > 0) { problems.push( - `README entries that do not match any template directory:\n${unexpectedEntries - .map((entry) => `- ${entry.path}`) - .join("\n")}`, + `README entries that do not match any template directory:\n${unexpectedEntries.map((entry) => `- ${entry}`).join("\n")}`, ); } @@ -231,7 +294,7 @@ function main() { } console.log( - `README template index matches ${expectedEntries.length} first-level template directories.`, + `README template index matches ${expectedRows.length} template rows covering ${expectedPaths.size} first-level template directories.`, ); }