Gutterpress proof

Markdown & CSS, typeset for printOpen source · by Dimm City

CLI reference

Command-line interface for Gutterpress — markdown to print-ready PDF.

The CLI is for power users who want to script builds, run in CI, batch-process projects, or work outside the desktop app. If you just want to write a book and export a PDF, use the desktop app instead.

Install

Standalone binary (no Node, no Bun required)

Download for your platform from the latest release:

Platform Binary
Linux x64 gutterpress-cli-linux-x64
Linux ARM64 gutterpress-cli-linux-arm64
macOS Apple Silicon gutterpress-cli-macos-arm64
macOS Intel gutterpress-cli-macos-x64
Windows x64 gutterpress-cli-windows-x64.exe

Move the binary somewhere on your PATH, mark it executable (chmod +x), and you're done.

Every new GitHub release includes SHA256SUMS.txt. Verify the hash for your download before running it, especially when bypassing Gatekeeper or SmartScreen. See the installation guide for commands and the complete supported-platform matrix.

From Homebrew (macOS and Linux)

brew tap dimm-city/gutterpress https://github.com/dimm-city/gutterpress.git
brew install dimm-city/gutterpress/gutterpress

From Scoop (Windows x64)

scoop bucket add gutterpress https://github.com/dimm-city/gutterpress.git
scoop install gutterpress/gutterpress

From npm

npm install -g gutterpress

Node.js 22 or newer is required for the npm install.

Installing with an npm git URL is intentionally unsupported. This repository is a Bun monorepo, its generated dist/ is not committed, and the repository root is not the published CLI package. Use one of the installs above, or clone the repository and run bun install when contributing to Gutterpress itself.

System requirements

The CLI needs a Chromium-based browser for PDF generation, and a few external tools for PDF post-processing and validation depending on which features you use. See User Guide: Chapter 7 — System Setup for the full per-feature requirements matrix.

The short version: CLI PDF rendering needs a Chromium-based browser. Optional PDF/X output additionally needs Ghostscript and qpdf; the desktop app uses its bundled browser for standard PDF export.

Quick start

# Scaffold a new project (manifest + starter chapter + stylesheet)
  gutterpress new "My First Book" --preset dtrpg

# Build a PDF from a project directory
  gutterpress build ./my-book

# Live native-engine preview (automatic updates over WebSocket)
  gutterpress preview ./my-book

# Custom output path
  gutterpress build ./my-book --out dist/my-book.pdf

# Print-ready PDF/X (CMYK + ICC profile, validation enabled)
  gutterpress build ./my-book --format pdfx --icc path/to/profile.icc

# HTML output (a self-contained directory with book.html + assets)
  gutterpress build ./my-book --format html --out dist/my-book/

Project layout

A Gutterpress project is a directory. The CLI doesn't impose much structure; the most common shape is:

my-book/
├─ manifest.yaml      ← optional but recommended; metadata + config
├─ chapter-01.md      ← markdown files, processed in alphabetical order
├─ chapter-02.md         (or in the order listed in manifest.yaml#source.files)
├─ css/               ← your stylesheets
│  └─ print.css
└─ images/            ← images referenced from markdown or CSS

The CLI discovers assets from what the book actually references — there is no directory list to keep in sync. Fonts are no exception: a font can live anywhere in the project; @font-face { src: url(...) } in your CSS resolves relative to that CSS file, wherever it is, and the build embeds it automatically. Images referenced from markdown or HTML must live inside the project folder (they keep their own relative path in the output); images referenced only from CSS may live anywhere the CSS can reach.

See User Guide: Chapter 1 — Getting Started for a full first-project walkthrough and examples/ for working starters.

Manifest

manifest.yaml is where you control everything that isn't authored in markdown — book title, the page-size preset, custom styles, extensions (looks and plugins), validation rules, PDF/X configuration. It is the only recognized project manifest filename. The schema lives in docs/schema-autocomplete.md for YAML autocomplete in editors.

Minimal example:

title: "My Book"
authors:
  - "Your Name Here"

# Pick a page-size preset or supply page.width / page.height yourself
preset: dtrpg

styles:
  - css/print.css

source:
  files:
    - chapter-01.md
    - chapter-02.md

The full configuration cascade is CLI flags > manifest.yaml > preset defaults. See the configuration reference for details.

Commands

Gutterpress has 8 subcommands. new, preview, build, and publish are the primary author commands; validate and preflight are CI / advanced checks; doctor reports system readiness; and ext manages the project's extensions (plugins, looks, component libraries). Every command also accepts --help for the authoritative, always-current flag list (gutterpress <command> --help) — this section is regenerated from the same source.

gutterpress new

Scaffold a new book, plugin or theme from an embedded starter template — the fastest way to start (see Quick start).

--kind book is the default. Every new book picks the vendor preset it's designed for: dtrpg (DriveThruRPG print-on-demand), book (neutral 6x9in trade book), or custom (you supply the trim size in points).

--kind plugin and --kind theme scaffold an extension package instead — a folder with a package.json a book can load. The plugin starter carries a declarative marker table, a hand-written markdown-it rule, component CSS and a bun test fixture suite; the theme starter carries the six-file layered CSS architecture (tokens / base / components / page-templates / page-rules / book), each sheet opening with its own OWNS / MUST NOT CONTAIN contract header. Both include a README explaining which conventions are load-bearing. An extension needs no preset, trim size or publish target, so the book-only flags below are rejected rather than ignored when --kind names one — and --prefix/--description are likewise rejected for a book.

gutterpress new <name> [--kind <id>] [options]

  --kind <id>              What to create: book (default), plugin, theme
  --prefix <str>           Class/custom-property prefix an extension claims (default: its slug, e.g. "field-notes-"); --kind plugin|theme only
  --description <text>     One-line description recorded in the extension's metadata; --kind plugin|theme only
  --preset <id>            Vendor preset the book is designed for: dtrpg, book, custom (required for a book)
  --author <name>          Author name to record in the project
  --dir <path>             Parent directory to create the project in (default: current directory)
  --folder <name>          Folder name to create (default: a slug of the project name)
  --template <id>          Starter template: book, zine, technical (default: book)
  --targets <ids>          Publish targets recorded in the manifest (comma-separated: dtrpg, itch; or "none") — default: the preset's
  --page-width <pt>        Trim width in points, 72pt = 1in (required with --preset custom; optional override otherwise)
  --page-height <pt>       Trim height in points, 72pt = 1in (required with --preset custom; optional override otherwise)
  --page-tolerance <pt>    Allowed trim deviation when validating a built PDF (default: 0.5)
  --git                    Initialise local version history (default: true; use --no-git to skip)
  --no-git

gutterpress preview

Live HTML preview server by default. Every change — a single Markdown edit, CSS, manifest, multi-file, deletion, or other structural change — reloads the full document over WebSocket. This path is pure JS and needs no external tools. Pass --format pdf or --format pdfx for a one-shot build-and-open instead. --manifest applies only to those one-shot PDF/PDF-X modes; live HTML preview discovers the project manifest from its input directory.

Before it loads plugins, preview downloads any pinned npm extension whose copy is missing (see Restoring pinned extensions). If that fails it warns and previews anyway; the one-shot PDF modes fail fast.

gutterpress preview [input-dir] [options]

  --format <fmt>          html (default, live HMR) | pdf | pdfx
  --port <n>              Bind port                     (default: 3579, html only)
  --host <h>              Bind host                     (default: 127.0.0.1). Pass 0.0.0.0 to expose on the LAN.
  --no-watch              Disable file watching (html only)
  --open                  Automatically open browser (default: true; use --no-open to skip)
  --no-open
  --verbose               Enable verbose output
  --debug                 Debug mode (preserve temporary files)
  --out <dir>             Output directory                              (pdf|pdfx only)
  --pdfx-flavor <flavor>  PDF/X flavor: x1a | x3                        (pdfx only)
  --icc <path>            Path to ICC profile (required for --format pdfx)
  --manifest <path>       Path to manifest.yaml                          (pdf|pdfx only)
  --strip-annotations     Strip PDF annotations for PDF/X compliance    (pdfx only)
  --skip-lint             Skip the CSS print-safety check               (pdf|pdfx only)
  --skip-pre-validate     Skip pre-build validation                     (pdf|pdfx only)
  --skip-post-validate    Skip post-build PDF/X validation              (pdfx only)
  --allow-shrink          Build anyway when content is wider than the page content box (pdf|pdfx only)

gutterpress build

Build a PDF (default) or HTML output. Pipeline: validate:pre → convert → assets → build → validate:post. The CSS print-safety check (remote urls, risky print effects, page-containment) runs once, inside validate:pre, as the source.stylelint check — there is no separate lint phase.

Before it loads plugins, build downloads any pinned npm extension whose copy is missing — a fresh clone has the pins but not the files — and prints one Downloaded name@version (pinned in manifest.yaml) line per package. If a download fails, the build stops and names the extension, why, and how to retry. See Restoring pinned extensions.

gutterpress build [input-dir] [options]

  --format <fmt>          pdf | pdfx | html       (default: pdf)
  --out <path>            Output file or directory. For pdf|pdfx, --out may also be a .pdf file path.
  --title <title>         Override manifest title
  --pdfx-flavor <flavor>  PDF/X flavor: x1a | x3   (--format pdfx only)
  --icc <path>            Path to ICC profile (required for --format pdfx)
  --manifest <path>       Path to manifest.yaml
  --strip-annotations     Strip PDF annotations for PDF/X compliance
  --skip-lint             Skip the CSS print-safety check (default: it runs for pdf/pdfx)
  --skip-pre-validate     Skip pre-build validation
  --skip-post-validate    Skip post-build PDF/X validation
  --allow-shrink          Build anyway when content is wider than the page content box. Chromium then scales the WHOLE book down to fit it — the build reports that whole-document scale (e.g. "about 0.72x its declared size") plus every offender, as warnings.

A --out <dir> is shared between formats safely: each build delivers only what its own format produces, so it never disturbs another format's output already sitting in that folder. --format html writes book.html (with the viewer bundle), index.html, and referenced assets. --format pdf/pdfx writes only its own PDF (e.g. my-book-pdf.pdf). This is what makes the two-command sequence below safe:

gutterpress build --format html --out ./_site
gutterpress build --format pdf --out ./_site

./_site ends up with the paginating book.html from the first command plus the PDF from the second — the pdf build never overwrites book.html.

gutterpress publish

Push a built PDF/HTML artifact to a publishing platform (itch.io, DriveThruRPG, Amazon KDP, Azure Static Web Apps, Shopify, Google Drive) or copy it to a local folder (local, the desktop's default first destination), headlessly and CI-safely. Credentials live in a 0600 user-config store (never in the project); provider env vars override it for CI. Most providers connect with a pasted API key; Google Drive connects through your browser instead (--connect opens the sign-in page — nothing to paste).

gutterpress publish [project] [options]

  --provider <id>     local | itch | drivethrurpg | kdp | azure-swa | shopify | gdrive
  --list               List providers and connection status
  --connect            Store an API key for --provider (from --token, the provider's env var, or piped stdin) — or, for gdrive, open the browser to connect
  --disconnect         Forget the stored key for --provider
  --account <label>    Named-credential label for --connect/--disconnect (keep several accounts per provider); omit for the default
  --token <key>        API key for --connect (prefer stdin/env var to keep it out of shell history)
  --file <path>        Artifact to publish (PDF path, or HTML export dir). Default: the manifest's output location
  --manifest <path>    Path to manifest.yaml
  --dry-run            Preflight only; don't contact the platform
  --json               Machine-readable JSON output (CI)
  --open               Open the result page / guided upload page in the browser
# List providers and connection status
gutterpress publish --list

# Store an API key for itch.io, then publish
gutterpress publish --provider itch --connect
gutterpress publish --provider itch ./my-book

# Google Drive connects via your browser, not a pasted key
gutterpress publish --provider gdrive --connect
gutterpress publish --provider gdrive ./my-book

gutterpress validate

Run the validation pipeline (pre-build source checks and/or post-build PDF checks). Tools that aren't installed are skipped with a warning — they don't fail the run. See User Guide: Chapter 6 — Validation for the full check list and User Guide: Chapter 7 — System Setup for which external tools each check needs.

The positional directory and --pdf/--input are independent: the positional (or --input) sets the pre-build source directory, --pdf separately points at a built PDF for post-build checks. --input overrides the positional if both are given. When a source directory is given, missing pinned npm extensions are downloaded first, as for build (a failed download exits 3 with the reason). This holds for preflight too. The Downloaded … lines go to stderr, so --format json output stays parseable.

gutterpress validate [dir] [options]

  --pdf <path>         Path to the PDF file to validate (post-build checks)
  --input <dir>        Source directory for pre-build checks (overrides the positional directory)
  --manifest <path>    Path to manifest.yaml
  --category <c>       Comma-separated categories: source, pdf, asset, heuristic
  --only <ids>         Run only these check IDs/selectors (comma-separated)
  --skip <ids>         Skip these check IDs/selectors (comma-separated)
  --format <fmt>       text (default) | json
  --phase <p>          pre | post | all | pre-build | post-build   (default: all)
  --target <t>         Publish targets to validate against (comma-separated, e.g. dtrpg,itch), overriding the manifest's `targets:`
  --fix                Rewrite the source markdown in place with markdownlint's auto-fixes (source.markdownlint only)

--fix is the only thing validation ever writes, and only when you ask for it: it applies markdownlint's own auto-fixes to the markdown, prints every file it rewrote, and still reports whatever cannot be fixed mechanically. It uses the same .markdownlint.* config the source.markdownlint check uses, and does nothing when no config is found. A file with uncommitted changes is rewritten too — you get a notice, not a refusal.

It writes only when source.markdownlint is actually part of the run, so --skip source.markdownlint, an --only that names other checks, or a --phase/--category that filters the source checks out all leave your markdown untouched — --fix can never rewrite a file for a check the report does not mention.

Two narrowed runs replace the former lint and audit commands (removed in 0.11.10 — each was a thin alias over this same pipeline):

# CSS print-safety only — remote url(), rasterizing effects, page containment
# (the check `gutterpress lint <dir>` used to run; findings list file and line:col)
gutterpress validate my-book --only source.stylelint

# Asset checks only — image DPI / format / color space, fonts
# (what `gutterpress audit <dir>` used to run)
gutterpress validate my-book --category asset --phase pre

gutterpress preflight

Run a deterministic print preflight against an already-built PDF and write a GO/FIX/NO-GO report (JSON + Markdown) — the automatable gate for CI before handing a PDF to a printer.

gutterpress preflight [dir] --pdf <path> [options]

  --pdf <path>              Path to the PDF file to preflight   (required)
  --input <dir>             Optional source directory for pre-build checks (overrides the positional directory)
  --manifest <path>         Path to manifest.yaml
  --target <t>              Publish targets to preflight against (comma-separated, e.g. dtrpg,itch), overriding the manifest's `targets:`
  --report-dir <dir>        Output directory for preflight reports (default: alongside the PDF)
  --name <name>             Base filename for report outputs

Exits 1 when the computed status is NO-GO (errors, or a required check skipped/failed).

gutterpress doctor

Report the Gutterpress version, platform and config paths, and whether each external tool is available. Missing tools include the features that use them and platform-specific installation guidance.

gutterpress doctor

gutterpress ext

List, add, remove, enable, or disable the project's extensions. Markdown-it plugins, looks (stylesheets) and component libraries are all extensions — one extensions: list in manifest.yaml, in load order (see Extensions below). These are the same shared-lib functions the desktop app's Look and Features views call, so an extension added from the terminal is the same entry the GUI shows.

gutterpress ext

  --help    Show ext subcommands (list, add, remove, enable, disable, search)

Every subcommand takes the project directory as an optional trailing positional (default: the current directory).

gutterpress ext list

List the project's extensions in load (= cascade) order: each specifier as written, whether it is bundled, a path, or an npm package, what it carries (markdown, styles, snippets, components), and any warning — not installed, not found, not pinned.

gutterpress ext list ./my-book

gutterpress ext add

Add an extension. What SOURCE is decides what happens:

Adding something already listed re-pins or updates that entry instead of adding a second one.

An npm install also makes sure the book's .gitignore ignores plugins/npm/ (append-only; an author's own !plugins/npm/ re-include is respected and the install warns that downloaded extensions shouldn't be committed). The manifest pin is what travels with the book; the downloaded copy does not.

Restoring pinned extensions

A fresh clone has manifest.yaml's name@version pins but not the downloaded copies under plugins/npm/. build, validate, preflight and preview download any missing (or incomplete) pinned version before they load plugins — exactly the pinned version, through the same registry-resolved, hash-verified, load-tested install as ext add, without touching the manifest. Builds and checks do it inside the shared plugin-loading step, so every build and export (including the desktop's) and every check run does it, and fails with the extension, the reason and how to retry when it cannot. Nothing is downloaded, and the network is not used, when every copy is present; unpinned names, local paths and bundled features are never touched. The plugin loader itself stays offline. The desktop app does the same when it opens a book.

Once a package's pinned version is installed (by ext add or a restore), its other downloaded versions under plugins/npm/<name>/ are deleted — a cache that a pulled pin or branch switch would otherwise leave to pile up — except a version another enabled manifest entry still pins. A failed install removes the folders it created and nothing else.

gutterpress ext add <source> [dir] [options]

  --export <name>    Named module export to use as the plugin function (npm and path extensions only)
  --look             Treat SOURCE as a built-in look id (clean-book, zine, technical-doc): copy it into extensions/ and add it
gutterpress ext add markdown-it-highlightjs ./my-book
gutterpress ext add markdown-it-highlightjs@4.3.0 ./my-book
gutterpress ext add markdown-it-emoji@3.0.0 ./my-book --export full
gutterpress ext add markdown-it-mark ./my-book
gutterpress ext add ./plugins/field-notes ./my-book
gutterpress ext add zine ./my-book --look
gutterpress ext add ./parchment.zip ./my-book
gutterpress ext add https://example.com/looks/cool/ ./my-book

gutterpress ext remove

Remove an extension from the manifest, by the specifier as written there (for an npm package the bare name is enough). An npm extension's vendored copy under plugins/npm/ is deleted too; a path extension's folder is yours and is never touched.

gutterpress ext remove markdown-it-highlightjs ./my-book
gutterpress ext remove ./extensions/zine ./my-book

gutterpress ext enable / gutterpress ext disable

Turn a configured extension off without removing it (disable writes enabled: false on the entry, so the toggle is reversible), or back on.

gutterpress ext disable markdown-it-mark ./my-book
gutterpress ext enable markdown-it-mark ./my-book

Search npm for extensions — the same list the desktop app's Features view shows under "Find more on npm." Not project-scoped (no dir argument): it queries the registry for packages tagged gutterpress (Gutterpress extensions: plugins, looks, component libraries) or markdown-it-plugin (the markdown-it ecosystem's own tag — those work in Gutterpress unchanged), and prints each match's name@version, which kind it is, and its description, ending with the ext add command to install it.

gutterpress ext search [query]
gutterpress ext search
gutterpress ext search footnote

An empty or omitted query lists the most relevant tagged packages. To publish an extension, npm publish it with "gutterpress" in its keywords — there is no index to be added to and nothing to register. Set GUTTERPRESS_NPM_REGISTRY to point at a different registry (http/https only) — useful for a private mirror, or for testing; the installer uses the same setting, and only accepts tarballs from that registry's own origin.

Exit codes

Every command follows the same exit-code contract, so CI can branch on the result without parsing output:

Code Meaning
0 Clean — no findings, nothing to fix.
1 Findings — the command ran fine but reported findings/validation failures (validate/preflight findings, a build quality-gate rejection).
2 Usage — the invocation itself was wrong: a bad flag, positional argument, preset, or value.
3 Pipeline — the build/render/export pipeline itself failed for a reason unrelated to usage or findings (I/O error, missing tool, renderer crash).

This applies uniformly across build, preview, validate, preflight, publish, ext, new, and doctor.

Extensions

Gutterpress uses markdown-it under the hood, so pure-JavaScript plugins that follow the (md, options) => void signature work without a Gutterpress-specific API. Plugins, looks (stylesheets) and component libraries are all extensions: one extensions: list in manifest.yaml, in load order. Every entry is a bare specifier, and its form says what it is:

extensions:
  # a feature bundled with Gutterpress — nothing to install, works offline
  - markdown-it-mark
  # a folder or plugin file, relative to manifest.yaml — referenced in place
  - ./extensions/clean-book
  - ./plugins/my-custom-plugin.js
  # an npm package, pinned to the exact version `gutterpress ext add` installed
  - markdown-it-highlightjs@4.3.0
  # the object form, only when an entry needs more than its specifier
  - use: markdown-it-emoji@3.0.0
    export: full                 # the plugin function is a named export
  - use: markdown-it-anchor@9.2.0
    options:
      level: 2
  - use: ./plugins/drafts.js
    enabled: false               # keep the entry, skip loading it

An extension folder (or npm package) describes itself in its package.json — there is no second, Gutterpress-specific manifest file. npm's own fields are read as-is, and everything Gutterpress needs sits under one optional "gutterpress" key:

{
  "name": "field-notes",
  "description": "Margin notes and term boxes",
  "author": "You",
  "keywords": ["gutterpress", "markdown-it-plugin"],
  "main": "plugin.js",
  "gutterpress": {
    "styles": ["styles/plugin.css"],
    "snippets": "snippets",
    "components": "components.yaml",
    "tokensFile": "styles/tokens.css"
  }
}

main is the markdown-it plugin — npm's own convention — so a plain markdown-it plugin package needs nothing else at all. A look needs only gutterpress.styles (an ordered list, relative to the folder); a component library carries both. gutterpress.markdown exists for the one case main can't express: a package whose main is not the plugin. Folders still carrying the removed gutterpress.json or theme.json fail to load with a message showing the package.json that replaces them.

Order is load order: a later entry's markdown runs after earlier entries' (and sees their output) and its CSS wins ties — each extension's stylesheets are wrapped in their own cascade layer (@layer ext.<name>), declared in list order, so this holds however an extension writes its CSS; the project's own styles: stay unlayered and load after every extension, beating all of them at any specificity. There is no priority and no path:/name: wrapper — a manifest still carrying plugins: fails with a message that prints the same entries rewritten as extensions:. The engine: and engineStyles: keys are gone too (there is one engine; move any engineStyles entries to the end of styles:).

Pinned npm packages and their runtime dependencies live under plugins/npm/ as a plain nested node_modules tree, which Node's own module resolution loads. The pin travels with the project; the downloaded copy is gitignored, and build, validate, preflight and preview download a missing pinned version first (nothing is fetched when every copy is present). Each tarball's integrity is checked against the registry's hash when it is installed. Install/build scripts, native addon compilation, bundled node_modules, and non-registry dependency selectors are intentionally unsupported. Only install packages you trust: extensions run unsandboxed with the process's full filesystem and network privileges.

Use the entry's export field, or ext add --export <name>, for packages that expose a named plugin function instead of a default export.

A plugin's declared markers (export const markers) are its components. Each one's example snippet — snippets/<marker>.md by default, or the marker's own snippet: path — is what the desktop editor inserts for it, and an optional validate(component) function on the marker checks what authors put inside; its problems appear in gutterpress validate and the desktop Problems panel.

See User Guide: Chapter 5 — Plugins for the full list contract and for authoring custom plugins, and Chapter 4 — Styling & Theming for looks.

CI / scripting

The standalone binary is the easiest way — drop it in a GitHub Actions step and you're done:

- name: Build PDF
  run: |
    curl -L -o gutterpress \
      https://github.com/dimm-city/gutterpress/releases/latest/download/gutterpress-cli-linux-x64
    chmod +x gutterpress
    sudo apt-get install -y google-chrome-stable ghostscript
    ./gutterpress build ./my-book --out dist/my-book.pdf

The binary is self-contained except for the system tools described in User Guide: Chapter 7 — System Setup. On a runner with Chrome and Ghostscript present, you don't need a separate Node or Bun install.

Troubleshooting

License

MPL-2.0