Skip to content
mdzip-projectPublic

About

CLI to create and extract .mdz files

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

mdz command-line interface (CLI)

A cross-platform command-line interface for creating, extracting, validating, and inspecting .mdz files.

The .mdz format is a portable, self-contained document format that packages one or more Markdown content files together with their associated assets into a single ZIP archive. See the MDZ specification for full details.


Installation

Although the CLI is built with C#, binaries are distributed as prebuilt release assets. .NET is not required for normal CLI use.

One-line install

Windows (WinGet):

winget install MDZip.Cli

Windows (PowerShell, will put mdz.cmd in %LOCALAPPDATA%\Microsoft\WindowsApps):

irm "https://raw.githubusercontent.com/mdzip-project/mdzip-cli/refs/heads/main/scripts/install.ps1?nocache=$(Get-Date -UFormat %s)" | iex

If you need to pin a specific release version during install:

$env:MDZ_VERSION='vX.Y.Z'
irm "https://raw.githubusercontent.com/mdzip-project/mdzip-cli/refs/heads/main/scripts/install.ps1?nocache=$(Get-Date -UFormat %s)" | iex

Linux/macOS system-wide install (recommended if you can use sudo):

curl -fsSL https://raw.githubusercontent.com/mdzip-project/mdzip-cli/refs/heads/main/scripts/install.sh | sudo sh

Linux/macOS (no sudo required, but you might need to fix PATH):

curl -fsSL https://raw.githubusercontent.com/mdzip-project/mdzip-cli/refs/heads/main/scripts/install.sh | sh

If mdz is not found after non-sudo install, add ~/.local/bin to your PATH:

# current shell
export PATH="$HOME/.local/bin:$PATH"

# persist for bash
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc

# persist for zsh
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc && source ~/.zshrc

After install, run:

mdz --help

Usage

mdz <command> [options]
mdz create <source> <output> [options]
mdz add <archive> <entry-path> <file>
mdz remove <archive> <entry-path>
mdz extract <archive> [options]
mdz validate <archive>
mdz ls <archive> [options]
mdz inspect <archive>

Note: <archive> is the .mdz file path; .mdz extension is optional.

Root options

Option Description
-v, --version Show version information
-?, -h, --help Show help and usage information

Commands

Command Description
create Create an .mdz archive from a source directory
add Add or replace a file entry in an existing .mdz archive
remove Remove a file entry from an existing .mdz archive
extract Extract the contents of an .mdz archive
validate Validate an .mdz archive against the specification
ls List the contents of an .mdz archive
inspect Inspect metadata and manifest information of an .mdz archive
textconv Print an archive as plain text, so git diff shows what changed inside it

mdz create <source> <output> [options]

Creates an .mdz archive from all files in a source directory. If no unambiguous entry point is found and you are in an interactive terminal, mdz prompts to generate a default index.md.

mdz create ./my-doc-folder my-doc.mdz --title "My Document" --author "Jane Smith" --entry-point index.md
mdz create --source ./my-doc-folder --output my-doc.mdz --force
mdz create ./my-doc-folder my-doc.mdz --create-index
mdz create ./my-doc-folder my-doc.mdz --map-files --entry-point index.md
mdz create ./my-doc-folder my-doc.mdz --mode project --entry-point index.md
mdz create ./my-doc-folder my-doc.mdz --filter "**/*.md" --filter "**/*.png" --filter "**/*.jpg" --filter "**/*.jpeg" --filter "**/*.webp" --filter "**/*.svg"
Option Description
--source, -s Required source directory (can also be provided positionally as <source>)
--output, -o Required output archive path (can also be provided positionally as <output>). If no extension is supplied, .mdz is added automatically
--filter, -fi Glob filter (archive-relative, repeatable). If omitted, defaults to markdown and common image files
--force, -f Overwrite output file if it already exists
--create-index, -ci Auto-generate index.md when no unambiguous entry point can be resolved
--map-files, -mf * Write/update manifest.json files[] mapping (path, originalPath, title). Invalid source paths are sanitized when needed
--title, -t * Sets manifest.title
--entry-point, -e * Sets manifest.entryPoint. Must name a .md or .markdown file in the archive, with exact case
--language, -l * Sets manifest.language (BCP 47, for example en, fr-CA). Defaults to en when another * option is passed
--author, -a * Sets manifest.author.name
--description, -d * Sets manifest.description
--mode * Sets manifest.mode to document or project
--doc-version * Sets manifest.version (for example 1.0.0)

Notes:

  • Every archive mdz create writes has a manifest.json with spec.version, title, entryPoint and mode (spec 1.2.0 §14.1). Without * options, the title is the source folder name and the entry point is discovered.
  • * = Manifest metadata option. If none is passed and --filter packages a manifest.json from the source, that manifest is used as-is (unknown fields kept) and only missing required fields are added. Otherwise the source manifest.json is replaced by the generated one.
  • Entry point discovery follows spec §5.5: index.md, else the one root Markdown file. Root README, AGENTS, LICENSE, CHANGELOG and CONTRIBUTING files are set aside when another root Markdown file exists.
  • manifest.mode defaults to document when omitted; use --mode project for multi-document project archives.
  • manifest.producer is optional in the spec; this CLI writes it by default.
  • If manifest-writing options are used without --title, title defaults to the source folder name.
  • If invalid archive path characters are found and --map-files is not passed, interactive mode prompts to enable file mapping.

Example manifest generated by mdz create:

{
  "spec": {
    "name": "mdzip-spec",
    "version": "1.2.0"
  },
  "title": "My Document",
  "entryPoint": "index.md",
  "mode": "document",
  "language": "en",
  "producer": {
    "application": {
      "name": "mdz-cli",
      "version": "<current mdz-cli version>",
      "url": "https://github.com/mdzip-project/mdzip-cli"
    },
    "core": {
      "name": "MDZip.Core",
      "version": "<current MDZip.Core version>",
      "url": "https://github.com/mdzip-project/mdzip-core"
    }
  },
  "author": {
    "name": "Jane Smith"
  }
}

mdz add <archive> <entry-path> <file>

Adds a local file into an existing archive at the given archive-relative path. If the entry path already exists, it is replaced.

add, remove and manifest set keep unknown manifest fields and every untouched entry byte-for-byte, and add a manifest.json to an archive that has none. A change that would make the archive open at a different entry point (for example removing index.md when other root Markdown files remain) is refused with ERR_ENTRYPOINT_CHANGED; set --entry-point with mdz manifest set first.

mdz add my-doc.mdz assets/images/cover.png ./cover.png
mdz add my-doc.mdz chapter-02.md ./chapter-02.md

mdz remove <archive> <entry-path>

Removes a file entry from an existing archive.

mdz remove my-doc.mdz assets/images/old-cover.png

mdz extract <archive> [options]

Extracts an .mdz archive to a destination directory.

mdz extract my-doc.mdz --output ./extracted
Option Description
--allow-invalid Extract even if archive validation fails
--output, -o Destination directory. Defaults to a directory named after the archive in the current folder

mdz validate <archive>

Validates an .mdz archive against the specification. Exits with code 0 if valid, 1 if invalid.

Errors carry the spec's error codes, for example ERR_ENTRYPOINT_INVALID (a malformed entryPoint), ERR_ENTRYPOINT_MISSING (no such entry; a case-only match is suggested), ERR_DUPLICATE_PATH (entries equal after NFC normalization) and ERR_ZIP_INVALID (compression other than stored or DEFLATE). Symbolic link entries are reported as warnings and are never followed or extracted as links. A missing manifest.json is a note, not a warning: the archive still conforms.

mdz validate my-doc.mdz

mdz ls <archive> [options]

Lists the contents of an .mdz archive.

mdz ls my-doc.mdz
mdz ls my-doc.mdz --long
Option Description
--long, -l Show detailed information (size, compressed size, last modified)

mdz inspect <archive>

Displays metadata and manifest information from a .mdz archive.

mdz inspect my-doc.mdz

inspect surfaces manifest.spec.version (falling back to legacy mdz if present), manifest.mode, and other key manifest metadata.


mdz textconv <archive>

Prints an archive as plain text for Git. Git treats an .mdz as binary, so git diff and git log -p normally show only "Binary files differ". With textconv set up, they show the lines that changed in each document inside the archive.

Set it up once per repository, and commit the .gitattributes line:

echo "*.mdz diff=mdz" >> .gitattributes

Then once in each clone (Git doesn't let a repository choose commands to run, so every clone opts in):

git config diff.mdz.textconv "mdz textconv"
git config diff.mdz.cachetextconv true

Clones without the git config lines still work: they show "Binary files differ" as before.

Each file in the archive becomes a section headed ===== path =====: manifest.json first, then the rest in path order. Text files (Markdown, JSON, SVG, CSS and similar) are printed in full; binary files such as images are one line with their size and SHA-256, so a replaced image shows up as a changed line:

 ===== index.md =====
 # Story

-- Users can reset their password
+- Users can reset their password by email
+- The link expires after 1 hour
 ===== images/login.png =====
-[binary, 48211 bytes, sha256 9f2c…]
+[binary, 51007 bytes, sha256 1ab4…]

It helps wherever Git computes the diff on your machine: git diff, git log -p, git show, and editors' Git views. The diff views on GitHub and Azure DevOps don't run it, and it doesn't merge: two branches changing the same .mdz still conflict (#14 proposes a merge driver). A file that isn't a readable archive, such as a Git LFS pointer, is printed as it is, so the diff never fails. The output matches mdzip.org's scripts/mdz-textconv.mjs.


Archive structure

A .mdz file follows the MDZ specification:

document.mdz
|- index.md               # Recommended entry point
|- manifest.json          # Metadata (mdz always writes one)
|- chapter-01.md
`- assets/
   `- images/
      `- cover.png

Paths inside an archive are case-sensitive and compared after Unicode NFC normalization (spec §5.4.1): mdz cat doc.mdz Index.md won't open index.md, but it will suggest it.

Development

Implementation note: this project is written in C# and targets .NET 10.

Browser Builder (experimental)

A standalone browser page is available at mdzip.org/packager.html. It accepts a dropped or selected folder, builds an .mdz, previews the resolved entry-point markdown, and provides a download link for the generated archive.

Building from source requires the .NET 10 SDK (Windows, macOS and Linux). The release downloads, install scripts and WinGet package don't need .NET installed.

# Build
dotnet build

# Test
dotnet test

# Run directly
dotnet run --project src/mdz.Cli/mdz.Cli.csproj -- --help

# Publish a single self-contained executable (example: Windows x64)
dotnet restore .\mdz-cli.slnx --configfile .\NuGet.Config
dotnet publish src/mdz.Cli/mdz.Cli.csproj -c Release -r win-x64 --self-contained true --no-restore -p:DebugType=None -p:DebugSymbols=false -o out/win-x64

# Or use the helper script (restores + verifies the mdz.exe/mdz output)
powershell -ExecutionPolicy Bypass -File .\scripts\publish-single.ps1 -Rid win-x64 -ConfigFile .\NuGet.Config

WinGet release automation is documented in docs/winget.md.

About

CLI to create and extract .mdz files

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages