
Launch Video
FreeCreate high-quality product videos from terminal outputs.
Free · Opens the source repo
What Launch Video does
Launch Video is a skill designed to generate product videos by capturing real terminal user interface (TUI) frames from Hunk, compositing them into polished videos, and encoding them in popular formats. This skill operates through a three-stage pipeline: capturing, composing, and encoding. The capture stage uses a PTY to drive Hunk headlessly, taking styled snapshots of terminal frames as PNG files. The composition stage utilizes Playwright to render these images on an HTML canvas in Chromium, applying necessary visual elements such as window chrome, captions, and transitions. Finally, the encoding stage uses ffmpeg to compile the PNGs into video formats like MP4 and WebM.
This skill is particularly useful for developers and designers looking to create feature demos, workflow explainers, or product launch videos without relying on screen recordings or mockups. The generated videos maintain the authenticity of the terminal experience, showcasing the actual capabilities of the Hunk tool. Users can choose from different editorial scopes, whether they want to highlight a single feature or compile a comprehensive overview of a full release. The pipeline is flexible, allowing for custom video creation by authoring specific scenes and storyboards.
To get started, users need to have a Hunk source checkout and run the scripts provided in the scripts/launch-video/ directory. The skill is Unix-only and requires a compatible environment with Node.js and Bash. The process is streamlined, with clear commands for capturing keyframes, setting up the compositor, and encoding the final video. Users can iterate on specific scenes without needing to re-capture the entire video, making it efficient for rapid development cycles.
When to use it
Use this skill when you need to produce high-quality videos that demonstrate terminal-based applications or workflows, especially for product launches or feature announcements.
When not to use it
This skill is not suitable for non-Unix environments or for users who require extensive editing capabilities beyond the compositing and encoding stages.
What you can build with it
Feature Demo Video
Create a short video showcasing a specific feature of Hunk by capturing and composing only the relevant scenes.
Full Release Roundup
Compile a comprehensive video that highlights multiple features from a recent release, using the full-release recipe.
Custom Workflow Explainer
Author a set of scenes to produce a tutorial or announcement video that explains a particular workflow using Hunk.
How to install Launch Video
View source1. Install with the skills CLI
npx skills add modem-dev/hunk/launch-video --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by modem-devHunk video pipeline
Maintainer-only: requires a hunk source checkout (the pipeline lives in
scripts/launch-video/, which never ships to npm). Unix-only — the capture
scripts exec /bin/bash.
Generates product videos where every terminal frame is the real Hunk TUI — no screen recording, no mockups. Three stages:
capture.ts (bun) drive Hunk over a PTY, snap styled keyframes to PNG
compose.mjs (node + Playwright) render each PNG on a 1920x1080 HTML stage in Chromium
ffmpeg encode the composited PNGs at 30fps to mp4/webm
Playwright controls headless Chromium: for each planned frame it loads the terminal PNG onto the HTML stage, applies the window chrome, cards, captions, and transition state, then screenshots the completed stage back to PNG. ffmpeg sequences those composited screenshots into the final videos.
The generic machinery (PTY driving, keyframe rendering, storyboard planning,
Chromium compositing, the stage template) is the @hunk/term-video workspace
package in packages/term-video/; scripts/launch-video/ holds only Hunk's
scenes, captions, and cards on top of it.
Choosing a recipe
The pipeline is not release-specific. Choose the editorial scope, then use the same capture → composite → encode stages:
- Single feature: a short demonstration of one capability or workflow. Use the single-feature recipe below and capture only the required scene.
- Full release: a multi-feature roundup based on a release's changelog or highlights. Use the full-release recipe and update the canonical storyboard.
- Custom video: author any set of scenes and
SHOTSfor tutorials, comparisons, announcements, or workflow explainers; follow the scene and storyboard rules below.
Creating a video
Expect ~3–6 min for capture and ~2–4 min for compose — run both with a long
timeout (or in the background); each logs per-snap / per-shot progress.
compose.mjs needs node ≥ 18 on PATH (bun alone is not enough).
# 0. dependencies (tuistory + ghostty-opentui are devDependencies)
bun install # if a postinstall hook fails in a sandbox, retry with --ignore-scripts
# 1. capture keyframes. Output defaults to <repo>/.video-work/ regardless of
# cwd (pass a path argument to override), but the PROCESS must run from the
# repo root — see gotchas.
bun run scripts/launch-video/capture.ts
# 2. one-time portable compositor setup. Playwright installs a Chromium build
# that exactly matches its browser driver (see gotchas to reuse a system or
# sandbox browser instead).
printf '{"name":"hunk-video-work","private":true}\n' > .video-work/package.json
cd .video-work
bun add playwright playwright-core
bunx playwright install chromium
cd ..
# 3. composite the storyboard
node scripts/launch-video/compose.mjs .video-work
# 4. encode
cd .video-work
ffmpeg -y -f concat -safe 0 -i concat.txt -vf "fps=30,format=yuv420p" \
-c:v libx264 -preset slow -crf 18 -movflags +faststart launch.mp4
ffmpeg -y -f concat -safe 0 -i concat.txt -vf "fps=30,format=yuv420p" \
-c:v libvpx-vp9 -b:v 0 -crf 32 -row-mt 1 launch.webm
Iterate on one scene without re-capturing the rest:
SCENES=review bun run scripts/launch-video/capture.ts # comma-separated scene names
Scene names are the wants("...") guards in capture.ts's main(). Note
SCENES= only narrows capture; compose.mjs preflights that every frame its
SHOTS table references exists in frames/ and fails fast listing any missing
ones, so a full composite still needs every scene captured at least once.
Single-feature recipe
For a short test, demo, or one-feature announcement, capture and composite only the scene for that feature:
-
Pick one user-visible capability and find its scene name in the
wants("...")guards. If it does not have a scene, author one using the guidance below. Capture only that scene:SCENES=review bun run scripts/launch-video/capture.ts -
Make a scratch compositor beside the canonical one so its imports and repo-relative paths continue to work:
cp scripts/launch-video/compose.mjs scripts/launch-video/compose-one-feature.mjs -
In the scratch copy, trim
SHOTSto an opening card, only the selected feature's frames, and an outro card. Rewrite those cards and captions for the scoped cut. Sequence lengths must still match the captured frame names. -
Composite into the same work directory, then run the normal ffmpeg commands with descriptive output names:
node scripts/launch-video/compose-one-feature.mjs .video-work cd .video-work ffmpeg -y -f concat -safe 0 -i concat.txt -vf "fps=30,format=yuv420p" \ -c:v libx264 -preset slow -crf 18 -movflags +faststart hunk-0.18-line-review.mp4 ffmpeg -y -f concat -safe 0 -i concat.txt -vf "fps=30,format=yuv420p" \ -c:v libvpx-vp9 -b:v 0 -crf 32 -row-mt 1 hunk-0.18-line-review.webm cd .. rm scripts/launch-video/compose-one-feature.mjs
Keep the scratch compositor uncommitted. The canonical compose.mjs remains
the checked-in reference storyboard.
Full-release recipe
- Read the release section in
CHANGELOG.md. If it has a hand-written Highlights list (0.18.0 has one; Changesets does not generate them), use that list as the storyboard. Otherwise distill 4–6 user-visible headlines from the Minor Changes — per-PR entries are too granular to shoot — and confirm the shortlist with the user before capturing. - Rewrite the canonical storyboard's editorial surface (next section), adding or adjusting capture scenes as needed (see "Authoring scenes").
- Capture every scene referenced by the full storyboard, composite it, and encode both formats using the main workflow above.
- Verify the complete cut (see "Verification") and deliver both files.
Per-video editorial surface
The capture machinery is reusable, but the storyboard is editorial content for one video. Rewrite it to match the video's scope. As of this writing, the checked-in reference storyboard is the full 0.18.0 release video:
compose.mjs: the wholeSHOTStable;OPEN_CARD(version badge);OUTRO_CARD(headline, install commands, footer);EXTENSIONS_CARD; every<span class="badge">NEW</span>in captions — a NEW badge is a claim about this release, so drop or move them as features age.capture.ts: the scene functions and thewants()guards inmain()are the current storyboard's scene list, plus hunk-side glue (launchHunk,launchHunkShell,createDemoRepo, the keyboard probe).
Reusable machinery lives in @hunk/term-video (packages/term-video/) —
extend it there, don't fork it into the scripts: createKeyframer,
launchApp/launchShell, createCommandWrapper, typeCommand,
ensureKeyboardIsLive, makeSceneFilter (src/capture.ts); the unit-tested
storyboard planner with the caption/timing semantics (src/plan.mjs);
composeStoryboard with font/Chromium resolution and the missing-keyframe
preflight (src/compose.mjs); and the stage template (src/stage.html).
Environment gotchas
Sandbox-specific bullets are marked; each cost real debugging time.
-
Run
capture.tswith bun from the repo root. tuistory uses subpath self-imports (tuistory/pty) that only resolve inside this repo'snode_modules; running the script from elsewhere resolves tuistory from bun's global cache and crashes. (Output location is unaffected — it defaults to<repo>/.video-work/viaimport.meta.url.) -
ghostty-opentui is a transitive dep (via tuistory) with an exports map: import
"ghostty-opentui/image"(not.../dist/image.js), resolved relative to tuistory —@hunk/term-video/capture'screateKeyframerdoes theBun.resolveSyncdance. -
Playwright must match the Chromium it drives. The portable setup above installs
playwrightandplaywright-coretogether, then downloads their matching Chromium build. It works on macOS and Linux and is preferred when bandwidth and browser downloads are available; leaveCHROMIUM_PATHunset so Playwright uses that managed browser. On Linux, if Chromium reports missing system libraries, runbunx playwright install-deps chromium(it may require sudo) before retrying.To reuse an existing system, CI, or sandbox Chromium instead, install the
playwright-coreversion provided by that environment and set its executable explicitly:cd .video-work bun add playwright-core@<matching-version> cd .. CHROMIUM_PATH=/path/to/chromium node scripts/launch-video/compose.mjs .video-workCommon executable locations include
$(command -v chromium)or$(command -v google-chrome)on Linux and/Applications/Google Chrome.app/Contents/MacOS/Google Chromeon macOS. In an environment with a preinstalled Playwright toolchain, read that toolchain'spackage.jsonto get the exact driver version. For example, the Anthropic sandbox exposes it through/opt/pw-browsers/.links/*:cat "$(cat /opt/pw-browsers/.links/* | head -1)/package.json" | grep '"version"' # e.g. "1.56.1" -> bun add playwright-core@1.56.1compose.mjspicks its browser as$CHROMIUM_PATH, then/opt/pw-browsers/chromiumwhen present, then Playwright's managed browser. -
Give
.video-work/its ownpackage.jsonbeforebun add. Without one, bun walks up and installs into the repo'spackage.json— revert withgit checkout package.json bun.lockif that happens. -
Chromium needs
--allow-file-access-from-files(already incompose.mjs): the stage samples each keyframe through a canvas to color-match the window background, and file:// images taint the canvas without it. -
mp4 needs an ffmpeg with libx264. Sandbox:
apt-get install ffmpeg(runapt-get updatefirst if packages 404). macOS:brew install ffmpeg. Verifyffmpeg -encoders | grep -E 'libx264|libvpx-vp9'shows both before encoding — playwright's bundledffmpeg-*/ffmpeg-linuxonly does VP8/WebM and cannot produce the mp4. -
Caption font: JetBrains Mono ships inside ghostty-opentui;
findCaptionFontinpackages/term-video/src/compose.mjssearches bun's isolated layout (node_modules/.bun/node_modules/…) then a hoistednode_modules/…. If it still throwscaption font not found, locate the file withfind node_modules -name jetbrains-mono-nerd.ttfand pass it asfontPathtocomposeStoryboard.
Authoring scenes (capture.ts)
- Shared geometry is 140x32 cells rendered at fontSize 16 / dpr 2 → 2688x1536 PNGs. Keep every scene at this size so all frames fit one window.
- Helpers:
createDemoRepo()andlaunchHunkShell()are hunk-side glue in the script (git repo built fromexamples/2-mini-app-refactor; interactive bash with a realhunkcommand on PATH and a clean❯prompt);snap,typeCommand,launchApp/launchShell, andcreateCommandWrappercome from@hunk/term-video/capture. - Always
waitForTexton scene-specific content before the first snap, and callensureKeyboardIsLive()before scripted keypresses — the first key after startup can be dropped (real race, the helper toggles?to prove keys land). - Animation = one snap per keypress. Cursor walks and typing effects are
just every
j/k/character captured as its own frame and played back at 0.2–0.3s per frame. Prefer this over sparse keyframes: three stills read as a slideshow, per-press frames read as motion. renderTerminalToImageauto-trims trailing blank rows, so short outputs (CLI scenes) produce short PNGs — the stage handles this by sampling the image's bottom-left pixel and painting the window body to match.manifest.jsonis a capture-side inventory of the current run only;compose.mjsignores it (frames resolve by name fromSHOTS), and after aSCENES=run it is partial whileframes/stays cumulative.- Demo content that must exist: STML notes come from
examples/9-agent-markup-notes(launch with--experimental), extension scenes fromexamples/extensions/loaded via--extension <path>(explicit paths skip the repo trust prompt). The pager pipe isgit diff | hunk pager— barehunkon piped stdin prints help. Sidebar toggle iss; comment draft isc, save with Ctrl+S (\x13).
Storyboard model (compose.mjs)
SHOTSis the whole edit: one entry per shot,durin seconds, played as unique frames + per-frame durations in an ffmpeg concat list (holds cost one frame, so runtime is dominated by transitions, not length).capKeyis caption identity: the caption slides in only whencapKeychanges, and continuation shots that share acapKeywithout restatingcaptionkeep the previous caption on screen. Sequences (walks, typing) are generated withArray.fromspreads.- Sequence lengths must match capture loop bounds:
walk("j", 10)incapture.tsproducesreview-walk-00..09, consumed byArray.from({length: 9})(+ the opening frame) inSHOTS. Change one side and the other breaks — the preflight check names any frame that's missing. enter: truefades/scales the surface in — use it for cards and the first terminal shot only.- Caption HTML vocabulary:
<span class="badge">NEW</span>amber pill,<span class="hl">amber highlight,<span class="dim">muted. Cards usebadge/h1/h2/sub/cmds+cmd/footclasses frompackages/term-video/src/stage.html. - Target pacing: money shots hold 3–4s, context shots 2–3s, typing/walk frames 0.2–0.6s; keep the total near 60s.
Content accuracy (learned the hard way)
- Verify install commands against reality, not the README: check
npm view hunkdiff dist-tags. A prerelease needsnpm i -g hunkdiff@beta;brew install hunkonly serves stable (homebrew-core Autobump, lags npm) — omit brew on prerelease cards. - Label demo extensions as examples. The triage board, CSS palette, and
semver views are
examples/extensions/, not shipped features — caption them with a dimmedexample:prefix. The real features are the APIs (sidebars, file views, commands, dialogs). - Window titles are decorative but must not lie: the shell scenes run bash,
so keep their titles generic (
shell — …) rather than naming a shell the capture doesn't launch. - STML requires
--experimental; say so on the outro card. - The video is silent — never imply audio in the video or its announcement copy.
Verification and delivery
- Eyeball keyframes in
.video-work/frames/(Read renders PNGs) after capture — especially new scenes — before compositing. - After encoding, extract spot frames with
ffmpeg -y -ss <t> -i launch.mp4 -frames:v 1 check.pngat: a mid-animation point (captions must persist), each new scene, and the outro. Check duration withffprobe -show_entries format=duration. - Outputs land at
.video-work/launch.mp4and.video-work/launch.webm..video-work/is gitignored — never commit the video or its frames. Send both files to the user directly (mp4: social/Slack; webm: web embeds), report duration and file sizes, and flag if the mp4 exceeds ~10 MB (Slack) or ~15 MB (X). Copy them elsewhere only if the user names a destination.
Frequently asked questions about Launch Video
Similar skills
Markdown to HTML Conversion
Efficiently convert Markdown documents to HTML.
Code Tour
Create structured walkthroughs for codebases.
Acquire Codebase Knowledge
Streamline onboarding with comprehensive codebase documentation.
Documentation & Modernization
Streamline codebase documentation and modernization planning.
Azure Resource Visualizer
Generate architecture diagrams for Azure resources.
CLAUDE.md Improver
Optimize your CLAUDE.md files for better project context.
