Repository navigation
Add VHS demo skill #14334
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Add VHS demo skill #14334
Changes from 1 commit
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| --- | ||
| name: vhs-demo | ||
| description: Use when creating a terminal GIF, recording a CLI demonstration, authoring a VHS tape, or producing visual before/after evidence for reviewers. | ||
| compatibility: Requires VHS 0.11.0+, ffmpeg, ffprobe, ttyd, and a VHS-supported shell. | ||
| --- | ||
|
|
||
| # VHS Demo | ||
|
|
||
| Produce a short, repeatable GIF that shows real commands and unedited output from | ||
| caller-selected executables. The GIF is evidence of one observed flow, not proof | ||
| of general correctness and not a replacement for tests. | ||
|
|
||
| ## Evidence contract | ||
|
|
||
| A finished demo must make these claims defensible: | ||
|
|
||
| - VHS rendered a saved tape; no fallback recorder or `vhs publish` was used. | ||
| - Every visible command ran against an identified executable. | ||
| - Hidden setup created state but did not create, paste, rewrite, or overlay the | ||
| output attributed to the product. | ||
| - Configuration, credentials, hosts, and data were isolated and synthetic by | ||
| default. | ||
| - The native-resolution animation was inspected, including any transient state. | ||
|
|
||
| The caller decides where the artifact is ultimately used. Return local paths and | ||
| evidence; do not require an upload, PR edit, or repository change. | ||
|
|
||
| ## Workflow | ||
|
|
||
| ### 1. Define the claim | ||
|
|
||
| Record the intended audience or destination, the behavior to evidence, exact | ||
| visible commands, required initial state, and any size constraint. Storyboard | ||
| the visible beats and their approximate duration before writing the tape. | ||
|
|
||
| Prefer one coherent claim per GIF. Use a synthetic supporting service or fixture | ||
| only when the real product cannot safely or deterministically reach the required | ||
| state; disclose and remove it afterward. The visible command must still run the | ||
| selected real executable. Never substitute a fake or wrapper that generates | ||
| product output. Split success and error flows when combining them would obscure | ||
| either result. | ||
|
|
||
| For before/after evidence, identify the controlled variable. Keep the shell, | ||
| terminal geometry, commands, typing speed, synthetic data, and equivalent | ||
| application state fixed. | ||
|
|
||
| ### 2. Prove the toolchain and executables | ||
|
|
||
| Run the platform preflight from the repository root: | ||
|
|
||
| ```sh | ||
| .github/skills/vhs-demo/scripts/preflight.sh | ||
| ``` | ||
|
|
||
| ```powershell | ||
| & .\.github\skills\vhs-demo\scripts\preflight.ps1 | ||
| ``` | ||
|
|
||
| If VHS, ffmpeg, ffprobe, or ttyd is missing or cannot execute, or VHS is older | ||
| than 0.11.0, name the failure and stop. Do not install tools or substitute | ||
| asciinema, screen recording, hosted VHS, or fabricated terminal text. | ||
|
|
||
| The caller chooses every executable, including separate baseline and changed | ||
| binaries. Before recording each one, capture its absolute path, version, source | ||
| or revision, and preferably SHA-256. Build separate binaries from separate | ||
| checkouts; never overwrite one binary or switch the active checkout between | ||
| takes. | ||
|
|
||
| Invoke absolute paths in visible commands. If natural spelling such as `gh` is | ||
| part of the presentation, create an isolated command-name mapping to that exact | ||
| path and put it ahead of the tape's `PATH`. Do not trust aliases or the ordinary | ||
| `PATH`. | ||
|
|
||
| ### 3. Isolate the take | ||
|
|
||
| Use the platform's secure temporary-directory API to create one workspace per | ||
| GIF. Keep the tape, GIF, fixtures, config, command mappings, and inspection | ||
| frames there, never in the repository. | ||
|
|
||
| Use synthetic names, repositories, hosts, and content. Remap home and config | ||
| roots into the workspace, then clear inherited credential variables and | ||
| quarantine every config source the selected product can read. For `gh`, point | ||
| `GH_CONFIG_DIR` at an empty private directory and clear `GH_TOKEN`, | ||
| `GITHUB_TOKEN`, `GH_ENTERPRISE_TOKEN`, `GH_HOST`, and `GH_REPO`. If the behavior | ||
| inherently needs a service, use only an approved synthetic endpoint and account; | ||
| stop if safe credentials or data are unavailable. | ||
|
|
||
| Select a shell accepted by `Set Shell` in the installed VHS version and use only | ||
| that shell's syntax in `Type` commands. An executable being installed does not | ||
| mean VHS supports it. VHS 0.11 shell definitions suppress startup profiles and | ||
| set a generic prompt; confirm both with a probe before trusting prompt waits or | ||
| `PATH`. | ||
|
|
||
| ### 4. Author and render the tape | ||
|
|
||
| Adapt this structure, replacing every placeholder: | ||
|
|
||
| ```text | ||
| Output "<ABSOLUTE_TEMP_PATH>/demo.gif" | ||
| Require <SHELL_COMMAND> | ||
| Set Shell <SHELL_COMMAND> | ||
| Set Width <WIDTH_FOR_CONTENT> | ||
| Set Height <HEIGHT_FOR_CONTENT> | ||
| Set FontSize 22 | ||
| Set FontFamily "<VERIFIED_MONOSPACE_FONT>" | ||
| Set TypingSpeed 35ms | ||
| Set CursorBlink false | ||
| Set WaitTimeout 120s | ||
|
|
||
| Env GH_CONFIG_DIR "<ABSOLUTE_TEMP_PATH>/config" | ||
| Env HOME "<ABSOLUTE_TEMP_PATH>/home" | ||
| Env XDG_CONFIG_HOME "<ABSOLUTE_TEMP_PATH>/config" | ||
| Env GH_TOKEN "" | ||
| Env GITHUB_TOKEN "" | ||
| Env GH_ENTERPRISE_TOKEN "" | ||
| Env GH_HOST "" | ||
| Env GH_REPO "" | ||
| Env NO_COLOR "1" | ||
|
|
||
| Hide | ||
| Type "<SHELL-SPECIFIC SETUP AND CLEAR>" | ||
| Enter | ||
| Wait+Line /<DETERMINISTIC PROMPT>/ | ||
| Show | ||
|
|
||
| Type "<EXPLICIT EXECUTABLE> <VISIBLE ARGUMENTS>" | ||
| Enter | ||
| Wait+Screen /<MEANINGFUL EXPECTED OUTPUT>/ | ||
| Sleep 2s | ||
| ``` | ||
|
|
||
| Use hidden commands only for fixtures, isolated config, approved synthetic | ||
| services, deterministic prompts, directory changes, and clearing the screen. | ||
| Type every visible command and let it run. Use `Wait+Screen` for observable | ||
| results and condition-based waits for prompts; use `Sleep` only for pacing. | ||
| Let waits capture spinners and other motion rather than sleeping past them. | ||
|
|
||
| Verify the selected font is installed and resolves as monospaced using the | ||
| platform font registry or a native-resolution ruler probe. Render a sample | ||
| frame, then size width, height, and font for the content. Increase geometry | ||
| instead of shrinking or clipping text. | ||
|
|
||
| Run `vhs <tape>`. A parse or directive failure after preflight is an | ||
| incompatibility: report it rather than deleting a safeguard. | ||
|
|
||
| ### 5. Inspect the evidence | ||
|
|
||
| From the repository root, pass the matching helper an inspection directory | ||
| outside the repository: | ||
|
|
||
| ```sh | ||
| .github/skills/vhs-demo/scripts/inspect.sh <demo.gif> <inspection-dir> | ||
| ``` | ||
|
|
||
| ```powershell | ||
| & .\.github\skills\vhs-demo\scripts\inspect.ps1 <demo.gif> <inspection-dir> | ||
| ``` | ||
|
|
||
| The helper rejects repository paths, verifies GIF metadata, and extracts | ||
| native-resolution first, final, one-frame-per-second, and every-frame sequences. | ||
|
|
||
| Inspect the images and the tape together. Confirm executable selection, visible | ||
| commands, unedited output, intentional wrapping, readable monospaced text, | ||
| clean first and final frames, useful pacing, and absence of secrets, private | ||
| paths, real hosts, identities, notifications, and hidden setup. Inspect every | ||
| frame when checking privacy or any spinner, crash, exit, relaunch, or other | ||
| transient claim. Fix unsafe input at its source and rerender. Never redact the | ||
| GIF after capture. | ||
|
|
||
| ### 6. Return evidence | ||
|
|
||
| Report the absolute tape and GIF paths; dimensions, duration, frame count when | ||
| available, and size; visible commands; each executable's path, version, | ||
| revision/source, and hash when recorded; whether sampled or all frames were | ||
| inspected; the intended use; and any limitation on what the demo proves. | ||
|
|
||
| ## Stop shortcuts | ||
|
|
||
| | Shortcut | Required response | | ||
| | --- | --- | | ||
| | "Use my existing login/config" | Isolate config and use synthetic credentials or stop. | | ||
| | "Both clips say `gh`, so selection is obvious" | Map the name to each proven binary explicitly. | | ||
| | "Any recorder is fine if VHS is missing" | Stop and report the failed prerequisite. | | ||
| | "Put the GIF in the repo for convenience" | Keep all generated media in a temporary workspace. | | ||
| | "The final frame proves the spinner" | Inspect the transient frame sequence. | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| param( | ||
| [Parameter(Mandatory, Position = 0)] | ||
| [string]$Gif, | ||
| [Parameter(Mandatory, Position = 1)] | ||
| [string]$InspectionDirectory | ||
| ) | ||
|
|
||
| $ErrorActionPreference = "Stop" | ||
|
|
||
| $gifItem = Get-Item -LiteralPath $Gif -ErrorAction Stop | ||
| if ($gifItem.Length -eq 0) { | ||
| throw "GIF is empty: $Gif" | ||
| } | ||
| if (($gifItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) { | ||
| throw "GIF must not be a symbolic link or reparse point: $Gif" | ||
| } | ||
|
|
||
| $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..\..")).Path | ||
| if (Test-Path -LiteralPath $InspectionDirectory) { | ||
| throw "Inspection directory already exists: $InspectionDirectory" | ||
| } | ||
| $inspectionLeaf = Split-Path -Leaf $InspectionDirectory | ||
| $inspectionParentInput = Split-Path -Parent $InspectionDirectory | ||
| if ([string]::IsNullOrEmpty($inspectionParentInput)) { | ||
| $inspectionParentInput = "." | ||
| } | ||
| $inspectionParent = ( | ||
| Resolve-Path -LiteralPath $inspectionParentInput -ErrorAction Stop | ||
| ).Path | ||
| $inspectionPath = Join-Path $inspectionParent $inspectionLeaf | ||
| $comparison = if ( | ||
| [Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT | ||
| ) { | ||
| [StringComparison]::OrdinalIgnoreCase | ||
| } else { | ||
| [StringComparison]::Ordinal | ||
| } | ||
| $repoPrefix = $repoRoot.TrimEnd( | ||
| [IO.Path]::DirectorySeparatorChar, | ||
| [IO.Path]::AltDirectorySeparatorChar | ||
| ) + [IO.Path]::DirectorySeparatorChar | ||
|
|
||
| foreach ($candidate in @($gifItem.FullName, $inspectionPath)) { | ||
| if ($candidate.Equals($repoRoot, $comparison) -or | ||
| $candidate.StartsWith($repoPrefix, $comparison)) { | ||
| throw "Generated media must be outside the repository: $candidate" | ||
| } | ||
| } | ||
|
|
||
| New-Item -ItemType Directory -Path $inspectionPath | Out-Null | ||
|
|
||
| $probe = & ffprobe -v error -select_streams v:0 ` | ||
| -show_entries "stream=codec_name,width,height,nb_frames:format=format_name,duration,size" ` | ||
| -of "default=noprint_wrappers=1" $gifItem.FullName | ||
| if ($LASTEXITCODE -ne 0) { | ||
| throw "ffprobe failed for: $Gif" | ||
| } | ||
| $probeText = $probe -join [Environment]::NewLine | ||
| Write-Output $probeText | ||
| if ($probe -notcontains "codec_name=gif") { | ||
| throw "Expected GIF video codec: $Gif" | ||
| } | ||
| if ($probe -notcontains "format_name=gif") { | ||
| throw "Expected GIF container: $Gif" | ||
| } | ||
|
|
||
| $sampled = Join-Path $inspectionPath "sampled" | ||
| New-Item -ItemType Directory -Force -Path $sampled | Out-Null | ||
|
|
||
| & ffmpeg -v error -y -i $gifItem.FullName -frames:v 1 ` | ||
| (Join-Path $inspectionPath "first.png") | ||
| if ($LASTEXITCODE -ne 0) { throw "Failed to extract first frame" } | ||
|
|
||
| & ffmpeg -v error -y -sseof -0.1 -i $gifItem.FullName -frames:v 1 ` | ||
| (Join-Path $inspectionPath "final.png") | ||
|
williammartin marked this conversation as resolved.
Resolved
|
||
| if ($LASTEXITCODE -ne 0) { throw "Failed to extract final frame" } | ||
|
|
||
| & ffmpeg -v error -y -i $gifItem.FullName -vf "fps=1" ` | ||
| (Join-Path $sampled "frame-%04d.png") | ||
| if ($LASTEXITCODE -ne 0) { throw "Failed to extract sampled frames" } | ||
|
|
||
| $all = Join-Path $inspectionPath "all" | ||
| New-Item -ItemType Directory -Force -Path $all | Out-Null | ||
| & ffmpeg -v error -y -i $gifItem.FullName ` | ||
| (Join-Path $all "frame-%06d.png") | ||
| if ($LASTEXITCODE -ne 0) { throw "Failed to extract all frames" } | ||
|
|
||
| Write-Output "Inspection frames: $inspectionPath" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| #!/bin/sh | ||
|
|
||
| set -eu | ||
|
|
||
| usage() { | ||
| printf 'usage: %s <demo.gif> <inspection-dir>\n' "$0" >&2 | ||
| exit 2 | ||
| } | ||
|
|
||
| [ "$#" -eq 2 ] || usage | ||
| gif=$1 | ||
| inspection_dir=$2 | ||
|
|
||
| [ -s "$gif" ] || { | ||
| printf 'GIF does not exist or is empty: %s\n' "$gif" >&2 | ||
| exit 1 | ||
| } | ||
| [ ! -L "$gif" ] || { | ||
| printf 'GIF must not be a symbolic link: %s\n' "$gif" >&2 | ||
| exit 1 | ||
| } | ||
|
|
||
| script_dir=$(CDPATH= cd -P "$(dirname "$0")" && pwd) | ||
| repo_root=$(CDPATH= cd -P "$script_dir/../../../.." && pwd) | ||
| gif_dir=$(CDPATH= cd -P "$(dirname "$gif")" && pwd) | ||
| [ ! -e "$inspection_dir" ] || { | ||
| printf 'Inspection directory already exists: %s\n' "$inspection_dir" >&2 | ||
| exit 1 | ||
| } | ||
| inspection_parent=$(CDPATH= cd -P "$(dirname "$inspection_dir")" && pwd) || { | ||
| printf 'Inspection parent directory does not exist: %s\n' "$(dirname "$inspection_dir")" >&2 | ||
| exit 1 | ||
| } | ||
| inspection_dir="$inspection_parent/$(basename "$inspection_dir")" | ||
|
|
||
| case "$gif_dir/" in | ||
| "$repo_root/"*) | ||
| printf 'GIF must be outside the repository: %s\n' "$gif" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
| case "$inspection_dir/" in | ||
| "$repo_root/"*) | ||
| printf 'Inspection directory must be outside the repository: %s\n' "$inspection_dir" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
|
|
||
| mkdir "$inspection_dir" | ||
|
|
||
| probe=$(ffprobe -v error -select_streams v:0 \ | ||
| -show_entries stream=codec_name,width,height,nb_frames:format=format_name,duration,size \ | ||
| -of default=noprint_wrappers=1 "$gif") | ||
| printf '%s\n' "$probe" | ||
| printf '%s\n' "$probe" | grep -Eq '^codec_name=gif$' || { | ||
| printf 'Expected GIF video codec: %s\n' "$gif" >&2 | ||
| exit 1 | ||
| } | ||
| printf '%s\n' "$probe" | grep -Eq '^format_name=gif$' || { | ||
| printf 'Expected GIF container: %s\n' "$gif" >&2 | ||
| exit 1 | ||
| } | ||
|
|
||
| mkdir -p "$inspection_dir/sampled" | ||
| ffmpeg -v error -y -i "$gif" -frames:v 1 "$inspection_dir/first.png" | ||
| ffmpeg -v error -y -sseof -0.1 -i "$gif" -frames:v 1 "$inspection_dir/final.png" | ||
|
williammartin marked this conversation as resolved.
Resolved
|
||
| ffmpeg -v error -y -i "$gif" -vf fps=1 \ | ||
| "$inspection_dir/sampled/frame-%04d.png" | ||
|
|
||
| mkdir -p "$inspection_dir/all" | ||
| ffmpeg -v error -y -i "$gif" "$inspection_dir/all/frame-%06d.png" | ||
|
|
||
| printf 'Inspection frames: %s\n' "$inspection_dir" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| $ErrorActionPreference = "Stop" | ||
|
|
||
| function Invoke-VersionProbe { | ||
| param( | ||
| [Parameter(Mandatory)] | ||
| [string]$Name, | ||
| [Parameter(Mandatory)] | ||
| [string[]]$Arguments | ||
| ) | ||
|
|
||
| $command = Get-Command $Name -CommandType Application -ErrorAction SilentlyContinue | ||
| if ($null -eq $command) { | ||
| throw "VHS demo preflight failed: missing required tool: $Name" | ||
| } | ||
|
|
||
| $output = (& $command.Path @Arguments 2>&1 | Out-String).Trim() | ||
| if ($LASTEXITCODE -ne 0) { | ||
| throw "VHS demo preflight failed: $Name exists at $($command.Path) but could not execute" | ||
| } | ||
| if ([string]::IsNullOrWhiteSpace($output)) { | ||
| throw "VHS demo preflight failed: $Name returned no version information" | ||
| } | ||
|
|
||
| $firstLine = ($output -split "\r?\n")[0] | ||
| Write-Host "${Name}: $($command.Path) ($firstLine)" | ||
| return $output | ||
| } | ||
|
|
||
| $vhsOutput = Invoke-VersionProbe -Name "vhs" -Arguments @("--version") | ||
| $match = [regex]::Match($vhsOutput, 'v?(\d+\.\d+(?:\.\d+)?)') | ||
| if (-not $match.Success) { | ||
| throw "VHS demo preflight failed: unable to parse VHS version from: $vhsOutput" | ||
| } | ||
| if ([version]$match.Groups[1].Value -lt [version]"0.11.0") { | ||
| throw "VHS demo preflight failed: VHS 0.11.0 or newer is required; found $($match.Groups[1].Value)" | ||
| } | ||
|
|
||
| Invoke-VersionProbe -Name "ffmpeg" -Arguments @("-version") | Out-Null | ||
| Invoke-VersionProbe -Name "ffprobe" -Arguments @("-version") | Out-Null | ||
| Invoke-VersionProbe -Name "ttyd" -Arguments @("--version") | Out-Null | ||
|
|
||
| Write-Output "VHS demo prerequisites satisfied." |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.