#!/usr/bin/env bash # make.sh — minimax-pdf unified CLI # Usage: bash make.sh [options] # # Commands: # check Verify all dependencies # fix Auto-install missing dependencies # run --title T --type TYPE Full pipeline → output.pdf # --out FILE Output path (default: output.pdf) # --author A --date D # --subtitle S # --abstract A Optional abstract text for cover # --cover-image URL Optional cover image URL/path # --content FILE Path to content.json (optional) # demo Build a full-featured demo to demo.pdf # # Document types: # report proposal resume portfolio academic general # minimal stripe diagonal frame editorial # magazine darkroom terminal poster # # Content block types: # h1 h2 h3 body bullet numbered callout table # image figure code math chart flowchart bibliography # divider caption pagebreak spacer # # Exit codes: 0 success, 1 usage error, 2 dep missing, 3 runtime error set -euo pipefail SCRIPTS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PY="python3" NODE="node" # ── Colour helpers ───────────────────────────────────────────────────────────── red() { printf '\033[0;31m%s\033[0m\n' "$*"; } green() { printf '\033[0;32m%s\033[0m\n' "$*"; } yellow() { printf '\033[0;33m%s\033[0m\n' "$*"; } bold() { printf '\033[1m%s\033[0m\n' "$*"; } # ── check ────────────────────────────────────────────────────────────────────── cmd_check() { local ok=true bold "Checking dependencies..." # Python if command -v python3 &>/dev/null; then green " ✓ python3 $(python3 --version 2>&1 | awk '{print $2}')" else red " ✗ python3 not found" ok=false fi # reportlab if python3 -c "import reportlab" 2>/dev/null; then green " ✓ reportlab" else yellow " ⚠ reportlab not installed (run: make.sh fix)" ok=false fi # pypdf if python3 -c "import pypdf" 2>/dev/null; then green " ✓ pypdf" else yellow " ⚠ pypdf not installed (run: make.sh fix)" ok=false fi # Node.js if command -v node &>/dev/null; then green " ✓ node $(node --version)" else red " ✗ node not found — cover rendering unavailable" ok=false fi # Playwright if node -e "require('playwright')" 2>/dev/null || \ node -e "require(require('child_process').execSync('npm root -g').toString().trim()+'/playwright')" 2>/dev/null; then green " ✓ playwright" else yellow " ⚠ playwright not found (run: make.sh fix)" ok=false fi # matplotlib (optional — required for math/chart/flowchart; degrades gracefully) if python3 -c "import matplotlib" 2>/dev/null; then green " ✓ matplotlib (math, chart, flowchart blocks enabled)" else yellow " ⚠ matplotlib not installed — math/chart/flowchart blocks degrade to text (run: make.sh fix)" fi if $ok; then green "\nAll dependencies satisfied." exit 0 else yellow "\nSome dependencies missing. Run: bash make.sh fix" exit 2 fi } # ── fix ──────────────────────────────────────────────────────────────────────── cmd_fix() { bold "Installing missing dependencies..." local rc=0 # Python packages if command -v python3 &>/dev/null; then python3 -m pip install --break-system-packages -q reportlab pypdf matplotlib 2>/dev/null \ || python3 -m pip install -q reportlab pypdf matplotlib 2>/dev/null \ || { yellow " pip install failed — try: pip install reportlab pypdf matplotlib"; rc=3; } green " ✓ Python packages installed (reportlab, pypdf, matplotlib)" fi # Playwright if command -v npm &>/dev/null; then npm install -g playwright --silent 2>/dev/null && \ npx playwright install chromium --silent 2>/dev/null && \ green " ✓ Playwright + Chromium installed" || \ { yellow " playwright install failed — try manually"; rc=3; } else yellow " npm not found — cannot install Playwright automatically" rc=2 fi if [[ $rc -eq 0 ]]; then green "\nAll dependencies installed. Run: bash make.sh check" fi exit $rc } # ── run ──────────────────────────────────────────────────────────────────────── cmd_run() { local title="Untitled Document" local type="general" local author="" local date="" local subtitle="" local abstract="" local cover_image="" local accent="" local cover_bg="" local content_file="" local out="output.pdf" local workdir workdir="$(mktemp -d)" # Parse options while [[ $# -gt 0 ]]; do case "$1" in --title) title="$2"; shift 2 ;; --type) type="$2"; shift 2 ;; --author) author="$2"; shift 2 ;; --date) date="$2"; shift 2 ;; --subtitle) subtitle="$2"; shift 2 ;; --abstract) abstract="$2"; shift 2 ;; --cover-image) cover_image="$2"; shift 2 ;; --accent) accent="$2"; shift 2 ;; --cover-bg) cover_bg="$2"; shift 2 ;; --content) content_file="$2"; shift 2 ;; --out) out="$2"; shift 2 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done bold "Building: $title" echo " Type : $type" echo " Output : $out" # Step 1: tokens echo "" bold "Step 1/4 Generating design tokens..." local accent_args=() [[ -n "$accent" ]] && accent_args+=(--accent "$accent") [[ -n "$cover_bg" ]] && accent_args+=(--cover-bg "$cover_bg") $PY "$SCRIPTS/palette.py" \ --title "$title" --type "$type" \ --author "$author" --date "$date" \ --out "$workdir/tokens.json" \ "${accent_args[@]+"${accent_args[@]}"}" # Inject optional cover fields into tokens.json if [[ -n "$abstract" || -n "$cover_image" ]]; then PDF_ABSTRACT="$abstract" PDF_COVER_IMAGE="$cover_image" PDF_TOKENS="$workdir/tokens.json" \ $PY - <<'PYEOF' import json, os with open(os.environ["PDF_TOKENS"]) as f: t = json.load(f) abstract = os.environ.get("PDF_ABSTRACT", "") cover_image = os.environ.get("PDF_COVER_IMAGE", "") if abstract: t["abstract"] = abstract if cover_image: t["cover_image"] = cover_image with open(os.environ["PDF_TOKENS"], "w") as f: json.dump(t, f, indent=2) PYEOF fi cat "$workdir/tokens.json" | $PY -c " import json,sys t=json.load(sys.stdin) print(f' Mood : {t[\"mood\"]}') print(f' Pattern : {t[\"cover_pattern\"]}') print(f' Fonts : {t[\"font_display\"]} / {t[\"font_body\"]}')" # Step 2: cover HTML + render echo "" bold "Step 2/4 Rendering cover..." local subtitle_args=() [[ -n "$subtitle" ]] && subtitle_args=(--subtitle "$subtitle") $PY "$SCRIPTS/cover.py" \ --tokens "$workdir/tokens.json" \ --out "$workdir/cover.html" \ "${subtitle_args[@]+"${subtitle_args[@]}"}" $NODE "$SCRIPTS/render_cover.js" \ --input "$workdir/cover.html" \ --out "$workdir/cover.pdf" green " ✓ Cover rendered" # Step 3: body echo "" bold "Step 3/4 Rendering body pages..." if [[ -z "$content_file" ]]; then # Generate a minimal placeholder body cat > "$workdir/content.json" <<'JSON' [ {"type":"h1", "text":"Document Body"}, {"type":"body", "text":"Replace this with your content.json file using --content path/to/content.json"}, {"type":"body", "text":"See the content.json schema in the skill README for the full list of supported block types: h1, h2, h3, body, bullet, callout, table, pagebreak, spacer."} ] JSON content_file="$workdir/content.json" yellow " No content file provided — using placeholder body." fi $PY "$SCRIPTS/render_body.py" \ --tokens "$workdir/tokens.json" \ --content "$content_file" \ --out "$workdir/body.pdf" green " ✓ Body rendered" # Step 4: merge echo "" bold "Step 4/4 Merging and QA..." $PY "$SCRIPTS/merge.py" \ --cover "$workdir/cover.pdf" \ --body "$workdir/body.pdf" \ --out "$out" \ --title "$title" # Cleanup rm -rf "$workdir" } # ── fill ────────────────────────────────────────────────────────────────────── cmd_fill() { local input="" out="" values="" data_file="" inspect_only=false while [[ $# -gt 0 ]]; do case "$1" in --input) input="$2"; shift 2 ;; --out) out="$2"; shift 2 ;; --values) values="$2"; shift 2 ;; --data) data_file="$2"; shift 2 ;; --inspect) inspect_only=true; shift ;; *) echo "Unknown option: $1"; exit 1 ;; esac done if [[ -z "$input" ]]; then echo "Usage: make.sh fill --input form.pdf [--out filled.pdf] [--values '{...}'] [--data values.json] [--inspect]" exit 1 fi if $inspect_only || [[ -z "$out" && -z "$values" && -z "$data_file" ]]; then bold "Inspecting form fields in: $input" $PY "$SCRIPTS/fill_inspect.py" --input "$input" return fi bold "Filling form: $input → $out" local val_args="" if [[ -n "$values" ]]; then val_args="--values $values"; fi if [[ -n "$data_file" ]]; then val_args="--data $data_file"; fi $PY "$SCRIPTS/fill_write.py" --input "$input" --out "$out" $val_args } # ── reformat ─────────────────────────────────────────────────────────────────── cmd_reformat() { local input="" title="Reformatted Document" type="general" local author="" date="" out="output.pdf" subtitle="" local tmpdir tmpdir="$(mktemp -d)" while [[ $# -gt 0 ]]; do case "$1" in --input) input="$2"; shift 2 ;; --title) title="$2"; shift 2 ;; --type) type="$2"; shift 2 ;; --author) author="$2"; shift 2 ;; --date) date="$2"; shift 2 ;; --subtitle) subtitle="$2"; shift 2 ;; --out) out="$2"; shift 2 ;; *) echo "Unknown option: $1"; exit 1 ;; esac done if [[ -z "$input" ]]; then echo "Usage: make.sh reformat --input source.md --title T --type TYPE --out output.pdf" exit 1 fi bold "Parsing: $input" $PY "$SCRIPTS/reformat_parse.py" --input "$input" --out "$tmpdir/content.json" green " ✓ Parsed to content.json" bold "Applying design and building PDF..." local sub_args=() [[ -n "$subtitle" ]] && sub_args=(--subtitle "$subtitle") cmd_run \ --title "$title" --type "$type" \ --author "$author" --date "$date" \ --content "$tmpdir/content.json" \ --out "$out" \ "${sub_args[@]+"${sub_args[@]}"}" rm -rf "$tmpdir" } # ── demo ────────────────────────────────────────────────────────────────────── cmd_demo() { local tmpdir tmpdir="$(mktemp -d)" cat > "$tmpdir/content.json" <<'JSON' [ {"type":"h1", "text":"Executive Summary"}, {"type":"body", "text":"This document was generated by minimax-pdf — a skill for creating visually polished PDFs. Every design decision is rooted in the document type and content, not a generic template."}, {"type":"callout", "text":"Key insight: design tokens flow from palette.py through every renderer, keeping cover and body visually consistent."}, {"type":"h1", "text":"How It Works"}, {"type":"h2", "text":"The Token Pipeline"}, {"type":"body", "text":"The palette.py script infers a color palette and typography pair from the document type. These tokens are written to tokens.json and consumed by every downstream script."}, {"type":"numbered","text":"palette.py generates color tokens, font selection, and the cover pattern"}, {"type":"numbered","text":"cover.py renders the cover HTML using the selected pattern"}, {"type":"numbered","text":"render_cover.js uses Playwright to convert the HTML cover to PDF"}, {"type":"numbered","text":"render_body.py builds inner pages from content.json using ReportLab"}, {"type":"numbered","text":"merge.py combines cover + body and runs final QA checks"}, {"type":"h2", "text":"Cover Patterns"}, {"type":"table", "headers": ["Pattern", "Document type", "Visual character"], "rows": [ ["fullbleed", "report, general", "Deep background · dot-grid texture"], ["split", "proposal", "Left dark panel · right dot-grid"], ["typographic", "resume, academic", "Oversized display type · first-word accent"], ["atmospheric", "portfolio", "Dark bg · radial glow · dot-grid"], ["magazine", "magazine", "Cream bg · centered · hero image"], ["darkroom", "darkroom", "Navy bg · centered · grayscale image"], ["terminal", "terminal", "Near-black · grid lines · monospace"], ["poster", "poster", "White · thick sidebar · oversized title"] ] }, {"type":"h1", "text":"Data Visualisation"}, {"type":"h2", "text":"Performance Metrics (Chart)"}, {"type":"body", "text":"Charts are rendered natively using matplotlib with a color palette derived from the document accent. No external chart services or image files required."}, {"type":"chart", "chart_type": "bar", "title": "Quarterly Performance", "labels": ["Q1", "Q2", "Q3", "Q4"], "datasets": [ {"label": "Revenue", "values": [120, 145, 132, 178]}, {"label": "Expenses", "values": [95, 108, 99, 122]} ], "y_label": "USD (thousands)", "caption": "Quarterly revenue vs. expenses" }, {"type":"h2", "text":"Market Share (Pie Chart)"}, {"type":"chart", "chart_type": "pie", "labels": ["Product A", "Product B", "Product C", "Other"], "datasets": [{"values": [42, 28, 18, 12]}], "caption": "Annual market share by product line" }, {"type":"pagebreak"}, {"type":"h1", "text":"Mathematics"}, {"type":"body", "text":"Display math is rendered via matplotlib mathtext — no LaTeX binary installation required. Inline references use standard [N] notation in body text."}, {"type":"math", "text":"E = mc^2", "label":"(1)"}, {"type":"math", "text":"\\int_0^\\infty e^{-x^2}\\,dx = \\frac{\\sqrt{\\pi}}{2}", "label":"(2)"}, {"type":"math", "text":"\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}", "caption":"Basel problem (Euler, 1734)"}, {"type":"h1", "text":"Process Flow"}, {"type":"body", "text":"Flowcharts are drawn directly using matplotlib patches — no Graphviz or external tools needed. Supported node shapes: rect, diamond, oval, parallelogram."}, {"type":"flowchart", "nodes": [ {"id":"start", "label":"Start", "shape":"oval"}, {"id":"input", "label":"Receive Input", "shape":"parallelogram"}, {"id":"valid", "label":"Valid?", "shape":"diamond"}, {"id":"proc", "label":"Process Data", "shape":"rect"}, {"id":"err", "label":"Return Error", "shape":"rect"}, {"id":"out", "label":"Return Result", "shape":"parallelogram"}, {"id":"end", "label":"End", "shape":"oval"} ], "edges": [ {"from":"start", "to":"input"}, {"from":"input", "to":"valid"}, {"from":"valid", "to":"proc", "label":"Yes"}, {"from":"valid", "to":"err", "label":"No"}, {"from":"proc", "to":"out"}, {"from":"err", "to":"end"}, {"from":"out", "to":"end"} ], "caption": "Data validation and processing flow" }, {"type":"h1", "text":"Code Example"}, {"type":"code", "language":"python", "text":"# Design token pipeline\ntokens = palette.build_tokens(\n title=\"Annual Report\",\n doc_type=\"report\",\n author=\"J. Smith\",\n date=\"March 2026\",\n)\nhtml = cover.render(tokens)\npdf = render_cover(html)"}, {"type":"h1", "text":"Design Principles"}, {"type":"body", "text":"The aesthetic system is documented in design/design.md. The core rule: every design decision must be rooted in the document content and purpose. A color chosen because it fits the content will always outperform a color chosen because it seems safe."}, {"type":"h2", "text":"Restraint over decoration"}, {"type":"body", "text":"The page is done when there is nothing left to remove. Accent color appears on section rules only — not on headings, not on bullets. No card components, no drop shadows."}, {"type":"callout", "text":"A PDF passes the quality bar when a designer would not be embarrassed to hand it to a client."}, {"type":"pagebreak"}, {"type":"bibliography", "title": "References", "items": [ {"id":"1","text":"Bringhurst, R. (2004). The Elements of Typographic Style (3rd ed.). Hartley & Marks."}, {"id":"2","text":"Cairo, A. (2016). The Truthful Art: Data, Charts, and Maps for Communication. New Riders."}, {"id":"3","text":"Hochuli, J. & Kinross, R. (1996). Designing Books: Practice and Theory. Hyphen Press."} ] } ] JSON cmd_run \ --title "minimax-pdf demo" \ --type "report" \ --author "minimax-pdf skill" \ --date "$(date '+%B %Y')" \ --subtitle "A demonstration of the token-based design pipeline" \ --content "$tmpdir/content.json" \ --out "demo.pdf" rm -rf "$tmpdir" } # ── dispatch ─────────────────────────────────────────────────────────────────── main() { if [[ $# -lt 1 ]]; then bold "minimax-pdf — make.sh" echo "" echo "Usage: bash make.sh [options]" echo "" echo "Commands:" echo " check Verify all dependencies" echo " fix Auto-install missing deps" echo " run --title T --type TYPE CREATE: full pipeline → PDF" echo " [--author A] [--date D] [--subtitle S]" echo " [--abstract A] [--cover-image URL]" echo " [--accent #HEX] [--cover-bg #HEX]" echo " [--content content.json] [--out output.pdf]" echo " fill --input f.pdf FILL: inspect or fill form fields" echo " reformat --input doc.md REFORMAT: parse doc → apply design → PDF" echo " demo Build a full-featured demo PDF" exit 0 fi case "$1" in check) cmd_check ;; fix) cmd_fix ;; run) shift; cmd_run "$@" ;; fill) shift; cmd_fill "$@" ;; reformat) shift; cmd_reformat "$@" ;; demo) cmd_demo ;; *) echo "Unknown command: $1"; exit 1 ;; esac } main "$@"