How I Replaced Document Creation with Three CLI Tools

One day in April, I realized I almost never need to create a PDF-bound document from scratch. Most of the time I'm just filling out something someone else sent me. The things I actually need to write and export from scratch boil down to three: resume, invoice, and cover letter.

This article is also published on SSPAI.

One day in April, I realized I almost never need to create a PDF-bound document from scratch. Most of the time I’m just filling out something someone else sent me. The things I actually need to write and export from scratch boil down to three: resume, invoice, and cover letter.

With such clear use cases, I wondered if I could automate all three and completely remove “document creation” from my workflow.

Core Principle

My requirement: Don’t save PDFs. Only save the source files that generate PDFs.

PDF is a final artifact, but you can’t diff it, and you can’t batch-modify it. I want to manage source files as plain text, and generate PDFs on demand with a single command. Clean disk, clean mind.

1. Resume: JSON Resume + resumed

The core content of a resume stays largely the same, but you occasionally need to add or remove items.

I found JSON Resume, an open resume data standard. All resume content is stored in JSON, like this:

{
"basics": {
"name": "David Weng",
"email": "david@owo.lu"
},
"work": [
{
"name": "BC + AI Ecosystem",
"position": "ContentOps & Graphic Designer",
"startDate": "2025-11-01",
"highlights": ["Designed 50+ branded assets..."]
}
]
}

Once the resume is structured, content and styling are completely separated. Rendering is handled by resumed, a command-line tool that turns JSON into HTML or PDF with a single command:

npx resumed render resume.json \
-t @jsonresume/jsonresume-theme-consultant-polished \
-o resume.html

resume.json is the single source of truth. Change the theme parameter and the same data outputs a completely different layout. To customize a resume for different positions, just duplicate the JSON, tweak a few fields, and you never have to touch layout code.

A side note: why not Typst for resumes? Because the JSON Resume approach fundamentally solves the “swap themes without touching content” problem. It’s not a layout tool—it’s a data structure. I can store work experience, skills, and projects as modular blocks, then assemble them like LEGO for different companies, generate different resume.json files, and use resumed to output a PDF with one click. This separation of content and form is something LaTeX or Typst can’t match.

JSON Resume also has an online Registry. I dump my resume.json to a GitHub Gist, set up a GitHub Action to auto-sync, and from then on, visiting registry.jsonresume.org/thedavidweng always shows the latest version.

2. Invoice: One Command, One Invoice

The invoice need is dead simple: fill in a few fields, generate a properly formatted PDF.

I started with Invoify, a browser-based invoice generator. It’s actually quite good—you can export the filled form as JSON and import it as a template next time. But it requires a browser and can’t be automated in the terminal, which means no scripting. You still have to click a few buttons each time.

Later I switched to maaslalani/invoice, a pure CLI invoice tool:

invoice generate \
--from "David Weng" \
--to "Client Inc." \
--item "Consulting" --quantity 10 --rate 50 \
--tax 0.05 \
--note "Due within 30 days."

One command, one invoice. No browser, no GUI.

Now the workflow is clean: in my Obsidian notes, I store each invoice’s generation command in a code block. Not a single PDF on my disk. Need an invoice? Copy the command, run it, get the PDF. The source file is the CLI command itself.

3. Cover Letter: From LaTeX to Typst

First Attempt: LaTeX

LaTeX gives you pixel-level formatting control. Using it for a cover letter is using a sledgehammer to crack a nut.

The most discouraging part is the size. A full MacTeX installation is over 4 GB; even BasicTeX is several hundred MB. And LaTeX syntax isn’t very AI-friendly—the markup is a bit twisty and error-prone.

Final Solution: Typst

Then I discovered Typst, a modern typesetting system that’s much simpler than LaTeX.

A cover letter source file now looks like this:

#set page(paper: "a4", margin: 1in)
#set text(font: "Times New Roman", size: 11pt)
#set par(justify: true)
#align(right)[
David Weng \
david@owo.lu \
Vancouver, BC, Canada
#v(0.4em)
April 28, 2026
]
#v(1.6em)
Hiring Committee \
#v(1.5em)
Dear Hiring Committee...
#v(0.8em)
#align(right)[
Sincerely,
#v(0.55em)
David Weng
]

Readability is almost like plain text, and generating a PDF is still one command:

typst compile cover-letter.typ
LaTeXTypst
Installation size2–4 GB~40 MB
Syntax complexityHighClose to Markdown
Compilation speedSecondsMilliseconds
AI-friendlinessLowHigh

Skill-ifying: Turning Three Tools into Reusable Commands

I packaged these three workflows into three reusable skills and added them to my skill repository: https://github.com/thedavidweng/skills/tree/main/document-generation

Final Workflow

Three document types, three tools, three skills, each handling its own lane:

DocumentToolSource FormatGenerate Command
ResumeJSON Resume + resumed.jsonnpx resumed render
Invoicemaaslalani/invoiceCLI command itselfinvoice generate
Cover LetterTypst.typtypst compile

They share one principle: all source files are plain text. They can be managed with Git, edited with any editor, and read/written/understood by AI. PDF is just a temporarily generated build artifact—generate when needed, discard after use.

This workflow pulled me out of the loop of “open Word → manually adjust layout → export PDF → save to a folder you’ll forget the name of.” Now my document management is just code management: write the source file, navigate to the skill directory, run the command, done.