UTPM Complete Guide

November 15, 2025 · View on GitHub

Welcome to the complete guide for UTPM (Unofficial Typst Package Manager)! This guide covers everything from basic usage to advanced development topics.

Quick Navigation:


For Users

Installation

Download the latest release from GitHub Releases.

From Source

You'll need Rust installed. Get it from rustup.rs.

# Clone the repository
git clone https://github.com/typst-community/utpm.git
cd utpm

# Build and install
cargo install --path .

Or, for a development build:

cargo build --release
cp ./target/release/utpm ~/.cargo/bin/

Basic Usage

UTPM has two main command groups:

  1. utpm project (or utpm prj) - Manage your Typst projects
  2. utpm packages (or utpm pkg) - Manage installed packages

Creating a New Package

# Create a new package interactively
utpm prj init

# Or use CLI mode (non-interactive)
utpm prj init --cli \
  --name my-package \
  --version 0.1.0 \
  --description "My awesome Typst package"

This creates:

  • typst.toml - Your package manifest
  • src/ directory - For your source files
  • examples/ directory - For example files
  • A default entrypoint file (main.typ)

Linking Your Package for Development

When you're developing a package, you want to test it in Typst without publishing it first.

# Link your current package to the local package directory
utpm prj link

# Link with a specific namespace (default is "local")
utpm prj link preview

# Force overwrite if package already exists
utpm prj link --force

After linking, you can use your package in Typst:

#import "@local/my-package:0.1.0": *

What gets copied? By default, UTPM respects:

  • .gitignore - Git ignore rules
  • .typstignore - Typst-specific ignore rules
  • [tool.utpm] exclude in typst.toml - Custom exclude patterns

You can control this with flags:

# Don't respect .gitignore
utpm prj link --no-git-ignore

# Don't respect .typstignore
utpm prj link --no-typst-ignore

# Create a symlink instead of copying files
utpm prj link --no-copy

Excluding Files from Your Package

Edit your typst.toml to exclude files when linking or publishing:

[package]
name = "my-package"
version = "0.1.0"
# ... other package fields ...

[tool.utpm]
exclude = [
  ".git",           # Git directory
  ".github",        # GitHub workflows
  "*.md",           # All Markdown files
  "tests/",         # Test directory
  "examples/",      # Examples directory
]

Pattern syntax:

  • * - Matches any characters except / (e.g., *.md matches README.md)
  • ** - Matches any characters including / (e.g., **/*.md matches docs/guide.md)
  • ? - Matches any single character
  • !pattern - Negates the pattern (includes files that would otherwise be excluded)

Cloning Packages from Typst Universe

# Clone a specific version
utpm prj clone @preview/example:1.0.0

# Clone latest version
utpm prj clone @preview/example

Managing Package Versions

# Bump to a specific version
utpm prj bump 1.2.0

# Bump with semantic versioning
utpm prj bump --major    # 1.0.0 -> 2.0.0
utpm prj bump --minor    # 1.0.0 -> 1.1.0
utpm prj bump --patch    # 1.0.0 -> 1.0.1

The bump command updates:

  • typst.toml version field
  • Any other files you specify with --include flag

Syncing Dependencies

# Update all dependencies to their latest versions
utpm prj sync

# Check for updates without applying them
utpm prj sync --check

Listing Installed Packages

# List all packages
utpm pkg list

# List with tree view
utpm pkg list --tree

# List as JSON
utpm -o json pkg list

Getting Metadata from Your Package

Extract metadata for use in scripts or CI/CD:

# Get all metadata
utpm prj metadata

# Get specific field (outputs plain text)
VERSION=$(utpm prj metadata -f version)
NAME=$(utpm prj metadata -f name)
AUTHORS=$(utpm prj metadata -f authors)

# Use in a script
echo "Building $NAME version $VERSION"
git tag "v$VERSION"

# Get as JSON for complex processing
utpm -o json prj metadata | jq '.version'

Available fields: name, version, entrypoint, authors, license, description, repository, homepage, keywords, categories, disciplines, compiler, exclude

Unlinking Packages

# Remove a package from local storage
utpm pkg unlink @local/my-package:0.1.0

Dry-Run Mode

Before making changes, you can see what would happen:

# See what would be linked without actually linking
utpm --dry-run prj link

# See what would be bumped
utpm --dry-run prj bump 2.0.0

Verbose Output

For debugging or understanding what UTPM is doing:

# Enable trace-level logging
utpm -v trace prj link

# Or set environment variable
UTPM_DEBUG=trace utpm prj link

Output Formats

UTPM can output in different formats for scripting:

# JSON output
utpm -o json pkg list

# YAML, TOML, HJSON (experimental, requires manual build)
utpm -o yaml pkg list

For Package Authors

Creating a High-Quality Package

1. Initialize with Good Metadata

utpm prj init

Fill in all the metadata fields:

  • name: Short, descriptive, lowercase with hyphens
  • version: Start with 0.1.0, follow semantic versioning
  • authors: Your name and email
  • license: Use a standard license (MIT, Apache-2.0, GPL-3.0, etc.)
  • description: One-line description of your package
  • repository: Link to your Git repository
  • keywords: Help users discover your package
  • categories: Classify your package

2. Structure Your Package

Recommended structure:

my-package/
├── typst.toml              # Package manifest
├── README.md               # Documentation
├── LICENSE                 # License file
├── main.typ                # Main entrypoint
├── src/                    # Additional source files
│   ├── utils.typ
│   └── styles.typ
├── examples/               # Example usage
│   ├── basic.typ
│   └── advanced.typ
└── tests/                  # Tests (not included in package)
    └── test.typ

3. Configure File Exclusion

Add to your typst.toml:

[tool.utpm]
exclude = [
  ".git",
  ".github",
  ".gitignore",
  "*.md",              # Exclude markdown (keep documentation in repository only)
  "tests/",            # Don't include tests in the package
  "examples/",         # Don't include examples in the package
  "*.typ.bak",         # Exclude backup files
  ".vscode/",          # Exclude editor configs
  ".idea/",
]

Or keep documentation and examples:

[tool.utpm]
exclude = [
  ".git",
  ".github",
  "tests/",
  "*.bak",
]

4. Test Locally

# Link your package
utpm prj link

# Create a test file
cat > test.typ << 'EOF'
#import "@local/my-package:0.1.0": *

// Test your package here
EOF

# Compile with Typst
typst compile test.typ

5. Version Your Package

Follow semantic versioning:

  • Major (1.0.0 → 2.0.0): Breaking changes
  • Minor (1.0.0 → 1.1.0): New features, backward compatible
  • Patch (1.0.0 → 1.0.1): Bug fixes
# Bump version
utpm prj bump 0.2.0

# Or use semantic flags
utpm prj bump --patch

6. Publish (Coming Soon)

Publishing to Typst Universe is currently in development. For now, share your package via:

  • GitHub repository
  • Manual installation instructions
  • Git-based installation with utpm pkg install

For Contributors

Setting Up Development Environment

Prerequisites

  1. Rust (latest stable): Get from rustup.rs
  2. Just (command runner): cargo install just
  3. Git: For version control

Clone and Setup

# Clone the repository
git clone https://github.com/typst-community/utpm.git
cd utpm

# Install git hooks (optional but recommended)
just setup-hooks

Development Workflow

Daily Commands

# Format your code
just fmt

# Check for compilation errors
just check

# Run tests
just test

# Run linter (Clippy)
just clippy

# Run all checks (format, lint, test) - do this before committing!
just ci

# Auto-fix formatting and linting issues
just fix

Building and Testing

# Build in debug mode (faster compilation, slower runtime)
just build

# Build in release mode (slower compilation, optimized binary)
just build-release

# Install locally for testing
just install-local    # Copies to ~/.cargo/bin/utpm

# Run with arguments
just run prj init

All Available Commands

Run just --list to see all commands:

just --list

Code Standards

1. Formatting

We use rustfmt with project-specific configuration in rustfmt.toml.

Rules:

  • Max line width: 100 characters
  • Edition: 2024
  • Use shorthand for try!?
  • Reorder imports alphabetically

Before committing:

cargo fmt --all
# Or
just fmt

Check formatting:

cargo fmt --all -- --check
# Or
just fmt-check

2. Linting

We use Clippy to catch common mistakes and enforce best practices.

Configuration in clippy.toml:

  • Cognitive complexity threshold: 25
  • Max line length: 100
  • Warn on common mistakes

Before committing:

cargo clippy --all-targets --all-features -- -D warnings
# Or
just clippy

Auto-fix issues:

cargo clippy --fix --allow-dirty
# Or
just clippy-fix

3. Testing

All new features must include tests.

Run tests:

cargo test --all-features
# Or
just test

Run with output:

cargo test --all-features -- --nocapture
# Or
just test-verbose

For detailed testing documentation, see Testing Guide:

  • 60+ tests covering utils, commands, and integration workflows
  • Test helpers and utilities
  • Running specific test suites
  • Writing new tests
  • Test coverage and debugging

Git Workflow

Commit Messages

We follow Conventional Commits:

<type>(<scope>): <description>

[optional body]

[optional footer]

Types:

  • feat: New feature
  • fix: Bug fix
  • docs: Documentation changes
  • style: Code style changes (formatting, etc.)
  • refactor: Code refactoring
  • test: Adding or updating tests
  • chore: Maintenance tasks

Examples:

git commit -m "feat(metadata): add field extraction with -f flag"
git commit -m "fix(link): respect typst_ignore flag when adding .typstignore"
git commit -m "docs: update README with metadata command examples"

Git Hooks

The project includes optional pre-commit hooks that run checks before each commit.

Install:

just setup-hooks

What the hooks do:

  1. Check code formatting
  2. Run Clippy linter
  3. Run all tests

If any check fails, the commit is blocked until you fix it.

Remove hooks:

just remove-hooks

Project Architecture

UTPM is structured as follows:

src/
├── main.rs                 # Entry point, CLI setup, logging
├── commands.rs             # CLI argument definitions
├── utils.rs                # Utility module aggregator
├── commands/               # Command implementations
│   ├── init.rs            # Create new packages
│   ├── link.rs            # Link packages for development
│   ├── unlink.rs          # Remove linked packages
│   ├── clone.rs           # Clone from Typst Universe
│   ├── publish.rs         # Publish to Universe (WIP)
│   ├── bump.rs            # Version bumping
│   ├── sync.rs            # Dependency syncing
│   ├── metadata.rs        # Metadata extraction
│   ├── install.rs         # Install from git repos
│   ├── get.rs             # Get package info
│   ├── list.rs            # List packages
│   ├── package_path.rs    # Show package paths
│   └── generate.rs        # Generate shell completions
└── utils/                  # Utility modules
    ├── dryrun.rs          # Dry-run mode support
    ├── git.rs             # Git operations
    ├── macros.rs          # Custom macros
    ├── output.rs          # Output format handling
    ├── paths.rs           # Path utilities
    ├── specs.rs           # Configuration parsing
    └── state.rs           # Error types

Key Dependencies

  • clap (4.5.39): CLI argument parsing with derive macros
  • typst-kit (0.13.1): Core Typst functionality
  • typst-syntax (0.13.1): Parsing Typst manifests
  • tokio (1.45.1): Async runtime
  • ignore (0.4.23): File filtering with gitignore patterns
  • serde (1.0): Serialization/deserialization
  • semver (1.0.26): Semantic versioning
  • tracing (0.1.41): Structured logging

Adding a New Command

Step 1: Create Command File

Create src/commands/mycommand.rs:

use crate::utils::*;
use tracing::instrument;

/// Execute the mycommand command
#[instrument(skip(cmd))]
pub async fn run(cmd: &MyCommandArgs) -> Result<bool> {
    utpm_log!(trace, "executing mycommand");
    
    // Your implementation here
    
    utpm_log!(info, "Command completed successfully");
    Ok(true)
}

Step 2: Define Arguments

Add to src/commands.rs:

pub mod mycommand;

#[derive(Parser, Clone, Debug, PartialEq)]
pub struct MyCommandArgs {
    /// Description of the argument
    #[arg(short, long)]
    pub my_arg: Option<String>,
}

Step 3: Add to Command Enum

In src/commands.rs, add to the appropriate enum:

#[derive(Subcommand, Clone, Debug, PartialEq)]
pub enum ProjectArgs {
    // ... existing commands ...
    
    /// My new command
    MyCommand(MyCommandArgs),
}

Step 4: Wire Up Dispatcher

In src/main.rs, add to the match statement:

match commands {
    // ... existing commands ...
    Commands::Project(ProjectArgs::MyCommand(cmd)) => {
        commands::mycommand::run(&cmd).await
    },
}

Step 5: Write Tests

Add tests to your command file:

#[cfg(test)]
mod tests {
    use super::*;
    
    #[tokio::test]
    async fn test_mycommand() {
        // Your tests here
    }
}

Code Patterns

Error Handling

Use the utpm_bail! macro for errors:

use crate::utils::*;

// Simple error
utpm_bail!(Manifest);

// Error with arguments
utpm_bail!(AlreadyExist, name, version, "Additional info");

Logging

Use the utpm_log! macro for all output:

// Simple message
utpm_log!(info, "Package linked successfully");

// With structured data
utpm_log!(trace, "Processing file", "path" => &path, "size" => size);

// With format strings
utpm_log!(info, "Found {} packages", count);

Dry-Run Mode

Always check dry-run before file operations:

use crate::utils::dryrun::get_dry_run;

if !get_dry_run() {
    // Only perform actual operations if not in dry-run mode
    fs::write(&path, content)?;
}

// Always log success, even in dry-run
utpm_log!(info, "File written successfully");

Async Functions

All command functions must be async:

#[instrument(skip(cmd))]
pub async fn run(cmd: &CommandArgs) -> Result<bool> {
    // Implementation
    Ok(true)
}

Debugging Tips

Enable Trace Logging

# Via flag
utpm -v trace prj link

# Via environment variable
UTPM_DEBUG=trace utpm prj link

Use Dry-Run Mode

utpm --dry-run prj link

Override Directories

# Test with custom directories
UTPM_DATA_DIR=/tmp/test-data utpm prj link
UTPM_CACHE_DIR=/tmp/test-cache utpm prj link

JSON Output for Debugging

utpm -o json pkg list | jq '.'

Common Issues

"Format check failed"

# Fix:
just fmt

"Clippy warnings"

# Fix:
just clippy-fix

"Tests failed"

# Run with output to see details:
just test-verbose

"Git hooks blocking commit"

If hooks are too strict for your workflow:

# Temporarily bypass (not recommended)
git commit --no-verify

# Or remove hooks
just remove-hooks

Technical Reference

Package Locations

UTPM manages packages in two locations:

1. Local Packages (User-Created)

Path: $DATA_DIR/typst/packages/{namespace}/{name}/{version}/

Default locations:

  • Linux: ~/.local/share/typst/packages/
  • macOS: ~/Library/Application Support/typst/packages/
  • Windows: %APPDATA%\typst\packages\

Override: Set UTPM_DATA_DIR environment variable

Used for:

  • Packages created with utpm prj init
  • Packages linked with utpm prj link
  • Custom namespaces (usually local)

2. Cache Packages (Downloaded)

Path: $CACHE_DIR/typst/packages/{namespace}/{name}/{version}/

Default locations:

  • Linux: ~/.cache/typst/packages/
  • macOS: ~/Library/Caches/typst/packages/
  • Windows: %LOCALAPPDATA%\typst\packages\

Override: Set UTPM_CACHE_DIR environment variable

Used for:

  • Packages cloned with utpm prj clone
  • Packages downloaded by Typst compiler
  • Usually preview namespace

File Filtering System

UTPM uses the ignore crate to filter files when linking or publishing packages.

Ignore Files Supported

  1. .gitignore - Git ignore patterns (enabled by default)
  2. .typstignore - Typst-specific ignore (enabled by default)
  3. .ignore - Generic ignore file (disabled by default, enable with -i)
  4. Global .gitignore - User's global git ignore (enabled by default)
  5. .git/info/exclude - Git's local exclude file (enabled by default)
  6. [tool.utpm] exclude - Patterns in typst.toml

Controlling Ignore Files

# Disable .gitignore
utpm prj link --no-git-ignore

# Disable .typstignore
utpm prj link --no-typst-ignore

# Enable .ignore file
utpm prj link --ignore

# Disable global .gitignore
utpm prj link --no-git-global-ignore

# Disable .git/info/exclude
utpm prj link --no-git-exclude

Pattern Syntax

The patterns follow standard glob syntax:

  • * - Match any characters except /

    • Example: *.md matches README.md but not docs/README.md
  • ** - Match any characters including /

    • Example: **/*.md matches README.md and docs/README.md
  • ? - Match exactly one character

    • Example: file?.txt matches file1.txt, fileA.txt
  • [abc] - Match any character in brackets

    • Example: file[0-9].txt matches file0.txt through file9.txt
  • ! - Negate pattern (include files that would be excluded)

    • Example: !important.md includes important.md even if *.md is excluded
  • / at start - Match from root of package

    • Example: /tests/ matches only tests/ at root, not src/tests/
  • / at end - Only match directories

    • Example: build/ matches directory build/ but not file build

Priority Order

When a file matches multiple patterns, UTPM applies them in this order:

  1. [tool.utpm] exclude patterns in typst.toml
  2. .typstignore (if enabled)
  3. .gitignore (if enabled)
  4. .ignore (if enabled)
  5. Global .gitignore (if enabled)
  6. .git/info/exclude (if enabled)

Later patterns can override earlier ones using ! negation.

Environment Variables

Logging

  • UTPM_DEBUG - Set log level: trace, debug, info, warn, error
    UTPM_DEBUG=trace utpm prj link
    

Paths

  • UTPM_DATA_DIR - Override local package directory

    UTPM_DATA_DIR=/custom/path utpm prj link
    
  • UTPM_CACHE_DIR - Override cache directory

    UTPM_CACHE_DIR=/custom/cache utpm prj clone @preview/example:1.0.0
    
  • UTPM_CURRENT_DIR - Override current working directory

    UTPM_CURRENT_DIR=/path/to/project utpm prj link
    

Output Formats

UTPM supports multiple output formats for scripting and automation.

Text (Default)

Human-readable output:

utpm pkg list

JSON

Machine-readable JSON:

utpm -o json pkg list

Use with jq for processing:

utpm -o json pkg list | jq '.[] | select(.namespace == "local")'

YAML, TOML, HJSON (Experimental)

These formats are experimental and require manual build:

# Enable features
cargo build --release --features yaml,toml,hjson

# Use
utpm -o yaml pkg list
utpm -o toml pkg list
utpm -o hjson pkg list

Package Manifest Format

UTPM uses Typst's standard typst.toml format with an optional [tool.utpm] section.

Minimal Example

[package]
name = "my-package"
version = "0.1.0"
entrypoint = "main.typ"
authors = ["Your Name <you@example.com>"]
license = "MIT"
description = "A short description"

Complete Example

[package]
name = "my-awesome-package"
version = "1.0.0"
entrypoint = "main.typ"
authors = [
  "First Author <first@example.com>",
  "Second Author <second@example.com>"
]
license = "MIT"
description = "A comprehensive package for awesome things"
homepage = "https://example.com/my-awesome-package"
repository = "https://github.com/username/my-awesome-package"
keywords = ["awesome", "package", "typst"]
categories = ["layout", "visualization"]
disciplines = ["mathematics", "engineering"]
compiler = "0.12.0"

[template]
path = "template"
entrypoint = "main.typ"
thumbnail = "thumbnail.png"

[tool.utpm]
exclude = [
  ".git",
  ".github",
  "*.md",
  "tests/",
  "examples/",
]

Field Reference

Required fields:

  • name - Package name (lowercase, hyphens allowed)
  • version - Semantic version (e.g., "1.0.0")
  • entrypoint - Main file (usually "main.typ")

Recommended fields:

  • authors - List of authors with optional email
  • license - SPDX license identifier
  • description - One-line description

Optional fields:

  • homepage - Package homepage URL
  • repository - Source repository URL
  • keywords - Search keywords (array of strings)
  • categories - Package categories (array of strings)
  • disciplines - Academic disciplines (array of strings)
  • compiler - Minimum Typst version required

Template fields (for templates):

  • template.path - Path to template directory
  • template.entrypoint - Template entrypoint file
  • template.thumbnail - Preview image

UTPM fields:

  • tool.utpm.exclude - Files to exclude when linking/publishing

Semantic Versioning

UTPM follows Semantic Versioning 2.0.0.

Version format: MAJOR.MINOR.PATCH

  • MAJOR: Incompatible API changes
  • MINOR: New functionality, backward compatible
  • PATCH: Bug fixes, backward compatible

Examples:

  • 1.0.02.0.0: Breaking change
  • 1.0.01.1.0: New feature
  • 1.0.01.0.1: Bug fix

Use utpm prj bump to update versions:

# Specific version
utpm prj bump 1.2.3

# Semantic increment
utpm prj bump --major    # 1.0.0 → 2.0.0
utpm prj bump --minor    # 1.0.0 → 1.1.0
utpm prj bump --patch    # 1.0.0 → 1.0.1

FAQ

General Questions

Q: What is the difference between utpm prj link and utpm prj clone?

A: link is for local development - it copies (or symlinks) your current project to the local package directory. clone downloads a package from Typst Universe to your cache directory.

Q: Where are packages stored?

A: Local packages (from link) go to ~/.local/share/typst/packages/ on Linux. Downloaded packages (from clone) go to ~/.cache/typst/packages/. See Package Locations.

Q: Can I use UTPM without Rust installed?

A: Yes, if you download a pre-built binary from the releases page. You only need Rust to build from source.

Q: Does UTPM work on Windows?

A: Yes! UTPM supports Linux, macOS, and Windows.

Package Development

Q: Why are my files not being linked?

A: Check your .gitignore, .typstignore, and [tool.utpm] exclude patterns. Use --dry-run to see what would be copied:

utpm --dry-run prj link

Q: How do I test my package before publishing?

A: Use utpm prj link to link it locally, then import it in a test Typst file:

#import "@local/my-package:0.1.0": *

Q: Should I include examples and tests in my package?

A: It depends. Examples can help users understand your package, but they increase package size. Tests should generally be excluded. Configure via [tool.utpm] exclude.

Q: How do I version my package?

A: Follow semantic versioning. Use utpm prj bump to update versions consistently across files.

Contributing

Q: Do I need to install git hooks?

A: No, they're optional. But they help catch issues before pushing to GitHub. Install with just setup-hooks.

Q: My pre-commit hook is failing, what do I do?

A: Run the checks manually to see the errors:

just ci

Then fix the issues and commit again.

Q: How do I run just one test?

A:

cargo test test_name

Q: Can I contribute if I'm new to Rust?

A: Absolutely! Start with documentation improvements, bug reports, or small features. Check the issues labeled "good first issue" on GitHub.

Troubleshooting

Q: utpm: command not found

A: Make sure ~/.cargo/bin is in your PATH:

echo 'export PATH="$HOME/.cargo/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Q: Permission denied when installing

A: Don't use sudo with cargo. Instead:

cargo install --path .

Q: UTPM is linking files I want to exclude

A: Check your patterns in [tool.utpm] exclude. Remember:

  • Patterns are relative to package root
  • Use / at the end for directories: tests/
  • Use ** for recursive: **/*.bak

Q: How do I reset everything and start fresh?

A:

# Remove local packages
rm -rf ~/.local/share/typst/packages/local/my-package

# Remove cache
rm -rf ~/.cache/typst/packages/

# Unlink a specific package
utpm pkg unlink @local/my-package:0.1.0

Getting Help


License

UTPM is licensed under the MIT License. See LICENSE for details.