From 4b49dcd7b6198c0be780b6a107ed29c75e600b6c Mon Sep 17 00:00:00 2001 From: anil-bd Date: Mon, 25 May 2026 11:05:18 +0200 Subject: [PATCH] feat(help): add Examples section to every customer-facing command's --help AWS CLI, gh, and Stripe CLI all ship an Examples section in every command's --help output. Until this change, none of brightdata's customer-facing commands did. The first reflex on hitting an unfamiliar command is --help; seeing Usage + Options without examples forces the user to alt-tab to the README, which is a 1,200-line file that does not surface examples discoverably either. Adds an Examples section to the six Scraper-Studio-adjacent commands. Examples use real, public, scrape-friendly domains (Hacker News, Y Combinator, real SERP queries) per house-style C5. No example.com. Implementation - new src/utils/help.ts with format_examples() + add_examples() so every command renders the section identically (`# description` then `$ cmd` per example, blank line between examples, "Examples:" header) - six command files each gain three examples covering: minimal happy path, one useful variant, one production pattern - attached via Commander's addHelpText('after', ...), which renders during outputHelp() (the --help path) without affecting helpInformation() or parse-time behavior Tests - src/__tests__/utils/help.test.ts: ten tests - format_examples() unit (3): empty input, single, multiple with blank- line separator - add_examples() integration (1): on a fresh Command, renders correctly - regression contract (6): each of `scraper create`, `scraper run`, `discover`, `search`, `pipelines`, `scrape` has an Examples block, has at least one $ brightdata command inside it, and uses no example.com URL in the Examples block - the regression test renders help via configureOutput() + outputHelp() to capture the addHelpText output (helpInformation() does not include it) - full suite: 183 pass / 9 fail. The 9 failures all pre-exist on main and are unrelated to this change (verified by running the suite on the stashed baseline) What this does NOT do - does not touch the misleading https://example.com/webhook default value on `--deliver-webhook` (that is issue N10, its own PR) - does not change Usage, Options, Arguments, or any command behavior - does not change exit codes, output streams, or stdout/stderr routing - does not add a dependency Refs: audit issue N13 (scraper-studio-cli-demo/ISSUES.md), bright-dx-writer critique (scraper-studio-cli-demo/CRITIQUE.md), DX content pattern C5 --- src/__tests__/utils/help.test.ts | 104 +++++++++++++++++++++++++++++++ src/commands/dataset.ts | 19 ++++++ src/commands/discover.ts | 19 ++++++ src/commands/scrape.ts | 19 ++++++ src/commands/scraper.ts | 45 +++++++++++++ src/commands/search.ts | 18 ++++++ src/utils/help.ts | 28 +++++++++ 7 files changed, 252 insertions(+) create mode 100644 src/__tests__/utils/help.test.ts create mode 100644 src/utils/help.ts diff --git a/src/__tests__/utils/help.test.ts b/src/__tests__/utils/help.test.ts new file mode 100644 index 0000000..b058d57 --- /dev/null +++ b/src/__tests__/utils/help.test.ts @@ -0,0 +1,104 @@ +import {describe, it, expect} from 'vitest'; +import {Command} from 'commander'; +import {format_examples, add_examples} from '../../utils/help'; +import type {Example} from '../../utils/help'; +import {scraper_command} from '../../commands/scraper'; +import {discover_command} from '../../commands/discover'; +import {search_command} from '../../commands/search'; +import {pipelines_command} from '../../commands/dataset'; +import {scrape_command} from '../../commands/scrape'; + +describe('utils/help.format_examples', ()=>{ + it('returns empty string for no examples', ()=>{ + expect(format_examples([])).toBe(''); + }); + + it('renders a single example with comment + dollar-prefixed command', ()=>{ + const exs: Example[] = [{description: 'Do a thing', command: 'cmd x'}]; + const out = format_examples(exs); + expect(out).toContain('\nExamples:\n'); + expect(out).toContain(' # Do a thing'); + expect(out).toContain(' $ cmd x'); + }); + + it('separates multiple examples with a blank line', ()=>{ + const exs: Example[] = [ + {description: 'A', command: 'cmd a'}, + {description: 'B', command: 'cmd b'}, + ]; + const out = format_examples(exs); + expect(out).toMatch(/cmd a\n\n # B/); + }); +}); + +describe('utils/help.add_examples attaches to a Commander command', ()=>{ + it('appears in the rendered --help output', ()=>{ + const cmd = new Command('demo') + .description('A demo command') + .argument('', 'The thing'); + add_examples(cmd, [ + {description: 'Demo this', command: 'demo widget'}, + ]); + let captured = ''; + cmd.configureOutput({writeOut: (s)=>{ captured += s; }}); + cmd.outputHelp(); + expect(captured).toContain('Examples:'); + expect(captured).toContain('# Demo this'); + expect(captured).toContain('$ demo widget'); + }); +}); + +// Helper: render the FULL --help output, including addHelpText('after', ...) +// content (which helpInformation() alone does not include). +const render_help = (cmd: Command): string=>{ + let captured = ''; + cmd.configureOutput({ + writeOut: (s)=>{ captured += s; }, + writeErr: (s)=>{ captured += s; }, + }); + cmd.outputHelp(); + return captured; +}; + +// Regression: every Scraper-Studio-adjacent command must ship an Examples +// section. This is the contract this PR establishes; if a future command +// or refactor drops the Examples block, this test fires. +describe('every customer-facing command has Examples in --help', ()=>{ + const scraper_create = + scraper_command.commands.find(c=>c.name() == 'create')!; + const scraper_run = + scraper_command.commands.find(c=>c.name() == 'run')!; + + const cases: [string, Command][] = [ + ['scraper create', scraper_create], + ['scraper run', scraper_run], + ['discover', discover_command], + ['search', search_command], + ['pipelines', pipelines_command], + ['scrape', scrape_command], + ]; + + for (const [name, cmd] of cases) + { + it(`${name} --help includes an Examples section`, ()=>{ + const help = render_help(cmd); + const examples_idx = help.indexOf('\nExamples:\n'); + expect(examples_idx, `${name} is missing the Examples section`) + .toBeGreaterThan(-1); + const examples_block = help.slice(examples_idx); + // At least one example (`$ brightdata ...`) must be present + // inside the Examples block. + expect(examples_block, + `${name} Examples section has no $ brightdata command`) + .toMatch(/\$\s+brightdata\s/); + // House style C5: examples use real, scrape-friendly domains, + // never the fake example.com / example.test placeholders. + // (The `--deliver-webhook` default in option text is N10, out + // of scope for this PR, so we scope this check to the Examples + // block only, not the whole --help output.) + expect(examples_block, + `${name} examples leak the fake example.com domain`) + .not.toMatch(/https?:\/\/(www\.)?example\.com/); + }); + } +}); diff --git a/src/commands/dataset.ts b/src/commands/dataset.ts index 0331cb4..5b3772b 100644 --- a/src/commands/dataset.ts +++ b/src/commands/dataset.ts @@ -4,6 +4,7 @@ import {get, post} from '../utils/client'; import {print, dim, fail} from '../utils/output'; import {start as start_spinner} from '../utils/spinner'; import {parse_timeout, poll_until} from '../utils/polling'; +import {add_examples} from '../utils/help'; import type { Webdata_format, Webdata_opts, @@ -305,4 +306,22 @@ const pipelines_command = new Command('pipelines') .option('-k, --api-key ', 'Override API key') .action(handle_pipelines); +add_examples(pipelines_command, [ + { + description: 'List every available pipeline type', + command: 'brightdata pipelines list', + }, + { + description: 'Scrape a single Amazon product page', + command: 'brightdata pipelines amazon_product ' + +'https://www.amazon.com/dp/B08N5WRWNW --pretty', + }, + { + description: 'Pull a LinkedIn person profile by URL, save as CSV', + command: 'brightdata pipelines linkedin_person_profile ' + +'https://www.linkedin.com/in/satyanadella --format csv ' + +'-o profile.csv', + }, +]); + export {pipelines_command, handle_pipelines}; diff --git a/src/commands/discover.ts b/src/commands/discover.ts index f8368ce..79e9a60 100644 --- a/src/commands/discover.ts +++ b/src/commands/discover.ts @@ -4,6 +4,7 @@ import {ensure_authenticated} from '../utils/auth'; import {start as start_spinner} from '../utils/spinner'; import {parse_timeout, poll_until} from '../utils/polling'; import {print, print_table, dim, fail, is_tty} from '../utils/output'; +import {add_examples} from '../utils/help'; import type { Discover_request, Discover_trigger_response, @@ -184,5 +185,23 @@ const discover_command = new Command('discover') .option('-k, --api-key ', 'Override API key') .action(handle_discover); +add_examples(discover_command, [ + { + description: 'Discover and rank URLs relevant to a query', + command: 'brightdata discover "open-source AI agent frameworks"', + }, + { + description: 'Add an AI intent filter and cap results', + command: 'brightdata discover "vector databases" ' + +'--intent "production-ready, self-hostable" --num-results 10', + }, + { + description: 'Localize to a country and include page content as ' + +'markdown', + command: 'brightdata discover "best coffee in Berlin" ' + +'--country DE --include-content', + }, +]); + export {discover_command, handle_discover, build_request, extract_status, format_markdown, print_discover_table}; diff --git a/src/commands/scrape.ts b/src/commands/scrape.ts index 2fde6d8..5dd31e6 100644 --- a/src/commands/scrape.ts +++ b/src/commands/scrape.ts @@ -5,6 +5,7 @@ import {ensure_authenticated} from '../utils/auth'; import {resolve} from '../utils/config'; import {start as start_spinner} from '../utils/spinner'; import {print, success, fail, info} from '../utils/output'; +import {add_examples} from '../utils/help'; import type { Scrape_format, Scrape_request, @@ -106,4 +107,22 @@ const scrape_command = new Command('scrape') .option('-k, --api-key ', 'Override API key') .action(handle_scrape); +add_examples(scrape_command, [ + { + description: 'Scrape a public page and get markdown (default ' + +'format)', + command: 'brightdata scrape https://news.ycombinator.com', + }, + { + description: 'Return JSON with response metadata, save to a file', + command: 'brightdata scrape https://news.ycombinator.com ' + +'--format json --pretty -o hn.json', + }, + { + description: 'Geo-target Germany with a mobile user agent', + command: 'brightdata scrape https://www.google.com/search?q=heise ' + +'--country de --mobile', + }, +]); + export {scrape_command, handle_scrape}; diff --git a/src/commands/scraper.ts b/src/commands/scraper.ts index 9ab4400..77845a9 100644 --- a/src/commands/scraper.ts +++ b/src/commands/scraper.ts @@ -1,4 +1,5 @@ import {Command} from 'commander'; +import {add_examples} from '../utils/help'; import {post, get} from '../utils/client'; import {load as load_config} from '../utils/config'; import {ensure_authenticated} from '../utils/auth'; @@ -572,6 +573,50 @@ const run_subcommand = new Command('run') .option('-k, --api-key ', 'Override API key') .action(handle_run_scraper); +add_examples(create_subcommand, [ + { + description: 'Build a scraper for a public page (AI generation ' + +'takes 5 to 10 minutes)', + command: 'brightdata scraper create https://news.ycombinator.com ' + +'"Extract the top 30 stories: title, url, points, author, ' + +'comment count."', + }, + { + description: 'Name the scraper and save the full AI output for ' + +'inspection', + command: 'brightdata scraper create https://www.ycombinator.com/' + +'companies?batch=W26 "For each company card, extract name, ' + +'vertical, tagline, link" --name yc-w26 --pretty -o create.json', + }, + { + description: 'Custom delivery webhook (default is a stub, set ' + +'this when wiring to your own backend)', + command: 'brightdata scraper create https://news.ycombinator.com ' + +'"Extract top stories" --deliver-webhook ' + +'https://your-app.test/scraper-callback', + }, +]); + +add_examples(run_subcommand, [ + { + description: 'Run a scraper against a single URL (async, polls ' + +'until done)', + command: 'brightdata scraper run c_mp3tuab31lswoxvpws ' + +'https://news.ycombinator.com --pretty', + }, + { + description: 'Sync mode for small fast pages (server-side 25 to ' + +'50 second cap)', + command: 'brightdata scraper run c_mp3tuab31lswoxvpws ' + +'https://news.ycombinator.com --sync', + }, + { + description: 'Save output as CSV (extension chooses format)', + command: 'brightdata scraper run c_mp3tuab31lswoxvpws ' + +'https://news.ycombinator.com -o stories.csv', + }, +]); + const scraper_command = new Command('scraper') .description('Build and manage Bright Data scrapers') .addCommand(create_subcommand) diff --git a/src/commands/search.ts b/src/commands/search.ts index fdce82a..d342e32 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -4,6 +4,7 @@ import {ensure_authenticated} from '../utils/auth'; import {resolve} from '../utils/config'; import {start as start_spinner} from '../utils/spinner'; import {print, print_table, fail, dim} from '../utils/output'; +import {add_examples} from '../utils/help'; import type { Search_engine, Search_type, @@ -280,4 +281,21 @@ const search_command = new Command('search') .option('-k, --api-key ', 'Override API key') .action(handle_search); +add_examples(search_command, [ + { + description: 'Web search via Google (default engine)', + command: 'brightdata search "web scraping best practices 2026"', + }, + { + description: 'News search localized to a country', + command: 'brightdata search "AI agent funding" ' + +'--type news --country us --pretty', + }, + { + description: 'Bing image search, mobile device profile', + command: 'brightdata search "san francisco skyline" ' + +'--engine bing --type images --device mobile', + }, +]); + export {search_command, handle_search}; diff --git a/src/utils/help.ts b/src/utils/help.ts new file mode 100644 index 0000000..468793c --- /dev/null +++ b/src/utils/help.ts @@ -0,0 +1,28 @@ +import {Command} from 'commander'; + +type Example = { + description: string; // one-line "what this does" + command: string; // the actual shell command, real domains, no example.com +}; + +const format_examples = (examples: Example[]): string=>{ + if (!examples.length) + return ''; + const blocks = examples.map(ex=> + ` # ${ex.description}\n $ ${ex.command}`).join('\n\n'); + return '\nExamples:\n'+blocks+'\n'; +}; + +// Attach an "Examples:" section to a Commander command, rendered after the +// auto-generated Usage/Options block when the user runs ` --help`. +// +// House style: +// - Use real, public, scrape-friendly domains (Hacker News, Y Combinator, +// real SERP queries). Never example.com or fake IDs. +// - One line of comment per example, then the literal command on the next. +// - Order: simplest first, then a useful variant or two. Cap at 3-4. +const add_examples = (cmd: Command, examples: Example[]): Command=> + cmd.addHelpText('after', format_examples(examples)); + +export {add_examples, format_examples}; +export type {Example};