Skip to content

CLI reference

manni site exposes four verbs. start, build and preview find the site, detect its framework, and run the framework’s own command with inherited stdio. annotate joins the graph’s pins and statements to the built pages, as data. site has no default subcommand. A bare manni site prints usage and exits 2.

Terminal window
manni site [options] <command> [command options] [dir] [-- <args...>]

Global options are accepted before the subcommand.

OptionDescription
-V, --versionPrint the manni version and exit.
-h, --helpPrint help for the program or a subcommand.

Start, build and preview the docs site. Every command on this page lives under it. site accepts its own -V, --version, which reports the manni version, and -h, --help. Run with no subcommand it prints usage, exit 2.

Terminal window
manni site [options] <command> [command options] [dir] [-- <args...>]

Run the framework’s dev server, with hot reload, until Ctrl-C or SIGTERM stops it. A stop by signal exits 0. Flare has no development server, so start stops with exit 2 there.

Terminal window
manni site start [dir] [options] [-- <args...>]
ArgumentRequiredDescription
[dir]noThe site’s directory. Omitted: site.dir, else found by search. One directory, not a list.
OptionArgumentDefaultDescription
--port<n>noneThe port, an integer from 1 to 65535. Without it, the port of a local collection url:, else the framework’s default. See the port and host.
--host<host>noneThe host to bind. Without it, the host of a local collection url:, else the framework’s default. Refused for Mintlify and Fern, whose dev servers take no host.
-c, --config<file>discoveredPath to a manni config file.

Run the framework’s production build once. A build that fails exits 2, and the framework’s own output above the last line says why. Mintlify and Fern have no local build, so build stops with exit 2 there. build takes no --port or --host, because nothing is served. With site.annotate.after-build set to true, a successful build is followed by annotate, and a failed one is not.

Terminal window
manni site build [dir] [options] [-- <args...>]
ArgumentRequiredDescription
[dir]noThe site’s directory. Omitted: site.dir, else found by search.
OptionArgumentDefaultDescription
-c, --config<file>discoveredPath to a manni config file.

Run build, then serve the output until Ctrl-C or SIGTERM stops it. It always builds first, so it never serves stale output. A build that fails exits 2 before anything is served. The serve step is the framework’s own preview command, or the built-in server for a framework that has none. With site.annotate.after-build set to true, preview annotates the build before it serves it.

Flare is the exception. Its build is slow and runs only where Flare is installed, so preview serves the output of the last site build without building. With no output there, it exits 2.

Terminal window
manni site preview [dir] [options] [-- <args...>]
ArgumentRequiredDescription
[dir]noThe site’s directory. Omitted: site.dir, else found by search.
OptionArgumentDefaultDescription
--port<n>noneThe port the output is served on, an integer from 1 to 65535. Without it, the port of a local collection url:, else the framework’s default.
--host<host>noneThe host the output is served on. Without it, the host of a local collection url:, else the framework’s default.
-c, --config<file>discoveredPath to a manni config file.

Join the chunk artifact manni graph publish writes to the built site, and write the result into each built page as data. Each placed block gains data-manni-* attributes, and each page gains one JSON island. Nothing visible changes: annotate writes no mark, stylesheet or script. It is not a gate. It exits 0 whatever the pins say, and 2 when it cannot run. Show readers what is proven walks through it.

Terminal window
manni site annotate [dir] [options]
ArgumentRequiredDescription
[dir]noThe site’s directory. Omitted: site.dir, else found by search.
OptionArgumentDefaultDescription
--out<dir>noneThe built site, relative to the working directory. Without it, site.out, else the framework’s output from the table below.
--chunks<file>noneThe chunk manifest. Without it, chunks.json in the directory of graph.out, where manni graph publish writes it.
-f, --format<format>prettypretty or json.
-c, --config<file>discoveredPath to a manni config file.

A framework is needed only for its default output and its routes. With --out or site.out, a site no framework marks is annotated too.

Each pin’s claim lines, and each statement’s lines, are joined to the built HTML in two steps.

  1. Lines. A block stamped by @hawkeyexl/manni/site/rehype whose lines overlap the claim. The innermost stamped blocks win, so a claim over two list items lands on the items, not on the list.
  2. Text. With no stamped block, the claim rendered as plain text. Either one block holds it, or one run of adjacent blocks makes it up. Inside a longer claim, a block shorter than 12 characters does not count.

A claim whose text appears twice is not placed, and the report says so. A tie is never broken by document order. Statements join by lines alone. Every data-manni-src stamp is removed afterwards, from every page. So annotate once per build: a second run over the same build finds no stamps, and places by text alone.

Add the plugin to the framework’s rehype plugins. For Starlight that is astro.config.mjs:

import manniLines from "@hawkeyexl/manni/site/rehype";
export default defineConfig({
markdown: { rehypePlugins: [manniLines] },
});

A page with no stamps is found by its route. <path> is the page’s path below the content directory, without its extension, and an index page maps to its directory.

FrameworkOutputContentRouteLines step
Starlightdistsrc/content/docs<path>/index.htmlthe rehype plugin
Docusaurusbuilddocsdocs/<path>/index.htmlthe rehype plugin
Nextraoutpages<path>.htmlthe rehype plugin
Fumadocsoutcontent/docsdocs/<path>.htmlthe rehype plugin
Rspressdoc_builddocs<path>.htmlthe rehype plugin
VitePress.vitepress/dist under its rootits root<path>.htmlnone, text only
MkDocs, Zensicalsitedocs<path>/index.htmlnone, text only
Sphinx<src>/_build/html<src><path>.htmlnone, text only
Hugopubliccontent<path>/index.htmlnone, text only
Jekyll_sitethe site’s directory<path>.htmlnone, text only

Mintlify and Fern build on their own hosts, so there is no local site to annotate. annotate does not read Flare output, so it exits 2 for a Flare site. Nextra and Fumadocs write static pages only with output: "export" in next.config.

AttributeOnValue
data-manni-nodea placed block, or a headingThe graph node IRI of the section, or of the graph:Block a block statement mints.
data-manni-pinsa block with placed pinsSpace-separated citation ids. A citation with no id appears as #<n>, its place in the page’s citations.
data-manni-statea block with placed pinscurrent when no pin on it has an error-severity finding, otherwise drifted.
data-manni-verifieda block or heading whose statement sets last-verifiedThe day or timestamp, as written.
data-manni-verified-bya block or heading whose statement sets verified-bySpace-separated verified-by[].tool values.
data-manni-statementa block or heading with a statementcurrent, changed or unpinned.
data-manni-applies-toa heading whose statement’s section sets applies-toSpace-separated values.
data-manni-commit<main>, or <body> without oneThe docs commit. Absent outside git.
data-manni-last-verified<main>, or <body> without oneThe page’s last-verified, when set.

The island is one <script type="application/json" id="manni-provenance">, before </body>. It holds { page, commit, worktree, dates, records }. records are the page’s provenance records from the chunk rows, unchanged, in lines order. A browser never runs it, so a strict content security policy allows it. An encrypted source stays its ciphertext token.

pretty prints one line, then one line per page that has something to say:

docs/dist: 258 pages annotated. 4163 of 4202 pins placed, 39 listed on their page only. 2 blocks carry a verification.
cite/fix/index.html: 1 pin not placed, because its text appears twice: decode-sample-changed.

json prints one object:

{
"out": "docs/dist",
"pages": 258,
"pins": { "total": 4202, "placed": 4163 },
"statements": { "total": 2, "placed": 2 },
"unplaced": [
{ "page": "cite/fix/index.html", "kind": "pin", "ids": ["decode-sample-changed"], "reason": "repeated-text" }
],
"drifted": []
}

reason is repeated-text, no-block or no-page. page is the built page relative to the output, or the source page when it has no built page. A statement’s ids are its lines.

Everything after -- is appended verbatim to the command manni runs, after npm’s own -- where one is needed. The arguments are space-separated, because they are argv. An argument the shell would split, such as "My Docs", is quoted for a configured command, so it arrives as one argument. Under preview they go to the serve step, not the build. A build’s flags belong in site.commands.build. The built-in server takes no arguments, so preview with arguments after -- is refused there, exit 2.

Terminal window
$ manni site preview -- --open
manni: Starlight in docs/. Running npm run build
manni: Starlight in docs/. Annotating docs/dist from docs/public/graph/chunks.json.
docs/dist: 258 pages annotated. 4163 of 4202 pins placed, 39 listed on their page only. 2 blocks carry a verification.
manni: Starlight in docs/. Running npm run preview -- --host 127.0.0.1 --port 4321 --open

start, build and preview have no -f, --format. They print no report, so the framework’s own output is the output. annotate takes no arguments after --, because it runs no framework command.

  1. [dir] when it is given.
  2. Else site.dir, resolved against the config file’s directory.
  3. Else a search, where the first match wins. It looks in the config file’s directory, or the working directory when there is no config. Then it looks in that directory’s docs/, website/ and site/.

In a directory, the markers in the framework table decide the framework. Fern is the exception to “in a directory”. Its marker is fern/fern.config.json, and the site’s directory is the parent of fern/. Two frameworks in one directory are refused, exit 2. A [dir] with no marker is refused too, unless site.commands sets every command the verb needs.

FrameworkMarkerstartbuildpreviewPort / host flagsDefault port
Mintlifymint.json, or docs.json with Mintlify’s $schema, or with a theme and a navigationmint devnonenone--port / none3000
Fernfern/fern.config.jsonfern docs devnonenone--port / none3000
Starlightan @astrojs/starlight dependencydev, else astro devbuild, else astro buildpreview, else astro preview--port / --host4321
Docusaurusan @docusaurus/core dependencystart, else docusaurus startbuildserve, else docusaurus serve--port / --host3000
VitePressa vitepress dependencydocs:dev, else vitepress dev <root>docs:builddocs:preview--port / --host5173
Nextraa nextra dependencydev, else next devbuildstart, else next start--port / --hostname3000
Fumadocsa fumadocs-core dependencydev, else next devbuildstart, else next start--port / --hostname3000
Rspressan rspress or @rspress/core dependencydev, else rspress devbuildpreview--port / --host3000
MkDocsmkdocs.ymlmkdocs servemkdocs buildthe built-in server over site/-a host:port8000
Zensicalzensical.toml, or mkdocs.yml with zensical in the requirements or pyproject.tomlzensical servezensical buildthe built-in server over site/-a host:port8000
Sphinxconf.py or source/conf.pysphinx-autobuild <src> <src>/_build/htmlsphinx-build -M html <src> <src>/_buildthe built-in server over _build/html--port / --host8000
Hugohugo.toml, hugo.yaml or hugo.jsonhugo serverhugothe built-in server over public/--port / --bind1313
Jekyll_config.yml, with a Gemfile naming jekyll or github-pagesjekyll servejekyll buildthe built-in server over _site/--port / --host4000
Flarea .flprj filenonemadbuild -project <flprj> -target <target>the built-in server over Output/<USERNAME>/<target>/none4321

A dependency counts in dependencies or devDependencies of the site directory’s package.json.

  • A script, else a binary. Where a cell names a script and then a binary, the script runs when package.json has it, as npm run dev. It carries the project’s own flags. Otherwise the binary runs through the package manager.
  • The package manager. The packageManager field of package.json names it. Without one, the lockfile found walking up to the git root does. Without either, it is npm.
  • Python and Ruby. MkDocs, Zensical and Sphinx run under uv run when a uv.lock is present, or poetry run for a poetry.lock. Jekyll runs under bundle exec.
  • Output directories. The built-in server reads site.out, else the framework’s default output directory. A custom site_dir or publishDir needs site.out.
  • Flare. The target is tools.flare.target, else the project’s only target. A project with several targets and none named exits 2. madbuild is found on PATH, then in the folder tools.flare.home names, then in the newest MadCap Flare install under Program Files. USERNAME is the account running manni, which madbuild writes into the output path. The Flare guide walks through it.

start and preview pick the port and the host in this order:

  1. --port and --host.
  2. The url: of a declared collection, when its host is localhost, 127.0.0.1 or [::1].
  3. The framework’s default. manni passes no port or host flag then, so the framework’s own default stands.

The url: is the one manni a11y check seeds its crawl from, so the served site and the crawl agree. When several collections name different local host and port pairs, manni warns and uses the framework’s default. Neither flag applies to a command set in site.commands, and passing one is refused.

MkDocs, Zensical, Sphinx, Hugo, Jekyll and Flare have no preview server of their own. For those, preview serves the build output with a small static server inside manni.

  • It binds 127.0.0.1, or the host chosen above.
  • It mounts the output at the collection url: path when there is one, so a site built under /manni/ answers there. Collections that share one local server mount at the path they have in common, such as /manni/ for /manni/guides/ and /manni/api/.
  • It serves index.html for a directory, else Default.htm, where Flare’s output starts. It serves 404.html for a missing path when the output has one.
  • It serves .html and .htm files as HTML.
  • It prints its URL to stdout, as http://127.0.0.1:8000/.

Every key under site: is optional. This repository’s own config sets only annotate.after-build, because detection finds Starlight in docs/.

manni.config.yaml
site:
# The site's directory, relative to this file.
dir: docs
# Whole command lines, run through the shell in `dir`.
commands:
start: pnpm dev --port 4000
build: pnpm build
preview: npx serve dist -l 4000
# The built site, relative to `dir`.
out: dist
annotate:
# `build` and `preview` run `annotate` after a successful build.
after-build: true
KeyTypeDefaultDescription
site.dirstringdetectedThe site’s directory, resolved against the config file.
site.commands.startstringdetectedReplaces the dev-server command.
site.commands.buildstringdetectedReplaces the build command. preview runs it too.
site.commands.previewstringdetectedReplaces the serve step of preview. The build still runs first.
site.outstringthe framework’s outputThe built site, relative to site.dir. annotate and the built-in server read it.
site.annotate.after-buildbooleanfalseWhen true, build and preview run annotate after a successful build. start never annotates.

A set command replaces the detected one for its verb, and the others stay detected. When every command a verb needs is set, detection is skipped. So a framework the table does not list works through config alone, and dir then defaults to the config file’s directory. Any other key under site: is refused, exit 2.

Every diagnostic goes to stderr, prefixed manni:. Stdout carries the framework’s own output, the built-in server’s URL and the report of annotate.

WhenLineExit
Before a detected command runsmanni: Starlight in docs/. Running npm run dev -- --host 127.0.0.1 --port 4321n/a
Before a configured command runsmanni: Running site.commands.start in docs/: pnpm dev --port 4000n/a
The built-in server is listening (stdout)http://127.0.0.1:8000/n/a
Ctrl-C or SIGTERM stops a server, or preview during its buildnothing0
A build succeededthe framework’s output0
Local collection URLs disagree (a warning)manni: collections site and api name different local URLs. Using Starlight's default port; pass --port to pick one.n/a
No site foundmanni: no docs site found in ./, docs/, website/ or site/. Pass the site's directory, or set site.commands.start in manni.config.yaml.2
[dir] has no markermanni: no docs framework detected in website/. Set site.commands.build in manni.config.yaml.2
Two frameworks in one directorymanni: docs/ holds more than one docs site: Docusaurus (@docusaurus/core), MkDocs (mkdocs.yml). Set site.commands.start in manni.config.yaml.2
No local build (Mintlify, Fern)manni: Mintlify has no local build. Run manni site start, or set site.commands.build.2
start on Flaremanni: Flare has no development server. Run manni site build, then manni site preview.2
A Flare project with several targets and none namedmanni: Widget.flprj has 2 targets. Name the one to build with tools.flare.target.2
A Flare project with no targetsmanni: Widget.flprj has no targets, so there is nothing to build. Add a target in Flare.2
tools.flare.target names no targetmanni: tools.flare.target "PDF" names no target in Widget.flprj. Its targets are HTML5 and Print.2
madbuild is not foundmanni: madbuild was not found. Install MadCap Flare, or set tools.flare.home to the folder that holds madbuild.exe.2
preview on Flare before a buildmanni: No Flare output at Output/maya/HTML5. Run manni site build first.2
Flare, with no USERNAME and no system usermanni: manni cannot tell whose Output folder to read, because USERNAME is not set and the system names no user. Set USERNAME to the account that runs madbuild.2
The binary is not installedmanni: mint not found on PATH. Install Mintlify's CLI, or set site.commands.start.2
A runner manni adds is not installedmanni: uv not found on PATH. Install uv, or set site.commands.start.2
The binary cannot startmanni: hugo could not start: spawn EACCES2
Node dependencies are not installedmanni: astro is not installed for docs/. Run npm ci in docs/ first.2
The command failedmanni: npm run build exited with code 1.2
The port is taken (built-in server)manni: port 8000 is in use. Pass --port, or stop the process holding it.2
A bad --portmanni: --port must be an integer from 1 to 65535, got "abc".2
--port with a configured commandmanni: --port does not apply to site.commands.start. Put the port in that command.2
--host with a configured commandmanni: --host does not apply to site.commands.start. Put the host in that command.2
--host on Mintlify or Fernmanni: Mintlify's dev server takes no host option. Drop --host.2
preview on Mintlify or Fern, with only site.commands.build setmanni: Mintlify has no local preview. Run manni site start, or set site.commands.preview.2
Arguments after -- for the built-in servermanni: MkDocs previews through manni's built-in server, which takes no extra arguments. Drop the arguments after --.2
A -c file that does not existmanni: Config file not found: "ci/manni.config.yaml".2
[dir] does not existmanni: website/ does not exist.2
An unknown config keymanni.config.yaml: `site:` has unknown key "framework". Supported keys: dir, commands, out, annotate.2
A config value of the wrong typemanni.config.yaml: site.commands.start must be a string.2
An empty configured commandmanni.config.yaml: site.commands.build must not be empty.2
A wrong after-build typemanni.config.yaml: site.annotate.after-build must be a boolean.2

annotate prints these. A line under the success line is indented by two spaces, and names the built page relative to the output.

WhenLineExit
Before the joinmanni: Starlight in docs/. Annotating docs/dist from docs/public/graph/chunks.json.n/a
Annotated (stdout)docs/dist: 258 pages annotated. 4163 of 4202 pins placed, 39 listed on their page only. 2 blocks carry a verification.0
A pin’s text appears twice (stdout)cite/fix/index.html: 1 pin not placed, because its text appears twice: decode-sample-changed.0
No block holds a pin’s text (stdout)key/ci/index.html: 1 pin not placed, because no block holds its text: no-key-message.0
No block holds a statement’s lines (stdout)cite/set-up/index.html: 1 statement not placed, because no block holds lines 53-54.0
A page with no built page (stdout)docs/src/content/docs/x.mdx: no built page at docs/dist/x/index.html.0
A block with an error-severity pin (stdout)cite/index.html: 1 block drifted, because a source changed: fetch-timeout.0
A unified framework with no stamps (a warning)manni: docs/dist carries no line stamps, so blocks are placed by text alone. Add @hawkeyexl/manni/site/rehype to the framework's rehype plugins.n/a
A page source that cannot be read (a warning)manni: docs/src/content/docs/x.mdx could not be read, so its pins are placed by line alone: ENOENT: no such file or directory, open 'docs/src/content/docs/x.mdx'.n/a
Chunks published outside gitmanni: chunks.json carries no commit, so pages get no data-manni-commit.n/a
No built sitemanni: no built site at docs/dist. Run manni site build first, or pass --out.2
No chunk artifactmanni: docs/public/graph/chunks.json does not exist; run manni graph publish.2
Not a chunk manifestmanni: notes.json is not a chunk manifest; run manni graph publish.2
An artifact from another graphmanni: docs/public/graph/chunks.json was built from another graph; run manni graph publish.2
A Nextra or Fumadocs server buildmanni: docs/out does not exist. Nextra writes static pages only with output: "export" in next.config.2
Mintlify or Fernmanni: Mintlify builds on its own host, so there is no local site to annotate.2
Flaremanni: manni site annotate does not read Flare output.2
No site found, and no --outmanni: no docs site found in ./, docs/, website/ or site/. Pass the site's directory, or pass --out.2
No framework, and no --outmanni: no docs framework detected in website/. Pass --out, or set site.out in manni.config.yaml.2
Two frameworks, and no --outmanni: docs/ holds more than one docs site: Docusaurus (@docusaurus/core), MkDocs (mkdocs.yml). Pass --out, or set site.out in manni.config.yaml.2
An unknown --formatmanni: Unknown --format "sarif". Use pretty or json.2

A hint that names site.commands.<verb> names the verb that ran. The current directory shows as ./, as in Starlight in ./. for a site at the root. Two Node frameworks in one directory are named by their package, in table order. The missing-dependencies hint names the install command of the package manager. That is npm ci when an npm lockfile exists, else npm install, pnpm install, yarn install or bun install. Exit 1 is never used, because nothing here is a finding. A failed build is an operational error, and the framework has already printed why.

CodeMeaning
0The build succeeded, a signal stopped the server, or annotate ran, whatever the pins say.
2Nothing to run, a usage error, or the command manni ran failed.

In this repository unless noted.

# The dev server, on the port the collection url: names
$ manni site start
manni: Starlight in docs/. Running npm run dev -- --host 127.0.0.1 --port 4321
# A production build, annotated, because site.annotate.after-build is true here
$ manni site build
manni: Starlight in docs/. Running npm run build
manni: Starlight in docs/. Annotating docs/dist from docs/public/graph/chunks.json.
docs/dist: 258 pages annotated. 4163 of 4202 pins placed, 39 listed on their page only. 2 blocks carry a verification.
# Build, annotate, then serve; a11y check in another terminal needs no flags
$ manni site preview
manni: Starlight in docs/. Running npm run build
manni: Starlight in docs/. Annotating docs/dist from docs/public/graph/chunks.json.
docs/dist: 258 pages annotated. 4163 of 4202 pins placed, 39 listed on their page only. 2 blocks carry a verification.
manni: Starlight in docs/. Running npm run preview -- --host 127.0.0.1 --port 4321
# A directory named, and a port chosen (a Docusaurus repository)
$ manni site start website --port 3001
manni: Docusaurus in website/. Running npm run start -- --port 3001
# The built-in server (an MkDocs repository with uv)
$ manni site preview
manni: MkDocs in docs/. Running uv run mkdocs build
http://127.0.0.1:8000/
# A configured command (site.commands.start: pnpm dev --port 4000)
$ manni site start
manni: Running site.commands.start in docs/: pnpm dev --port 4000
# Join the graph's pins to the built site, after manni graph publish
$ manni site annotate
manni: Starlight in docs/. Annotating docs/dist from docs/public/graph/chunks.json.
docs/dist: 258 pages annotated. 4163 of 4202 pins placed, 39 listed on their page only. 2 blocks carry a verification.
# The unplaced pins whose text repeats, for a script
$ manni site annotate -f json | jq '.unplaced[] | select(.reason == "repeated-text")'
# Every annotate option at once
$ manni site annotate docs --out docs/dist --chunks docs/public/graph/chunks.json -f json -c manni.config.yaml
# Everything at once
$ manni site preview docs -c ci/manni.config.yaml --host 0.0.0.0 --port 4000 -- --open
manni: Starlight in docs/. Running npm run build
manni: Starlight in docs/. Running npm run preview -- --host 0.0.0.0 --port 4000 --open

In CI, start preview in the background and wait for the port before the crawl. A server started with & returns at once, and the build runs first:

- run: npx @hawkeyexl/manni site preview > preview.log 2>&1 &
- run: until curl -fsS http://127.0.0.1:4321/manni/ > /dev/null; do sleep 1; done
- run: npx @hawkeyexl/manni a11y check -f github