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

@knapsack/mdbase-cli

v0.2.2

Published

CLI for mdbase-style typed markdown collections: validate, query, read, update, links, schema

Readme

@knapsack/mdbase-cli

A TypeScript CLI for typed markdown collections: markdown files with YAML frontmatter, validated against schemas declared in _types/*.md files and configured by an mdbase.yaml at the collection root.

It is a clean-room implementation of the command surface of the retired upstream mdbase CLI: validate, query, read, update, links, and schema. No upstream code is used; the behavior was reconstructed from observed output and locked in with a parity harness.

Install

pnpm add -D @knapsack/mdbase-cli

Published to npmjs.org. No registry configuration, authentication, or token is required.

Versions up to 0.2.1 were published to GitHub Packages instead. That registry requires an authenticated request for every read, including of public packages, which is why this package moved.

Requires Node 22.18 or newer.

Usage

Every command needs an mdbase.yaml at the collection root, found by walking up from cwd, the same way .git is found. Paths on the command line are cwd-relative; output paths are always root-relative.

Help

mdbase --help prints the command index, the resolution rules, and the exit-code contract. mdbase <command> --help (or mdbase help <command>) prints one command's arguments, flags with their legal values, output shape per format, exit semantics, worked examples, and the gotchas that otherwise produce a confidently wrong invocation. Help always goes to stdout and exits 0, including with no collection in sight. Bare mdbase remains a usage error on stderr, exit 2.

mdbase --help
mdbase query --help
mdbase --help --format json

That last form emits the entire CLI as a machine-readable spec: every command, flag, enum, output shape, exit rule, and example as JSON. It exists for agents. An LLM driving this CLI can parse the spec once instead of pattern-matching prose, and jq gets you a single command's surface without reading the rest.

mdbase --help --format json | jq '.commands[] | select(.name=="query") | .flags'

The help text is generated from one declarative spec in src/help.ts, and test/cli.test.ts asserts every command's flags appear in its help, so a flag added to a parser without a help entry fails CI rather than shipping undocumented.

validate

mdbase validate [paths...] [--format text|json]

Validates frontmatter against the schemas in _types/*.md. With no paths, validates the whole collection; with paths, only those files, still checked against the full type set so link targets resolve.

A path may also be a directory: it expands to every collection file under it, with excludes and dot-directories respected. Like every path argument, directories are cwd-relative, so mdbase validate . from the root validates everything, while . from a subdirectory validates just that subtree. A directory containing no collection files checks 0 files and exits 0. File and directory arguments mix freely.

mdbase validate
mdbase validate .
mdbase validate guides people/ada.md
mdbase validate people/ada.md --format json

Notable flags: --format json emits {findings, counts: {errors, warnings, files}} instead of one line per finding plus a summary line.

query

mdbase query [path] [-t|--type <type>] [--fields a,b,c] [--format json|tsv|table|paths]

Lists collection files with their resolved types and frontmatter, optionally filtered by type and/or path prefix, optionally projected to specific fields.

mdbase query people/ --fields title,role --format table
mdbase query -t article --format json

Notable flags: -t/--type filters to files resolving to that type (unknown type names throw, exit 2); --fields limits columns in tsv/table output (JSON always includes full frontmatter); --format paths prints bare paths only, one per line, which pipes into xargs.

read

mdbase read <path> [--format text|json|yaml] [--body]

Prints one file's resolved types and frontmatter, and its body with --body. This is not validation: an excluded or untyped file still reads fine, just with types: [].

mdbase read people/ada.md
mdbase read people/ada.md --format json --body

update

mdbase update <path> -f key=value [-f key=value ...]

Sets one or more frontmatter fields, validating the candidate content before writing anything. Values coerce to the field's declared type; key order, comments, and the body are preserved byte-for-byte; any now_on_write generated field on the type regenerates.

mdbase update people/ada.md -f role=maintainer
mdbase update projects/atlas.md -f priority=2 -f status=active

Notable flags: repeat -f for multiple fields in one write. If validation finds any severity-error finding on the candidate, findings print and nothing touches disk (exit 1). The file is never left half-written.

links check

mdbase links check [path]

Scans body markdown links for broken relative .md targets. This is separate from schema-declared link fields, which are covered under Schema features below. With no path, scans the whole collection; with a path, scans just that file or directory prefix.

mdbase links check
mdbase links check guides/

schema

mdbase schema <type> [--format text|json|yaml]

Prints one type's field definitions as declared in _types/*.md.

mdbase schema article
mdbase schema person --format json

Exit codes

  • 0 — clean: no severity-error findings (validate/update), or nothing broken (links check).
  • 1 — findings: at least one severity-error finding, or at least one broken link.
  • 2 — usage/config error: bad flags, no mdbase.yaml found, a file not found, an invalid type-schema definition.

Warnings never affect the exit code. A collection with only warning-severity findings still exits 0.

Schema features supported

Type files (_types/*.md) declare fields under a fields: map; each field has a type plus optional modifiers:

  • Field types: string, number, boolean, date, time, enum, list, link.
  • required — missing (or explicit null) on a required field is missing-required; null on an optional field is skipped entirely, not validated.
  • values — the enum's allowed values. Enum fields must declare at least one; an empty or absent values fails type-file load, not collection validation.
  • min / max — numeric range, checked on the coerced value.
  • unique — list fields only; duplicate items (post-coercion) are a finding.
  • items — a nested FieldDef describing a list's element type. List items are validated (and read-coerced) against it but never top-level coerced in output.
  • target / validate_exists on link fields — target names the type a link must resolve to; validate_exists makes a missing target a broken-link finding. They are independent: target is only checked when the target does resolve, so a link with target set but no validate_exists, pointing at nothing, is silently not a finding, because the target-type check has nothing to inspect. Set both when a link must exist AND be the right type.
  • generated: now / generated: now_on_write — auto-populated fields, always UTC, shaped by the field's own type: date fields get YYYY-MM-DD, time fields get HH:MM:SS, anything else gets a full ISO-8601 timestamp.

Not implemented, and failing loud at type-file load: cross-file uniqueness, path_traversal findings, min_length/max_length/min_items/max_items, wikilink syntax in link fields, and upstream's integer/datetime/object/ any field types.

Development

  • src → src imports use .js specifiers (e.g. import { foo } from './foo.js'), even though the source file is foo.ts. This is standard TypeScript-with-nodenext style: tsc rewrites nothing at the specifier level, so the specifier must already match the emitted dist/foo.js.
  • test → src imports use real .ts specifiers (e.g. import { foo } from '../src/foo.ts'). Tests run directly against source via Node's native TypeScript type-stripping, which resolves literal .ts specifiers rather than compiled output.
  • Tests run without a build: pnpm test runs node --test "test/*.test.ts" directly, no compile step required.
  • scripts/ts-loader.mjs: Node's native TypeScript type-stripping does not remap a relative .js specifier to a sibling .ts file (a deliberate Node decision, see nodejs/loaders#214), but that remapping is exactly what the src → src .js-specifier convention above needs once one src module imports another, since pnpm test runs .ts sources directly with no build step. tsc itself resolves the convention fine, which is what makes pnpm build and pnpm typecheck work, so the gap is Node's runtime resolver alone. The test script loads scripts/ts-loader.mjs via --import, which uses node:module's registerHooks to retry a failed relative .js resolution as .ts before giving up. This only affects pnpm test; build output and typechecking are untouched.
  • Two tsconfigs: tsconfig.json (used by pnpm build) scopes rootDir/include to src only, so dist mirrors just the package's public surface. tsconfig.test.json extends it with include: ["src", "test", "scripts"], rootDir: ".", noEmit: true, and allowImportingTsExtensions: true, so pnpm typecheck also typechecks tests, which tsconfig.json alone would silently skip.

License

MIT. See LICENSE.