Programmatic API (@truspec/core)
The CLI, the MCP server, the web UI, and the VS Code extension are all thin shells over one engine: @truspec/core. You can import it directly to build your own tooling — a custom runner, a CI script, a code-gen step, or an editor integration.
npm install @truspec/coreThe engine is pure TypeScript (ships its own types) and side-effect-free. The browser-safe modules (format, runner) have no platform dependencies; the filesystem/server modules (workspace, spec, importers, mock) use Node APIs and are kept out of the main entry on purpose.
Requires Node ≥ 22 for the filesystem/server modules (it uses the platform
fetchand modern streaming APIs).
Modules
Import each capability from its subpath:
import { parse, RequestSchema } from "@truspec/core/format";
import { runRequest } from "@truspec/core/runner";
import { runPath, discoverRequests } from "@truspec/core/workspace";
import { driftReport, coverageReport } from "@truspec/core/spec";
import { importPostmanFile } from "@truspec/core/importers";
import { startMockServer } from "@truspec/core/mock";| Subpath | Purpose | Platform |
|---|---|---|
@truspec/core/format | Parse / serialize / validate files; schemas & types. | browser-safe |
@truspec/core/runner | Execute one request; interpolation, auth, assertions, scripts. | browser-safe |
@truspec/core/workspace | Discovery, folder inheritance, env + secret resolution, run a path. | Node |
@truspec/core/spec | OpenAPI parsing, drift, coverage, scaffold, live probe. | Node |
@truspec/core/importers | Postman, Bruno, Insomnia, curl & HAR → .tspec.yaml. | Node |
@truspec/core/exporters | .tspec.yaml → a Postman collection. | Node |
@truspec/core/codegen | One request → a runnable snippet in 17 clients/languages. | browser-safe |
@truspec/core/lint | Static checks over a collection. | Node |
@truspec/core/docs | A collection → deterministic Markdown. | Node |
@truspec/core/jsonpath | The JSONPath subset assertions and captures use. | browser-safe |
@truspec/core/mock | Local mock server from a spec. | Node |
@truspec/core/http | Shared HTTP-server lifecycle (a shutdown that always terminates). | Node |
@truspec/core/schema/*.json | The published JSON Schema files, importable directly. | any |
The main entry (@truspec/core) re-exports format and runner as namespaces; prefer the subpaths above so bundlers can tree-shake and you don't pull Node modules into a browser build.
Every row of that table is checked against package.json's exports map by a test, in both directions — a subpath that ships undocumented and a documented subpath that does not ship are both failures.
format — parse, validate, serialize
The format module is the single most useful import: it validates that something is a legal TruSpec file before you write it.
parse
parse exposes three helpers — parse.request, parse.folderConfig, and parse.environment — each with the same four methods:
import { parse } from "@truspec/core/format";
// parse(text) → throws on invalid input, with a readable message
const req = parse.request.parse(yamlText);
// safeParse(text) → never throws; { ok, data?, error?, issues? }
const result = parse.request.safeParse(yamlText);
if (!result.ok) console.error(result.error);
// validate(value) → validate an already-parsed object (e.g. an agent's write payload)
const v = parse.request.validate({ name: "x", url: "{{base}}/x" });
// serialize(value) → validate, then emit clean, diff-friendly YAML
const yaml = parse.request.serialize(req);| Method | Signature | Behavior |
|---|---|---|
parse | (text: string) => T | Parse YAML; throws on invalid input. |
safeParse | (text: string) => ParseResult<T> | Parse YAML without throwing. |
validate | (value: unknown) => ParseResult<T> | Validate an in-memory object. |
serialize | (value: T) => string | Validate, then serialize to YAML. |
serialize validates before producing YAML — the hard rule that lets agents and scripts never emit an invalid file. ParseResult<T> is { ok: boolean; data?: T; error?: string; issues?: ZodIssue[] }.
Schemas & types
The Zod schemas and their inferred TypeScript types are exported for building and validating requests programmatically:
import {
RequestSchema, FolderConfigSchema, EnvironmentSchema,
Assertion, Auth, Body, CaptureSource, HttpMethod,
SCHEMA_VERSION,
} from "@truspec/core/format";
import type {
TruSpecRequest, TruSpecFolderConfig, TruSpecEnvironment,
TruSpecAssertion, TruSpecAuth, TruSpecBody, TruSpecMethod, TruSpecCaptureSource,
} from "@truspec/core/format";buildJsonSchemas
Generate the JSON Schema documents from the Zod source (this is what pnpm gen:schema calls):
import { buildJsonSchemas } from "@truspec/core/format";
const { request, folder, environment } = buildJsonSchemas();runner — execute a request
runRequest runs a single, already-parsed request and evaluates its assertions. It never throws — every failure (network error, unresolved variable, failed assertion) lands in the returned RunResult.
import { runRequest } from "@truspec/core/runner";
const result = await runRequest(request, {
folder, // optional TruSpecFolderConfig to inherit from
vars: { baseUrl: "http://localhost:4000", petId: "1" },
timeoutMs: 10_000, // default 30000; 0 disables
});
console.log(result.ok, result.response?.status);
for (const a of result.assertions) console.log(a.ok, a.message);RunContext
| Field | Type | Notes |
|---|---|---|
folder | TruSpecFolderConfig | Inherited base URL / headers / auth. |
vars | Vars | Variables for interpolation. |
fetch | typeof fetch | Inject a custom fetch (tests, proxies). Defaults to global fetch. |
now | () => number | Inject a clock for deterministic durations. |
timeoutMs | number | Per-request timeout. Default 30000; 0 disables. |
maxResponseBytes | number | Cap on the response body (default 50 MB) to bound memory. |
RunResult
One request's outcome. This is the object truspec run --json, the MCP tools and the web client all read, so treat it as a contract: a field is added, never repurposed.
| Field | Type | Present |
|---|---|---|
name | string | always |
request | { method, url } | always — as sent, after interpolation |
filePath | string? | when the request came from a file |
ok | boolean | always — every assertion passed and no error |
error | string? | when the request could not be completed at all |
missingVars | string[]? | {{names}} that resolved to nothing |
response | object? | absent when nothing came back (see below) |
assertions | AssertionResult[] | always (may be empty) |
captured | Record<string, VarValue>? | values capture saved for later requests |
missedCaptures | { name, source, reason? }[]? | captures that matched nothing |
redirects | string[]? | hops followed, with options.followRedirects |
redirectLimitHit | boolean? | the chain stopped at maxRedirects, not at the server |
retries | number? | re-sends, with options.retries |
scriptLogs | { level, message }[]? | lines script.pre/script.post printed with console.* — present on a failure result too |
iteration | number? | 1-based row/repeat index, for --data / --repeat |
response:
| Field | Type | Present |
|---|---|---|
status / statusText | number / string | always |
durationMs | number | always |
headers | Record<string, string> | always — keys lowercased |
bodyText | string | always — decoded with the response's charset, BOM stripped |
bytes | number? | size as it arrived, which is not bodyText.length |
binary | boolean? | the body is not text; bodyText is a lossy decode of it |
bodyBase64 | string? | the real bytes of a binary body, when small enough to carry |
events | SseEvent[]? | parsed text/event-stream events ({ event?, data, id? }) |
streamTruncated | boolean? | the stream was closed at the event cap or by the timeout, not by the server |
Lower-level building blocks
The runner also exports the pieces runRequest is built from, in case you need them directly:
| Export | Purpose |
|---|---|
resolveRequest(req, opts) | Interpolate + apply folder/auth → a concrete EffectiveRequest. |
interpolate(str, vars) / interpolateDeep(obj, vars) | {{var}} substitution, reporting missing names. |
jsonpath(value, path) | The JSONPath selector used by assertions and capture. |
evaluateAssertions(list, view) | Run assertions against a ResponseView. |
evaluateCaptures(map, view) | Extract captured variables from a response. |
runPreScript(src, vars) / runPostScript(src, view, vars) | The script runtimes. |
deepEqual(a, b) | Structural equality used by equals assertions. |
workspace — discover & run a collection
This module turns a directory of files into a run, applying discovery, folder inheritance, environment loading, and secret resolution.
runPath
The function behind truspec run:
import { runPath } from "@truspec/core/workspace";
const summary = await runPath("./api", {
env: "local", // environments/local.env.yaml
cwd: process.cwd(),
spec: "./openapi.yaml",// optional — validate each spec-linked response against its schema
// vars, fetch, now, timeoutMs, processEnv all optional
});
console.log(summary.passed, summary.failed, summary.ok);Returns a WorkspaceRunResult: { results: RunResult[], passed, failed, skipped, ok, missingSecrets }, plus skippedRequests when bail stopped the run — { name, filePath, iteration? } for each request that was selected but never sent, so a reporter can name them rather than leaving them out of the record. It parses every request, runs them in order (then path) so captures chain forward, and masks declared secrets in the reported results.
Other helpers
| Export | Purpose |
|---|---|
discoverRequests(dir) | Recursively list request files (cycle-safe, workspace-confined). |
findWorkspaceRoot(dir) | Walk up to a dir with environments/ or .git. |
loadEnvironment(from, name) / buildVars(env, processEnv) | Load an env file; build its variable map (resolving secrets). |
loadFolderChain(leaf, root) / mergeFolderConfigs(chain) | Compose folder config root → leaf. |
loadDotenv(dir) | Parse a workspace .env. |
confinePath(cwd, target) | Resolve a path and reject anything that escapes the workspace (symlink-safe). |
spec — drift, coverage, contract, OpenAPI
The functions behind truspec drift / coverage / contract / gen.
import {
driftReport, liveDriftReport, coverageReport, contractReport,
scaffoldFromSpec, writeScaffold, parseOpenApi, validateAgainstSchema,
} from "@truspec/core/spec";
const drift = driftReport("./api", "./openapi.yaml"); // DriftReport
const live = await liveDriftReport("./api", "./openapi.yaml", "https://api.example.com");
const cov = coverageReport("./api", "./openapi.yaml", 80); // CoverageReport (min 80%)
const ctr = await contractReport("./api", "./openapi.yaml", { env: "local" }); // ContractReport
// Scaffold stubs from a spec, then write them:
const result = scaffoldFromSpec(specText, { baseUrlVar: "baseUrl" });
writeScaffold(result.files, "./api");| Export | Returns | Notes |
|---|---|---|
parseOpenApi(text) | OpenApiSummary | Flat operation list (with response schemas) from an OpenAPI 3 doc. |
driftReport(dir, specPath) | DriftReport | { added, removed, changed, ok, … }. |
liveDriftReport(dir, specPath, baseUrl, opts?) | Promise<DriftReport> | Adds liveMissing from a GET/HEAD probe. |
coverageReport(dir, specPath, minPercent?) | CoverageReport | { covered, uncovered, percent, ok }. |
contractReport(dir, specPath, opts?) | Promise<ContractReport> | Runs the collection; { conformed, violations, skipped, untested, ok }. |
validateAgainstSchema(value, schema, doc) | SchemaViolation[] | Validate any value against an OpenAPI 3 schema subset. |
scaffoldFromSpec(text, opts?) | ScaffoldResult | { files, skipped }. |
writeScaffold(files, outDir) | string[] | Paths written. |
computeDrift / computeCoverage | reports | Pure functions over parsed operations. |
probeLiveOperations(ops, baseUrl, opts?) | Promise<LiveProbeResult> | The GET/HEAD live probe. |
See Spec sync for what these reports mean.
importers — Postman & Bruno
import { importPostmanFile, importBrunoDir, writeImport } from "@truspec/core/importers";
const result = importPostmanFile("./postman_collection.json"); // ImportResult
// result.files: { path, content }[] · result.warnings: string[] · result.stats
writeImport(result, "./api");ImportResult is { files: ImportedFile[]; warnings: string[]; stats: { requests, folders } }. See Importing.
mock — local mock server
import { startMockServer } from "@truspec/core/mock";
const handle = await startMockServer(specText, { port: 4000, delayMs: 0, validate: false });
// handle: { port, url, routes, close }
await handle.close();| Option | Default | Notes |
|---|---|---|
port | 0 (free port) | Port to bind. |
host | 127.0.0.1 | Bind address. |
delayMs | 0 | Per-response latency. |
validate | false | Validate requests against the spec (400 on mismatch). |
See Mock server.
End-to-end example
Validate, write, run, and check coverage — entirely in code:
import { parse } from "@truspec/core/format";
import { runPath } from "@truspec/core/workspace";
import { coverageReport } from "@truspec/core/spec";
import { writeFileSync, mkdirSync } from "node:fs";
// 1. Build and validate a request, then write it.
const req = parse.request.validate({
name: "Get pet",
method: "GET",
url: "{{baseUrl}}/pets/{{petId}}",
assertions: [{ type: "status", equals: 200 }],
spec: { operation: "GET /pets/{id}" },
});
if (!req.ok) throw new Error(req.error);
mkdirSync("api", { recursive: true });
writeFileSync("api/get-pet.tspec.yaml", parse.request.serialize(req.data!));
// 2. Run the collection.
const run = await runPath("./api", { env: "local" });
console.log(`${run.passed}/${run.results.length} passed`);
// 3. Gate on coverage.
const cov = coverageReport("./api", "./openapi.yaml", 80);
if (!cov.ok) throw new Error(`Coverage ${cov.percent}% below 80%`);See also
- File format — the shapes
formatvalidates. - Core concepts — discovery, inheritance, and the run pipeline these functions implement.
- CLI reference — the command-line surface over these same functions.