Spec2Deck
Turn an openapi.yaml into a presentation — but pick who it's for first.
This is a working prototype of the idea from a shared ChatGPT conversation:
programmers keep the truth in openapi.yaml, everyone else wants "a document
that makes sense" — a PPTX for a business stakeholder, a PDF/HTML for a
manager, full technical detail for another developer. Same spec, three
completely different documents, picked from a dropdown.
openapi.yaml ──▶ parse ──▶ internal model ──▶ [Developer|Business|Manager view] ──▶ PPTX / HTML / Markdown
What's actually real here
- Core engine (
src/core/): OpenAPI parser (local $ref resolution,
no external tooling), an audience layer that decides how much technical
detail to show and how to phrase things, and four generators (Markdown,
HTML, PPTX via pptxgenjs, PDF via pdfkit).
- CLI (
src/cli.ts): fully working, tested end-to-end against the
sample spec below. This is the fastest way to see it work.
- VS Code extension (
src/extension.ts): registers a right-click
command ("Spec2Deck: Generate API Presentation...") with a QuickPick for
audience + output format, same engine underneath. It compiles cleanly
against @types/vscode, but hasn't been click-tested inside a real VS
Code Extension Development Host (no GUI in this environment) — see
"Trying the extension for real" below.
- No AI/Claude call anywhere yet — see
core/audience.ts's own comment for
the one function (purposeFor) that's the actual drop-in slot for it.
Everything works today with a deterministic, template-based fallback.
- Licensing (
src/core/license.ts, src/core/watermark.ts,
src/licenseStore.ts): Free tier = PDF preview (watermarked) and
Markdown, unlimited use. HTML and PPTX are Pro-only exports outright.
Pro = pay once, activate a license key, get the same PDF preview but
clean, plus real HTML and a fully-editable .pptx. Deliberately not
gated on file count or endpoint count — see license.ts's own docstring
for why that's trivially defeated (just split the spec into smaller
files).
⚠️ Simplified 2026-09-07, two changes same day. First: PPTX used to
carry a watermark the same way HTML did (a slide.addText(...) shape) —
found via real testing that this is trivially removable (PPTX is
inherently editable; select the shape, delete it). No shape-based
watermark inside an editable format can ever be un-removable. Second,
rather than leave "request pptx, quietly get a pdf instead" as the rule
(confusing), HTML and PPTX became Pro-only outright and PDF became the
one universal preview format everyone can generate — watermarked free,
clean licensed, same generator either way. See generators/pdf.ts's and
watermark.ts's own header comments for the full reasoning.
Licensing model (pay once, not per file)
Free — unlimited files, unlimited runs. "pdf" (watermarked) and
"markdown" only — "html"/"pptx" are skipped with a message.
Pro — one-time purchase, "Activate License" command — "pdf" is clean,
plus real, editable, unwatermarked "html"/"pptx" unlock.
Backend is Gumroad's own native License Key
feature (added directly on the "Spec2Deck Pro" product page, no separate
tool needed) + its free License Verification API
(POST https://api.gumroad.com/v2/licenses/verify, no secret token
required). Real product created 2026-09-07 — GUMROAD_PRODUCT_ID in
core/license.ts is the real Product ID from the live "Spec2Deck Pro"
product's Content tab, so verifyLicenseKeyRemote now actually calls
Gumroad for real. (Earlier drafts of this doc/code targeted Lemonsqueezy
instead — switched to Gumroad after confirming Thailand payout support
directly in the Gumroad dashboard, see Payments settings' own "minimum
payout threshold for Thailand is $100" message.)
Tested (node dist/cli.js ... --license-key <key>), including against a
real purchased Gumroad key:
- No key →
--output pdf writes a watermarked preview (rendered thumbnails
checked by eye, including mixed Thai/English text); --output html,pptx
are skipped with a "requires a Pro license" message, nothing written.
- Malformed key → rejected locally, no network call, same free-tier output.
- Real valid key →
"License: valid — Pro output (no watermark)"; pdf is
clean (no watermark stamped), html/pptx are written — the .pptx
confirmed clean by unzipping it and grepping every slide's XML for the
watermark string.
In the VS Code extension: Spec2Deck: Activate License... command
prompts for a key, verifies it, stores it in SecretStorage (not a plain
settings file), and caches the yes/no result in globalState so every
"Generate" doesn't re-hit the network. A status bar item
("Spec2Deck Free" / "Spec2Deck Pro ✓") shows current state and is a
shortcut to the Activate command. Deactivate License command clears it
(useful for testing, or moving a key off a machine).
Selling it — the two accounts this needs (not something I can set up for you)
- VS Code Marketplace Publisher — for distributing the extension
(free to list; the Marketplace itself has no paid-extension concept).
Needs a Microsoft account → Azure DevOps org → Personal Access Token →
vsce publish.
- Gumroad store — for selling the Pro unlock. Handles payment and
issues the license keys this code validates against (payout requires
connecting a payout method in Gumroad's Payments settings before the
product can go live for real sale).
Both need real identity/payment details, so they're on you, not something
this session can do on your behalf.
Try it (30 seconds)
npm install
npm run build
node dist/cli.js sample/openapi.yaml --audience developer,business,manager --output markdown,pdf --out-dir out
open out/api-business.pdf # or api-developer.pdf, api-manager.md
# with a real Pro license key, html/pptx unlock too:
node dist/cli.js sample/openapi.yaml --audience business --output pdf,html,pptx --license-key <KEY> --out-dir out
The sample spec (sample/openapi.yaml) is the exact "Customer API" example
from the original conversation — /auth/login, /customer/profile,
/customers, /customers/{id}.
Compare out/api-developer.pdf (HTTP verbs, types, status codes) against
out/api-business.pdf (plain-language "ใช้สำหรับ...", "ข้อมูลที่ส่ง",
"ผลลัพธ์") — same spec, deliberately different documents. That contrast is
the whole product idea.
Trying the extension for real
This repo isn't opened as a VS Code workspace in this session, so the
extension side has only been compiled, not clicked through. To actually
try it:
- Open this folder in VS Code.
npm install && npm run build.
- Press
F5 (Run Extension) — opens a new "Extension Development Host"
window.
- In that window, open
sample/openapi.yaml, right-click it in the
Explorer → Spec2Deck: Generate API Presentation... → pick an
audience → pick output format(s).
- Files land in
docs/ next to the spec.
Project layout
src/
core/
model.ts internal types (ApiModel, EndpointModel, ...)
parseOpenApi.ts openapi.yaml/json -> ApiModel
audience.ts the "same data, different story" logic
license.ts Gumroad license-key validation (framework-free)
watermark.ts free-tier watermark text, used by pdf.ts only
generators/
markdown.ts always free, never watermarked
pdf.ts the universal preview -- watermarked free, clean licensed
html.ts Pro only -- clean, unwatermarked
pptx.ts Pro only -- clean, unwatermarked, fully editable
cli.ts standalone CLI (no VS Code needed)
extension.ts VS Code command + QuickPick UI + status bar
licenseStore.ts VS Code SecretStorage/globalState wrapper around license.ts
assets/fonts/ bundled OFL-licensed Thai font (pdf.ts needs a real embedded
Unicode font -- PDF viewers can't be assumed to have one)
sample/openapi.yaml Customer API example from the original chat
Known rough edges (prototype, not a product)
- Schema flattening is one level deep by design (a slide shouldn't show 4
levels of nested objects) — deeply nested request/response bodies will
look sparse.
$ref resolution is local-document only (#/components/...); no
external file refs or remote $refs.
- PPTX layout is fixed-position text boxes, not a real template engine.
Fixed 2026-09-07: found via a real 46-schema, 40-endpoint spec (the
Grid Eagle Eye API) that the Data Model slide put every schema
side-by-side on ONE slide — dividing 9" by 46 columns produced ~0.2"-wide
columns, unreadable. Now paginates at
SCHEMAS_PER_SLIDE = 3 and caps
each schema to MAX_FIELDS_SHOWN = 12 fields (+N more beyond that);
the endpoint-list slide got the same treatment
(MAX_ENDPOINTS_PER_LIST_SLIDE = 15) since a real resource group already
had 10. Still not real reflow (fixed page size, not "shrink text to
fit"), just a hard cap instead of unbounded growth into a fixed box.
npm audit flags a transitive image-size DoS advisory inside
pptxgenjs's own dependency tree. Not exploitable here (this tool never
feeds it untrusted images), but worth knowing before shipping this
anywhere real.
generators/pdf.ts embeds the bundled Thai font as .woff, deliberately
NOT .woff2 — the currently pinned pdfkit/fontkit version pair silently
drops every base Thai glyph (only combining marks survive) from a woff2
embed, confirmed by rendering a real thumbnail, not just "no exception."
woff2 renders fine standalone through fontkit alone; the bug is
specifically in pdfkit's embedding of it. Re-verify visually before ever
touching this.
pdfkit cannot be bundled by esbuild (its ICC color-profile path
resolution breaks under esbuild's CJS output) — it ships unbundled, a
real node_modules/pdfkit (+ its own dependency tree) inside the
packaged extension. See esbuild.js's and .vscodeignore's own comments.
Where the original idea goes from here
The chat's own pitch was "one engine, many micro-tools" —
OpenAPI -> {PPTX, PDF, HTML, Markdown, Diagrams} today, then reuse the
same core/generators/ against a different parser: Postman collection,
SQL schema, GitHub README, etc. The parser/generator split here was built
with exactly that in mind — a new input just needs a new parseX.ts that
produces an ApiModel (or a sibling model), nothing downstream changes.