npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bruno-lifecycle-adapter

v0.1.0

Published

Wraps Bruno CLI and exposes an observable execution lifecycle for external integrations.

Readme

bruno-lifecycle-adapter

A TypeScript package that wraps Bruno CLI and exposes an observable execution lifecycle for external integrations.

It runs Bruno collections through bru run, captures process output, parses generated reports, and emits typed lifecycle events such as run start/end, request execution, test results, assertions, stdout, and stderr.

The adapter is designed to be independent from Bruno's core, with a clean architecture, strong typing, automated tests, and a stable internal contract that can evolve even if Bruno's report format changes over time.


Installation

npm install bruno-lifecycle-adapter

Bruno CLI must be installed separately (the adapter calls bru run):

npm install -g @usebruno/cli

Usage

import { BrunoLifecycleAdapter } from 'bruno-lifecycle-adapter';

const adapter = new BrunoLifecycleAdapter();

adapter.on('run:started', (event) => {
  console.log(`Run started (pid ${event.pid}), runId: ${event.runId}`);
});

adapter.on('request:finished', (event) => {
  console.log(`${event.requestName} → ${event.status} (${event.responseStatus ?? '?'})`);
});

adapter.on('test:finished', (event) => {
  console.log(`  test "${event.testName}": ${event.status}`);
});

adapter.on('assertion:result', (event) => {
  console.log(`  assertion "${event.assertion}": ${event.passed ? 'pass' : 'fail'}`);
});

const summary = await adapter.run({
  cwd: process.cwd(),
  collectionPath: './my-collection',
  env: 'local',
  reporterJsonPath: './tmp/bruno-report.json',
});

console.log('Exit code:', summary.exitCode);
console.log('Passed requests:', summary.passedRequests, '/', summary.totalRequests);

Configuration

interface AdapterRunConfig {
  /** Working directory where `bru run` is executed. */
  cwd: string;

  /** Path to the Bruno collection directory or specific `.bru` file. */
  collectionPath: string;

  /** Environment name passed to `--env`. */
  env?: string;

  /** Path where Bruno writes its JSON report (`--reporter-json`). */
  reporterJsonPath?: string;

  /** When true, passes `-r` to `bru run` to recurse into sub-folders. */
  recursive?: boolean;

  /** Extra CLI arguments appended verbatim after the collection path. */
  extraArgs?: string[];

  /** Path to the `bru` binary. Defaults to `bru` (resolved from PATH). */
  bruBin?: string;

  /** Timeout in milliseconds for the entire run. `0` means no timeout. */
  timeoutMs?: number;
}

Event Subscription

// Subscribe and get an unsubscribe handle
const unsubscribe = adapter.on('stdout', (event) => {
  process.stdout.write(event.chunk);
});

// Subscribe once – fires only on the first emission
adapter.once('run:finished', (event) => {
  console.log('Total duration:', event.durationMs, 'ms');
});

// Unsubscribe manually
adapter.off('stderr', myStderrHandler);

// Or use the returned unsubscribe function
unsubscribe();

Lifecycle Events

| Event | Reliability | Description | |---|---|---| | run:starting | native | Emitted before spawning the process | | run:started | native | Emitted once the child process has a PID | | run:finished | native | Emitted when the process exits with code 0 | | run:failed | native | Emitted when the process exits with non-zero code, times out, or fails to spawn | | request:discovered | inferred | Emitted per .bru request file found in the collection directory, fired after the process exits and before report-derived events | | request:started | derived | Emitted immediately before each request:finished or request:skipped (derived from the JSON report; fires in batch after the run) | | request:finished | derived | Emitted per non-skipped request after successful report parsing | | request:skipped | derived | Emitted per skipped request after successful report parsing | | test:started | derived | Emitted immediately before each test:finished (derived from the JSON report; fires in batch after the run) | | test:finished | derived | Emitted per test result after successful report parsing | | assertion:result | derived | Emitted per assertion after successful report parsing | | stdout | native | Raw stdout chunk from the bru run process | | stderr | native | Raw stderr chunk from the bru run process | | report:json:ready | derived | Emitted only after the JSON report is parsed successfully |

Event Reliability

Each event payload includes a reliability field:

  • native – directly observed from process stdout/stderr or exit code
  • derived – computed from one or more native signals (e.g. parsed report data)
  • inferred – best-effort; could not be confirmed from available signals

Event Payloads

All event payloads extend EventMetadata:

interface EventMetadata {
  runId: string;       // UUID identifying this run
  timestamp: string;   // ISO 8601 timestamp
  reliability: 'native' | 'derived' | 'inferred';
}

Example payloads:

interface RunStartedEvent extends EventMetadata {
  event: 'run:started';
  cwd: string;
  collectionPath: string;
  pid: number;
}

interface RequestFinishedEvent extends EventMetadata {
  event: 'request:finished';
  requestName: string;
  requestFile: string;
  status: 'discovered' | 'started' | 'finished' | 'skipped' | 'failed';
  responseStatus?: number;
  durationMs?: number;
  error?: { message: string; stack?: string };
}

interface TestFinishedEvent extends EventMetadata {
  event: 'test:finished';
  requestName: string;
  testName: string;
  status: 'started' | 'passed' | 'failed' | 'skipped';
  error?: { message: string; stack?: string };
}

Execution Summary

adapter.run() resolves with an ExecutionSummary:

interface ExecutionSummary {
  runId: string;
  collectionPath: string;
  startedAt: string;
  finishedAt: string;
  durationMs: number;
  exitCode: number;
  status: 'starting' | 'running' | 'finished' | 'failed' | 'cancelled';
  totalRequests: number;
  passedRequests: number;
  failedRequests: number;
  skippedRequests: number;
  totalTests: number;
  passedTests: number;
  failedTests: number;
  totalAssertions: number;
  passedAssertions: number;
  failedAssertions: number;
  requests: RequestResult[];
  error?: ErrorDetail;
}

Custom Report Parser

You can supply your own report parser to handle custom Bruno report formats:

import { BrunoLifecycleAdapter } from 'bruno-lifecycle-adapter';
import type { ReportParserContract } from 'bruno-lifecycle-adapter';

class MyCustomParser implements ReportParserContract {
  async parse(reportPath: string): Promise<ExecutionSummary> {
    // read and map your format
  }
}

const adapter = new BrunoLifecycleAdapter(new MyCustomParser());

Limitations

  • No native per-request events from process stdout. Bruno CLI does not emit structured events to stdout; per-request and per-test events are derived from the JSON report. Subscribe to stdout for raw output.
  • Report events require reporterJsonPath. Without a JSON report path, request:finished, test:finished, assertion:result, and report:json:ready are not emitted.
  • Bruno JSON schema may change. The parser maps known fields and falls back gracefully, but new Bruno versions may change the report schema. Inspect the raw stdout events if you need forward compatibility.
  • Timeout kills the process with SIGTERM. The process is sent SIGTERM on timeout; clean shutdown depends on the OS and Bruno's signal handling.

Project Structure

src/
  domain/
    events.ts          # All lifecycle event types and execution models
    models.ts          # AdapterRunConfig and ReportParserContract interfaces
  application/
    BrunoLifecycleAdapter.ts  # Main adapter (spawns bru, emits events)
  infrastructure/
    TypedEventBus.ts          # Strongly-typed event bus
    BrunoJsonReportParser.ts  # Bruno JSON report → internal model
  shared/
    utils.ts                  # generateRunId, nowIso, elapsedMs
  index.ts                    # Public exports
tests/
  unit/
    TypedEventBus.test.ts
    BrunoJsonReportParser.test.ts
    BrunoLifecycleAdapter.test.ts
    BruFileScanner.test.ts
  integration/
    adapter.integration.test.ts   # End-to-end integration tests
    fixtures/
      bruno-project/              # Sample Bruno collection
        bruno.json
        environments/local.bru
        health-check.bru
        echo-post.bru
        multi-request-flow/
          01-list-items.bru
          02-get-item-by-id.bru
      external-listener/          # External JS consumer package
        package.json
        index.js
  e2e/
    app-server.js                 # Standalone HTTP server for Docker E2E
    run.js                        # Docker E2E test runner entry-point

Development

npm install
npm run build        # compile with tsup
npm test             # run unit tests with vitest
npm run test:unit    # explicit alias for unit tests
npm run test:e2e     # run full E2E suite inside Docker Compose
npm run typecheck    # tsc --noEmit
npm run lint         # eslint
npm run format       # prettier --write

Integration Tests

Integration tests verify the adapter against a real Bruno installation.

Prerequisites

  1. Build the adapter:
    npm run build
  2. Install Bruno CLI:
    npm install -g @usebruno/cli
  3. Install external listener dependencies:
    npm run test:integration:setup

Running integration tests

npm run test:integration

Full CI-equivalent local run

This command reproduces the exact CI execution sequence:

npm run ci:local

It runs: build → lint → typecheck → unit tests → integration setup → integration tests.

Note: Integration tests fail (they do not skip) when bru is not found on PATH. This is intentional — a passing run without Bruno would give false confidence. Unit tests (npm test) run without Bruno.

How integration tests work

  1. A minimal HTTP server is started on port 47891 to serve the Bruno collection's requests locally — no external network calls required.
  2. The external listener (tests/integration/fixtures/external-listener/) is spawned as a child process. It imports the adapter, subscribes to all lifecycle events, and writes structured JSON-lines to stdout for each event.
  3. The integration test captures the listener's stdout and asserts that expected events were logged — confirming that the adapter correctly surfaces lifecycle information to external consumers.

CI

Integration tests run automatically on pull requests targeting main via the .github/workflows/integration.yml workflow. They do not run on feature branch pushes to keep CI fast.


E2E Tests

E2E tests verify the adapter end-to-end inside isolated Docker containers — no host-level Bruno installation is required.

Running E2E tests

docker compose up --build --exit-code-from e2e --abort-on-container-exit

Or via the npm script:

npm run test:e2e

How E2E tests work

The docker-compose.yml starts two services:

  1. app (node:20-alpine): a minimal HTTP server exposing the same routes as the integration test server (/health, /echo, /items, /items/1). Reachable within the Compose network as http://app:47891.
  2. e2e (built from Dockerfile): installs Bruno CLI globally, resolves the adapter from the local dist/, installs the external listener, and runs tests/e2e/run.js.

The e2e container exits 0 on success, 1 on failure. docker compose up --exit-code-from e2e propagates that exit code to the host.

CI

E2E tests run automatically on pull requests targeting main via the .github/workflows/e2e.yml workflow. Two jobs run in sequence:

  1. docker-build — verifies the Dockerfile builds successfully.
  2. e2e — runs docker compose up with the full stack.

License

MIT