Skip to content

Latest commit

 

History

History
275 lines (206 loc) · 8.01 KB

File metadata and controls

275 lines (206 loc) · 8.01 KB

Plugins

Browse has a plugin system that lets you add custom commands and hook into the command lifecycle. Plugins are TypeScript or JavaScript files that export a BrowsePlugin object.

Quick start

  1. Create a plugin file:
// plugins/hello.ts
// Import the plugin types from the browse repo's src/plugin.ts
// (adjust the relative path to where browse is checked out)
import type { BrowsePlugin } from "../path/to/browse/src/plugin.ts";

const plugin: BrowsePlugin = {
  name: "hello",
  version: "1.0.0",
  commands: [
    {
      name: "hello",
      summary: "Say hello",
      usage: "browse hello [name]",
      handler: async (ctx) => {
        const name = ctx.args[0] ?? "world";
        return { ok: true, data: `Hello, ${name}!` };
      },
    },
  ],
  reporters: [
    {
      name: "teamcity",
      render: ({ flowName, results }) =>
        `##teamcity[testSuiteFinished name='${flowName}' count='${results.length}']`,
    },
  ],
};

export default plugin;
  1. Register it in browse.config.json:
{
  "environments": {},
  "plugins": ["./plugins/hello.ts"]
}
  1. Use it:
browse hello Dan
# Hello, Dan!

Plugin definition

A plugin default-exports a BrowsePlugin object:

type BrowsePlugin = {
  name: string;        // Unique plugin name
  version: string;     // Semver version
  commands?: PluginCommand[];
  reporters?: CustomReporter[];
  hooks?: PluginHooks;
};

Commands

Each command defines a name, help text, and an async handler:

type PluginCommand = {
  name: string;           // Must not collide with built-in or other plugin commands
  summary: string;        // One-line description for `browse help`
  usage: string;          // Full usage text for `browse help <command>`
  flags?: string[];       // Known flags for validation (e.g. ["--json", "--verbose"])
  timeoutExempt?: boolean; // If true, exempt from global --timeout
  handler: (ctx: CommandContext) => Promise<Response>;
};

CommandContext

Every handler receives a context bag with everything it needs:

type CommandContext = {
  page: Page;                              // Active Playwright page
  context: BrowserContext;                 // Session's browser context
  config: BrowseConfig | null;            // Loaded browse config
  args: string[];                         // Command arguments
  sessionState: Record<string, unknown>;  // Per-plugin, per-session state
  request: {
    session?: string;
    json?: boolean;
    timeout?: number;
  };
};

sessionState persists across commands within a session and is scoped per plugin. Use it to track state between commands without managing your own maps.

Response

Handlers return the standard browse response:

type Response =
  | { ok: true; data: string }
  | { ok: false; error: string };

Custom reporters

Plugins can contribute custom flow reporters that are available through browse flow --reporter <name> and browse test-matrix --reporter <name>.

type CustomReporter = {
  name: string;  // Must not collide with built-in reporters like junit or json
  render: (ctx: ReporterRenderContext) => string;
};

type ReporterRenderContext = {
  flowName: string;
  results: StepResult[];
  durationMs: number;
};

Reporter names must be unique across all loaded plugins. Collisions with built-in reporters or another plugin reporter are skipped with a warning.

Hooks

Plugins can hook into the lifecycle of any command (built-in or plugin):

type PluginHooks = {
  init?: (config: BrowseConfig | null) => Promise<void>;
  beforeCommand?: (cmd: string, ctx: CommandContext) => Promise<Response | void>;
  afterCommand?: (cmd: string, ctx: CommandContext, response: Response) => Promise<void>;
  cleanup?: () => Promise<void>;
};
Hook When Use case
init Daemon startup, after plugin is loaded Set up resources, validate config
beforeCommand Before any command executes Auth gating, logging, rate limiting. Return a Response to short-circuit
afterCommand After any command executes Logging, metrics, telemetry
cleanup Daemon shutdown Release resources, flush buffers

Discovery

Plugins are discovered from two sources:

1. Config file

List plugin paths in browse.config.json:

{
  "plugins": [
    "./plugins/my-plugin.ts",        
    "/absolute/path/plugin.ts",       
    "browse-plugin-lighthouse"        
  ]
}
  • Relative paths resolve from the config file's directory
  • Absolute paths are used as-is
  • Bare names are resolved as npm packages via import()

2. Global plugins directory

Any .ts or .js files in ~/.browse/plugins/ are automatically loaded. This is useful for personal plugins that apply across all projects.

Config-declared plugins take precedence on name collision.

3. Marketplace discovery

Browse can also help you discover official starters and published community plugins before you install them:

browse plugins official
browse plugins search slack
browse plugins search jira --limit 10
  • browse plugins official lists first-party plugin starters, including their in-repo source paths
  • browse plugins search <query> searches npm for packages tagged with the browse-plugin keyword
  • Add the global --json flag for machine-readable output

Official starter plugins

Browse ships first-party starter plugins under examples/plugins/ for common team integrations:

  • ./examples/plugins/slack/index.ts — send a message to a Slack webhook
  • ./examples/plugins/discord/index.ts — send a message to a Discord webhook
  • ./examples/plugins/jira/index.ts — create a JIRA issue for the current page

Register them directly in browse.config.json while the standalone packages are being formalised:

{
  "plugins": ["./examples/plugins/slack/index.ts"]
}

Each plugin directory includes its own README with the required environment variables and usage examples.

Error handling

The plugin system is designed to be resilient:

  • Load failures are non-fatal — a bad plugin is skipped with a warning, other plugins and the daemon still work
  • Command handlers are wrapped in try/catch — a throwing handler returns { ok: false, error: "Plugin 'name' error: ..." } instead of crashing the daemon
  • Hook errors are isolated — a throwing beforeCommand hook produces an error response; a throwing afterCommand or cleanup hook is silently ignored
  • Init failures don't prevent registration — if init throws, the plugin's commands and hooks are still registered
  • Name collisions are rejected — a plugin command that collides with a built-in command or another plugin is skipped with a warning

Flag validation

If your command defines flags, browse validates them before your handler runs. Unknown flags produce a helpful error:

{
  name: "audit",
  flags: ["--json", "--verbose", "--threshold"],
  handler: async (ctx) => { /* ... */ },
}
browse audit --unknown-flag
# Error: Unknown flag '--unknown-flag' for command 'audit'.

Commands without flags skip validation (useful for commands that accept freeform arguments).

Timeout

Plugin commands go through the same timeout system as built-in commands (default 30s, configurable via --timeout). Set timeoutExempt: true for long-running commands like crawlers or report generators.

Publishing plugins

For npm distribution, create a package with a default export:

browse-plugin-foo/
  index.ts    # exports default BrowsePlugin
  package.json

Users install and reference it by package name:

npm install browse-plugin-foo
{
  "plugins": ["browse-plugin-foo"]
}

For type safety during development, import the types from the browse repo's src/plugin.ts (browse is not published to npm, so use a relative path to a checkout — see examples/plugins/ for working examples):

import type { BrowsePlugin, CommandContext } from "../path/to/browse/src/plugin.ts";