diff --git a/.factory-plugin/marketplace.json b/.factory-plugin/marketplace.json index e37a544..4eecc39 100644 --- a/.factory-plugin/marketplace.json +++ b/.factory-plugin/marketplace.json @@ -24,6 +24,12 @@ "source": "./plugins/core", "category": "core" }, + { + "name": "droid-control", + "description": "Terminal, browser, and computer automation for testing, demos, QA, and computer-use tasks", + "source": "./plugins/droid-control", + "category": "automation" + }, { "name": "autoresearch", "description": "Autonomous experiment loop for optimization research. Try an idea, measure it, keep what works, discard what doesn't, repeat. Works standalone or as a mission worker.", diff --git a/README.md b/README.md index 899db23..e87fd00 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,20 @@ Skills for continuous learning and improvement. - `frontend-design` - Build web apps, websites, HTML pages with good design - `browser-navigation` - Browser automation with agent-browser +### droid-control + +Terminal, browser, and computer automation for Droids. Record demos, verify behavior claims, and run QA flows. + +**Commands:** `/demo`, `/verify`, `/qa-test` + +**Skills:** `droid-control` (orchestrator), `tuistory`, `true-input`, `agent-browser`, `droid-cli`, `pty-capture`, `capture`, `compose`, `verify`, `showcase` + +See [plugins/droid-control/README.md](plugins/droid-control/README.md) for details. + +### autoresearch + +Autonomous experiment loop for optimization research. Try an idea, measure it, keep what works, discard what doesn't, repeat. Works standalone or as a mission worker. + ## Plugin Structure Each plugin follows the Factory plugin format: diff --git a/plugins/droid-control/.factory-plugin/plugin.json b/plugins/droid-control/.factory-plugin/plugin.json new file mode 100644 index 0000000..16e1250 --- /dev/null +++ b/plugins/droid-control/.factory-plugin/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "droid-control", + "description": "Terminal and browser automation for testing, demos, QA, and computer-use tasks", + "version": "1.0.0" +} diff --git a/plugins/droid-control/ARCHITECTURE.md b/plugins/droid-control/ARCHITECTURE.md new file mode 100644 index 0000000..519cab6 --- /dev/null +++ b/plugins/droid-control/ARCHITECTURE.md @@ -0,0 +1,59 @@ +# Architecture + +## The problem + +Put all driver knowledge, recording lifecycle, video rendering, and verification logic in one skill and two things happen. First, the droid loads 3000 tokens of Windows KVM docs to record a tuistory demo on Linux. Second, and this is the one that actually hurts, the droid gets all the information at once and has to figure out what's relevant *right now*. It makes worse decisions, skips steps it shouldn't, and invents steps it doesn't need to. + +## UX for droids + +Droids aren't exempt from information architecture. They have finite context, they get distracted by irrelevant detail, and they degrade under overload. + +Every skill in this plugin is a surface the droid interacts with at a specific moment in a workflow: scoped to what it needs right now, actionable on first read, with an explicit handoff to the next surface. The same instincts that make a good CLI or a good API apply. Don't dump everything. Sequence the information. Make the next step obvious. + +A command like `/demo` doesn't contain Remotion props or driver-specific logic. It parses intent, builds commitments, and tells the droid which skills to load. The droid never sees video rendering details while it's still planning what to record. + +## Waterfall routing + +Skills chain into each other without hardcoding. The orchestrator doesn't call skills or build a pipeline. It tells the droid which skills to load based on three independent lookups. Once loaded, each skill's exit is the next skill's entry: + +``` +/demo parses intent, commits deliverables + → orchestrator routes to driver + stage + artifact skills + → capture launches the app, records, hands off clips + metadata + → compose receives clips, builds Remotion props, renders video + → verify checks the output against the original commitments +``` + +No state machine or orchestration framework. Just documents whose outputs naturally feed into the next document's inputs. The droid follows the waterfall because each skill makes the next step obvious, not because something forces it to. Complex multi-stage workflows emerge from skill composition rather than control flow. + +## Task delegation + +Because each stage's inputs and outputs are explicit, mechanical work naturally decomposes into worker tasks. The parent agent retains planning and editorial control; workers execute exact commands and return file paths. + +Capture workers for both branches run in parallel. They need tctl commands and worktree paths, not PR context. The render worker gets a props JSON and clip paths. It doesn't need to know what the PR does or why the demo matters. Verification stays with the parent because it requires the original commitments and judgment about whether the evidence holds up. + +The skill boundaries are the delegation boundaries. You don't need a separate delegation framework because the decomposition into self-contained stages already defines what can be farmed out, what needs creative judgment, and what's too trivial to be worth the overhead. + +## Orthogonal routing + +The orchestrator makes three independent lookups: + +- **Target**: what are you driving? (droid-cli, other TUI, web app, byte capture) +- **Stage**: what does the workflow need? (capture, compose, verify) +- **Artifact**: does compose need polish tools? (showcase) + +These compose without cross-product explosion. 6 targets + 3 stages + 1 artifact route = 10 skills, not 18. Adding a new target means writing one skill and adding one row to the routing table. Every existing stage and artifact skill works with it immediately. + +## Hybrid handoffs + +Commands hand off to the compose stage with two sections: structured fields for mechanical decisions, natural language for creative ones. + +Structured: layout, speed, preset, fidelity, effects tier. These have correct answers. A side-by-side layout is either `side-by-side` or it isn't. + +Natural language: what the viewer should take away, which moments to hold, how to frame the story. These are editorial judgments that benefit from the droid's understanding of the PR context. + +Two failure modes this prevents: over-specifying creative decisions up front (the droid produces rigid, paint-by-numbers output) and under-specifying mechanical params (the droid hallucinates presets and layouts). The effects tier is a concrete example. The command commits a single word ("utilitarian" or "full"), and compose makes specific, grounded choices after capture, when it has actual recordings to look at. + +## Platform isolation + +Platform-specific content lives in `platforms/` subdirs under the relevant skill. A droid on Linux loads `true-input/platforms/linux.md`. It never sees the Windows KVM or macOS QEMU docs. This is a routing decision, not a reading-comprehension test. diff --git a/plugins/droid-control/NOTICES.md b/plugins/droid-control/NOTICES.md new file mode 100644 index 0000000..a99d593 --- /dev/null +++ b/plugins/droid-control/NOTICES.md @@ -0,0 +1,25 @@ +# Third-Party Notices + +This plugin depends on several third-party tools and libraries. They are not bundled -- each is installed separately by the user. Their respective licenses apply at the point of installation and use. + +## Video rendering + +- **[Remotion](https://www.remotion.dev/)** -- React-based video renderer used by the compose/showcase pipeline. Remotion is free for individuals, small teams (<=3 employees), and non-profits. Larger companies require a [company license](https://www.remotion.pro/). See the [full license terms](https://github.com/remotion-dev/remotion/blob/main/LICENSE.md). +- **[React](https://react.dev/)** -- MIT License +- **[Zod](https://zod.dev/)** -- MIT License + +## Terminal automation + +- **[tuistory](https://github.com/nicholasgasior/tuistory)** -- virtual PTY automation CLI +- **[asciinema](https://asciinema.org/)** -- terminal session recorder (GPL-3.0) +- **[agg](https://github.com/asciinema/agg)** -- asciinema GIF generator (Apache-2.0) + +## Browser automation + +- **[agent-browser](https://docs.factory.ai/)** -- Playwright-backed browser automation CLI + +## System tools + +- **[ffmpeg](https://ffmpeg.org/)** -- multimedia framework (LGPL-2.1+ / GPL-2.0+, depending on build configuration) +- **[cage](https://github.com/cage-kiosk/cage)** -- Wayland kiosk compositor (MIT) +- **[wtype](https://github.com/atx/wtype)** -- Wayland keystroke injection (MIT) diff --git a/plugins/droid-control/README.md b/plugins/droid-control/README.md new file mode 100644 index 0000000..0a36a0f --- /dev/null +++ b/plugins/droid-control/README.md @@ -0,0 +1,102 @@ +# droid-control + +Terminal, browser, and computer automation plugin for Droids. + +Droids can read and write code. This plugin enables them to *operate* it: launch apps, type commands, click buttons, record what happens, and produce polished video evidence of it. No human hands required (they don't have any). + +## What you get + +**Record a demo video from a PR:** + +``` +/demo pr-1847 +``` + +Droid reads the PR, scripts the interactions that prove the change works, records both branches in parallel, and renders a side-by-side comparison video. Factory preset for cinematic warmth, macos preset for clean and utilitarian. + +**Verify a behavior claim:** + +``` +/verify "ESC cancels streaming in bash mode" +``` + +Droid launches the app, attempts the claim, and reports what actually happened, with screenshots and text snapshots as evidence. If the claim is false, that's a valid finding, not a failure. + +**Run a QA flow against a web app:** + +``` +/qa-test https://app.example.com -- login, create a project, invite a member +``` + +Droid drives the browser through the flow, captures each step, and reports pass/fail with annotated screenshots. + +## Quick start + +```bash +# Register the Factory plugins marketplace (if not already added) +droid plugin marketplace add https://github.com/Factory-AI/factory-plugins + +# Install the plugin +droid plugin install droid-control@factory-plugins --scope user + +# Install Remotion dependencies (one-time, only needed for video rendering) +# Find the plugin install path with: droid plugin list --scope user +cd /remotion && npm install +``` + +Or use the `/plugins` UI: Browse tab, select droid-control, install. + +Then open a Droid session and run `/demo`, `/verify`, or `/qa-test`. + +## Commands + +### `/demo` + +Plans and records a demo video. Accepts a PR number, GitHub URL, or free-text description. Comparison PRs get side-by-side layout by default; new features get single-branch. Add "showcase" for cinematic polish, "keys" for keystroke overlay. + +### `/verify` + +Tests a specific behavior claim and reports findings with evidence. Frames the droid as an investigator. Anti-fabrication rules prevent staging evidence to match expected outcomes. + +### `/qa-test` + +Automated QA against terminal CLIs or web/Electron apps. Accepts a URL, CLI command, or app description with optional test steps after `--`. + +## How it works + +Three layers: + +- **Orchestrator** -- routes each request through three independent lookups (target, stage, artifact) to determine which skills to load. ~93 lines. +- **10 atom skills** -- self-contained background knowledge loaded on demand. Driver atoms (tuistory, true-input, agent-browser), target atoms (droid-cli, pty-capture), stage atoms (capture, compose, verify), and a polish atom (showcase). +- **3 commands** -- thin intent declarations that parse arguments into commitments, then delegate to atoms via hybrid handoffs. + +Every workflow flows through **capture → compose → verify**. Commands declare *what* to produce; atoms own *how*. + +## Video rendering + +The compose stage uses [Remotion](https://www.remotion.dev/) (React-based video renderer) for all compositing. 6 visual presets, automatic cinematic layers (warm backgrounds, floating particles, noise overlay, motion blur transitions), and effect-driven layers (spotlight, zoom, keystroke overlay, section headers). + +The `render-showcase.sh` helper handles the full pipeline: `.cast` conversion via `agg`, clip staging, duration detection, Remotion render, and cleanup. + +## Prerequisites + +| Stage | Platform | Required | +|---|---|---| +| tuistory | All | `tuistory`, `asciinema`, `agg` | +| true-input | Linux/Wayland | `cage`, `wtype`, Wayland terminal | +| true-input | Windows (KVM) | `libvirt`, `qemu`, KVM VM with SSH | +| true-input | macOS (QEMU) | `qemu`, `socat`, macOS VM with SSH | +| agent-browser | All | `agent-browser` | +| compose | All | `ffmpeg`, `ffprobe`, `agg` | +| showcase | All | Node.js (>= 18), Chrome/Chromium | + +```bash +npm install -g tuistory # virtual PTY driver +pip install asciinema # terminal recording +cargo install --git https://github.com/asciinema/agg # .cast → .gif converter +sudo apt-get install -y ffmpeg # video processing +agent-browser install # browser automation (downloads Chromium) +cd plugins/droid-control/remotion && npm install # Remotion (video rendering) +``` + +Only install what you need for your use case. Terminal demos need tuistory, asciinema, agg, and ffmpeg. Web/Electron automation just needs agent-browser. diff --git a/plugins/droid-control/bin/tctl b/plugins/droid-control/bin/tctl new file mode 100755 index 0000000..4f35ec5 --- /dev/null +++ b/plugins/droid-control/bin/tctl @@ -0,0 +1,1119 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="/tmp/tctl-sessions" +mkdir -p "$ROOT_DIR" + +print_help() { + cat <<'HELP' +tctl -- unified terminal control wrapper + +Usage: + tctl launch [options] + tctl -s [args...] + tctl sessions + +Actions: + type Send literal text + press [keys...] Send a key chord (e.g. press shift enter) + wait [--timeout] Block until text or /regex/ appears + wait-idle [--timeout] Block until output stabilizes + snapshot [--trim] Print cleaned text from the session + screenshot [-o ] Capture compositor PNG (true-input only) + provenance Print launch repo root, branch, and commit + record start Start video recording (true-input only) + record stop Stop video recording + close Tear down the session + +Launch options: + -s, --session Session name (default: default) + --backend tuistory | true-input | ghostty | kitty | alacritty + --terminal Override terminal for true-input + --cols Columns (default: 120) + --rows Rows (default: 36) + --cwd Working directory (tuistory) + --repo-root Git worktree root for provenance + droid-dev launches + --env Environment variable (repeatable) + --tmux Wrap the command in tmux with RGB-safe tuistory defaults + --record Record from launch (.cast or .mp4) + +Notes: + tuistory recording wraps the PTY so it must be set at launch. + true-input auto-selects: ghostty > kitty > alacritty. + Each true-input session gets an isolated Wayland runtime directory. +HELP +} + +die() { + echo "tctl: $*" >&2 + exit 1 +} + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || die "required command not found: $1" +} + +runtime_dir_value() { + if [[ -n "${XDG_RUNTIME_DIR:-}" ]]; then + printf '%s\n' "$XDG_RUNTIME_DIR" + else + printf '/run/user/%s\n' "$(id -u)" + fi +} + +session_slug() { + printf '%s\n' "${1//[^[:alnum:]]/-}" +} + +session_runtime_dir() { + printf '%s/tctl/%s\n' "$(runtime_dir_value)" "$(session_slug "$1")" +} + +wait_for_wayland_socket() { + local pid="$1" + local socket_path="$2" + local timeout_ms="$3" + local deadline=$(( $(date +%s%3N) + timeout_ms )) + + while (( $(date +%s%3N) <= deadline )); do + [[ -S "$socket_path" ]] && return 0 + kill -0 "$pid" >/dev/null 2>&1 || return 1 + sleep 0.05 + done + + return 1 +} + +quote_sh() { + printf '%q' "$1" +} + +session_dir() { + printf '%s/%s\n' "$ROOT_DIR" "$1" +} + +command_file() { + printf '%s/command.txt\n' "$(session_dir "$1")" +} + +runner_file() { + printf '%s/run-child.sh\n' "$(session_dir "$1")" +} + +tmux_config_file() { + printf '%s/tmux.conf\n' "$(session_dir "$1")" +} + +tmux_runner_file() { + printf '%s/run-tmux-child.sh\n' "$(session_dir "$1")" +} + +logged_runner_file() { + printf '%s/run-logged-child.sh\n' "$(session_dir "$1")" +} + +tuistory_recording_runner_file() { + printf '%s/run-tuistory-recording.sh\n' "$(session_dir "$1")" +} + +asciinema_pid_file() { + printf '%s/asciinema.pid\n' "$(session_dir "$1")" +} + +provenance_file() { + printf '%s/provenance\n' "$(session_dir "$1")" +} + +meta_file() { + printf '%s/meta\n' "$(session_dir "$1")" +} + +load_meta() { + local session="$1" + local meta + meta="$(meta_file "$session")" + [[ -f "$meta" ]] || die "unknown session: $session" + # shellcheck disable=SC1090 + source "$meta" +} + +write_meta() { + local session="$1" + local dir + dir="$(session_dir "$session")" + mkdir -p "$dir" + cat > "$(meta_file "$session")" </dev/null 2>&1 \ + || die "--repo-root is not a git worktree: $repo_root" +} + +write_provenance() { + local session="$1" + local repo_root="$2" + local file branch commit + file="$(provenance_file "$session")" + + if [[ -z "$repo_root" ]]; then + rm -f "$file" + return 0 + fi + + branch="$(git -C "$repo_root" rev-parse --abbrev-ref HEAD 2>/dev/null || printf 'unknown')" + commit="$(git -C "$repo_root" rev-parse HEAD 2>/dev/null || printf 'unknown')" + cat > "$file" </dev/null 2>&1; then + TERMINAL="ghostty" + elif command -v kitty >/dev/null 2>&1; then + TERMINAL="kitty" + elif command -v alacritty >/dev/null 2>&1; then + TERMINAL="alacritty" + else + die "no true-input terminal found (ghostty, kitty, alacritty)" + fi + ;; + ghostty|kitty|alacritty) + BACKEND="true-input" + TERMINAL="$backend" + ;; + *) + die "unsupported backend: $backend" + ;; + esac +} + +save_session_state() { + SESSION="$1" + write_meta "$1" +} + +write_session_runner() { + local session="$1" + local command="$2" + local cwd="$3" + shift 3 + local envs=("$@") + + local dir runner command_path + dir="$(session_dir "$session")" + runner="$(runner_file "$session")" + command_path="$(command_file "$session")" + mkdir -p "$dir" + printf '%s\n' "$command" > "$command_path" + + { + echo '#!/usr/bin/env bash' + echo 'set -euo pipefail' + printf 'COMMAND_FILE=%q\n' "$command_path" + if [[ -n "$cwd" ]]; then + printf 'cd %q\n' "$cwd" + fi + + local kv name value + for kv in "${envs[@]}"; do + [[ "$kv" == *=* ]] || die "env must be KEY=VALUE: $kv" + name="${kv%%=*}" + value="${kv#*=}" + [[ "$name" =~ ^[A-Za-z_][A-Za-z0-9_]*$ ]] \ + || die "invalid environment variable name: $name" + printf 'export %s=%q\n' "$name" "$value" + done + + # shellcheck disable=SC2016 + echo 'if [[ -z "${TERM:-}" ]] || ! infocmp "$TERM" >/dev/null 2>&1; then' + echo ' export TERM=xterm-256color' + echo 'fi' + + # Force color output in virtual PTYs: Node.js/chalk suppresses colors + # when the PTY doesn't advertise support. FORCE_COLOR=3 = truecolor. + # Prepend as env-prefix on exec so they override anything the login + # shell profile may have set (e.g. FORCE_COLOR=0 from CI tooling). + # shellcheck disable=SC2016 + echo 'exec env FORCE_COLOR=3 COLORTERM=truecolor bash -lc "$(< "$COMMAND_FILE")"' + } > "$runner" + + chmod +x "$runner" + RUNNER_FILE="$runner" +} + +write_tmux_runner() { + local session="$1" + local runner tmux_config tmux_runner tmux_socket_name + runner="${RUNNER_FILE:-$(runner_file "$session")}" + tmux_config="$(tmux_config_file "$session")" + tmux_runner="$(tmux_runner_file "$session")" + tmux_socket_name="tctl-$(session_slug "$session")" + + cat > "$tmux_config" <<'TMUX' +set -g default-terminal "tmux-256color" +set -qas terminal-features ",xterm-256color:RGB" +set -as terminal-overrides ",xterm-256color:Tc" +set-environment -g COLORTERM truecolor +set -g escape-time 50 +set -g mode-keys vi +TMUX + + { + echo '#!/usr/bin/env bash' + echo 'set -euo pipefail' + printf 'RUNNER_FILE=%q\n' "$runner" + printf 'TMUX_CONFIG=%q\n' "$tmux_config" + printf 'TMUX_SOCKET_NAME=%q\n' "$tmux_socket_name" + printf 'TMUX_SESSION_NAME=%q\n' "$tmux_socket_name" + echo 'export TERM=xterm-256color' + echo 'exec tmux -f "$TMUX_CONFIG" -L "$TMUX_SOCKET_NAME" new-session -s "$TMUX_SESSION_NAME" "$RUNNER_FILE"' + } > "$tmux_runner" + + chmod +x "$tmux_runner" + RUNNER_FILE="$tmux_runner" + TMUX_SOCKET_NAME="$tmux_socket_name" +} + +write_logged_runner() { + local session="$1" + local log_file="$2" + local runner logged_runner + runner="${RUNNER_FILE:-$(runner_file "$session")}" + logged_runner="$(logged_runner_file "$session")" + + { + echo '#!/usr/bin/env bash' + echo 'set -euo pipefail' + printf 'LOG_FILE=%q\n' "$log_file" + printf 'RUNNER_FILE=%q\n' "$runner" + # shellcheck disable=SC2016 + echo 'exec script -q -f -O "$LOG_FILE" -c "$RUNNER_FILE"' + } > "$logged_runner" + + chmod +x "$logged_runner" + LOGGED_RUNNER_FILE="$logged_runner" +} + +write_tuistory_recording_runner() { + local session="$1" + local record_path="$2" + local runner pid_file wrapper + runner="${RUNNER_FILE:-$(runner_file "$session")}" + pid_file="$(asciinema_pid_file "$session")" + wrapper="$(tuistory_recording_runner_file "$session")" + + { + echo '#!/usr/bin/env bash' + echo 'set -euo pipefail' + printf 'RUNNER_FILE=%q\n' "$runner" + printf 'RECORD_PATH=%q\n' "$record_path" + printf 'PID_FILE=%q\n' "$pid_file" + cat <<'SH' +# IMPORTANT: run asciinema in the foreground so it owns the session TTY. +# Running it in the background breaks stdin forwarding to interactive TUIs +# (Ink/React), causing typed keys to be echoed by the outer PTY instead of +# reaching the child process. +rm -f "$PID_FILE" +echo "$$" > "$PID_FILE" +exec asciinema rec --overwrite --command "$RUNNER_FILE" "$RECORD_PATH" +SH + } > "$wrapper" + + chmod +x "$wrapper" + printf '%s\n' "$wrapper" +} + +pid_is_expected_asciinema_recording() { + local pid="$1" + local expected_record_path="$2" + [[ "$pid" =~ ^[0-9]+$ ]] || return 1 + local cmdline="" + if [[ -r "/proc/$pid/cmdline" ]]; then + cmdline="$(tr '\0' ' ' < "/proc/$pid/cmdline" 2>/dev/null || true)" + else + cmdline="$(ps -p "$pid" -o command= 2>/dev/null || true)" + fi + [[ -n "$cmdline" ]] || return 1 + [[ "$cmdline" == *"asciinema"* ]] || return 1 + [[ "$cmdline" == *" rec "* ]] || return 1 + if [[ -n "$expected_record_path" ]]; then + [[ "$cmdline" == *"$expected_record_path"* ]] || return 1 + fi + return 0 +} + +cleanup_tuistory_asciinema() { + local session="$1" + local record_path="$2" + local pid_file pid + pid_file="$(asciinema_pid_file "$session")" + pid="$(cat "$pid_file" 2>/dev/null || true)" + + if [[ -n "$pid" ]] && kill -0 "$pid" >/dev/null 2>&1; then + if pid_is_expected_asciinema_recording "$pid" "$record_path"; then + kill -TERM "$pid" >/dev/null 2>&1 || true + local deadline=$(( $(date +%s%3N) + 2000 )) + while kill -0 "$pid" >/dev/null 2>&1 && (( $(date +%s%3N) <= deadline )); do + sleep 0.05 + done + if kill -0 "$pid" >/dev/null 2>&1; then + kill -KILL "$pid" >/dev/null 2>&1 || true + fi + fi + fi + + rm -f "$pid_file" + + if [[ -z "$record_path" ]]; then + return 0 + fi + + local orphan_pid orphan_ppid orphan_comm orphan_args + while read -r orphan_pid orphan_ppid orphan_comm orphan_args; do + [[ "$orphan_ppid" == "1" ]] || continue + [[ "$orphan_comm" == "asciinema" || "$orphan_args" == *"asciinema"* ]] || continue + [[ "$orphan_args" == *" rec "* ]] || continue + [[ "$orphan_args" == *"$record_path"* ]] || continue + + kill -TERM "$orphan_pid" >/dev/null 2>&1 || true + local deadline=$(( $(date +%s%3N) + 2000 )) + while kill -0 "$orphan_pid" >/dev/null 2>&1 && (( $(date +%s%3N) <= deadline )); do + sleep 0.05 + done + if kill -0 "$orphan_pid" >/dev/null 2>&1; then + kill -KILL "$orphan_pid" >/dev/null 2>&1 || true + fi + done < <(ps -eo pid=,ppid=,comm=,args= 2>/dev/null || true) +} + +launch_tuistory() { + local session="$1" + local cols="$2" + local rows="$3" + local record_path="$4" + require_cmd tuistory + local launch_cmd="$RUNNER_FILE" + if [[ -n "$record_path" ]]; then + require_cmd asciinema + cleanup_tuistory_asciinema "$session" "$record_path" + launch_cmd="$(write_tuistory_recording_runner "$session" "$record_path")" + fi + local args=(launch "$launch_cmd" -s "$session" --cols "$cols" --rows "$rows") + tuistory "${args[@]}" +} + +launch_true_input() { + local session="$1" + local record_path="$2" + + require_cmd cage + require_cmd wtype + require_cmd script + require_cmd "$TERMINAL" + + local dir log_file terminal_cmd runtime_dir socket_path + dir="$(session_dir "$session")" + runtime_dir="$(session_runtime_dir "$session")" + socket_path="$runtime_dir/wayland-0" + log_file="$dir/pty.log" + mkdir -p "$dir" + : > "$log_file" + rm -rf "$runtime_dir" + mkdir -p "$runtime_dir" + chmod 700 "$runtime_dir" + write_logged_runner "$session" "$log_file" + + case "$TERMINAL" in + ghostty) + terminal_cmd=(ghostty --window-decoration=false --confirm-close-surface=false -e "$LOGGED_RUNNER_FILE") + ;; + kitty) + terminal_cmd=(kitty "$LOGGED_RUNNER_FILE") + ;; + alacritty) + terminal_cmd=(alacritty -e "$LOGGED_RUNNER_FILE") + ;; + *) + die "unsupported true-input terminal: $TERMINAL" + ;; + esac + + WAYLAND_DISPLAY_NAME="wayland-0" + RUNTIME_DIR="$runtime_dir" + LOG_FILE="$log_file" + RECORD_PATH="$record_path" + RECORDER_PID="" + CAGE_PID="" + WARMED_UP="0" + save_session_state "$session" + + XDG_RUNTIME_DIR="$runtime_dir" \ + WLR_BACKENDS="${WLR_BACKENDS:-headless}" \ + WLR_LIBINPUT_NO_DEVICES="${WLR_LIBINPUT_NO_DEVICES:-1}" \ + cage -- "${terminal_cmd[@]}" >/dev/null 2>&1 & + CAGE_PID="$!" + wait_for_wayland_socket "$CAGE_PID" "$socket_path" 5000 \ + || die "true-input compositor did not create $socket_path" + + if [[ -n "$record_path" ]]; then + start_true_input_recording "$session" "$record_path" + else + save_session_state "$session" + fi +} + +strip_log() { + local log_file="$1" + python3 - "$log_file" <<'PY' +import re +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +if not path.exists(): + raise SystemExit(0) +text = path.read_bytes().decode('utf-8', 'replace') +text = text.replace('\r\n', '\n').replace('\r', '\n') +text = re.sub(r'^Script started on .*$\n?', '', text, flags=re.M) +text = re.sub(r'^Script done on .*$\n?', '', text, flags=re.M) +text = re.sub(r'\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)', '', text) +text = re.sub(r'\x1bP.*?\x1b\\', '', text, flags=re.S) +text = re.sub(r'\x1b\[[0-?]*[ -/]*[@-~]', '', text) +text = re.sub(r'\x1b[@-_]', '', text) +text = text.replace('\x08', '') +text = ''.join(ch for ch in text if ch in ('\n', '\t') or ord(ch) >= 32) +print(text, end='') +PY +} + +trim_text() { + python3 -c 'import sys +text = sys.stdin.read() +lines = text.splitlines() +while lines and not lines[-1].strip(): + lines.pop() +print("\\n".join(line.rstrip() for line in lines))' +} + +wait_for_pattern_in_log() { + local log_file="$1" + local pattern="$2" + local timeout_ms="$3" + local deadline=$(( $(date +%s%3N) + timeout_ms )) + while (( $(date +%s%3N) <= deadline )); do + local snapshot + snapshot="$(strip_log "$log_file")" + if SNAPSHOT_TEXT="$snapshot" python3 - "$pattern" <<'PY' +import re +import sys +import os +pattern = sys.argv[1] +text = os.environ.get("SNAPSHOT_TEXT", "") +if len(pattern) >= 2 and pattern.startswith('/') and pattern.rfind('/') > 0: + last = pattern.rfind('/') + body = pattern[1:last] + flags_str = pattern[last+1:] + flags = 0 + if 'i' in flags_str: + flags |= re.I + matched = re.search(body, text, flags) is not None +else: + matched = pattern in text +raise SystemExit(0 if matched else 1) +PY + then + return 0 + fi + sleep 0.1 + done + return 1 +} + +wait_for_idle_in_log() { + local log_file="$1" + local timeout_ms="$2" + local deadline=$(( $(date +%s%3N) + timeout_ms )) + local stable_for=0 + local last_size=-1 + while (( $(date +%s%3N) <= deadline )); do + local size=0 + [[ -f "$log_file" ]] && size=$(stat -c %s "$log_file") + if [[ "$size" == "$last_size" ]]; then + stable_for=$(( stable_for + 100 )) + if (( stable_for >= 500 )); then + return 0 + fi + else + stable_for=0 + last_size="$size" + fi + sleep 0.1 + done + return 1 +} + +true_input_env() { + export WAYLAND_DISPLAY="$WAYLAND_DISPLAY_NAME" + export XDG_RUNTIME_DIR="${RUNTIME_DIR:-$(runtime_dir_value)}" + export WLR_BACKENDS="${WLR_BACKENDS:-headless}" + export WLR_LIBINPUT_NO_DEVICES="${WLR_LIBINPUT_NO_DEVICES:-1}" +} + +ensure_true_input_warmup() { + local session="$1" + if [[ "${WARMED_UP:-0}" != "1" ]]; then + true_input_env + wtype -M shift -m shift >/dev/null 2>&1 || true + WARMED_UP="1" + save_session_state "$session" + fi +} + +translate_key_name() { + case "$1" in + enter|return) printf 'Return' ;; + esc|escape) printf 'Escape' ;; + tab) printf 'Tab' ;; + space) printf 'space' ;; + backspace) printf 'BackSpace' ;; + delete) printf 'Delete' ;; + insert) printf 'Insert' ;; + up) printf 'Up' ;; + down) printf 'Down' ;; + left) printf 'Left' ;; + right) printf 'Right' ;; + home) printf 'Home' ;; + end) printf 'End' ;; + pageup) printf 'Page_Up' ;; + pagedown) printf 'Page_Down' ;; + *) printf '%s' "$1" ;; + esac +} + +is_modifier() { + case "$1" in + ctrl|control|alt|shift|meta|super) return 0 ;; + *) return 1 ;; + esac +} + +true_input_press() { + local keys=("$@") + true_input_env + if [[ ${#keys[@]} -eq 1 ]]; then + local key="${keys[0]}" + if [[ ${#key} -eq 1 ]]; then + wtype "$key" + return + fi + fi + + local mods=() + local nonmods=() + local k + for k in "${keys[@]}"; do + if is_modifier "$k"; then + case "$k" in + control) mods+=(ctrl) ;; + super) mods+=(meta) ;; + *) mods+=("$k") ;; + esac + else + nonmods+=("$k") + fi + done + + if [[ ${#nonmods[@]} -eq 0 ]]; then + die "press requires at least one non-modifier key" + fi + if [[ ${#nonmods[@]} -gt 1 ]]; then + die "true-input press currently supports one key chord at a time" + fi + + local key_name="${nonmods[0]}" + if [[ ${#mods[@]} -eq 0 && ${#key_name} -eq 1 ]]; then + wtype "$key_name" + return + fi + + local args=() + for k in "${mods[@]}"; do + args+=(-M "$k") + done + args+=(-k "$(translate_key_name "$key_name")") + local idx=$(( ${#mods[@]} - 1 )) + while (( idx >= 0 )); do + args+=(-m "${mods[$idx]}") + idx=$(( idx - 1 )) + done + wtype "${args[@]}" +} + +start_true_input_recording() { + local session="$1" + local output="$2" + load_meta "$session" + [[ "$BACKEND" == "true-input" ]] || die "record start is only supported for true-input sessions; for tuistory, relaunch with --record" + [[ -z "$RECORDER_PID" ]] || die "recording already active for session: $session" + require_cmd wf-recorder + true_input_env + mkdir -p "$(dirname "$output")" + WAYLAND_DISPLAY="$WAYLAND_DISPLAY_NAME" wf-recorder -f "$output" >/dev/null 2>&1 & + RECORDER_PID="$!" + RECORD_PATH="$output" + save_session_state "$session" +} + +stop_true_input_recording() { + local session="$1" + load_meta "$session" + [[ "$BACKEND" == "true-input" ]] || die "tuistory recordings stop when the session exits; use close to finalize the cast" + [[ -n "$RECORDER_PID" ]] || die "no active recorder for session: $session" + kill -INT "$RECORDER_PID" >/dev/null 2>&1 || true + wait "$RECORDER_PID" 2>/dev/null || true + RECORDER_PID="" + save_session_state "$session" +} + +cmd_launch() { + [[ $# -ge 1 ]] || die "launch requires a command string" + local command="$1" + shift + local session="default" + local backend="tuistory" + local terminal_override="" + local cols="120" + local rows="36" + local cwd="" + local repo_root="" + local tmux_enabled="0" + local record_path="" + local envs=() + + while [[ $# -gt 0 ]]; do + case "$1" in + -s|--session) + session="$2" + shift 2 + ;; + --backend) + backend="$2" + shift 2 + ;; + --terminal) + terminal_override="$2" + shift 2 + ;; + --cols) + cols="$2" + shift 2 + ;; + --rows) + rows="$2" + shift 2 + ;; + --cwd) + cwd="$2" + shift 2 + ;; + --repo-root) + repo_root="$2" + shift 2 + ;; + --env) + envs+=("$2") + shift 2 + ;; + --tmux) + tmux_enabled="1" + shift + ;; + --record) + record_path="$2" + shift 2 + ;; + -h|--help) + print_help + exit 0 + ;; + *) + die "unknown launch option: $1" + ;; + esac + done + + if [[ -z "$repo_root" ]]; then + repo_root="$(launch_env_value DROID_DEV_REPO_ROOT "${envs[@]}" || true)" + fi + + if [[ -n "$repo_root" ]]; then + require_git_worktree_root "$repo_root" + if [[ -n "$cwd" && "$cwd" != "$repo_root" ]]; then + die "--cwd must match --repo-root when --repo-root is set" + fi + cwd="$repo_root" + fi + + if command_uses_droid_dev "$command"; then + [[ -n "$repo_root" ]] \ + || die "droid-dev launches require --repo-root or --env DROID_DEV_REPO_ROOT=" + if ! launch_env_value DROID_DEV_REPO_ROOT "${envs[@]}" >/dev/null 2>&1; then + envs+=("DROID_DEV_REPO_ROOT=$repo_root") + fi + fi + + [[ ! -e "$(session_dir "$session")/meta" ]] || die "session already exists: $session" + resolve_backend "$backend" "$terminal_override" + if [[ "$tmux_enabled" == "1" && "$BACKEND" != "tuistory" ]]; then + die "--tmux is only supported with --backend tuistory" + fi + write_session_runner "$session" "$command" "$cwd" "${envs[@]}" + TMUX_SOCKET_NAME="" + if [[ "$tmux_enabled" == "1" ]]; then + require_cmd tmux + write_tmux_runner "$session" + fi + SESSION="$session" + COMMAND="$command" + COLS="$cols" + ROWS="$rows" + CWD="$cwd" + WAYLAND_DISPLAY_NAME="" + LOG_FILE="" + LOGGED_RUNNER_FILE="" + RECORD_PATH="$record_path" + RECORDER_PID="" + CAGE_PID="" + RUNTIME_DIR="" + WARMED_UP="0" + REPO_ROOT="$repo_root" + save_session_state "$session" + write_provenance "$session" "$repo_root" + + if [[ "$BACKEND" == "tuistory" ]]; then + launch_tuistory "$session" "$cols" "$rows" "$record_path" + else + launch_true_input "$session" "$record_path" + fi +} + +cmd_sessions() { + find "$ROOT_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort +} + +cmd_provenance() { + local session="$1" + load_meta "$session" + local file + file="$(provenance_file "$session")" + [[ -f "$file" ]] || die "no launch provenance recorded for session: $session" + cat "$file" +} + +cmd_type() { + local session="$1" + shift + [[ $# -ge 1 ]] || die "type requires text" + load_meta "$session" + if [[ "$BACKEND" == "tuistory" ]]; then + tuistory -s "$session" type "$1" + else + ensure_true_input_warmup "$session" + true_input_env + wtype "$1" + fi +} + +cmd_press() { + local session="$1" + shift + [[ $# -ge 1 ]] || die "press requires at least one key" + load_meta "$session" + if [[ "$BACKEND" == "tuistory" ]]; then + tuistory -s "$session" press "$@" + else + ensure_true_input_warmup "$session" + true_input_press "$@" + fi +} + +cmd_wait() { + local session="$1" + shift + [[ $# -ge 1 ]] || die "wait requires a pattern" + local pattern="$1" + shift + local timeout_ms="5000" + while [[ $# -gt 0 ]]; do + case "$1" in + --timeout) + timeout_ms="$2" + shift 2 + ;; + *) + die "unknown wait option: $1" + ;; + esac + done + load_meta "$session" + if [[ "$BACKEND" == "tuistory" ]]; then + tuistory -s "$session" wait "$pattern" --timeout "$timeout_ms" + else + wait_for_pattern_in_log "$LOG_FILE" "$pattern" "$timeout_ms" || die "timeout waiting for pattern: $pattern" + echo OK + fi +} + +cmd_wait_idle() { + local session="$1" + shift + local timeout_ms="500" + while [[ $# -gt 0 ]]; do + case "$1" in + --timeout) + timeout_ms="$2" + shift 2 + ;; + *) + die "unknown wait-idle option: $1" + ;; + esac + done + load_meta "$session" + if [[ "$BACKEND" == "tuistory" ]]; then + tuistory -s "$session" wait-idle --timeout "$timeout_ms" + else + wait_for_idle_in_log "$LOG_FILE" "$timeout_ms" || die "timeout waiting for idle state" + fi +} + +cmd_snapshot() { + local session="$1" + shift + local trim=0 + while [[ $# -gt 0 ]]; do + case "$1" in + --trim) + trim=1 + shift + ;; + *) + die "unknown snapshot option: $1" + ;; + esac + done + load_meta "$session" + if [[ "$BACKEND" == "tuistory" ]]; then + local args=(-s "$session" snapshot) + (( trim )) && args+=(--trim) + tuistory "${args[@]}" + else + local output + output="$(strip_log "$LOG_FILE")" + if (( trim )); then + printf '%s' "$output" | trim_text + else + printf '%s' "$output" + fi + fi +} + +cmd_screenshot() { + local session="$1" + shift + local output="" + while [[ $# -gt 0 ]]; do + case "$1" in + -o|--output) + output="$2" + shift 2 + ;; + *) + die "unknown screenshot option: $1" + ;; + esac + done + load_meta "$session" + if [[ -z "$output" ]]; then + output="$(session_dir "$session")/screenshot-$(date +%s).png" + fi + if [[ "$BACKEND" == "tuistory" ]]; then + tuistory -s "$session" screenshot -o "$output" + else + require_cmd grim + true_input_env + WAYLAND_DISPLAY="$WAYLAND_DISPLAY_NAME" grim "$output" + printf '%s\n' "$output" + fi +} + +cmd_record() { + local session="$1" + shift + [[ $# -ge 1 ]] || die "record requires start or stop" + case "$1" in + start) + [[ $# -ge 2 ]] || die "record start requires an output path" + start_true_input_recording "$session" "$2" + ;; + stop) + stop_true_input_recording "$session" + ;; + *) + die "unknown record subcommand: $1" + ;; + esac +} + +cmd_close() { + local session="$1" + load_meta "$session" + if [[ "$BACKEND" == "tuistory" ]]; then + local close_status=0 + tuistory -s "$session" close || close_status=$? + cleanup_tuistory_asciinema "$session" "${RECORD_PATH:-}" + if [[ -n "${TMUX_SOCKET_NAME:-}" ]]; then + tmux -L "$TMUX_SOCKET_NAME" kill-server >/dev/null 2>&1 || true + fi + (( close_status == 0 )) || die "failed to close tuistory session: $session" + else + if [[ -n "$RECORDER_PID" ]]; then + kill -INT "$RECORDER_PID" >/dev/null 2>&1 || true + wait "$RECORDER_PID" 2>/dev/null || true + fi + if [[ -n "$CAGE_PID" ]]; then + kill "$CAGE_PID" >/dev/null 2>&1 || true + wait "$CAGE_PID" 2>/dev/null || true + fi + if [[ -n "$RUNTIME_DIR" ]]; then + rm -rf "$RUNTIME_DIR" + fi + fi + rm -rf "$(session_dir "$session")" +} + +main() { + if [[ $# -eq 0 ]]; then + print_help + exit 0 + fi + + case "$1" in + -h|--help|help) + print_help + exit 0 + ;; + launch) + shift + cmd_launch "$@" + exit 0 + ;; + sessions) + cmd_sessions + exit 0 + ;; + esac + + local session="default" + while [[ $# -gt 0 ]]; do + case "$1" in + -s|--session) + session="$2" + shift 2 + ;; + *) + break + ;; + esac + done + + [[ $# -ge 1 ]] || die "missing command" + local subcommand="$1" + shift + case "$subcommand" in + type) cmd_type "$session" "$@" ;; + press) cmd_press "$session" "$@" ;; + wait) cmd_wait "$session" "$@" ;; + wait-idle) cmd_wait_idle "$session" "$@" ;; + snapshot) cmd_snapshot "$session" "$@" ;; + screenshot) cmd_screenshot "$session" "$@" ;; + provenance) cmd_provenance "$session" ;; + record) cmd_record "$session" "$@" ;; + close) cmd_close "$session" ;; + *) die "unknown command: $subcommand" ;; + esac +} + +main "$@" diff --git a/plugins/droid-control/commands/demo.md b/plugins/droid-control/commands/demo.md new file mode 100644 index 0000000..80b92c2 --- /dev/null +++ b/plugins/droid-control/commands/demo.md @@ -0,0 +1,128 @@ +--- +description: Plan and record a demo video of a feature or PR +argument-hint: " [-- notes]" or "" +--- + +Load skills: **droid-control**. + +## Parse Arguments + +`$ARGUMENTS` can be: +- **PR reference** (`11386`, `#11386`, `pr-11386`, full GitHub URL) with optional `-- notes` after +- **Free-text description** of what to demo + +If a PR reference is found, fetch the PR description, diff, and linked ticket via `gh pr view`. The `-- notes` narrow scope or set constraints. + +Determine the deliverable requirements from the arguments. These are **commitments**, not suggestions -- every checked item must be present in the final output: + +- [ ] **Layout**: side-by-side (if "compare" / "before and after" or default for fixes) OR single-branch (if "single branch" / "new feature") +- [ ] **Showcase wrapping**: YES if "showcase", "polished", "hero", "landing page", "social", or "marketing" appears +- [ ] **Keystroke overlay**: YES if "keys", "keystrokes", or "key combos" appears (implies showcase wrapping) +- [ ] **Effects tier**: one of three tiers (default to **utilitarian** for demos, **full** for showcase; only **none** if user explicitly opts out) + +Effects tiers: + +| Tier | What's included | When | +|---|---|---| +| **utilitarian** | Zoom for readability, keystroke overlay for user actions | Default for all demos | +| **full** | All effect types -- spotlight, zoom, callout, keystroke overlay | Default when showcase wrapping is committed | +| **none** | No effects | Only if user explicitly says "no effects" | + +Do not plan specific effects here -- that's a compose-time decision made after capture, when you have actual recordings to work with. + +If showcase wrapping is committed, resolve the **preset** using the first matching rule: + +| User keywords | Preset | +|---|---| +| `factory`, `official`, `branded` | `factory` | +| `factory hero`, `factory landing` | `factory-hero` | +| `hero`, `landing page`, `social`, `marketing` | `hero` | +| `presentation`, `slides`, `deck` | `presentation` | +| `minimal`, `inline`, `docs embed` | `minimal` | +| _(none of the above — e.g., just "showcase" or "polished")_ | `macos` | + +State these commitments (including the resolved preset) in the demo plan. Present the plan and **wait for user approval** before recording. + +## Understand What to Prove + +For each change in the PR, ask: what could a viewer confuse this with? + +- A new mode could look like an existing mode renamed +- A fix could look like the bug not being triggered +- A performance gain is invisible without a timing reference + +Design the demo so the viewer sees something that **only happens if the feature works as claimed**. Both states (before/after, old/new, input/result) must appear on screen -- off-camera verification doesn't count. + +For simple PRs this is one sentence. For complex ones, sketch a brief table: + +| Claim | Confused with | What proves it | +|-------|--------------|----------------| +| ... | ... | ... | + +## Load Skills + +Use the **droid-control** routing tables. Do all three lookups: + +1. **Target route** -- find the row matching your target, load listed driver/target skills +2. **Stage route** -- load **capture** + **compose** + **verify** (demos always need all three) +3. **Artifact route** -- if showcase or keystroke overlay was committed, also load **showcase** + +## Plan the Interaction Script + +Script the sequence for each branch: + +1. Launch and establish visible baseline +2. Exercise the feature +3. Show the disambiguating result (the thing that rules out the null hypothesis) +4. Stress/edge case if visually interesting +5. Clean exit + +For comparison demos, both branches run **identical interactions** -- only the behavior differs. Verify state between steps; don't blindly fire the next key. + +## Capture + +Follow the **capture** atom. It owns recording lifecycle, pre-flight, keystroke logging, and evidence collection. + +Provide the capture stage with: +- Target app and branch(es) +- The interaction script from above +- Whether to emit a keystroke TSV + +**Delegation:** For before/after comparisons, capture both branches **in parallel** using worker subagents with `run_in_background=true`. Construct the exact `tctl` commands for each worker (see the delegation section in the droid-control skill). Wait for both to complete before proceeding to compose. + +## Compose + +Follow the **compose** atom. It owns the full video assembly pipeline. + +**Delegation:** Launch one worker subagent for the mechanical render: +- Worker A: render the final video via `render-showcase.sh` directly from `.cast` / `.mp4` inputs + +`render-showcase.sh` owns `.cast -> agg -> .mp4`, Remotion composition, fidelity profile selection, duration detection, and cleanup. Wait for the worker to finish, then verify the output. + +Hand compose a hybrid handoff: + +### Mechanical (structured) +- layout: side-by-side | single +- fidelity: auto | compact | standard | inspect (optional; auto => side-by-side=inspect, single=standard) +- labels: ["BEFORE ()", "AFTER ()"] +- speed: 3x +- title: "PR #11386 — Add --fork flag" +- subtitle: "Demo: --fork creates a forked session from current context" +- clips: [/tmp/before.cast, /tmp/after.cast] +- keys: /tmp/keys.tsv (if committed) +- preset: hero | macos | minimal | presentation | factory | factory-hero (if committed) +- effects tier: utilitarian | full | none +- output: /tmp/demo-pr-11386.mp4 + +### Creative (natural language) +What the viewer should take away. Which moments to hold. How to frame the story. Whether phase cards are warranted. The compose atom uses this -- along with the effects tier -- for editorial decisions: title card phrasing, trim points, emphasis, and choosing specific effects to apply. + +## Verify + +Follow the **verify** atom. It checks the final deliverable against the commitments from Parse Arguments. + +## Report + +- File path, resolution, duration, size +- What each phase demonstrates -- map back to the claims. Call out timestamps where the disambiguating evidence appears. +- What's not covered and why. diff --git a/plugins/droid-control/commands/qa-test.md b/plugins/droid-control/commands/qa-test.md new file mode 100644 index 0000000..5d838c1 --- /dev/null +++ b/plugins/droid-control/commands/qa-test.md @@ -0,0 +1,103 @@ +--- +description: Run an automated QA test flow against a terminal CLI or web/Electron app +argument-hint: "" or "" or " [-- focus area]" or "" +--- + +Load skills: **droid-control**. + +## Parse Arguments + +`$ARGUMENTS` can be: +- **URL** (`https://app.factory.ai`, `localhost:3000`) → web app +- **Electron app name** (`Slack`, `VS Code`, `Figma`) → Electron app via CDP +- **CLI command** (`droid-dev`, `htop`, `my-cli --flag`) → terminal TUI +- **PR reference** (`11386`) with optional `-- focus area` → infer target from the diff +- **Free-text description** ("test the login flow on staging") → infer target and flow + +If a PR reference is found, fetch the PR description and diff to determine what to test. + +Determine commitments: + +- [ ] **Video recording**: YES if "record", "video", or "demo" appears (implies compose stage) +- [ ] **Showcase**: YES if "polished", "showcase" appears (implies video + showcase) + +If showcase is committed, resolve the **preset** using the first matching rule: + +| User keywords | Preset | +|---|---| +| `factory`, `official`, `branded` | `factory` | +| `factory hero`, `factory landing` | `factory-hero` | +| `hero`, `landing page`, `social`, `marketing` | `hero` | +| `presentation`, `slides`, `deck` | `presentation` | +| `minimal`, `inline`, `docs embed` | `minimal` | +| _(none of the above)_ | `macos` | + +## Load Skills + +Use the **droid-control** routing tables: + +1. **Target route** -- find the row matching your target, load listed driver/target skills +2. **Stage route** -- load **capture** + **verify** always; load **compose** if video recording or showcase was committed +3. **Artifact route** -- if showcase committed, also load **showcase** + +## Define Test Steps + +If the user provides specific steps, use them. Otherwise, design a reasonable flow based on the target: + +**Web/Electron**: open page → wait for load → screenshot → interact with primary UI → verify state changes → screenshot → close. + +**Terminal**: launch app → wait for ready → snapshot → exercise primary features → verify output → snapshot → close. + +If the flow is ambiguous or success criteria are unclear, ask the user. + +## Capture + +Follow the **capture** atom. Provide: +- The target to launch +- The test steps as the interaction script +- Evidence capture at every step (snapshots for terminal, screenshots for browser) + +If a step fails: +- Record the failure with evidence +- Continue to the next step for maximum coverage +- Unless the failure blocks everything downstream (e.g., login failed) + +## Compose (if committed) + +Follow the **compose** atom if a video deliverable was committed. Hand it: + +### Mechanical +- layout: single +- clips: [paths to recordings] +- title: "QA Test: " +- output: /tmp/qa-.mp4 + +### Creative +What the test flow covers, which steps passed/failed, what the viewer should focus on. + +## Verify + +Follow the **verify** atom. It checks the deliverable and QA report completeness. + +## Report + +``` +## QA Test Report + +**Target:** +**Driver:** + +### Results + +| Step | Status | Notes | +|------|--------|-------| +| ... | PASS/FAIL | ... | + +### Issues Found + +- + +### Evidence + + +``` diff --git a/plugins/droid-control/commands/verify.md b/plugins/droid-control/commands/verify.md new file mode 100644 index 0000000..141340b --- /dev/null +++ b/plugins/droid-control/commands/verify.md @@ -0,0 +1,121 @@ +--- +description: Test a claim about behavior and report whether the evidence supports or refutes it +argument-hint: "" or " -- " +--- + +Load skills: **droid-control**. + +## Ground rule + +**You are an investigator, not an advocate.** Your job is to find out whether a claim is true, not to make it look true. A conclusive "this is broken" finding with clear evidence is just as valuable as a "this works" finding. Never fabricate, hardcode, or stage evidence to match an expected outcome. If the behavior you observe contradicts the claim, that is the result -- report it. + +## Parse Arguments + +`$ARGUMENTS` can be: +- **Direct claim** ("Shift+Enter inserts a newline in Ghostty", "resize fix no longer clears screen") +- **PR reference + claim** (`11386 -- the fork flag creates a new session`) +- **PR reference only** (`11386`) -- fetch the PR, identify the most important testable claim + +If a PR reference is found, fetch the PR description and diff for context. + +Determine commitments: + +- [ ] **Evidence type**: byte capture | screenshot | text snapshot | annotated browser snapshot +- [ ] **Comparison**: before/after (if the claim is about a change) OR single-state (if the claim is about current behavior) +- [ ] **Video proof**: YES if "video", "recording", "demo" appears (implies compose stage) +- [ ] **Showcase**: YES if "polished", "showcase" appears (implies video + showcase) + +If showcase is committed, resolve the **preset** using the first matching rule: + +| User keywords | Preset | +|---|---| +| `factory`, `official`, `branded` | `factory` | +| `factory hero`, `factory landing` | `factory-hero` | +| `hero`, `landing page`, `social`, `marketing` | `hero` | +| `presentation`, `slides`, `deck` | `presentation` | +| `minimal`, `inline`, `docs embed` | `minimal` | +| _(none of the above)_ | `macos` | + +## Understand What to Test + +Determine the single specific behavior to observe. What would a skeptic need to see? + +- Byte-level claim (keyboard encoding, escape sequences) → needs raw PTY capture +- Visual claim (rendering, layout, colors) → needs screenshot from a real compositor +- Functional claim (feature works, flow completes) → needs interaction + state verification + +## Load Skills + +Use the **droid-control** routing tables: + +1. **Target route** -- find the row matching your target, load listed driver/target skills +2. **Stage route** -- load **capture** + **verify** always; load **compose** if video proof or showcase was committed +3. **Artifact route** -- if showcase committed, also load **showcase** + +## Capture + +Follow the **capture** atom. Provide: +- The claim to test +- The evidence type(s) needed +- The minimal interaction sequence that demonstrates the behavior +- Whether this is a before/after comparison + +**If the behavior does not match the claim:** Do not retry the interaction hoping for a different result. Capture a snapshot or screenshot of the actual state. This is evidence. If you suspect your test procedure is wrong (e.g., wrong branch, missing build step), verify the environment first -- but if the environment is correct and the behavior is wrong, that is a finding, not an error on your part. + +## Compose (if committed) + +Follow the **compose** atom if a video deliverable was committed. Hand it: + +### Mechanical +- layout: side-by-side | single +- clips: [paths to recordings] +- title: "Verify: " +- output: ${RUN_DIR}/verify-.mp4 + +### Creative +What the evidence shows and why it is conclusive -- whether it supports or refutes the claim. + +## Verify + +Follow the **verify** atom. It checks the deliverable against your commitments. + +## Report + +``` +## Verify: + +**Environment:** +**Branch:** + +### Evidence + + + +### Conclusion + +**CONFIRMED** | **REFUTED** | **INCONCLUSIVE** + + +``` + +### When the claim is refuted + +If the evidence shows the behavior does not match the claim: + +1. State the expected behavior (from the claim) +2. State the observed behavior (from the evidence) +3. Include the evidence (snapshots, screenshots, hex dumps) inline +4. Note any environmental factors that might be relevant (branch, commit, terminal, OS) + +This is a valuable finding. The user asked you to test this claim precisely because they need to know whether it holds. + +### When the result is inconclusive + +If the environment prevented a clean test (e.g., missing dependency, build failure, test infra crash), report what blocked the test and what would be needed to resolve it. Do not guess at the outcome. + +## Do NOT + +- Retry a failing test more than once without changing the environment or procedure +- Hardcode expected output, mock responses, or stage a scenario to produce a desired result +- Assume that unexpected behavior means you made a mistake -- it may be a real bug +- Omit evidence that contradicts the claim diff --git a/plugins/droid-control/remotion/package-lock.json b/plugins/droid-control/remotion/package-lock.json new file mode 100644 index 0000000..1d15e15 --- /dev/null +++ b/plugins/droid-control/remotion/package-lock.json @@ -0,0 +1,5497 @@ +{ + "name": "droid-control-remotion", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "droid-control-remotion", + "version": "1.0.0", + "dependencies": { + "@remotion/cli": "4.0.445", + "@remotion/layout-utils": "4.0.445", + "@remotion/light-leaks": "4.0.445", + "@remotion/media": "4.0.445", + "@remotion/tailwind": "4.0.445", + "@remotion/transitions": "4.0.445", + "react": "^19", + "react-dom": "^19", + "remotion": "4.0.445", + "zod": "4.3.6" + }, + "devDependencies": { + "@types/react": "^19", + "tailwindcss": "^4", + "typescript": "^5.7" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.1.tgz", + "integrity": "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==", + "license": "MIT", + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.13.tgz", + "integrity": "sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-4.2.1.tgz", + "integrity": "sha512-CEypeeykO9AN7JWkr1OEOQb0HRzZlPWGwV0Ya6DuVgFdDi6g3ma/cPZ5ZPZM4AWQikDpq/0llnGGlIL+j8afzw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-1.2.4.tgz", + "integrity": "sha512-tfOuvUQeo7Hz+FcuOd3LfXVp+342pnWUJ7D2y8NUpu1Ww6xnTbHLpz018/y6rtbHifJ3iIEf9ttxXd8KG7nL0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-1.6.3.tgz", + "integrity": "sha512-pQPUPo32HW3/NuZxrwr3VJHE+vGqSTVI5gK4jGbuJ7eOFUrsTmZikXcVdInCVWOvuxK5xbCzwDWoTlZUCAKN+A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^4.1.0", + "@csstools/css-calc": "^1.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.6.1", + "@csstools/css-tokenizer": "^2.2.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@csstools/media-query-list-parser": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.13.tgz", + "integrity": "sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/postcss-cascade-layers": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-3.0.1.tgz", + "integrity": "sha512-dD8W98dOYNOH/yX4V4HXOhfCOnvVAg8TtsL+qCGNoKXuq5z2C/d026wGWgySgC8cajXXo/wNezS31Glj5GcqrA==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/selector-specificity": "^2.0.2", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-function": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-2.2.3.tgz", + "integrity": "sha512-b1ptNkr1UWP96EEHqKBWWaV5m/0hgYGctgA/RVZhONeP1L3T/8hwoqDm9bB23yVCfOgE9U93KI9j06+pEkJTvw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-color-parser": "^1.2.0", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/postcss-progressive-custom-properties": "^2.3.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-color-mix-function": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-1.0.3.tgz", + "integrity": "sha512-QGXjGugTluqFZWzVf+S3wCiRiI0ukXlYqCi7OnpDotP/zaVTyl/aqZujLFzTOXy24BoWnu89frGMc79ohY5eog==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-color-parser": "^1.2.0", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/postcss-progressive-custom-properties": "^2.3.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-font-format-keywords": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-2.0.2.tgz", + "integrity": "sha512-iKYZlIs6JsNT7NKyRjyIyezTCHLh4L4BBB3F5Nx7Dc4Z/QmBgX+YJFuUSar8IM6KclGiAUFGomXFdYxAwJydlA==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-gradients-interpolation-method": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-3.0.6.tgz", + "integrity": "sha512-rBOBTat/YMmB0G8VHwKqDEx+RZ4KCU9j42K8LwS0IpZnyThalZZF7BCSsZ6TFlZhcRZKlZy3LLFI2pLqjNVGGA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-color-parser": "^1.2.0", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/postcss-progressive-custom-properties": "^2.3.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-hwb-function": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-2.2.2.tgz", + "integrity": "sha512-W5Y5oaJ382HSlbdGfPf60d7dAK6Hqf10+Be1yZbd/TNNrQ/3dDdV1c07YwOXPQ3PZ6dvFMhxbIbn8EC3ki3nEg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-color-parser": "^1.2.0", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-ic-unit": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-2.0.4.tgz", + "integrity": "sha512-9W2ZbV7whWnr1Gt4qYgxMWzbevZMOvclUczT5vk4yR6vS53W/njiiUhtm/jh/BKYwQ1W3PECZjgAd2dH4ebJig==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^2.3.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-is-pseudo-class": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-3.2.1.tgz", + "integrity": "sha512-AtANdV34kJl04Al62is3eQRk/BfOfyAvEmRJvbt+nx5REqImLC+2XhuE6skgkcPli1l8ONS67wS+l1sBzySc3Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-float-and-clear": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-1.0.1.tgz", + "integrity": "sha512-eO9z2sMLddvlfFEW5Fxbjyd03zaO7cJafDurK4rCqyRt9P7aaWwha0LcSzoROlcZrw1NBV2JAp2vMKfPMQO1xw==", + "license": "CC0-1.0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-resize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-1.0.1.tgz", + "integrity": "sha512-x1ge74eCSvpBkDDWppl+7FuD2dL68WP+wwP2qvdUcKY17vJksz+XoE1ZRV38uJgS6FNUwC0AxrPW5gy3MxsDHQ==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-logical-viewport-units": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-1.0.3.tgz", + "integrity": "sha512-6zqcyRg9HSqIHIPMYdt6THWhRmE5/tyHKJQLysn2TeDf/ftq7Em9qwMTx98t2C/7UxIsYS8lOiHHxAVjWn2WUg==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-tokenizer": "^2.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-minmax": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-1.1.8.tgz", + "integrity": "sha512-KYQCal2i7XPNtHAUxCECdrC7tuxIWQCW+s8eMYs5r5PaAiVTeKwlrkRS096PFgojdNCmHeG0Cb7njtuNswNf+w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^1.2.4", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/media-query-list-parser": "^2.1.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-1.0.4.tgz", + "integrity": "sha512-IwyTbyR8E2y3kh6Fhrs251KjKBJeUPV5GlnUKnpU70PRFEN2DolWbf2V4+o/B9+Oj77P/DullLTulWEQ8uFtAA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-parser-algorithms": "^2.2.0", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/media-query-list-parser": "^2.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-nested-calc": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-2.0.2.tgz", + "integrity": "sha512-jbwrP8rN4e7LNaRcpx3xpMUjhtt34I9OV+zgbcsYAAk6k1+3kODXJBf95/JMYWhu9g1oif7r06QVUgfWsKxCFw==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-normalize-display-values": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-2.0.1.tgz", + "integrity": "sha512-TQT5g3JQ5gPXC239YuRK8jFceXF9d25ZvBkyjzBGGoW5st5sPXFVQS8OjYb9IJ/K3CdfK4528y483cgS2DJR/w==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-oklab-function": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-2.2.3.tgz", + "integrity": "sha512-AgJ2rWMnLCDcbSMTHSqBYn66DNLBym6JpBpCaqmwZ9huGdljjDRuH3DzOYzkgQ7Pm2K92IYIq54IvFHloUOdvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-color-parser": "^1.2.0", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/postcss-progressive-custom-properties": "^2.3.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-progressive-custom-properties": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-2.3.0.tgz", + "integrity": "sha512-Zd8ojyMlsL919TBExQ1I0CTpBDdyCpH/yOdqatZpuC3sd22K4SwC7+Yez3Q/vmXMWSAl+shjNeFZ7JMyxMjK+Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-relative-color-syntax": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-1.0.2.tgz", + "integrity": "sha512-juCoVInkgH2TZPfOhyx6tIal7jW37L/0Tt+Vcl1LoxqQA9sxcg3JWYZ98pl1BonDnki6s/M7nXzFQHWsWMeHgw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-color-parser": "^1.2.0", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/postcss-progressive-custom-properties": "^2.3.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-scope-pseudo-class": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-2.0.2.tgz", + "integrity": "sha512-6Pvo4uexUCXt+Hz5iUtemQAcIuCYnL+ePs1khFR6/xPgC92aQLJ0zGHonWoewiBE+I++4gXK3pr+R1rlOFHe5w==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-stepped-value-functions": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-2.1.1.tgz", + "integrity": "sha512-YCvdF0GCZK35nhLgs7ippcxDlRVe5QsSht3+EghqTjnYnyl3BbWIN6fYQ1dKWYTJ+7Bgi41TgqQFfJDcp9Xy/w==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-calc": "^1.1.1", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-2.2.4.tgz", + "integrity": "sha512-zPN56sQkS/7YTCVZhOBVCWf7AiNge8fXDl7JVaHLz2RyT4pnyK2gFjckWRLpO0A2xkm1lCgZ0bepYZTwAVd/5A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/color-helpers": "^2.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-text-decoration-shorthand/node_modules/@csstools/color-helpers": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-2.1.0.tgz", + "integrity": "sha512-OWkqBa7PDzZuJ3Ha7T5bxdSVfSCfTq6K1mbAhbO1MD+GSULGjrp45i5RudyJOedstSarN/3mdwu9upJE7gDXfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@csstools/postcss-trigonometric-functions": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-2.1.1.tgz", + "integrity": "sha512-XcXmHEFfHXhvYz40FtDlA4Fp4NQln2bWTsCwthd2c+MCnYArUYU3YaMqzR5CrKP3pMoGYTBnp5fMqf1HxItNyw==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-calc": "^1.1.1", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/postcss-unset-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-2.0.1.tgz", + "integrity": "sha512-oJ9Xl29/yU8U7/pnMJRqAZd4YXNCfGEdcP4ywREuqm/xMqcgDNDppYRoCGDt40aaZQIEKBS79LytUDN/DHf0Ew==", + "license": "CC0-1.0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.2.0.tgz", + "integrity": "sha512-+OJ9konv95ClSTOJCmMZqpd5+YGsB2S+x6w3E1oaM8UuR5j8nTNHYSz8c9BEPGDOCMQYIEEGlVPj/VY64iTbGw==", + "license": "CC0-1.0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss-selector-parser": "^6.0.10" + } + }, + "node_modules/@csstools/utilities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-1.0.0.tgz", + "integrity": "sha512-tAgvZQe/t2mlvpNosA4+CkMiZ2azISW5WPAcdSalZlEjQvUfghHxfQcrCiK/7/CrfAWVxyM88kGFYO82heIGDg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mediabunny/aac-encoder": { + "version": "1.39.2", + "resolved": "https://registry.npmjs.org/@mediabunny/aac-encoder/-/aac-encoder-1.39.2.tgz", + "integrity": "sha512-KD6KADVzAnW7tqhRFGBOX4uaiHbd0Yxvg0lfthj3wJLAEEgEBAvi43w+ZXWeEn54X/jpabrLe4bW/eYFFvlbUA==", + "license": "MPL-2.0", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + }, + "peerDependencies": { + "mediabunny": "^1.0.0" + } + }, + "node_modules/@mediabunny/flac-encoder": { + "version": "1.39.2", + "resolved": "https://registry.npmjs.org/@mediabunny/flac-encoder/-/flac-encoder-1.39.2.tgz", + "integrity": "sha512-VwBr3AzZTPEEPvt4aladZiXwOf3W293eq213zDupGQi/taS8WWNqDd3eBdf8FfvlbXATfbRiycXDKyQ0HlOZaQ==", + "license": "MPL-2.0", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + }, + "peerDependencies": { + "mediabunny": "^1.0.0" + } + }, + "node_modules/@mediabunny/mp3-encoder": { + "version": "1.39.2", + "resolved": "https://registry.npmjs.org/@mediabunny/mp3-encoder/-/mp3-encoder-1.39.2.tgz", + "integrity": "sha512-3rrodrGnUpUP8F2d1aRUl8IvjqK3jegkupbOzvOokooSAO5rXk2Lr5jZe7TnPeiVGiXfmnoJ7s9uyUOHlCd8qw==", + "license": "MPL-2.0", + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + }, + "peerDependencies": { + "mediabunny": "^1.0.0" + } + }, + "node_modules/@module-federation/error-codes": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/error-codes/-/error-codes-0.22.0.tgz", + "integrity": "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug==", + "license": "MIT" + }, + "node_modules/@module-federation/runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.22.0.tgz", + "integrity": "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/runtime-core": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-core": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-core/-/runtime-core-0.22.0.tgz", + "integrity": "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA==", + "license": "MIT", + "dependencies": { + "@module-federation/error-codes": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@module-federation/runtime-tools": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.22.0.tgz", + "integrity": "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/webpack-bundler-runtime": "0.22.0" + } + }, + "node_modules/@module-federation/sdk": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.22.0.tgz", + "integrity": "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g==", + "license": "MIT" + }, + "node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.22.0.tgz", + "integrity": "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime": "0.22.0", + "@module-federation/sdk": "0.22.0" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.0.7.tgz", + "integrity": "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@tybys/wasm-util": "^0.10.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remotion/bundler": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/bundler/-/bundler-4.0.445.tgz", + "integrity": "sha512-jA5VZfFeHv1gVfwdQAWUtKDDc7axVoB/wgCcmnlVh8jCKUZMJnUWewH5ac7FDEbZ/7zKie71naR8Ad0EgD4Hew==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/media-parser": "4.0.445", + "@remotion/studio": "4.0.445", + "@remotion/studio-shared": "4.0.445", + "@rspack/core": "1.7.6", + "@rspack/plugin-react-refresh": "1.6.1", + "esbuild": "0.25.0", + "loader-utils": "2.0.4", + "postcss": "8.5.1", + "postcss-value-parser": "4.2.0", + "react-refresh": "0.18.0", + "remotion": "4.0.445", + "source-map": "0.7.3", + "style-loader": "4.0.0", + "webpack": "5.105.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/cli": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/cli/-/cli-4.0.445.tgz", + "integrity": "sha512-lfAcT6tLSFmvdR1I4Re40Ym6JGMh/1muApJsn9blOkqJevAZr4lqK/Fjqf4pbcm2B4QslX6Uu5jWGoVJgli+fg==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/bundler": "4.0.445", + "@remotion/media-utils": "4.0.445", + "@remotion/player": "4.0.445", + "@remotion/renderer": "4.0.445", + "@remotion/studio": "4.0.445", + "@remotion/studio-server": "4.0.445", + "@remotion/studio-shared": "4.0.445", + "dotenv": "17.3.1", + "minimist": "1.2.6", + "prompts": "2.4.2", + "remotion": "4.0.445" + }, + "bin": { + "remotion": "remotion-cli.js", + "remotionb": "remotionb-cli.js", + "remotiond": "remotiond-cli.js" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/compositor-darwin-arm64": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-arm64/-/compositor-darwin-arm64-4.0.445.tgz", + "integrity": "sha512-yB2UNMb56l3grn1SpkHu4RMrmfXvC0j1hebgxUQYnp180AdhUq2oBHRArlm8O7HxctS7q0z2i4AYPh9oEkZkOw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-darwin-x64": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/compositor-darwin-x64/-/compositor-darwin-x64-4.0.445.tgz", + "integrity": "sha512-YFyuhS1wcKaIRptizZRjdcIHHg+MguHJGG4dqeSTSdxhYXky0+df9IuF/FFCN4ScWTbcn1KoaG/WdwglSId6/A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-gnu": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-gnu/-/compositor-linux-arm64-gnu-4.0.445.tgz", + "integrity": "sha512-OoZXonYdLI9t+LJAo3z3HlZ6ujrHKrS5sFw4snBg8mNIcjfblplDnydLXhV1PemNlVtAkU2ZUE0H5abqAQKoaQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-arm64-musl": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-arm64-musl/-/compositor-linux-arm64-musl-4.0.445.tgz", + "integrity": "sha512-fkqdErDMVbJdLlcfDh8LmEMZMfJAavxEOoVelLWkmDhNJnGvbPbDlwkITxg5H4U1ZBCjEF20lUAbLsIs7AD79Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-gnu": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-gnu/-/compositor-linux-x64-gnu-4.0.445.tgz", + "integrity": "sha512-JgcEWYsrIdVNkHYQsrBamrooXFPZRung7zPqMWIWmfM8TVXz2Ncid5JW8qMK+eRl/dQaBXw4V9WYaBfIj17JDA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-linux-x64-musl": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/compositor-linux-x64-musl/-/compositor-linux-x64-musl-4.0.445.tgz", + "integrity": "sha512-AAmWGt9TP2CrGRossBNBSerKMW8KIOpxSKTBjpaza5Yjjd+3S4XVhNlBnsTD/1cxw9HaQJ1um9detKsVev0Weg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@remotion/compositor-win32-x64-msvc": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/compositor-win32-x64-msvc/-/compositor-win32-x64-msvc-4.0.445.tgz", + "integrity": "sha512-qNa86D0j8kjFhU+t21/2IpyEsy1YIVHs9HkscxDOzN+SoW+9m3esWc4VSXnBZsu2PMq8zplYb79roQZGKk8BFw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@remotion/layout-utils": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/layout-utils/-/layout-utils-4.0.445.tgz", + "integrity": "sha512-VneNiJy6LkqO1VYyya+p+rt1lUfl88TpKz/lS84je6mUw/7++gzfhrVdWycFoQSralJxoft9885cU+hR9huwTw==", + "license": "MIT" + }, + "node_modules/@remotion/licensing": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/licensing/-/licensing-4.0.445.tgz", + "integrity": "sha512-IbRTYGNzw0CCqJpxpdMVYPDKU15gJKK3DEjxE9nU4UXJHCEaGglxU6KvnDK3ykjHawuMmND+gSSHpj++89l+Sg==", + "license": "MIT" + }, + "node_modules/@remotion/light-leaks": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/light-leaks/-/light-leaks-4.0.445.tgz", + "integrity": "sha512-9UIYrNP2uV9rtU/LeKIdXw5QIz7YzCf64cG6WH5PaKOHnMzHw8HfGlYwVS9s3+ruUSj2wnQPd2ZU8dZPYHiPVw==", + "license": "Remotion License", + "dependencies": { + "remotion": "4.0.445", + "zod": "4.3.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/media": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/media/-/media-4.0.445.tgz", + "integrity": "sha512-c/G6LP20nECovxzcuMb+80n6+Qea1APvC2Ewp8Mpr07IjAKVv9+FC+H4/omxhC/wXRXchwigEUV/Y964z0QCQg==", + "dependencies": { + "mediabunny": "1.39.2", + "remotion": "4.0.445", + "zod": "4.3.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/media-parser": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/media-parser/-/media-parser-4.0.445.tgz", + "integrity": "sha512-lKQnttWt2XCMQfr8eralZihnPAYDiJDG3uhhBfI13f4OHmKDSxjijP11w1k6JTbhIlER1Ijm/ZfSbQ3UjWQYgA==", + "license": "Remotion License https://remotion.dev/license" + }, + "node_modules/@remotion/media-utils": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/media-utils/-/media-utils-4.0.445.tgz", + "integrity": "sha512-pH7bM3Uq19I7zm0TR3kpRUfNh/jtBZjjLfV395kzO+ccdN1VQhfe5C/btQ/G0GIgwhP0mdm9jm8TZw2CTGQuSg==", + "license": "MIT", + "dependencies": { + "mediabunny": "1.39.2", + "remotion": "4.0.445" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/paths": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/paths/-/paths-4.0.445.tgz", + "integrity": "sha512-NRDlT0yJ4bgOLHjGFTENK3AOzAEhj06Tla3b6c0F9KtyrCGTHehmu2EE+FfA4tJl3VzH6j0xfQEhUZc/A62+JQ==", + "license": "MIT" + }, + "node_modules/@remotion/player": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/player/-/player-4.0.445.tgz", + "integrity": "sha512-AYhu6LXrL66FNOg5Md5DNF/BH0TQg7C26lESZozukVnSH6dmgJQgwtEcCH7l3hwDz+IJhZPn6Lbwo+2rpr2M+A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "remotion": "4.0.445" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/renderer/-/renderer-4.0.445.tgz", + "integrity": "sha512-RejUK9zQxNt1h6b5gNHRGDIjFnun3ZQ6yyGE8PBnSXyU8ONMKLDq+2iDsE3CRftWdld3uNtWrVAhSabW9QRmqA==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@remotion/licensing": "4.0.445", + "@remotion/streaming": "4.0.445", + "execa": "5.1.1", + "extract-zip": "2.0.1", + "remotion": "4.0.445", + "source-map": "^0.8.0-beta.0", + "ws": "8.17.1" + }, + "optionalDependencies": { + "@remotion/compositor-darwin-arm64": "4.0.445", + "@remotion/compositor-darwin-x64": "4.0.445", + "@remotion/compositor-linux-arm64-gnu": "4.0.445", + "@remotion/compositor-linux-arm64-musl": "4.0.445", + "@remotion/compositor-linux-x64-gnu": "4.0.445", + "@remotion/compositor-linux-x64-musl": "4.0.445", + "@remotion/compositor-win32-x64-msvc": "4.0.445" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/renderer/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remotion/shapes": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/shapes/-/shapes-4.0.445.tgz", + "integrity": "sha512-tPBHyoy8Fl3fHRDekeFJ7l6KG8H34KOAVexLx42AdhxNnMMKq2dqP1Mosqjw9mTdicKvZvrIbojZPQIbUhkb7g==", + "license": "MIT", + "dependencies": { + "@remotion/paths": "4.0.445" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/streaming": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/streaming/-/streaming-4.0.445.tgz", + "integrity": "sha512-NCy1XYSEGbPb/xkyt6WdFIlEawnYCFcprKHFCrZfAY7Pu7jgNuMDfgXDgmmQe3gvyuS6NxCEaJphZ6VToLoKKA==", + "license": "MIT" + }, + "node_modules/@remotion/studio": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/studio/-/studio-4.0.445.tgz", + "integrity": "sha512-wiGNDZHcvVbqyhlMCVypWqsd1l6JSiyc+7kcfBtSAABz1X4bWCmSuIC4K3s6CsOlhFzUjZJFtz3b3fi1a1Gzaw==", + "license": "MIT", + "dependencies": { + "@remotion/media-utils": "4.0.445", + "@remotion/player": "4.0.445", + "@remotion/renderer": "4.0.445", + "@remotion/studio-shared": "4.0.445", + "@remotion/web-renderer": "4.0.445", + "@remotion/zod-types": "4.0.445", + "mediabunny": "1.39.2", + "memfs": "3.4.3", + "open": "^8.4.2", + "remotion": "4.0.445", + "semver": "7.5.3", + "source-map": "0.7.3", + "zod": "4.3.6" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/studio-server": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/studio-server/-/studio-server-4.0.445.tgz", + "integrity": "sha512-fiSNJf0MroDILWLL611HAOiEXxAISe0g1Wk/c+guFixcPCFcAH4+BFGJrBPor+IO8GbO4MgWnt88CAMPf2ifTw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "7.24.1", + "@remotion/bundler": "4.0.445", + "@remotion/renderer": "4.0.445", + "@remotion/studio-shared": "4.0.445", + "memfs": "3.4.3", + "open": "^8.4.2", + "prettier": "3.8.1", + "recast": "0.23.11", + "remotion": "4.0.445", + "semver": "7.5.3", + "source-map": "0.7.3" + } + }, + "node_modules/@remotion/studio-shared": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/studio-shared/-/studio-shared-4.0.445.tgz", + "integrity": "sha512-OqGPFSklJ0E1p2P4sWwnh23xsnxWwhqGkz8payR49a+nYyueXaX+MvKLJrhiXgAMqt4L7507pqvYjRe2Ca96jw==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.445" + } + }, + "node_modules/@remotion/studio/node_modules/@remotion/zod-types": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/zod-types/-/zod-types-4.0.445.tgz", + "integrity": "sha512-LSxbdQZY/ZA3gaQ7A+j18kcrEjgTJBQ8mqSl52VJl4X4gDcUUWiYp9k4hFBUOhAEMXHF8KInvf+RPAzHfpSLWg==", + "license": "MIT", + "dependencies": { + "remotion": "4.0.445" + }, + "peerDependencies": { + "zod": "4.3.6" + } + }, + "node_modules/@remotion/tailwind": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/tailwind/-/tailwind-4.0.445.tgz", + "integrity": "sha512-5xt/LjuKyObI8jcvMNcIi4YYaUZSWXHoIOlRdVumX7N0bvn+3qPrQCIzbGk3QuFJL/JvDEzC9jx5mlkA6uu8xg==", + "license": "MIT", + "dependencies": { + "autoprefixer": "10.4.20", + "postcss": "8.4.47", + "postcss-loader": "^7.3.0", + "postcss-preset-env": "^8.3.2", + "style-loader": "4.0.0", + "tailwindcss": "3.4.13" + }, + "peerDependencies": { + "@remotion/bundler": "4.0.445" + } + }, + "node_modules/@remotion/tailwind/node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@remotion/tailwind/node_modules/tailwindcss": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", + "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@remotion/transitions": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/transitions/-/transitions-4.0.445.tgz", + "integrity": "sha512-f82c17oAaAMAYVnEEhqEmnotRVItg/52G47jRGN6wNBDGaqvnZ4kTFHyXxTTO3EVZXUau9l+hyGad9y4PufnUg==", + "license": "UNLICENSED", + "dependencies": { + "@remotion/paths": "4.0.445", + "@remotion/shapes": "4.0.445", + "remotion": "4.0.445" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@remotion/web-renderer": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/@remotion/web-renderer/-/web-renderer-4.0.445.tgz", + "integrity": "sha512-EHOFdrqe/T3gUS7W1XbmcjAAeCSNJ5CDAcqy6EZWIVOf8nXJbPuh820SohOHdyPBFs5weola/Dt/XiYCunisCg==", + "license": "UNLICENSED", + "dependencies": { + "@mediabunny/aac-encoder": "1.39.2", + "@mediabunny/flac-encoder": "1.39.2", + "@mediabunny/mp3-encoder": "1.39.2", + "@remotion/licensing": "4.0.445", + "mediabunny": "1.39.2", + "remotion": "4.0.445" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rspack/binding": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.7.6.tgz", + "integrity": "sha512-/NrEcfo8Gx22hLGysanrV6gHMuqZSxToSci/3M4kzEQtF5cPjfOv5pqeLK/+B6cr56ul/OmE96cCdWcXeVnFjQ==", + "license": "MIT", + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "1.7.6", + "@rspack/binding-darwin-x64": "1.7.6", + "@rspack/binding-linux-arm64-gnu": "1.7.6", + "@rspack/binding-linux-arm64-musl": "1.7.6", + "@rspack/binding-linux-x64-gnu": "1.7.6", + "@rspack/binding-linux-x64-musl": "1.7.6", + "@rspack/binding-wasm32-wasi": "1.7.6", + "@rspack/binding-win32-arm64-msvc": "1.7.6", + "@rspack/binding-win32-ia32-msvc": "1.7.6", + "@rspack/binding-win32-x64-msvc": "1.7.6" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.7.6.tgz", + "integrity": "sha512-NZ9AWtB1COLUX1tA9HQQvWpTy07NSFfKBU8A6ylWd5KH8AePZztpNgLLAVPTuNO4CZXYpwcoclf8jG/luJcQdQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.7.6.tgz", + "integrity": "sha512-J2g6xk8ZS7uc024dNTGTHxoFzFovAZIRixUG7PiciLKTMP78svbSSWrmW6N8oAsAkzYfJWwQpVgWfFNRHvYxSw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.7.6.tgz", + "integrity": "sha512-eQfcsaxhFrv5FmtaA7+O1F9/2yFDNIoPZzV/ZvqvFz5bBXVc4FAm/1fVpBg8Po/kX1h0chBc7Xkpry3cabFW8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.7.6.tgz", + "integrity": "sha512-DfQXKiyPIl7i1yECHy4eAkSmlUzzsSAbOjgMuKn7pudsWf483jg0UUYutNgXSlBjc/QSUp7906Cg8oty9OfwPA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.7.6.tgz", + "integrity": "sha512-NdA+2X3lk2GGrMMnTGyYTzM3pn+zNjaqXqlgKmFBXvjfZqzSsKq3pdD1KHZCd5QHN+Fwvoszj0JFsquEVhE1og==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.7.6.tgz", + "integrity": "sha512-rEy6MHKob02t/77YNgr6dREyJ0e0tv1X6Xsg8Z5E7rPXead06zefUbfazj4RELYySWnM38ovZyJAkPx/gOn3VA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rspack/binding-wasm32-wasi": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-wasm32-wasi/-/binding-wasm32-wasi-1.7.6.tgz", + "integrity": "sha512-YupOrz0daSG+YBbCIgpDgzfMM38YpChv+afZpaxx5Ml7xPeAZIIdgWmLHnQ2rts73N2M1NspAiBwV00Xx0N4Vg==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "1.0.7" + } + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.7.6.tgz", + "integrity": "sha512-INj7aVXjBvlZ84kEhSK4kJ484ub0i+BzgnjDWOWM1K+eFYDZjLdAsQSS3fGGXwVc3qKbPIssFfnftATDMTEJHQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.7.6.tgz", + "integrity": "sha512-lXGvC+z67UMcw58In12h8zCa9IyYRmuptUBMItQJzu+M278aMuD1nETyGLL7e4+OZ2lvrnnBIcjXN1hfw2yRzw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.7.6.tgz", + "integrity": "sha512-zeUxEc0ZaPpmaYlCeWcjSJUPuRRySiSHN23oJ2Xyw0jsQ01Qm4OScPdr0RhEOFuK/UE+ANyRtDo4zJsY52Hadw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/core": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.7.6.tgz", + "integrity": "sha512-Iax6UhrfZqJajA778c1d5DBFbSIqPOSrI34kpNIiNpWd8Jq7mFIa+Z60SQb5ZQDZuUxcCZikjz5BxinFjTkg7Q==", + "license": "MIT", + "dependencies": { + "@module-federation/runtime-tools": "0.22.0", + "@rspack/binding": "1.7.6", + "@rspack/lite-tapable": "1.1.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.1" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/lite-tapable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.1.0.tgz", + "integrity": "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw==", + "license": "MIT" + }, + "node_modules/@rspack/plugin-react-refresh": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@rspack/plugin-react-refresh/-/plugin-react-refresh-1.6.1.tgz", + "integrity": "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw==", + "license": "MIT", + "dependencies": { + "error-stack-parser": "^2.1.4", + "html-entities": "^2.6.0" + }, + "peerDependencies": { + "react-refresh": ">=0.10.0 <1.0.0", + "webpack-hot-middleware": "2.x" + }, + "peerDependenciesMeta": { + "webpack-hot-middleware": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/dom-mediacapture-transform": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.11.tgz", + "integrity": "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==", + "license": "MIT", + "dependencies": { + "@types/dom-webcodecs": "*" + } + }, + "node_modules/@types/dom-webcodecs": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.13.tgz", + "integrity": "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ==", + "license": "MIT" + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.14.tgz", + "integrity": "sha512-fOVLPAsFTsQfuCkvahZkzq6nf8KvGWanlYoTh0SVA0A/PIUxQGU2AOZAoD95n2gFLVDW/jP6sbGLny95nmEuHA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001785", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001785.tgz", + "integrity": "sha512-blhOL/WNR+Km1RI/LCVAvA73xplXA7ZbjzI4YkMK9pa6T/P3F2GxjNpEkyw5repTw9IvkyrjyHpwjnhZ5FOvYQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "license": "MIT", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-blank-pseudo": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-5.0.2.tgz", + "integrity": "sha512-aCU4AZ7uEcVSUzagTlA9pHciz7aWPKA/YzrEkpdSopJ2pvhIxiQ5sYeMz1/KByxlIo4XBdvMNJAVKMg/GRnhfw==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-has-pseudo": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-5.0.2.tgz", + "integrity": "sha512-q+U+4QdwwB7T9VEW/LyO6CFrLAeLqOykC5mDqJXc7aKZAhDbq7BvGT13VGJe+IwBfdN2o3Xdw2kJ5IxwV1Sc9Q==", + "license": "CC0-1.0", + "dependencies": { + "@csstools/selector-specificity": "^2.0.1", + "postcss-selector-parser": "^6.0.10", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/css-prefers-color-scheme": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-8.0.2.tgz", + "integrity": "sha512-OvFghizHJ45x7nsJJUSYLyQNTzsCU8yWjxAc/nhPQg1pbs18LMoET8N3kOweFDPy0JV0OSXN2iqRFhPBHYOeMA==", + "license": "CC0-1.0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/cssdb": { + "version": "7.11.2", + "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", + "integrity": "sha512-lhQ32TFkc1X4eTefGfYPvgovRSzIMofHkigfH8nWtyRL4XJLsRhJFreRvEgKzept7x1rjBuy3J/MurXLaFxW/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + } + ], + "license": "CC0-1.0" + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.331", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.331.tgz", + "integrity": "sha512-IbxXrsTlD3hRodkLnbxAPP4OuJYdWCeM3IOdT+CpcMoIwIoDfCmRpEtSPfwBXxVkg9xmBeY7Lz2Eo2TDn/HC3Q==", + "license": "ISC" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz", + "integrity": "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==", + "license": "Unlicense" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "license": "MIT", + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mediabunny": { + "version": "1.39.2", + "resolved": "https://registry.npmjs.org/mediabunny/-/mediabunny-1.39.2.tgz", + "integrity": "sha512-VcrisGRt+OI7tTPrziucJoCIPYIS/DEWY37TqzQVLWSUUHiyvsiRizEypQ3FOlhfIZ4ytAG/Mw4zxfetCTyKUg==", + "license": "MPL-2.0", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@types/dom-mediacapture-transform": "^0.1.11", + "@types/dom-webcodecs": "0.1.13" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/Vanilagy" + } + }, + "node_modules/memfs": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.4.3.tgz", + "integrity": "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg==", + "license": "Unlicense", + "dependencies": { + "fs-monkey": "1.0.3" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-attribute-case-insensitive": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-6.0.3.tgz", + "integrity": "sha512-KHkmCILThWBRtg+Jn1owTnHPnFit4OkqS+eKiGEOPIGke54DCeYGJ6r0Fx/HjfE9M9kznApCLcU0DvnPchazMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-clamp": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", + "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=7.6.0" + }, + "peerDependencies": { + "postcss": "^8.4.6" + } + }, + "node_modules/postcss-color-functional-notation": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-5.1.0.tgz", + "integrity": "sha512-w2R4py6zrVE1U7FwNaAc76tNQlG9GLkrBbcFw+VhUjyDDiV28vfZG+l4LyPmpoQpeSJVtu8VgNjE8Jv5SpC7dQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^2.3.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-hex-alpha": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-9.0.4.tgz", + "integrity": "sha512-XQZm4q4fNFqVCYMGPiBjcqDhuG7Ey2xrl99AnDJMyr5eDASsAGalndVgHZF8i97VFNy1GQeZc4q2ydagGmhelQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/utilities": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-color-rebeccapurple": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-8.0.2.tgz", + "integrity": "sha512-xWf/JmAxVoB5bltHpXk+uGRoGFwu4WDAR7210el+iyvTdqiKpDhtcT8N3edXMoVJY0WHFMrKMUieql/wRNiXkw==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-media": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-9.1.5.tgz", + "integrity": "sha512-GStyWMz7Qbo/Gtw1xVspzVSX8eipgNg4lpsO3CAeY4/A1mzok+RV6MCv3fg62trWijh/lYEj6vps4o8JcBBpDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^1.0.2", + "@csstools/css-parser-algorithms": "^2.2.0", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/media-query-list-parser": "^2.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-properties": { + "version": "13.3.12", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.3.12.tgz", + "integrity": "sha512-oPn/OVqONB2ZLNqN185LDyaVByELAA/u3l2CS2TS16x2j2XsmV4kd8U49+TMxmUsEU9d8fB/I10E6U7kB0L1BA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^1.0.13", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "@csstools/utilities": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-custom-selectors": { + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-7.1.12.tgz", + "integrity": "sha512-ctIoprBMJwByYMGjXG0F7IT2iMF2hnamQ+aWZETyBM0aAlyaYdVZTeUkk8RB+9h9wP+NdN3f01lfvKl2ZSqC0g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^1.0.13", + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1", + "postcss-selector-parser": "^6.1.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dir-pseudo-class": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-7.0.2.tgz", + "integrity": "sha512-cMnslilYxBf9k3qejnovrUONZx1rXeUZJw06fgIUBzABJe3D2LiLL5WAER7Imt3nrkaIgG05XZBztueLEf5P8w==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-double-position-gradients": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-4.0.4.tgz", + "integrity": "sha512-nUAbUXURemLXIrl4Xoia2tiu5z/n8sY+BVDZApoeT9BlpByyrp02P/lFCRrRvZ/zrGRE+MOGLhk8o7VcMCtPtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-progressive-custom-properties": "^2.3.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-visible": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-8.0.2.tgz", + "integrity": "sha512-f/Vd+EC/GaKElknU59esVcRYr/Y3t1ZAQyL4u2xSOgkDy4bMCmG7VP5cGvj3+BTLNE9ETfEuz2nnt4qkZwTTeA==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-focus-within": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-7.0.2.tgz", + "integrity": "sha512-AHAJ89UQBcqBvFgQJE9XasGuwMNkKsGj4D/f9Uk60jFmEBHpAL14DrnSk3Rj+SwZTr/WUG+mh+Rvf8fid/346w==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-font-variant": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", + "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-gap-properties": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-4.0.1.tgz", + "integrity": "sha512-V5OuQGw4lBumPlwHWk/PRfMKjaq/LTGR4WDTemIMCaMevArVfCCA9wBJiL1VjDAd+rzuCIlkRoRvDsSiAaZ4Fg==", + "license": "CC0-1.0", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-image-set-function": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-5.0.2.tgz", + "integrity": "sha512-Sszjwo0ubETX0Fi5MvpYzsONwrsjeabjMoc5YqHvURFItXgIu3HdCjcVuVKGMPGzKRhgaknmdM5uVWInWPJmeg==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-initial": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz", + "integrity": "sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-lab-function": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-5.2.3.tgz", + "integrity": "sha512-fi32AYKzji5/rvgxo5zXHFvAYBw0u0OzELbeCNjEZVLUir18Oj+9RmNphtM8QdLUaUnrfx8zy8vVYLmFLkdmrQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/css-color-parser": "^1.2.0", + "@csstools/css-parser-algorithms": "^2.1.1", + "@csstools/css-tokenizer": "^2.1.1", + "@csstools/postcss-progressive-custom-properties": "^2.3.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", + "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", + "license": "MIT", + "dependencies": { + "cosmiconfig": "^8.3.5", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/postcss-logical": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-6.2.0.tgz", + "integrity": "sha512-aqlfKGaY0nnbgI9jwUikp4gJKBqcH5noU/EdnIVceghaaDPYhZuyJVxlvWNy55tlTG5tunRKCTAX9yljLiFgmw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-nesting": { + "version": "11.3.0", + "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-11.3.0.tgz", + "integrity": "sha512-JlS10AQm/RzyrUGgl5irVkAlZYTJ99mNueUl+Qab+TcHhVedLiylWVkKBhRale+rS9yWIJK48JVzQlq3LcSdeA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/selector-specificity": "^2.0.0", + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-opacity-percentage": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-2.0.0.tgz", + "integrity": "sha512-lyDrCOtntq5Y1JZpBFzIWm2wG9kbEdujpNt4NLannF+J9c8CgFIzPa80YQfdza+Y+yFfzbYj/rfoOsYsooUWTQ==", + "funding": [ + { + "type": "kofi", + "url": "https://ko-fi.com/mrcgrtz" + }, + { + "type": "liberapay", + "url": "https://liberapay.com/mrcgrtz" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.2" + } + }, + "node_modules/postcss-overflow-shorthand": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-4.0.1.tgz", + "integrity": "sha512-HQZ0qi/9iSYHW4w3ogNqVNr2J49DHJAl7r8O2p0Meip38jsdnRPgiDW7r/LlLrrMBMe3KHkvNtAV2UmRVxzLIg==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-page-break": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", + "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8" + } + }, + "node_modules/postcss-place": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-8.0.1.tgz", + "integrity": "sha512-Ow2LedN8sL4pq8ubukO77phSVt4QyCm35ZGCYXKvRFayAwcpgB0sjNJglDoTuRdUL32q/ZC1VkPBo0AOEr4Uiw==", + "license": "CC0-1.0", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-preset-env": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-8.5.1.tgz", + "integrity": "sha512-qhWnJJjP6ArLUINWJ38t6Aftxnv9NW6cXK0NuwcLCcRilbuw72dSFLkCVUJeCfHGgJiKzX+pnhkGiki0PEynWg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "CC0-1.0", + "dependencies": { + "@csstools/postcss-cascade-layers": "^3.0.1", + "@csstools/postcss-color-function": "^2.2.3", + "@csstools/postcss-color-mix-function": "^1.0.3", + "@csstools/postcss-font-format-keywords": "^2.0.2", + "@csstools/postcss-gradients-interpolation-method": "^3.0.6", + "@csstools/postcss-hwb-function": "^2.2.2", + "@csstools/postcss-ic-unit": "^2.0.4", + "@csstools/postcss-is-pseudo-class": "^3.2.1", + "@csstools/postcss-logical-float-and-clear": "^1.0.1", + "@csstools/postcss-logical-resize": "^1.0.1", + "@csstools/postcss-logical-viewport-units": "^1.0.3", + "@csstools/postcss-media-minmax": "^1.0.4", + "@csstools/postcss-media-queries-aspect-ratio-number-values": "^1.0.4", + "@csstools/postcss-nested-calc": "^2.0.2", + "@csstools/postcss-normalize-display-values": "^2.0.1", + "@csstools/postcss-oklab-function": "^2.2.3", + "@csstools/postcss-progressive-custom-properties": "^2.3.0", + "@csstools/postcss-relative-color-syntax": "^1.0.2", + "@csstools/postcss-scope-pseudo-class": "^2.0.2", + "@csstools/postcss-stepped-value-functions": "^2.1.1", + "@csstools/postcss-text-decoration-shorthand": "^2.2.4", + "@csstools/postcss-trigonometric-functions": "^2.1.1", + "@csstools/postcss-unset-value": "^2.0.1", + "autoprefixer": "^10.4.14", + "browserslist": "^4.21.9", + "css-blank-pseudo": "^5.0.2", + "css-has-pseudo": "^5.0.2", + "css-prefers-color-scheme": "^8.0.2", + "cssdb": "^7.6.0", + "postcss-attribute-case-insensitive": "^6.0.2", + "postcss-clamp": "^4.1.0", + "postcss-color-functional-notation": "^5.1.0", + "postcss-color-hex-alpha": "^9.0.2", + "postcss-color-rebeccapurple": "^8.0.2", + "postcss-custom-media": "^9.1.5", + "postcss-custom-properties": "^13.2.0", + "postcss-custom-selectors": "^7.1.3", + "postcss-dir-pseudo-class": "^7.0.2", + "postcss-double-position-gradients": "^4.0.4", + "postcss-focus-visible": "^8.0.2", + "postcss-focus-within": "^7.0.2", + "postcss-font-variant": "^5.0.0", + "postcss-gap-properties": "^4.0.1", + "postcss-image-set-function": "^5.0.2", + "postcss-initial": "^4.0.1", + "postcss-lab-function": "^5.2.3", + "postcss-logical": "^6.2.0", + "postcss-nesting": "^11.3.0", + "postcss-opacity-percentage": "^2.0.0", + "postcss-overflow-shorthand": "^4.0.1", + "postcss-page-break": "^3.0.4", + "postcss-place": "^8.0.1", + "postcss-pseudo-class-any-link": "^8.0.2", + "postcss-replace-overflow-wrap": "^4.0.0", + "postcss-selector-not": "^7.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-pseudo-class-any-link": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-8.0.2.tgz", + "integrity": "sha512-FYTIuRE07jZ2CW8POvctRgArQJ43yxhr5vLmImdKUvjFCkR09kh8pIdlCwdx/jbFm7MiW4QP58L4oOUv3grQYA==", + "license": "CC0-1.0", + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-replace-overflow-wrap": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", + "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", + "license": "MIT", + "peerDependencies": { + "postcss": "^8.0.3" + } + }, + "node_modules/postcss-selector-not": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-7.0.2.tgz", + "integrity": "sha512-/SSxf/90Obye49VZIfc0ls4H0P6i6V1iHv0pzZH8SdgvZOPFkF37ef1r5cyWcMflJSFJ5bfuoluTnFnBBFiuSA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.13" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recast": { + "version": "0.23.11", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.11.tgz", + "integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/remotion": { + "version": "4.0.445", + "resolved": "https://registry.npmjs.org/remotion/-/remotion-4.0.445.tgz", + "integrity": "sha512-SiEueQVsc93G8BAC1U1Ein+9vAKU7/17AgyXmGCADWyywObnRSPC6j7uJmvEDtwsxGWycP3vDV4e+fNd+41rUw==", + "license": "SEE LICENSE IN LICENSE.md", + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.2.tgz", + "integrity": "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.2.tgz", + "integrity": "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz", + "integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "license": "BSD-2-Clause" + }, + "node_modules/webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/plugins/droid-control/remotion/package.json b/plugins/droid-control/remotion/package.json new file mode 100644 index 0000000..a9d1d39 --- /dev/null +++ b/plugins/droid-control/remotion/package.json @@ -0,0 +1,27 @@ +{ + "name": "droid-control-remotion", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "npx remotion studio", + "render": "npx remotion render", + "still": "npx remotion still" + }, + "dependencies": { + "@remotion/cli": "4.0.445", + "@remotion/layout-utils": "4.0.445", + "@remotion/light-leaks": "4.0.445", + "@remotion/media": "4.0.445", + "@remotion/tailwind": "4.0.445", + "@remotion/transitions": "4.0.445", + "react": "^19", + "react-dom": "^19", + "remotion": "4.0.445", + "zod": "4.3.6" + }, + "devDependencies": { + "@types/react": "^19", + "tailwindcss": "^4", + "typescript": "^5.7" + } +} diff --git a/plugins/droid-control/remotion/public/bg-halftone-rotor.jpg b/plugins/droid-control/remotion/public/bg-halftone-rotor.jpg new file mode 100644 index 0000000..d42f772 Binary files /dev/null and b/plugins/droid-control/remotion/public/bg-halftone-rotor.jpg differ diff --git a/plugins/droid-control/remotion/src/Root.tsx b/plugins/droid-control/remotion/src/Root.tsx new file mode 100644 index 0000000..1eb49bb --- /dev/null +++ b/plugins/droid-control/remotion/src/Root.tsx @@ -0,0 +1,36 @@ +import { Composition } from 'remotion'; +import { ShowcaseComposition, showcaseSchema } from './compositions/Showcase'; +import { calculateShowcaseDuration } from './lib/duration'; + +export const RemotionRoot: React.FC = () => { + return ( + <> + { + const fps = 30; + return { + durationInFrames: calculateShowcaseDuration(props, fps), + fps, + width: props.width ?? 1920, + height: props.height ?? 1080, + }; + }} + defaultProps={{ + clips: [], + layout: 'single' as const, + labels: [], + title: 'Demo', + subtitle: '', + preset: 'factory' as const, + keys: [], + effects: [], + width: 1920, + height: 1080, + }} + /> + + ); +}; diff --git a/plugins/droid-control/remotion/src/components/ActiveInputPulse.tsx b/plugins/droid-control/remotion/src/components/ActiveInputPulse.tsx new file mode 100644 index 0000000..3493bfd --- /dev/null +++ b/plugins/droid-control/remotion/src/components/ActiveInputPulse.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { AbsoluteFill, useCurrentFrame, useVideoConfig } from 'remotion'; + +const TARGET_HZ = 1.5; + +export const ActiveInputPulse: React.FC<{ + y?: string; + color?: string; + active?: boolean; +}> = ({ y = '50%', color = '#EE6018', active = true }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + if (!active) { + return null; + } + + // Oscillate opacity between 0.08 and 0.22 at ~1.5 Hz + const omega = (2 * Math.PI * TARGET_HZ) / fps; + const sinValue = Math.sin(frame * omega); + const opacity = 0.15 + sinValue * 0.07; // range: 0.08 .. 0.22 + + return ( + +
+ + ); +}; diff --git a/plugins/droid-control/remotion/src/components/Background.tsx b/plugins/droid-control/remotion/src/components/Background.tsx new file mode 100644 index 0000000..c2f0f3d --- /dev/null +++ b/plugins/droid-control/remotion/src/components/Background.tsx @@ -0,0 +1,126 @@ +import { + AbsoluteFill, + useCurrentFrame, + useVideoConfig, + interpolate, + staticFile, +} from 'remotion'; +import type { Palette } from '../lib/palettes'; +import type { PresetConfig } from '../lib/presets'; + +/** + * Full-bleed background layer. + * + * For warm (Factory) palettes: a radial vignette (dark center, warm + * amber-brown edges) with 4 asymmetric corner glow blobs whose opacity + * intensifies over the video duration — from a mild warm wash (~0.05) to + * a rich reddish-amber bloom (~0.2). + * + * For cool palettes (Catppuccin, etc.): a cool-toned radial or flat fill. + */ +export const Background: React.FC<{ + palette: Palette; + config: PresetConfig; + totalFrames?: number; +}> = ({ palette, config, totalFrames }) => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + const total = totalFrames ?? durationInFrames; + + const isWarm = palette.accent === '#EE6018'; + + if (isWarm) { + // Progressive warmth: 0 → 1 over full duration + const warmth = interpolate(frame, [0, total], [0, 1], { + extrapolateRight: 'clamp', + }); + + // Glow opacity ramps from subtle to prominent + const glowBase = interpolate(warmth, [0, 1], [0.05, 0.2]); + + return ( + + {/* Bottom-left: strongest warm amber glow */} +
+ + {/* Top-right: slightly cooler amber */} +
+ + {/* Bottom-right: deep red accent */} +
+ + {/* Top-left: faint Factory-orange kiss */} +
+ + {/* Halftone texture overlay */} + + + ); + } + + // Cool palettes — radial gradient or flat fill + if (config.bgStyle === 'gradient') { + return ( + + ); + } + + return ; +}; diff --git a/plugins/droid-control/remotion/src/components/BigDroidLogoOutro.tsx b/plugins/droid-control/remotion/src/components/BigDroidLogoOutro.tsx new file mode 100644 index 0000000..5d427a2 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/BigDroidLogoOutro.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; +import type { Palette } from '../lib/palettes'; + +export const BigDroidLogoOutro: React.FC<{ + palette: Palette; +}> = ({ palette }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + // Scale up from 0.8 to 1.0, fade in + const progress = interpolate(frame, [0, 1.5 * fps], [0, 1], { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + + const scale = interpolate(progress, [0, 1], [0.8, 1.0]); + const opacity = progress; + + const asciiLogo = ` + █████████ █████████ ████████ ███ █████████ + ███ ███ ███ ███ ███ ███ ███ ███ ███ + ███ ███ ███ ███ ███ ███ ███ ███ ███ + ███ ███ █████████ ███ ███ ███ ███ ███ + ███ ███ ███ ███ ███ ███ ███ ███ ███ + ███ ███ ███ ███ ███ ███ ███ ███ ███ + █████████ ███ ███ ████████ ███ █████████ + `.trim(); + + return ( + +
+ {asciiLogo} +
+
+ AUTONOMOUS ENGINEERING +
+
+ ); +}; diff --git a/plugins/droid-control/remotion/src/components/ColorGradeOverlay.tsx b/plugins/droid-control/remotion/src/components/ColorGradeOverlay.tsx new file mode 100644 index 0000000..83c870f --- /dev/null +++ b/plugins/droid-control/remotion/src/components/ColorGradeOverlay.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { AbsoluteFill } from 'remotion'; +import type { Palette } from '../lib/palettes'; + +/** + * Global colour-grade post-processing layer — analogous to a DaVinci Resolve + * colour grade applied over the final composite. + * + * - Factory (warm): warm amber tint via `mix-blend-mode: color` + * - Catppuccin (cool): cool blue tint at 75 % of the given intensity + * - A nested radial vignette darkens the corners for a cinematic frame. + * + * Place this as the **last** child in the z-stack so it sits above all + * content, including any noise overlays. + */ +export const ColorGradeOverlay: React.FC<{ + palette: Palette; + intensity?: number; +}> = ({ palette, intensity = 0.04 }) => { + const isWarm = palette.accent === '#EE6018'; + + const gradeColor = isWarm + ? `rgba(200, 120, 40, ${intensity})` + : `rgba(80, 100, 200, ${intensity * 0.75})`; + + return ( + + {/* Colour tint — shifts the entire frame's temperature */} + + + {/* Radial vignette — subtle darkening at the corners */} + + + ); +}; diff --git a/plugins/droid-control/remotion/src/components/DynamicShadowParallax.tsx b/plugins/droid-control/remotion/src/components/DynamicShadowParallax.tsx new file mode 100644 index 0000000..c88e353 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/DynamicShadowParallax.tsx @@ -0,0 +1,23 @@ +import { useCurrentFrame, useVideoConfig, interpolate } from 'remotion'; + +/** + * Provides subtle shadow-offset parallax that shifts over the video duration, + * creating a subliminal sense of depth as if the light source drifts with a + * slow Ken Burns camera move. + * + * offsetX: 0 → 2 px (horizontal drift) + * offsetY: 8 → 10 px (vertical drift deepens) + */ +export function useShadowParallax(): { offsetX: number; offsetY: number } { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + + const offsetX = interpolate(frame, [0, durationInFrames], [0, 2], { + extrapolateRight: 'clamp', + }); + const offsetY = interpolate(frame, [0, durationInFrames], [8, 10], { + extrapolateRight: 'clamp', + }); + + return { offsetX, offsetY }; +} diff --git a/plugins/droid-control/remotion/src/components/FanningRotorOutro.tsx b/plugins/droid-control/remotion/src/components/FanningRotorOutro.tsx new file mode 100644 index 0000000..91f518a --- /dev/null +++ b/plugins/droid-control/remotion/src/components/FanningRotorOutro.tsx @@ -0,0 +1,144 @@ +import React from 'react'; +import { AbsoluteFill, useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; +import type { Palette } from '../lib/palettes'; +import { RotorMark } from './RotorMark'; + +export const FanningRotorOutro: React.FC<{ + palette: Palette; +}> = ({ palette }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + // Phase 1: Fan out the 8 triangles (0s to 1.5s) + const fanDuration = 1.5 * fps; + // Phase 2: Crossfade to ASCII Droid (1.5s to 2.5s) + const fadeDuration = 1.0 * fps; + + // 8 overlapping wedges that form the RotorMark logo + const wedges = Array.from({ length: 8 }).map((_, i) => { + // 1. Scale up as a single stacked blade (frames 0 to 12) + const scaleProgress = interpolate(frame, [0, 12], [0, 1], { + easing: Easing.out(Easing.back(1.5)), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + + // 2. Fan out from the single blade (frames 12 to fanDuration) + const rotationProgress = interpolate(frame, [12, fanDuration], [0, i * 45], { + easing: Easing.out(Easing.cubic), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + + // Glitch effect: flash opacity slightly during the fanning phase + const isGlitching = frame > 12 && frame < 24 && frame % 3 === 0; + const baseOpacity = interpolate(frame, [0, 12], [0, 1], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + const opacity = isGlitching ? 0.5 : baseOpacity; + + return ( +
+ +
+ ); + }); + + // Crossfade between the rotor triangles and the ASCII logo + const wedgesOpacity = interpolate(frame, [fanDuration, fanDuration + fadeDuration], [1, 0], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + + const asciiOpacity = interpolate(frame, [fanDuration, fanDuration + fadeDuration], [0, 1], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + + const asciiLogo = ` + █████████ █████████ ████████ ███ █████████ + ███ ███ ███ ███ ███ ███ ███ ███ ███ + ███ ███ ███ ███ ███ ███ ███ ███ ███ + ███ ███ █████████ ███ ███ ███ ███ ███ + ███ ███ ███ ███ ███ ███ ███ ███ ███ + ███ ███ ███ ███ ███ ███ ███ ███ ███ + █████████ ███ ███ ████████ ███ █████████ + `.trim(); + + return ( + + {/* The fanning rotor layer */} +
+ {wedges} +
+ + {/* The ASCII Droid layer */} +
+
+ {asciiLogo} +
+
+ AUTONOMOUS ENGINEERING +
+
+
+ ); +}; diff --git a/plugins/droid-control/remotion/src/components/FloatingParticles.tsx b/plugins/droid-control/remotion/src/components/FloatingParticles.tsx new file mode 100644 index 0000000..89ab1d7 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/FloatingParticles.tsx @@ -0,0 +1,58 @@ +import React, { useMemo } from 'react'; +import { AbsoluteFill, useCurrentFrame } from 'remotion'; + +interface ParticleSeed { + baseX: number; + baseY: number; + offsetX: number; + offsetY: number; + size: number; +} + +export const FloatingParticles: React.FC<{ + count?: number; + color?: string; + opacity?: number; +}> = ({ count = 30, color = '#EE6018', opacity = 0.07 }) => { + const frame = useCurrentFrame(); + + // Deterministic particle seeds — stable across frames, varies only by count + const seeds = useMemo( + () => + Array.from({ length: count }, (_, i) => ({ + baseX: (i * 73 + 17) % 100, + baseY: (i * 47 + 31) % 100, + offsetX: (i * 13 + 7) * 0.1, + offsetY: (i * 19 + 11) * 0.1, + size: 2 + (i % 3) * 2, // 2, 4, or 6px + })), + [count] + ); + + return ( + + {seeds.map((p, i) => { + // Slow Lissajous paths driven by frame count + const dx = Math.sin(frame * 0.008 + p.offsetX) * 60; + const dy = Math.cos(frame * 0.006 + p.offsetY) * 40; + + return ( +
+ ); + })} + + ); +}; diff --git a/plugins/droid-control/remotion/src/components/GlitchTitle.tsx b/plugins/droid-control/remotion/src/components/GlitchTitle.tsx new file mode 100644 index 0000000..9376167 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/GlitchTitle.tsx @@ -0,0 +1,233 @@ +import React, { useMemo } from 'react'; +import { + AbsoluteFill, + useCurrentFrame, + useVideoConfig, + interpolate, + Easing, +} from 'remotion'; +import type { Palette } from '../lib/palettes'; + +const BLOCK_SIZE = 12; +const GRID_WIDTH = 800; +const GRID_HEIGHT = 120; + +interface PixelBlock { + /** Grid-assembled position (top-left corner) */ + x: number; + y: number; + /** Scattered offset from assembled position */ + scatterDx: number; + scatterDy: number; + /** true = accent color, false = text color */ + isAccent: boolean; +} + +/** + * Deterministic "garbled" subtitle: replace ~30% of characters with random ASCII, + * seeded by character index. + */ +function garbleText(text: string): string { + const chars: string[] = []; + for (let i = 0; i < text.length; i++) { + const shouldGarble = (i * 17 + 3) % 10 < 3; // ~30% + if (shouldGarble && text[i] !== ' ') { + const code = 33 + ((i * 17) % 94); + chars.push(String.fromCharCode(code)); + } else { + chars.push(text[i]); + } + } + return chars.join(''); +} + +/** + * Build a deterministic grid of pixel blocks that represent the title area. + * Each block gets a fixed scatter target computed from its index. + */ +function buildBlockGrid(): PixelBlock[] { + const cols = Math.floor(GRID_WIDTH / BLOCK_SIZE); + const rows = Math.floor(GRID_HEIGHT / BLOCK_SIZE); + const blocks: PixelBlock[] = []; + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + const i = row * cols + col; + blocks.push({ + x: col * BLOCK_SIZE, + y: row * BLOCK_SIZE, + scatterDx: Math.sin(i * 7.3 + 3.1) * 200, + scatterDy: Math.cos(i * 5.7 + 1.4) * 150, + isAccent: (i * 13 + 7) % 5 === 0, // ~20% accent + }); + } + } + + return blocks; +} + +export const GlitchTitle: React.FC<{ + title: string; + subtitle?: string; + palette: Palette; +}> = ({ title, subtitle, palette }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const blocks = useMemo(buildBlockGrid, []); + const garbled = useMemo( + () => (subtitle ? garbleText(subtitle) : undefined), + [subtitle] + ); + + // Phase boundaries in frames + const scatterEnd = Math.round(0.5 * fps); // ~15 frames at 30fps + const assembleEnd = Math.round(1.5 * fps); // ~45 frames at 30fps + + // --- Garbled subtitle fade-in (during scatter phase) --- + const garbledOpacity = interpolate(frame, [0, scatterEnd], [0, 1], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + + // --- Title text fade-in (after assembly completes) --- + const titleOpacity = interpolate( + frame, + [assembleEnd - Math.round(0.2 * fps), assembleEnd], + [0, 1], + { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + } + ); + + return ( + + {/* Garbled subtitle line above the pixel grid */} + {garbled && ( +
+ {garbled} +
+ )} + + {/* Pixel block grid container */} +
+ {/* Rendered pixel blocks */} + {/* Hoisted outside .map() — same value for every block */} + {(() => { + const scatterOpacity = interpolate(frame, [0, scatterEnd], [0, 1], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + const assembleProgress = interpolate( + frame, + [scatterEnd, assembleEnd], + [0, 1], + { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + } + ); + return blocks.map((block, i) => { + // Phase (c): subtle jitter when assembled + const jitterX = + frame >= assembleEnd ? Math.sin(frame * 0.1 + i) * 1.5 : 0; + const jitterY = + frame >= assembleEnd ? Math.cos(frame * 0.13 + i * 0.7) * 1.5 : 0; + + // Interpolate from scatter position to grid position + const dx = interpolate( + assembleProgress, + [0, 1], + [block.scatterDx, 0] + ); + const dy = interpolate( + assembleProgress, + [0, 1], + [block.scatterDy, 0] + ); + + const opacity = frame < scatterEnd ? scatterOpacity : 1; + const x = block.x + dx + jitterX; + const y = block.y + dy + jitterY; + + return ( +
+ ); + }); + })()} + + {/* Title text rendered on top of assembled blocks */} +
+ {title} +
+
+ + {/* Scanline overlay */} +
+ + ); +}; diff --git a/plugins/droid-control/remotion/src/components/GlowLines.tsx b/plugins/droid-control/remotion/src/components/GlowLines.tsx new file mode 100644 index 0000000..516ce02 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/GlowLines.tsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { + AbsoluteFill, + useCurrentFrame, + useVideoConfig, + interpolate, +} from 'remotion'; + +interface GlowEvent { + t: number; + y: string; // percentage within terminal content area + dur?: number; +} + +const DEFAULT_DURATION = 0.95; // seconds + +/** + * Renders a single glow bar for a content event. + * Fades in over 0.15s, holds for 0.3s, fades out over 0.5s. + */ +const GlowBar: React.FC<{ + event: GlowEvent; + color: string; + fps: number; + frame: number; +}> = ({ event, color, fps, frame }) => { + const totalDur = event.dur ?? DEFAULT_DURATION; + const startFrame = Math.round(event.t * fps); + + // Phase durations in frames + const fadeInFrames = Math.round(0.15 * fps); + const holdFrames = Math.round(0.3 * fps); + const fadeOutFrames = Math.max(0, Math.round((totalDur - 0.15 - 0.3) * fps)); + const endFrame = startFrame + fadeInFrames + holdFrames + fadeOutFrames; + + // Outside active range → don't render + if (frame < startFrame || frame > endFrame) { + return null; + } + + const fadeInEnd = startFrame + fadeInFrames; + const holdEnd = fadeInEnd + holdFrames; + + let opacity: number; + if (frame <= fadeInEnd) { + // Fade in + opacity = interpolate(frame, [startFrame, fadeInEnd], [0, 1], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + } else if (frame <= holdEnd) { + // Hold at full + opacity = 1; + } else { + // Fade out + opacity = interpolate(frame, [holdEnd, endFrame], [1, 0], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + } + + return ( +
+ ); +}; + +export const GlowLines: React.FC<{ + events: GlowEvent[]; + color?: string; +}> = ({ events, color = '#EE6018' }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + return ( + + {events.map((event, i) => ( + + ))} + + ); +}; diff --git a/plugins/droid-control/remotion/src/components/KeystrokeOverlay.tsx b/plugins/droid-control/remotion/src/components/KeystrokeOverlay.tsx new file mode 100644 index 0000000..ee776e8 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/KeystrokeOverlay.tsx @@ -0,0 +1,99 @@ +import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; +import type { Keystroke } from '../lib/schema'; +import type { Palette } from '../lib/palettes'; +import type { PresetConfig } from '../lib/presets'; + +const KeystrokePill: React.FC<{ + label: string; + palette: Palette; + config: PresetConfig; + enterFrame: number; + exitFrame: number; +}> = ({ label, palette, config, enterFrame, exitFrame }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const isVisible = frame >= enterFrame && frame < exitFrame; + if (!isVisible) return null; + + const localFrame = frame - enterFrame; + + // Pop-in animation over 0.2s + const enterProgress = interpolate(localFrame, [0, 0.2 * fps], [0, 1], { + easing: Easing.bezier(0.34, 1.56, 0.64, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + + // Fade out over last 0.15s + const totalFrames = exitFrame - enterFrame; + const fadeOutStart = totalFrames - 0.15 * fps; + const exitOpacity = interpolate( + localFrame, + [fadeOutStart, totalFrames], + [1, 0], + { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + } + ); + + const scale = interpolate(enterProgress, [0, 1], [0.8, 1]); + const enterOpacity = enterProgress; + + return ( +
+ {label} +
+ ); +}; + +export const KeystrokeOverlay: React.FC<{ + keys: Keystroke[]; + palette: Palette; + config: PresetConfig; +}> = ({ keys, palette, config }) => { + const { fps } = useVideoConfig(); + const defaultDur = 1.2; + + return ( + <> + {keys.map((key, i) => { + const enterFrame = Math.round(key.t * fps); + const naturalExit = Math.round((key.t + (key.dur ?? defaultDur)) * fps); + const nextStart = + i + 1 < keys.length ? Math.round(keys[i + 1].t * fps) : Infinity; + const exitFrame = Math.min(naturalExit, nextStart); + + return ( + + ); + })} + + ); +}; diff --git a/plugins/droid-control/remotion/src/components/MotionBlurTransition.tsx b/plugins/droid-control/remotion/src/components/MotionBlurTransition.tsx new file mode 100644 index 0000000..8b89b22 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/MotionBlurTransition.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { AbsoluteFill, interpolate } from 'remotion'; +import type { + TransitionPresentation, + TransitionPresentationComponentProps, +} from '@remotion/transitions'; + +export type MotionBlurProps = { + /** Peak blur radius in pixels (default 6). */ + maxBlur?: number; + /** Initial scale of the entering scene (default 1.03). */ + enterScale?: number; +}; + +/** + * Custom presentation component for @remotion/transitions. + * + * Combines three effects during the crossfade: + * (a) CSS filter: blur() — 6 px at the edges, 0 px at the center + * (b) scale 1.03 → 1.0 on the entering scene (camera-dolly feel) + * (c) opacity crossfade + */ +const MotionBlurPresentation: React.FC< + TransitionPresentationComponentProps +> = ({ + children, + presentationDirection, + presentationProgress, + passedProps, +}) => { + const maxBlur = passedProps.maxBlur ?? 6; + const enterScale = passedProps.enterScale ?? 1.03; + const isEntering = presentationDirection === 'entering'; + + let style: React.CSSProperties; + if (isEntering) { + const blur = interpolate(presentationProgress, [0, 1], [maxBlur, 0], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + const scale = interpolate(presentationProgress, [0, 1], [enterScale, 1], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + style = { + opacity: presentationProgress, + filter: `blur(${blur}px)`, + transform: `scale(${scale})`, + }; + } else { + const blur = interpolate(presentationProgress, [0, 1], [0, maxBlur], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + style = { + opacity: 1 - presentationProgress, + filter: `blur(${blur}px)`, + }; + } + + return {children}; +}; + +/** + * Factory function returning a TransitionPresentation compatible with + * ``. + */ +export const motionBlurTransition = ( + props?: MotionBlurProps +): TransitionPresentation => { + return { + component: MotionBlurPresentation, + props: props ?? {}, + }; +}; diff --git a/plugins/droid-control/remotion/src/components/NoiseOverlay.tsx b/plugins/droid-control/remotion/src/components/NoiseOverlay.tsx new file mode 100644 index 0000000..5cb5608 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/NoiseOverlay.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { AbsoluteFill } from 'remotion'; + +export const NoiseOverlay: React.FC<{ + opacity?: number; +}> = ({ opacity = 0.03 }) => { + return ( + + + + + + + + + ); +}; diff --git a/plugins/droid-control/remotion/src/components/RotorMark.tsx b/plugins/droid-control/remotion/src/components/RotorMark.tsx new file mode 100644 index 0000000..6c30e64 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/RotorMark.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +const ROTOR_PATH = + 'M396.406 180.565C395.606 180.367 394.858 179.997 394.213 179.483C393.568 178.968 393.041 178.321 392.671 177.585C392.299 176.848 392.092 176.04 392.061 175.216C392.031 174.391 392.179 173.571 392.496 172.809C403.427 146.207 408.25 124.922 400.466 116.012C379.851 92.371 297.179 139.382 270.819 155.303C270.113 155.728 269.324 155.995 268.505 156.086C267.687 156.178 266.858 156.091 266.076 155.833C265.294 155.575 264.576 155.152 263.973 154.591C263.369 154.031 262.894 153.346 262.579 152.586C251.499 126.04 239.85 107.576 228.044 106.775C196.75 104.634 171.526 196.337 164.142 226.226C163.945 227.026 163.577 227.775 163.064 228.419C162.55 229.065 161.903 229.591 161.167 229.962C160.432 230.334 159.625 230.541 158.801 230.571C157.977 230.602 157.157 230.454 156.396 230.137C129.794 219.206 108.499 214.383 99.5981 222.167C75.9574 242.782 122.96 325.454 138.881 351.814C139.307 352.519 139.575 353.308 139.667 354.128C139.759 354.946 139.674 355.776 139.416 356.558C139.158 357.341 138.733 358.059 138.172 358.662C137.61 359.266 136.925 359.74 136.163 360.053C109.626 371.133 91.1623 382.782 90.3522 394.588C88.22 425.882 179.915 451.107 209.813 458.491C210.612 458.69 211.358 459.059 212.001 459.573C212.644 460.087 213.169 460.735 213.538 461.47C213.909 462.205 214.117 463.011 214.146 463.834C214.177 464.656 214.03 465.477 213.714 466.236C202.783 492.838 197.96 514.132 205.744 523.034C226.359 546.675 309.041 499.672 335.4 483.751C336.106 483.325 336.896 483.057 337.715 482.965C338.534 482.872 339.363 482.958 340.145 483.217C340.928 483.475 341.645 483.899 342.249 484.461C342.852 485.022 343.327 485.708 343.641 486.469C354.721 513.006 366.36 531.471 378.175 532.281C409.47 534.413 434.693 442.718 442.068 412.82C442.267 412.02 442.637 411.272 443.152 410.629C443.666 409.985 444.314 409.46 445.051 409.089C445.787 408.719 446.595 408.512 447.419 408.483C448.243 408.453 449.063 408.601 449.824 408.919C476.425 419.85 497.711 424.663 506.621 416.888C530.262 396.273 483.25 313.591 467.329 287.232C466.906 286.526 466.64 285.736 466.55 284.919C466.459 284.1 466.546 283.272 466.804 282.49C467.062 281.708 467.485 280.991 468.045 280.388C468.604 279.785 469.288 279.308 470.047 278.992C496.593 267.912 515.057 256.263 515.858 244.457C517.999 213.162 426.295 187.94 396.406 180.565ZM360.503 150.564C366.518 161.346 335.521 233.191 312.467 283.443C312.082 284.283 311.449 284.985 310.652 285.454C309.856 285.923 308.934 286.138 308.012 286.068C307.09 285.998 306.211 285.647 305.495 285.063C304.778 284.478 304.258 283.689 304.003 282.8C294.692 250.127 284.05 211.739 272.663 179.15C272.216 177.871 272.238 176.474 272.726 175.21C273.214 173.946 274.136 172.897 275.325 172.25C303.761 156.719 352.421 136.095 360.503 150.564ZM224.226 159.456C236.098 162.827 264.981 235.547 284.208 287.382C284.529 288.247 284.577 289.191 284.346 290.085C284.114 290.979 283.615 291.781 282.915 292.383C282.214 292.986 281.346 293.359 280.427 293.453C279.508 293.548 278.583 293.359 277.774 292.912C248.063 276.422 213.416 256.776 182.317 241.785C181.1 241.194 180.131 240.191 179.584 238.953C179.036 237.715 178.946 236.323 179.329 235.025C188.481 203.964 208.277 154.94 224.226 159.456ZM134.151 262.11C144.924 256.095 216.778 287.093 267.02 310.147C267.861 310.532 268.563 311.166 269.032 311.963C269.501 312.759 269.716 313.681 269.646 314.602C269.575 315.524 269.225 316.402 268.64 317.119C268.056 317.835 267.267 318.357 266.378 318.611C233.714 327.922 195.316 338.564 162.727 349.952C161.45 350.396 160.055 350.373 158.793 349.885C157.531 349.397 156.483 348.477 155.837 347.288C140.334 318.852 119.673 270.192 134.151 262.11ZM143.043 398.388C146.405 386.516 219.133 357.632 270.968 338.405C271.834 338.085 272.778 338.037 273.672 338.268C274.567 338.5 275.368 338.999 275.971 339.699C276.572 340.4 276.947 341.268 277.041 342.186C277.135 343.105 276.946 344.031 276.499 344.84C260 374.552 240.353 409.198 225.362 440.287C224.777 441.509 223.774 442.482 222.535 443.031C221.296 443.581 219.902 443.671 218.603 443.286C187.541 434.189 138.518 414.338 143.043 398.388ZM245.698 488.463C239.673 477.69 270.679 405.837 293.733 355.594C294.119 354.753 294.753 354.051 295.549 353.582C296.346 353.113 297.267 352.899 298.189 352.969C299.111 353.038 299.99 353.389 300.706 353.973C301.422 354.558 301.943 355.348 302.197 356.237C311.508 388.9 322.151 427.299 333.538 459.887C333.982 461.166 333.957 462.56 333.468 463.823C332.979 465.085 332.056 466.132 330.866 466.777C302.439 482.28 253.77 502.941 245.726 488.463H245.698ZM381.974 479.57C370.093 476.209 341.21 403.481 321.983 351.646C321.661 350.779 321.612 349.833 321.843 348.937C322.074 348.041 322.575 347.238 323.277 346.635C323.979 346.032 324.85 345.659 325.77 345.566C326.691 345.473 327.618 345.665 328.426 346.115C358.129 362.605 392.784 382.261 423.874 397.252C425.095 397.839 426.065 398.842 426.613 400.081C427.161 401.32 427.249 402.713 426.863 404.012C417.719 435.12 397.924 484.095 381.974 479.57ZM472.049 376.916C461.267 382.94 389.423 351.934 339.171 328.88C338.331 328.494 337.628 327.861 337.159 327.064C336.69 326.268 336.477 325.346 336.547 324.425C336.616 323.503 336.966 322.625 337.551 321.908C338.135 321.191 338.925 320.671 339.814 320.417C372.486 311.106 410.876 300.463 443.464 289.075C444.745 288.631 446.141 288.655 447.405 289.145C448.668 289.635 449.717 290.558 450.364 291.748C465.857 320.175 486.519 368.843 472.049 376.916ZM463.157 240.639C459.787 252.52 387.067 281.404 335.233 300.631C334.365 300.953 333.42 301.002 332.524 300.771C331.628 300.539 330.825 300.039 330.222 299.337C329.619 298.635 329.247 297.764 329.154 296.843C329.06 295.923 329.251 294.996 329.702 294.187C346.192 264.485 365.838 229.829 380.829 198.739C381.418 197.521 382.421 196.551 383.66 196.004C384.898 195.456 386.291 195.366 387.589 195.751C418.65 204.894 467.673 224.689 463.157 240.639Z'; + +export const RotorMark: React.FC<{ + size?: number; + width?: number; + height?: number; + color?: string; + style?: React.CSSProperties; +}> = ({ size = 24, width, height, color = 'white', style }) => ( + + + +); diff --git a/plugins/droid-control/remotion/src/components/SectionHeader.tsx b/plugins/droid-control/remotion/src/components/SectionHeader.tsx new file mode 100644 index 0000000..689c9c0 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/SectionHeader.tsx @@ -0,0 +1,108 @@ +import React from 'react'; +import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; +import type { Section } from '../lib/schema'; +import type { Palette } from '../lib/palettes'; +import type { PresetConfig } from '../lib/presets'; + +const SectionTitle: React.FC<{ + title: string; + palette: Palette; + config: PresetConfig; + enterFrame: number; + exitFrame: number; +}> = ({ title, palette, config, enterFrame, exitFrame }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const isVisible = frame >= enterFrame && frame < exitFrame; + if (!isVisible) return null; + + const localFrame = frame - enterFrame; + + // Slide down & fade in + const enterProgress = interpolate(localFrame, [0, 0.4 * fps], [0, 1], { + easing: Easing.bezier(0.22, 1, 0.36, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + + const totalFrames = exitFrame - enterFrame; + const fadeOutStart = totalFrames - 0.3 * fps; + const exitProgress = interpolate( + localFrame, + [fadeOutStart, totalFrames], + [0, 1], + { + easing: Easing.bezier(0.32, 0, 0.67, 0), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + } + ); + + const opacity = + interpolate(enterProgress, [0, 1], [0, 1]) * + interpolate(exitProgress, [0, 1], [1, 0]); + const translateY = + interpolate(enterProgress, [0, 1], [-20, 0]) - + interpolate(exitProgress, [0, 1], [0, -20]); + + return ( +
+ {title} +
+ ); +}; + +export const SectionHeaderOverlay: React.FC<{ + sections: Section[]; + palette: Palette; + config: PresetConfig; +}> = ({ sections, palette, config }) => { + const { fps, durationInFrames } = useVideoConfig(); + + return ( + <> + {sections.map((sec, i) => { + const enterFrame = Math.round(sec.t * fps); + const nextStart = + i + 1 < sections.length + ? Math.round(sections[i + 1].t * fps) + : durationInFrames; + const exitFrame = Math.min(nextStart, durationInFrames); + + return ( + + ); + })} + + ); +}; diff --git a/plugins/droid-control/remotion/src/components/SectionTransition.tsx b/plugins/droid-control/remotion/src/components/SectionTransition.tsx new file mode 100644 index 0000000..4bd1fcc --- /dev/null +++ b/plugins/droid-control/remotion/src/components/SectionTransition.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { + AbsoluteFill, + useCurrentFrame, + useVideoConfig, + interpolate, + Easing, +} from 'remotion'; +import type { Section } from '../lib/schema'; + +const SWEEP_DURATION_S = 0.5; +const BLUR_PX = 28; + +const FrostedSweep: React.FC<{ + triggerFrame: number; +}> = ({ triggerFrame }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const sweepFrames = Math.round(SWEEP_DURATION_S * fps); + const endFrame = triggerFrame + sweepFrames; + + if (frame < triggerFrame || frame > endFrame) return null; + + const localFrame = frame - triggerFrame; + + // Band sweeps from left (-10%) to right (110%) + const bandX = interpolate(localFrame, [0, sweepFrames], [-10, 110], { + easing: Easing.bezier(0.4, 0, 0.2, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + + // Opacity: quick fade in, hold, quick fade out + const opacity = interpolate( + localFrame, + [0, sweepFrames * 0.15, sweepFrames * 0.85, sweepFrames], + [0, 1, 1, 0], + { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' }, + ); + + return ( + +
+ + ); +}; + +export const SectionTransitionOverlay: React.FC<{ + sections: Section[]; +}> = ({ sections }) => { + const { fps } = useVideoConfig(); + + // Skip the first section -- it marks the start, not a transition point + return ( + <> + {sections.slice(1).map((sec) => { + const triggerFrame = Math.round(sec.t * fps); + return ( + + ); + })} + + ); +}; diff --git a/plugins/droid-control/remotion/src/components/SpotlightOverlay.tsx b/plugins/droid-control/remotion/src/components/SpotlightOverlay.tsx new file mode 100644 index 0000000..b54ee55 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/SpotlightOverlay.tsx @@ -0,0 +1,77 @@ +import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; + +interface Region { + x: string; + y: string; + w: string; + h: string; +} + +function pctToNum(val: string): number { + return parseFloat(val.replace('%', '')) / 100; +} + +export const SpotlightOverlay: React.FC<{ + startTime: number; + duration: number; + region: Region; + dim?: number; +}> = ({ startTime, duration, region, dim = 0.6 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const startFrame = startTime * fps; + const endFrame = (startTime + duration) * fps; + + if (frame < startFrame || frame > endFrame) return null; + + const localFrame = frame - startFrame; + const totalFrames = endFrame - startFrame; + + // Fade in over 0.3s, fade out over 0.3s + const fadeIn = interpolate(localFrame, [0, 0.3 * fps], [0, 1], { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + const fadeOut = interpolate( + localFrame, + [totalFrames - 0.3 * fps, totalFrames], + [1, 0], + { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + } + ); + const opacity = Math.min(fadeIn, fadeOut); + + const rx = pctToNum(region.x); + const ry = pctToNum(region.y); + const rw = pctToNum(region.w); + const rh = pctToNum(region.h); + + // SVG mask: white everywhere except a black (transparent) cutout for the spotlight region. + // Use only the standard `maskImage` property (Chrome uses it in headless rendering). + const svgMask = `url("data:image/svg+xml,${encodeURIComponent( + `` + + `` + + `` + + `` + )}")`; + + return ( +
+ ); +}; diff --git a/plugins/droid-control/remotion/src/components/StaggeredPanelEntrance.tsx b/plugins/droid-control/remotion/src/components/StaggeredPanelEntrance.tsx new file mode 100644 index 0000000..3701207 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/StaggeredPanelEntrance.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; + +export const StaggeredPanelEntrance: React.FC<{ + delay?: number; + children: React.ReactNode; +}> = ({ delay = 0, children }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const startFrame = delay * fps; + const progress = interpolate( + frame, + [startFrame, startFrame + 0.5 * fps], + [0, 1], + { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + } + ); + + const scale = interpolate(progress, [0, 1], [0.92, 1]); + const opacity = interpolate(progress, [0, 1], [0, 1]); + const translateY = interpolate(progress, [0, 1], [12, 0]); + + return ( +
+ {children} +
+ ); +}; diff --git a/plugins/droid-control/remotion/src/components/TitleCard.tsx b/plugins/droid-control/remotion/src/components/TitleCard.tsx new file mode 100644 index 0000000..cac9bce --- /dev/null +++ b/plugins/droid-control/remotion/src/components/TitleCard.tsx @@ -0,0 +1,260 @@ +import { + AbsoluteFill, + useCurrentFrame, + useVideoConfig, + interpolate, + Easing, +} from 'remotion'; +import { fitText } from '@remotion/layout-utils'; +import type { Palette } from '../lib/palettes'; +import { RotorMark } from './RotorMark'; +import { BreathingZoom } from './ZoomEffect'; + +const TITLE_MAX_FONT = 64; +const SUBTITLE_MAX_FONT = 24; +const TITLE_FONT_FAMILY = "'Geist', system-ui, sans-serif"; + +export const TitleCard: React.FC<{ + title: string; + subtitle: string; + palette: Palette; + speedNote?: string; +}> = ({ title, subtitle, palette, speedNote }) => { + const frame = useCurrentFrame(); + const { fps, width } = useVideoConfig(); + + const titleFontSize = Math.min( + TITLE_MAX_FONT, + fitText({ + text: title, + withinWidth: width * 0.8, + fontFamily: TITLE_FONT_FAMILY, + fontWeight: 700, + letterSpacing: '-0.04em', + }).fontSize, + ); + + const subtitleFontSize = Math.min( + SUBTITLE_MAX_FONT, + fitText({ + text: subtitle || ' ', + withinWidth: width * 0.7, + fontFamily: TITLE_FONT_FAMILY, + fontWeight: 400, + }).fontSize, + ); + + // Tagline fades in first (monospace label above title) + const taglineProgress = interpolate(frame, [0, 0.4 * fps], [0, 1], { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + const taglineY = interpolate(taglineProgress, [0, 1], [15, 0]); + const taglineOpacity = taglineProgress; + + // Title: scale 1.08→1.0 + translateY 30→0 + fade in + const titleProgress = interpolate(frame, [0, 0.6 * fps], [0, 1], { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + const titleY = interpolate(titleProgress, [0, 1], [30, 0]); + const titleScale = interpolate(titleProgress, [0, 1], [1.08, 1.0]); + const titleOpacity = titleProgress; + + // Subtitle follows with a slight delay + const subtitleProgress = interpolate( + frame, + [0.15 * fps, 0.75 * fps], + [0, 1], + { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + } + ); + const subtitleY = interpolate(subtitleProgress, [0, 1], [20, 0]); + const subtitleOpacity = subtitleProgress; + + const isFactory = palette.accent === '#EE6018'; + + // --- Factory preset: spinning rotor leads the accent line --- + const SWEEP_WIDTH = 280; + const ROTOR_SIZE = 28; + const sweepStart = 0.15 * fps; + const sweepEnd = 0.7 * fps; + + // Rotor horizontal progress: 0 → SWEEP_WIDTH + const rotorProgress = interpolate(frame, [sweepStart, sweepEnd], [0, 1], { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + const rotorX = rotorProgress * SWEEP_WIDTH; + + // Continuous spin: ties to progress so it rolls naturally + const rotorRotation = interpolate(rotorProgress, [0, 1], [0, 720]); + + // Rotor fades in at the start, fades out after reaching the end + const rotorOpacity = interpolate( + frame, + [sweepStart, sweepStart + 0.08 * fps, sweepEnd, sweepEnd + 0.25 * fps], + [0, 1, 1, 0], + { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } + ); + + // Trail line grows to follow the rotor + const trailWidth = rotorX; + const trailOpacity = interpolate( + frame, + [sweepStart, sweepStart + 0.1 * fps], + [0, 1], + { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' } + ); + + // --- Non-factory: original simple line animation --- + const lineWidth = interpolate(frame, [0.2 * fps, 0.7 * fps], [0, 120], { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + const sweepPosition = interpolate( + frame, + [0.2 * fps, 0.2 * fps + 0.8 * fps], + [-200, 100], + { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + } + ); + + return ( + + + {/* Monospace tagline above title (shows speedNote if present) */} + {speedNote && ( +
+ {speedNote} +
+ )} + + {/* Title with glow, scale entrance, and accent text-shadow */} +
+ {title} +
+ + {/* Accent line — factory: spinning rotor with trailing line; other: gradient sweep */} + {isFactory ? ( +
+ {/* Trailing line: grows from left edge toward the rotor */} +
+ + {/* Spinning rotor at the leading edge */} +
+ +
+
+ ) : ( +
+ )} + +
+ {subtitle} +
+ + + ); +}; diff --git a/plugins/droid-control/remotion/src/components/Watermark.tsx b/plugins/droid-control/remotion/src/components/Watermark.tsx new file mode 100644 index 0000000..051bdc3 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/Watermark.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { + AbsoluteFill, + useCurrentFrame, + useVideoConfig, + interpolate, +} from 'remotion'; +import { RotorMark } from './RotorMark'; + +export const Watermark: React.FC<{ + opacity?: number; + size?: number; +}> = ({ opacity = 0.2, size = 48 }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const fadeIn = interpolate(frame, [0, fps], [0, opacity], { + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + + return ( + +
+ +
+
+ ); +}; diff --git a/plugins/droid-control/remotion/src/components/WindowChrome.tsx b/plugins/droid-control/remotion/src/components/WindowChrome.tsx new file mode 100644 index 0000000..597a4e5 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/WindowChrome.tsx @@ -0,0 +1,145 @@ +import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; +import type { Palette } from '../lib/palettes'; +import type { PresetConfig } from '../lib/presets'; + +const TrafficLight: React.FC<{ + color: string; + cx: number; +}> = ({ color, cx }) => ; + +const TrafficLightRing: React.FC<{ + color: string; + cx: number; +}> = ({ color, cx }) => ( + +); + +const WindowBar: React.FC<{ + config: PresetConfig; + palette: Palette; + width: number; + title?: string; +}> = ({ config, palette, width, title }) => { + if (config.bar === 'none') return null; + + const Dot = config.bar === 'rings' ? TrafficLightRing : TrafficLight; + const isRight = config.barSide === 'right'; + + const dots = ( + + + + + + ); + + return ( +
+ {dots} + {title && ( + + {title} + + )} +
+ ); +}; + +export const WindowChrome: React.FC<{ + config: PresetConfig; + palette: Palette; + width: number; + height: number; + title?: string; + /** Set to false when an ancestor already handles the entrance animation. */ + animate?: boolean; + children: React.ReactNode; +}> = ({ config, palette, width, height, title, animate = true, children }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + // Animate the window scaling in over 0.5s with a crisp ease-out. + // Skipped when `animate` is false (e.g. StaggeredPanelEntrance handles it). + const enterProgress = animate + ? interpolate(frame, [0, 0.5 * fps], [0, 1], { + easing: Easing.bezier(0.16, 1, 0.3, 1), + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }) + : 1; + + const scale = interpolate(enterProgress, [0, 1], [0.92, 1]); + const opacity = enterProgress; + + const barHeight = config.bar !== 'none' ? 36 : 0; + const contentHeight = height - barHeight; + + return ( +
+ +
+ {children} +
+
+ ); +}; diff --git a/plugins/droid-control/remotion/src/components/WindowReflection.tsx b/plugins/droid-control/remotion/src/components/WindowReflection.tsx new file mode 100644 index 0000000..6517d03 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/WindowReflection.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +// Uses -webkit-box-reflect to avoid rendering children twice (which would +// double Video decoding and hurt render performance). +export const WindowReflection: React.FC<{ + opacity?: number; + children: React.ReactNode; +}> = ({ opacity = 0.06, children }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/plugins/droid-control/remotion/src/components/ZoomEffect.tsx b/plugins/droid-control/remotion/src/components/ZoomEffect.tsx new file mode 100644 index 0000000..bda28d1 --- /dev/null +++ b/plugins/droid-control/remotion/src/components/ZoomEffect.tsx @@ -0,0 +1,133 @@ +import React from 'react'; +import { useCurrentFrame, useVideoConfig, interpolate, Easing } from 'remotion'; + +interface ZoomRegion { + x: string; + y: string; + w: string; + h: string; +} + +/** Parse a percentage string (e.g. "25%") to a 0–1 fraction. */ +function pctToFraction(val: string): number { + return parseFloat(val.replace('%', '')) / 100; +} + +/** Cinematic ease-in-out for directed zoom motion. */ +const ZOOM_EASING = Easing.bezier(0.25, 0.1, 0.25, 1); + +/** + * Directed zoom to a target region. + * + * Animates from the full view (scale=1) into a zoomed view where the + * target region fills the frame, then back out. + * + * Phases (relative to duration): + * 0–30% IN — scale up to target + * 30–70% HOLD — maintain zoom + * 70–100% OUT — scale back to 1 + */ +export const ZoomEffect: React.FC<{ + startTime: number; + duration: number; + to: ZoomRegion; + children: React.ReactNode; +}> = ({ startTime, duration, to, children }) => { + const frame = useCurrentFrame(); + const { fps } = useVideoConfig(); + + const startFrame = startTime * fps; + const endFrame = (startTime + duration) * fps; + const totalFrames = endFrame - startFrame; + + // Phase boundaries (absolute frame numbers) + const inEnd = startFrame + totalFrames * 0.3; + const holdEnd = startFrame + totalFrames * 0.7; + + // Convert percentage-based region to 0–1 fractions + const xFrac = pctToFraction(to.x); + const yFrac = pctToFraction(to.y); + const wFrac = pctToFraction(to.w); + const hFrac = pctToFraction(to.h); + + const targetScale = Math.min( + 1 / Math.max(0.01, wFrac), + 1 / Math.max(0.01, hFrac) + ); + + // Anchor scaling at the center of the target region so the camera + // "pushes in" toward that point. + const originX = (xFrac + wFrac / 2) * 100; + const originY = (yFrac + hFrac / 2) * 100; + + // Determine zoom progress (0 = full view, 1 = fully zoomed) based + // on the current phase. + let zoomProgress: number; + if (frame < startFrame || frame > endFrame) { + zoomProgress = 0; + } else if (frame <= inEnd) { + // IN — ease from 0 → 1 + zoomProgress = interpolate(frame, [startFrame, inEnd], [0, 1], { + easing: ZOOM_EASING, + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + } else if (frame <= holdEnd) { + // HOLD + zoomProgress = 1; + } else { + // OUT — ease from 1 → 0 + zoomProgress = interpolate(frame, [holdEnd, endFrame], [1, 0], { + easing: ZOOM_EASING, + extrapolateLeft: 'clamp', + extrapolateRight: 'clamp', + }); + } + + const currentScale = interpolate(zoomProgress, [0, 1], [1, targetScale]); + + return ( +
+ {children} +
+ ); +}; + +/** + * Ambient "breathing" Ken Burns zoom. + * + * Wraps children in a slowly scaling container (1.0 → 1.04) over the + * full composition duration, giving a subtle cinematic drift. + */ +export const BreathingZoom: React.FC<{ + children: React.ReactNode; +}> = ({ children }) => { + const frame = useCurrentFrame(); + const { durationInFrames } = useVideoConfig(); + + const scale = interpolate(frame, [0, durationInFrames], [1.0, 1.04], { + easing: Easing.bezier(0.45, 0, 0.55, 1), + extrapolateRight: 'clamp', + }); + + return ( +
+ {children} +
+ ); +}; diff --git a/plugins/droid-control/remotion/src/compositions/Showcase.tsx b/plugins/droid-control/remotion/src/compositions/Showcase.tsx new file mode 100644 index 0000000..8e26da3 --- /dev/null +++ b/plugins/droid-control/remotion/src/compositions/Showcase.tsx @@ -0,0 +1,315 @@ +import React, { useMemo } from 'react'; +import { z } from 'zod'; +import { AbsoluteFill, staticFile, useVideoConfig } from 'remotion'; +import { Video } from '@remotion/media'; +import { TransitionSeries, linearTiming } from '@remotion/transitions'; +import { motionBlurTransition } from '../components/MotionBlurTransition'; +import { + fidelitySchema, + presetSchema, + layoutSchema, + keystrokeSchema, + effectSchema, + sectionSchema, +} from '../lib/schema'; +import { getPalette } from '../lib/palettes'; +import { getPresetConfig } from '../lib/presets'; +import { Background } from '../components/Background'; +import { WindowChrome } from '../components/WindowChrome'; +import { TitleCard } from '../components/TitleCard'; +import { SectionHeaderOverlay } from '../components/SectionHeader'; +import { KeystrokeOverlay } from '../components/KeystrokeOverlay'; +import { SpotlightOverlay } from '../components/SpotlightOverlay'; +import { StaggeredPanelEntrance } from '../components/StaggeredPanelEntrance'; +import { NoiseOverlay } from '../components/NoiseOverlay'; +import { FloatingParticles } from '../components/FloatingParticles'; +import { ColorGradeOverlay } from '../components/ColorGradeOverlay'; +import { Watermark } from '../components/Watermark'; +import { ZoomEffect } from '../components/ZoomEffect'; +import { SectionTransitionOverlay } from '../components/SectionTransition'; +import { FanningRotorOutro } from '../components/FanningRotorOutro'; + +export const showcaseSchema = z.object({ + clips: z.array(z.string()), + layout: layoutSchema, + labels: z.array(z.string()), + title: z.string(), + subtitle: z.string(), + preset: presetSchema, + keys: z.array(keystrokeSchema), + effects: z.array(effectSchema), + sections: z.array(sectionSchema).optional(), + width: z.number().optional(), + height: z.number().optional(), + speedNote: z.string().optional(), + windowTitle: z.string().optional(), + clipDuration: z.number().optional(), + speed: z.number().positive().optional(), + fidelity: fidelitySchema.optional(), +}); + +const TITLE_DURATION_S = 4; +const TRANSITION_FRAMES = 15; +const MOTION_BLUR = motionBlurTransition(); + +const resolveFidelity = ( + props: z.infer +): 'compact' | 'standard' | 'inspect' => + props.fidelity ?? (props.layout === 'side-by-side' ? 'inspect' : 'standard'); + +const visualTreatmentByFidelity = { + compact: { noiseOpacity: 0.03, gradeIntensity: 0.04 }, + standard: { noiseOpacity: 0.02, gradeIntensity: 0.025 }, + inspect: { noiseOpacity: 0.008, gradeIntensity: 0.012 }, +} as const; + +const SingleLayout: React.FC<{ + clip: string; + config: ReturnType; + palette: ReturnType; + windowTitle?: string; +}> = ({ clip, config, palette, windowTitle }) => { + const { width, height } = useVideoConfig(); + + const frameW = width - 2 * config.margin; + const frameH = height - 2 * config.margin; + + return ( + + + + + ); +}; + +const SideBySideLayout: React.FC<{ + clips: string[]; + labels: string[]; + config: ReturnType; + palette: ReturnType; +}> = ({ clips, labels, config, palette }) => { + const { width, height } = useVideoConfig(); + + const totalW = width - 2 * config.margin; + const gap = 16; + const panelW = Math.floor((totalW - gap) / 2); + const panelH = height - 2 * config.margin; + + return ( + + {clips.slice(0, 2).map((clip, i) => ( + +
+ + +
+
+ ))} +
+ ); +}; + +export const ShowcaseComposition: React.FC> = ( + props +) => { + const { fps } = useVideoConfig(); + const palette = getPalette(props.preset); + const config = getPresetConfig(props.preset); + const isFactory = + props.preset === 'factory' || props.preset === 'factory-hero'; + const fidelity = resolveFidelity(props); + const visualTreatment = visualTreatmentByFidelity[fidelity]; + + const titleFrames = TITLE_DURATION_S * fps; + const clipFrames = Math.ceil((props.clipDuration ?? 60) * fps); + + const spotlights = useMemo( + () => + props.effects.filter( + (e): e is Extract => e.fx === 'spotlight' + ), + [props.effects] + ); + + const zooms = useMemo( + () => + props.effects.filter( + (e): e is Extract => e.fx === 'zoom' + ), + [props.effects] + ); + + return ( + + + + + + {/* Title card */} + + + + + {/* Crossfade from title to content */} + + + {/* Main content */} + + + + + {(() => { + let content = ( + <> + {props.layout === 'side-by-side' ? ( + + ) : props.clips[0] ? ( + + ) : null} + + ); + + // Apply zooms by wrapping content + zooms.forEach((zoom, i) => { + content = ( + + {content} + + ); + }); + + return content; + })()} + + {/* Spotlight overlays */} + {spotlights.map((spot, i) => ( + + ))} + + {/* Frosted sweep at section boundaries */} + {props.sections && props.sections.length > 1 && ( + + )} + + {/* Section Headers */} + {props.sections && props.sections.length > 0 && ( + + )} + + {/* Keystroke overlay */} + {props.keys.length > 0 && ( + + )} + + + + {/* Crossfade to outro */} + + + {/* Outro card */} + + + + + + {/* Watermark: factory presets only, above content, below noise/grade */} + {isFactory && } + + {/* Noise overlay: above content, below ColorGradeOverlay */} + + + {/* Colour grade: topmost layer — unifies colour temperature */} + + + ); +}; diff --git a/plugins/droid-control/remotion/src/index.ts b/plugins/droid-control/remotion/src/index.ts new file mode 100644 index 0000000..91fa0f3 --- /dev/null +++ b/plugins/droid-control/remotion/src/index.ts @@ -0,0 +1,4 @@ +import { registerRoot } from 'remotion'; +import { RemotionRoot } from './Root'; + +registerRoot(RemotionRoot); diff --git a/plugins/droid-control/remotion/src/lib/duration.ts b/plugins/droid-control/remotion/src/lib/duration.ts new file mode 100644 index 0000000..49ac021 --- /dev/null +++ b/plugins/droid-control/remotion/src/lib/duration.ts @@ -0,0 +1,22 @@ +import type { z } from 'zod'; +import type { showcaseSchema } from '../compositions/Showcase'; + +const TITLE_DURATION_S = 4; +const TRANSITION_FRAMES = 15; + +export function calculateShowcaseDuration( + props: z.infer, + fps = 30 +): number { + const titleFrames = TITLE_DURATION_S * fps; + + // Use the explicit clipDuration prop when provided; fall back to 60s for + // clips (can't probe ahead of time) or 10s for clip-less title-only videos. + const clipDurationS = + props.clipDuration ?? (props.clips.length > 0 ? 60 : 10); + const clipFrames = Math.ceil(clipDurationS * fps); + + const outroFrames = 3.5 * fps; + + return titleFrames + clipFrames + outroFrames - (2 * TRANSITION_FRAMES); +} diff --git a/plugins/droid-control/remotion/src/lib/palettes.ts b/plugins/droid-control/remotion/src/lib/palettes.ts new file mode 100644 index 0000000..a1babe4 --- /dev/null +++ b/plugins/droid-control/remotion/src/lib/palettes.ts @@ -0,0 +1,41 @@ +import type { Preset } from './schema'; + +export interface Palette { + bg: string; + surface: string; + accent: string; + border: string; + text: string; + muted: string; + success: string; + error: string; +} + +const factoryPalette: Palette = { + bg: '#0a0804', + surface: '#181818', + accent: '#EE6018', + border: '#342F2D', + text: '#f0e8e0', + muted: '#948781', + success: '#6FAB78', + error: '#D9363E', +}; + +const catppuccinPalette: Palette = { + bg: '#0d1117', + surface: '#181818', + accent: '#89b4fa', + border: '#313244', + text: '#cdd6f4', + muted: '#6c7086', + success: '#a6e3a1', + error: '#f38ba8', +}; + +export function getPalette(preset: Preset): Palette { + if (preset === 'factory' || preset === 'factory-hero') { + return factoryPalette; + } + return catppuccinPalette; +} diff --git a/plugins/droid-control/remotion/src/lib/presets.ts b/plugins/droid-control/remotion/src/lib/presets.ts new file mode 100644 index 0000000..3d38b79 --- /dev/null +++ b/plugins/droid-control/remotion/src/lib/presets.ts @@ -0,0 +1,72 @@ +import type { Preset } from './schema'; + +export interface PresetConfig { + bar: 'colorful' | 'rings' | 'none'; + barSide: 'left' | 'right'; + radius: number; + padding: number; + margin: number; + shadow: boolean; + bgStyle: 'solid' | 'gradient'; +} + +const presetConfigs: Record = { + macos: { + bar: 'colorful', + barSide: 'left', + radius: 12, + padding: 20, + margin: 60, + shadow: true, + bgStyle: 'solid', + }, + minimal: { + bar: 'none', + barSide: 'left', + radius: 8, + padding: 16, + margin: 32, + shadow: false, + bgStyle: 'solid', + }, + hero: { + bar: 'colorful', + barSide: 'left', + radius: 16, + padding: 24, + margin: 80, + shadow: true, + bgStyle: 'gradient', + }, + presentation: { + bar: 'colorful', + barSide: 'left', + radius: 12, + padding: 24, + margin: 48, + shadow: true, + bgStyle: 'solid', + }, + factory: { + bar: 'colorful', + barSide: 'left', + radius: 12, + padding: 20, + margin: 80, + shadow: true, + bgStyle: 'solid', + }, + 'factory-hero': { + bar: 'colorful', + barSide: 'left', + radius: 12, + padding: 24, + margin: 80, + shadow: true, + bgStyle: 'gradient', + }, +}; + +export function getPresetConfig(preset: Preset): PresetConfig { + return presetConfigs[preset]; +} diff --git a/plugins/droid-control/remotion/src/lib/schema.ts b/plugins/droid-control/remotion/src/lib/schema.ts new file mode 100644 index 0000000..3301038 --- /dev/null +++ b/plugins/droid-control/remotion/src/lib/schema.ts @@ -0,0 +1,75 @@ +import { z } from 'zod'; + +export const presetSchema = z.enum([ + 'macos', + 'minimal', + 'hero', + 'presentation', + 'factory', + 'factory-hero', +]); +export type Preset = z.infer; + +export const layoutSchema = z.enum(['single', 'side-by-side']); +export type Layout = z.infer; + +export const fidelitySchema = z.enum(['compact', 'standard', 'inspect']); +export type Fidelity = z.infer; + +export const keystrokeSchema = z.object({ + t: z.number(), + label: z.string(), + dur: z.number().optional(), +}); +export type Keystroke = z.infer; + +export const sectionSchema = z.object({ + t: z.number(), + title: z.string(), +}); +export type Section = z.infer; + +export const effectSchema = z.discriminatedUnion('fx', [ + z.object({ + fx: z.literal('fade-in'), + t: z.number(), + dur: z.number(), + }), + z.object({ + fx: z.literal('fade-out'), + t: z.number(), + dur: z.number(), + }), + z.object({ + fx: z.literal('zoom'), + t: z.number(), + dur: z.number(), + to: z.object({ + x: z.string(), + y: z.string(), + w: z.string(), + h: z.string(), + }), + ease: z.string().optional(), + }), + z.object({ + fx: z.literal('spotlight'), + t: z.number(), + dur: z.number(), + on: z.object({ + x: z.string(), + y: z.string(), + w: z.string(), + h: z.string(), + }), + dim: z.number().optional(), + }), + z.object({ + fx: z.literal('callout'), + t: z.number(), + dur: z.number(), + text: z.string(), + at: z.object({ x: z.string(), y: z.string() }), + }), +]); +export type Effect = z.infer; diff --git a/plugins/droid-control/remotion/tsconfig.json b/plugins/droid-control/remotion/tsconfig.json new file mode 100644 index 0000000..bf1b784 --- /dev/null +++ b/plugins/droid-control/remotion/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx"] +} diff --git a/plugins/droid-control/scripts/capture-terminal-bytes.py b/plugins/droid-control/scripts/capture-terminal-bytes.py new file mode 100755 index 0000000..4839187 --- /dev/null +++ b/plugins/droid-control/scripts/capture-terminal-bytes.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +import argparse +import json +import os +import re +import shlex +import shutil +import subprocess +import time +from pathlib import Path + + +SUPPORTED_BACKENDS = ("ghostty", "kitty", "alacritty") +SUPPORTED_COMBOS = ("enter", "shift-enter", "ctrl-l", "escape", "shift-tab") + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Capture exact PTY bytes for common key combos across supported true-input terminals." + ) + parser.add_argument( + "--backend", + action="append", + default=[], + help="Terminal backend to test (ghostty, kitty, alacritty, all). Repeatable.", + ) + parser.add_argument( + "--combo", + action="append", + default=[], + help="Key combo to test (enter, shift-enter, ctrl-l, escape, shift-tab, all). Repeatable.", + ) + parser.add_argument( + "--format", + choices=("table", "json"), + default="table", + help="Output format.", + ) + parser.add_argument("--cols", default="80", help="Terminal columns for the session.") + parser.add_argument("--rows", default="24", help="Terminal rows for the session.") + parser.add_argument( + "--wait-timeout", + default="15000", + help="Timeout in ms for initial READY wait.", + ) + parser.add_argument( + "--settle-time", + type=float, + default=0.6, + help="Seconds to wait after sending the key before snapshotting.", + ) + return parser.parse_args() + + +def resolve_targets(requested: list[str], supported: tuple[str, ...], label: str) -> list[str]: + if not requested or "all" in requested: + return list(supported) + + resolved: list[str] = [] + for item in requested: + if item not in supported: + raise SystemExit(f"unsupported {label}: {item}") + if item not in resolved: + resolved.append(item) + return resolved + + +def escaped_bytes(raw: bytes) -> str: + return "".join( + f"\\x{byte:02x}" if byte < 32 or byte >= 127 else chr(byte) for byte in raw + ) + + +def run(args: list[str], *, capture: bool = False) -> subprocess.CompletedProcess[str]: + kwargs = { + "check": True, + "text": True, + } + if capture: + kwargs["capture_output"] = True + else: + kwargs["stdout"] = subprocess.DEVNULL + return subprocess.run(args, **kwargs) + + +def send_combo(tctl: str, session: str, combo: str) -> None: + match combo: + case "enter": + run([tctl, "-s", session, "press", "enter"]) + case "shift-enter": + run([tctl, "-s", session, "press", "shift", "enter"]) + case "ctrl-l": + run([tctl, "-s", session, "press", "ctrl", "l"]) + case "escape": + run([tctl, "-s", session, "press", "esc"]) + case "shift-tab": + run([tctl, "-s", session, "press", "shift", "tab"]) + case _: + raise SystemExit(f"unsupported combo: {combo}") + + +def normalize_hex_snapshot(snapshot: str) -> str: + body = ( + snapshot.strip() + .replace("\\r\\n", "\n") + .replace("\\n", "\n") + .replace("\\r", "\n") + ) + if "READY" in body: + body = body.split("READY", 1)[1].strip() + return " ".join(body.split()) + + +def capture_matrix(skill_dir: Path, backends: list[str], combos: list[str], args: argparse.Namespace) -> list[dict[str, str]]: + tctl = str(skill_dir / "bin" / "tctl") + dumper = skill_dir / "scripts" / "pty-hex-dumper.py" + child_command = f"{shlex.quote(str(dumper))} --ready" + + results: list[dict[str, str]] = [] + for backend in backends: + installed = shutil.which(backend) is not None + for combo in combos: + if not installed: + results.append( + { + "backend": backend, + "combo": combo, + "status": "unavailable", + "hex": "", + "escaped": "", + "note": f"{backend} is not installed", + } + ) + continue + + session = f"capture-{backend}-{combo}-{os.getpid()}".replace("_", "-") + record = { + "backend": backend, + "combo": combo, + "status": "ok", + "hex": "", + "escaped": "", + "note": "", + } + + try: + run( + [ + tctl, + "launch", + child_command, + "-s", + session, + "--backend", + backend, + "--cols", + args.cols, + "--rows", + args.rows, + ] + ) + run( + [ + tctl, + "-s", + session, + "wait", + "READY", + "--timeout", + args.wait_timeout, + ] + ) + send_combo(tctl, session, combo) + time.sleep(args.settle_time) + snapshot = run( + [tctl, "-s", session, "snapshot", "--trim"], capture=True + ).stdout + hex_bytes = normalize_hex_snapshot(snapshot) + + if hex_bytes and not re.fullmatch( + r"(?:[0-9a-f]{2}(?: [0-9a-f]{2})*)", hex_bytes + ): + record["status"] = "unexpected-output" + record["note"] = snapshot.strip() + else: + raw = bytes.fromhex(hex_bytes) if hex_bytes else b"" + record["hex"] = hex_bytes + record["escaped"] = escaped_bytes(raw) + except subprocess.CalledProcessError as exc: + record["status"] = "error" + record["note"] = (exc.stderr or exc.stdout or str(exc)).strip() + finally: + subprocess.run( + [tctl, "-s", session, "close"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + text=True, + ) + + results.append(record) + + return results + + +def print_table(results: list[dict[str, str]]) -> None: + headers = ("backend", "combo", "status", "hex", "escaped", "note") + widths = {header: len(header) for header in headers} + for row in results: + for header in headers: + widths[header] = max( + widths[header], len(row.get(header, "").replace("\n", "\\n")) + ) + + def fmt(row: dict[str, str] | None = None) -> str: + values = row or {header: header for header in headers} + return " | ".join( + values.get(header, "").replace("\n", "\\n").ljust(widths[header]) + for header in headers + ) + + print(fmt()) + print("-+-".join("-" * widths[header] for header in headers)) + for row in results: + print(fmt(row)) + + +def main() -> int: + args = parse_args() + script_path = Path(__file__).resolve() + skill_dir = script_path.parent.parent + + backends = resolve_targets(args.backend, SUPPORTED_BACKENDS, "backend") + combos = resolve_targets(args.combo, SUPPORTED_COMBOS, "combo") + results = capture_matrix(skill_dir, backends, combos, args) + + if args.format == "json": + print(json.dumps(results, indent=2)) + else: + print_table(results) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/droid-control/scripts/macos/mac-ctl.sh b/plugins/droid-control/scripts/macos/mac-ctl.sh new file mode 100755 index 0000000..a0bff7b --- /dev/null +++ b/plugins/droid-control/scripts/macos/mac-ctl.sh @@ -0,0 +1,331 @@ +#!/usr/bin/env bash +# mac-ctl.sh — macOS VM control for agents via QEMU monitor socket +# All input via QEMU HMP `sendkey`. Nothing touches the host compositor. +# +# Required env vars: +# DROID_MAC_MONITOR — path to QEMU monitor unix socket +# DROID_MAC_SSH_HOST — SSH host alias for the macOS VM (from ~/.ssh/config) +# +# Optional: +# DROID_MAC_BOOT_SCRIPT — path to the QEMU boot script (for `up` command) +# DROID_MAC_BOOT_ARGS — extra env vars for the boot script (space-separated KEY=VAL) +# DROID_MAC_SHOT_DIR — screenshot output directory (default: /tmp) +# +# Usage: mac-ctl.sh [args] + +set -euo pipefail + +MONITOR="${DROID_MAC_MONITOR:?mac-ctl: set DROID_MAC_MONITOR (path to QEMU monitor socket)}" +SSH_HOST="${DROID_MAC_SSH_HOST:?mac-ctl: set DROID_MAC_SSH_HOST (SSH config host alias)}" +BOOT_SCRIPT="${DROID_MAC_BOOT_SCRIPT:-}" +BOOT_ARGS="${DROID_MAC_BOOT_ARGS:-}" +SHOT_DIR="${DROID_MAC_SHOT_DIR:-/tmp}" + +# ── QEMU monitor helpers ──────────────────────────────────────── +_qmp() { + # Send a command to QEMU HMP via the monitor socket. + # Uses socat with a short timeout; strips echo noise. + printf '%s\n' "$1" | socat -T1 - "UNIX-CONNECT:${MONITOR}" 2>/dev/null \ + | grep -v '^QEMU\|^(qemu)' || true +} + +_sendkey() { + # QEMU HMP sendkey: keys are separated by dashes for chords. + # e.g., _sendkey "shift-a" sends Shift+A + _qmp "sendkey $1" +} + +# ── char → QEMU keyname ───────────────────────────────────────── +# QEMU HMP sendkey uses its own key names (not KEY_A, just 'a'). +declare -A CHAR_KEYS=( + [a]=a [b]=b [c]=c [d]=d [e]=e + [f]=f [g]=g [h]=h [i]=i [j]=j + [k]=k [l]=l [m]=m [n]=n [o]=o + [p]=p [q]=q [r]=r [s]=s [t]=t + [u]=u [v]=v [w]=w [x]=x [y]=y + [z]=z + [0]=0 [1]=1 [2]=2 [3]=3 [4]=4 + [5]=5 [6]=6 [7]=7 [8]=8 [9]=9 + [" "]=spc + [-]=minus [=]=equal + ["["]=bracket_left ["]"]=bracket_right + [\\]=backslash [";"]=semicolon ["'"]=apostrophe + [,]=comma [.]=dot [/]=slash + ['`']=grave_accent +) + +# ── shifted chars ──────────────────────────────────────────────── +declare -A SHIFT_CHARS=( + ["!"]=1 ["@"]=2 ["#"]=3 ['$']=4 + ["%"]=5 ["^"]=6 ["&"]=7 ["*"]=8 + ["("]=9 [")"]=0 + [_]=minus ["+"]=equal + ["{"]=bracket_left ["}"]=bracket_right + ["|"]=backslash [":"]=semicolon ['"']=apostrophe + ["<"]=comma [">"]=dot ["?"]=slash + ["~"]=grave_accent +) + +# ── named keys → QEMU HMP key names ───────────────────────────── +declare -A NAMED_KEYS=( + [enter]=ret [return]=ret + [esc]=esc [escape]=esc + [tab]=tab [space]=spc + [backspace]=backspace [bs]=backspace + [delete]=delete [del]=delete + [insert]=insert [ins]=insert + [home]=home [end]=end + [pageup]=pgup [pgup]=pgup + [pagedown]=pgdn [pgdn]=pgdn + [up]=up [down]=down + [left]=left [right]=right + [ctrl]=ctrl [lctrl]=ctrl [rctrl]=ctrl_r + [alt]=alt [lalt]=alt [ralt]=alt_r + [shift]=shift [lshift]=shift [rshift]=shift_r + [super]=meta_l [win]=meta_l [meta]=meta_l + [cmd]=meta_l [command]=meta_l + [f1]=f1 [f2]=f2 [f3]=f3 [f4]=f4 + [f5]=f5 [f6]=f6 [f7]=f7 [f8]=f8 + [f9]=f9 [f10]=f10 [f11]=f11 [f12]=f12 + [capslock]=caps_lock [caps]=caps_lock +) + +# ── helpers ────────────────────────────────────────────────────── +ensure_running() { + if [[ ! -S "$MONITOR" ]]; then + echo "mac-ctl: monitor socket not found at $MONITOR" >&2 + exit 1 + fi + local status + status=$(_qmp "info status" | grep -o 'running\|paused' | head -1) + if [[ "$status" != "running" ]]; then + echo "mac-ctl: VM not running (status: ${status:-unknown})" >&2 + exit 1 + fi +} + +resolve_key() { + local name="${1,,}" + if [[ -n "${NAMED_KEYS[$name]+_}" ]]; then + echo "${NAMED_KEYS[$name]}" + elif [[ ${#1} -eq 1 && "$1" =~ [a-zA-Z] ]]; then + echo "${1,,}" + else + echo "mac-ctl: unknown key '$1'" >&2 + exit 1 + fi +} + +type_char() { + local ch="$1" + local lower="${ch,,}" + + if [[ "$ch" =~ ^[A-Z]$ ]]; then + _sendkey "shift-${lower}" + elif [[ -n "${CHAR_KEYS[$ch]+_}" ]]; then + _sendkey "${CHAR_KEYS[$ch]}" + elif [[ -n "${SHIFT_CHARS[$ch]+_}" ]]; then + _sendkey "shift-${SHIFT_CHARS[$ch]}" + else + echo "mac-ctl type: unsupported char '$ch'" >&2 + return 1 + fi +} + +type_text() { + local text="$1" + local i ch + for (( i=0; i<${#text}; i++ )); do + ch="${text:$i:1}" + type_char "$ch" || return 1 + sleep 0.08 + done +} + +# ── commands ───────────────────────────────────────────────────── +cmd_up() { + if [[ -S "$MONITOR" ]]; then + echo "mac-ctl: monitor socket already exists -- VM may be running" >&2 + return 1 + fi + if [[ -z "$BOOT_SCRIPT" ]]; then + echo "mac-ctl: set DROID_MAC_BOOT_SCRIPT to start the VM" >&2 + exit 1 + fi + # shellcheck disable=SC2086 + env MONITOR_SOCKET="$MONITOR" ENABLE_EVDEV_INPUT=0 $BOOT_ARGS \ + local logfile="${SHOT_DIR}/mac-ctl-boot.log" + nohup "$BOOT_SCRIPT" > "$logfile" 2>&1 & + echo "mac-ctl: started (PID $!, log: $logfile)" +} + +cmd_down() { + ensure_running + # Graceful: Cmd+Opt+Eject → Sleep, or just use SSH + ssh -o ConnectTimeout=5 "$SSH_HOST" 'sudo shutdown -h now' 2>/dev/null || true + echo "mac-ctl: shutdown sent via SSH" +} + +cmd_kill() { + _qmp "quit" + echo "mac-ctl: QEMU process terminated" +} + +cmd_status() { + if [[ ! -S "$MONITOR" ]]; then + echo "not running (no monitor socket)" + return + fi + _qmp "info status" | grep -oE 'running|paused|shutdown' | head -1 || echo "unknown" +} + +cmd_type() { + ensure_running + [[ -z "${1:-}" ]] && { echo "usage: mac-ctl.sh type " >&2; exit 1; } + type_text "$*" +} + +cmd_press() { + ensure_running + [[ -z "${1:-}" ]] && { echo "usage: mac-ctl.sh press [key ...]" >&2; exit 1; } + local chord="" + for tok in "$@"; do + local resolved + resolved="$(resolve_key "$tok")" + if [[ -z "$chord" ]]; then + chord="$resolved" + else + chord="${chord}-${resolved}" + fi + done + _sendkey "$chord" +} + +cmd_shot() { + ensure_running + local ppm="${SHOT_DIR}/mac-screenshot-$$.ppm" + local out="${1:-${SHOT_DIR}/mac-screenshot.png}" + _qmp "screendump ${ppm}" + sleep 0.3 + # Convert PPM to PNG (requires ffmpeg or ImageMagick on host) + if command -v magick &>/dev/null; then + magick "$ppm" "$out" 2>/dev/null + elif command -v convert &>/dev/null; then + convert "$ppm" "$out" 2>/dev/null + elif command -v ffmpeg &>/dev/null; then + ffmpeg -y -i "$ppm" "$out" 2>/dev/null + else + cp "$ppm" "$out" + echo "mac-ctl: saved as PPM (install ImageMagick for PNG)" >&2 + fi + rm -f "$ppm" + echo "mac-ctl: $out" +} + +cmd_terminal() { + ensure_running + # Spotlight → Terminal + _sendkey "meta_l-spc" + sleep 1 + type_text "Terminal" + sleep 0.5 + _sendkey "ret" + echo "mac-ctl: opening Terminal.app via Spotlight" +} + +cmd_iterm() { + ensure_running + _sendkey "meta_l-spc" + sleep 1 + type_text "iTerm" + sleep 0.5 + _sendkey "ret" + echo "mac-ctl: opening iTerm2 via Spotlight" +} + +cmd_spotlight() { + ensure_running + local query="${1:-}" + _sendkey "meta_l-spc" + sleep 0.8 + if [[ -n "$query" ]]; then + type_text "$query" + sleep 0.5 + _sendkey "ret" + fi +} + +cmd_ssh_cmd() { + ensure_running + if [[ $# -eq 0 ]]; then + echo "usage: mac-ctl.sh ssh " >&2 + echo " NOTE: SSH is for deployment/scripting ONLY." >&2 + echo " DO NOT use for TUI testing -- SSH PTY layer distorts input." >&2 + exit 1 + fi + ssh -o ConnectTimeout=5 "$SSH_HOST" "$@" +} + +cmd_wait_boot() { + local timeout="${1:-120}" + local elapsed=0 + echo -n "mac-ctl: waiting for SSH..." + while (( elapsed < timeout )); do + if ssh -o ConnectTimeout=3 "$SSH_HOST" 'true' 2>/dev/null; then + echo " ready (${elapsed}s)" + return 0 + fi + sleep 5 + elapsed=$((elapsed + 5)) + echo -n "." + done + echo " timeout after ${timeout}s" >&2 + return 1 +} + +cmd_keys() { + echo "Named keys for 'press':" + printf '%s\n' "${!NAMED_KEYS[@]}" | sort | column +} + +cmd_help() { + cat <<'EOF' +usage: mac-ctl.sh [args] + + Lifecycle: up | down | kill | status | wait-boot [timeout] + Input: type | press [key ...] + Capture: shot [path] | wait [secs] + Shortcuts: terminal | iterm | spotlight [query] + Info: keys | help + + SSH (deployment only -- NOT for TUI testing): + ssh run a command in the VM via SSH + + macOS-specific notes: + - `cmd` / `command` maps to the macOS Command key (meta_l) + - Chords: `press cmd c` sends Cmd+C, `press cmd shift 3` sends screenshot + - `up` requires DROID_MAC_BOOT_SCRIPT to be set + - `shot` outputs PPM → PNG (needs ImageMagick or ffmpeg on host) + - `wait-boot` polls SSH until the VM is ready (default 120s timeout) +EOF +} + +# ── dispatch ───────────────────────────────────────────────────── +case "${1:-help}" in + up) cmd_up ;; + down) cmd_down ;; + kill) cmd_kill ;; + status) cmd_status ;; + wait-boot) shift; cmd_wait_boot "$@" ;; + type) shift; cmd_type "$@" ;; + press) shift; cmd_press "$@" ;; + shot) shift; cmd_shot "$@" ;; + wait) sleep "${2:-1}" ;; + terminal) cmd_terminal ;; + iterm) cmd_iterm ;; + spotlight) shift; cmd_spotlight "$@" ;; + ssh) shift; cmd_ssh_cmd "$@" ;; + keys) cmd_keys ;; + help|-h|--help) cmd_help ;; + *) echo "mac-ctl: unknown command '$1'" >&2; cmd_help >&2; exit 1 ;; +esac diff --git a/plugins/droid-control/scripts/macos/mac-record.sh b/plugins/droid-control/scripts/macos/mac-record.sh new file mode 100755 index 0000000..36c9914 --- /dev/null +++ b/plugins/droid-control/scripts/macos/mac-record.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +# mac-record.sh — poll-based screen recorder for macOS QEMU VM +# Takes periodic screenshots via QEMU monitor and encodes to MP4. +# +# Required env vars: +# DROID_MAC_MONITOR — path to QEMU monitor unix socket +# +# Usage: +# mac-record.sh start [fps] +# mac-record.sh stop + +set -euo pipefail + +MONITOR="${DROID_MAC_MONITOR:?mac-record: set DROID_MAC_MONITOR}" +PID_FILE="/tmp/mac-record.pid" +FRAME_DIR="/tmp/mac-record-frames" + +_qmp() { + printf '%s\n' "$1" | socat -T1 - "UNIX-CONNECT:${MONITOR}" 2>/dev/null | grep -v '^QEMU\|^(qemu)' || true +} + +cmd_start() { + local output="${1:?usage: mac-record.sh start [fps]}" + local fps="${2:-5}" + local interval + interval=$(awk "BEGIN{printf \"%.3f\", 1/$fps}") + + [[ -f "$PID_FILE" ]] && { echo "mac-record: already recording (PID $(cat "$PID_FILE"))" >&2; exit 1; } + + rm -rf "$FRAME_DIR" + mkdir -p "$FRAME_DIR" + + ( + local n=0 + while true; do + local frame + frame=$(printf '%s/frame_%06d.ppm' "$FRAME_DIR" "$n") + _qmp "screendump ${frame}" >/dev/null + n=$((n + 1)) + sleep "$interval" + done + ) & + + local pid=$! + echo "$pid" > "$PID_FILE" + echo "$output" > /tmp/mac-record-output + echo "$fps" > /tmp/mac-record-fps + echo "mac-record: started (PID $pid, ${fps} fps → $output)" +} + +cmd_stop() { + [[ ! -f "$PID_FILE" ]] && { echo "mac-record: not recording" >&2; exit 1; } + + local pid + pid=$(cat "$PID_FILE") + kill "$pid" 2>/dev/null || true + wait "$pid" 2>/dev/null || true + rm -f "$PID_FILE" + + local output fps + output=$(cat /tmp/mac-record-output) + fps=$(cat /tmp/mac-record-fps) + + local frame_count + frame_count=$(find "$FRAME_DIR" -name 'frame_*.ppm' 2>/dev/null | wc -l) + if [[ "$frame_count" -eq 0 ]]; then + echo "mac-record: no frames captured" >&2 + exit 1 + fi + + echo "mac-record: encoding $frame_count frames → $output" + ffmpeg -y -framerate "$fps" -i "${FRAME_DIR}/frame_%06d.ppm" \ + -c:v libx264 -pix_fmt yuv420p -preset fast -crf 23 \ + "$output" 2>/dev/null + + rm -rf "$FRAME_DIR" /tmp/mac-record-output /tmp/mac-record-fps + echo "mac-record: $output" +} + +case "${1:-}" in + start) shift; cmd_start "$@" ;; + stop) cmd_stop ;; + *) echo "usage: mac-record.sh start [fps] | stop" >&2; exit 1 ;; +esac diff --git a/plugins/droid-control/scripts/pty-hex-dumper.py b/plugins/droid-control/scripts/pty-hex-dumper.py new file mode 100755 index 0000000..aaf71fc --- /dev/null +++ b/plugins/droid-control/scripts/pty-hex-dumper.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +import argparse +import sys +import termios +import tty + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Dump stdin as space-separated hex pairs." + ) + parser.add_argument( + "--ready", + action="store_true", + help="Print READY before reading stdin so callers can wait for initialization.", + ) + parser.add_argument( + "--no-raw", + action="store_true", + help="Do not switch stdin into raw mode before reading.", + ) + return parser.parse_args() + + +def main() -> int: + args = parse_args() + + if args.ready: + print("READY", flush=True) + + fd = sys.stdin.fileno() + old_attrs = None + if not args.no_raw and sys.stdin.isatty(): + old_attrs = termios.tcgetattr(fd) + tty.setraw(fd) + + try: + while True: + chunk = sys.stdin.buffer.read(1) + if not chunk: + break + sys.stdout.write(f"{chunk[0]:02x} ") + sys.stdout.flush() + finally: + if old_attrs is not None: + termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs) + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/plugins/droid-control/scripts/render-showcase.sh b/plugins/droid-control/scripts/render-showcase.sh new file mode 100755 index 0000000..6c915eb --- /dev/null +++ b/plugins/droid-control/scripts/render-showcase.sh @@ -0,0 +1,276 @@ +#!/usr/bin/env bash +# render-showcase.sh — Stage clips and render a Remotion showcase video +# +# Usage: +# render-showcase.sh --props props.json --output /tmp/out.mp4 clip1.cast [clip2.mp4] +# render-showcase.sh --props-inline '{"clips":...}' --output /tmp/out.mp4 clip1.cast +# +# What it does: +# 1. Converts .cast clips to .mp4 when needed, using a fidelity profile +# 2. Copies clip files into the Remotion public/ directory +# 3. Auto-patches clipDuration in props if missing (via ffprobe) +# 4. Runs npx remotion render Showcase +# 5. Cleans up staged/generated clips +# +# Prerequisites: ffmpeg, ffprobe, node, npm (with remotion deps installed) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REMOTION_DIR="${SCRIPT_DIR}/../remotion" +DROID_CLI_THEME='181818,e0d0c0,15161e,f7768e,9ece6a,e0af68,7aa2f7,bb9af7,7dcfff,a9b1d6,414868,f7768e,9ece6a,e0af68,7aa2f7,bb9af7,7dcfff,c0caf5' + +PROPS_FILE="" PROPS_INLINE="" OUTPUT="" FIDELITY_OVERRIDE="auto" CLIPS=() +WORK_DIR="$(mktemp -d /tmp/render-showcase-XXXXXX)" +STAGED=() +STAGED_BASES=() +STAGED_SOURCES=() + +cleanup() { + local f + for f in "${STAGED[@]}"; do + rm -f "$f" + done + rm -rf "$WORK_DIR" +} + +trap cleanup EXIT + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || { + echo "error: required command not found: $1" >&2 + exit 1 + } +} + +normalize_props() { + local props_json="$1" + local output_file="$2" + local fidelity_override="$3" + local props_source="$4" + PROPS_JSON="$props_json" python3 - "$output_file" "$fidelity_override" "$props_source" <<'PY' +import json +import os +import sys + +output_file = sys.argv[1] +fidelity_override = sys.argv[2] +props_source = sys.argv[3] +raw_props = os.environ["PROPS_JSON"] + +if not raw_props.strip(): + raise SystemExit(f"error: props JSON is empty: {props_source}") + +try: + props = json.loads(raw_props) +except json.JSONDecodeError as error: + raise SystemExit(f"error: props JSON is invalid: {props_source}: {error.msg} at line {error.lineno} column {error.colno}") + +if fidelity_override != "auto": + props["fidelity"] = fidelity_override + +fidelity = props.get("fidelity") +if fidelity is None: + fidelity = "standard" + props["fidelity"] = fidelity + +if props.get("width") is None: + props["width"] = 2560 if fidelity == "inspect" else 1920 +if props.get("height") is None: + props["height"] = 1440 if fidelity == "inspect" else 1080 + +with open(output_file, "w", encoding="utf-8") as f: + json.dump(props, f) + +print(fidelity) +PY +} + +props_number() { + local props_json="$1" + local key="$2" + PROPS_JSON="$props_json" python3 - "$key" <<'PY' +import json +import os +import sys + +key = sys.argv[1] +value = json.loads(os.environ["PROPS_JSON"]).get(key) +print("" if value is None else value) +PY +} + +rewrite_props_clips() { + local props_json="$1" + shift + PROPS_JSON="$props_json" python3 - "$@" <<'PY' +import json +import os +import sys + +props = json.loads(os.environ["PROPS_JSON"]) +props["clips"] = list(sys.argv[1:]) +print(json.dumps(props)) +PY +} + +cast_dimensions() { + local cast_path="$1" + python3 - "$cast_path" <<'PY' +import json +import sys + +with open(sys.argv[1], "r", encoding="utf-8") as f: + header = json.loads(f.readline()) + +print(header.get("width", 120), header.get("height", 36)) +PY +} + +convert_cast_clip() { + local cast_clip="$1" + local output_clip="$2" + local fidelity="$3" + local speed="$4" + + require_cmd agg + require_cmd ffmpeg + + local cols rows agg_fps_cap agg_idle_limit ffmpeg_crf ffmpeg_preset gif_clip + read -r cols rows < <(cast_dimensions "$cast_clip") + + case "$fidelity" in + compact) + agg_fps_cap="24" + agg_idle_limit="3" + ffmpeg_crf="21" + ffmpeg_preset="medium" + ;; + inspect) + agg_fps_cap="30" + agg_idle_limit="5" + ffmpeg_crf="14" + ffmpeg_preset="slow" + ;; + standard) + agg_fps_cap="30" + agg_idle_limit="5" + ffmpeg_crf="18" + ffmpeg_preset="slow" + ;; + *) + echo "error: unsupported fidelity profile: $fidelity" >&2 + exit 1 + ;; + esac + + gif_clip="${output_clip%.mp4}.gif" + agg --speed "$speed" \ + --renderer fontdue \ + --cols "$cols" \ + --rows "$rows" \ + --fps-cap "$agg_fps_cap" \ + --idle-time-limit "$agg_idle_limit" \ + --theme "$DROID_CLI_THEME" \ + "$cast_clip" \ + "$gif_clip" + + ffmpeg -y -i "$gif_clip" \ + -movflags +faststart \ + -pix_fmt yuv420p \ + -preset "$ffmpeg_preset" \ + -crf "$ffmpeg_crf" \ + -vf "scale=trunc(iw/2)*2:trunc(ih/2)*2" \ + "$output_clip" >/dev/null 2>&1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --props) PROPS_FILE="$2"; shift 2 ;; + --props-inline) PROPS_INLINE="$2"; shift 2 ;; + --fidelity) FIDELITY_OVERRIDE="$2"; shift 2 ;; + --output|-o) OUTPUT="$2"; shift 2 ;; + -h|--help) sed -n '2,10p' "$0" | sed 's/^# \?//'; exit 0 ;; + -*) echo "error: unknown option '$1'" >&2; exit 1 ;; + *) CLIPS+=("$1"); shift ;; + esac +done + +[[ -n "$OUTPUT" ]] || { echo "error: --output required" >&2; exit 1; } +[[ -n "$PROPS_FILE" || -n "$PROPS_INLINE" ]] || { echo "error: --props or --props-inline required" >&2; exit 1; } + +# Read props JSON +if [[ -n "$PROPS_FILE" ]]; then + [[ -r "$PROPS_FILE" ]] || { echo "error: props file is not readable: $PROPS_FILE" >&2; exit 1; } + PROPS=$(cat "$PROPS_FILE") + PROPS_SOURCE="$PROPS_FILE" +else + PROPS="$PROPS_INLINE" + PROPS_SOURCE="--props-inline" +fi + +NORMALIZED_PROPS="${WORK_DIR}/props.json" +FIDELITY="$(normalize_props "$PROPS" "$NORMALIZED_PROPS" "$FIDELITY_OVERRIDE" "$PROPS_SOURCE")" +PROPS=$(cat "$NORMALIZED_PROPS") +SPEED="$(props_number "$PROPS" "speed")" +if [[ -z "$SPEED" ]]; then + SPEED="1" +fi + +RENDER_ARGS=() +case "$FIDELITY" in + compact) + RENDER_ARGS+=(--codec=h264 --crf=21 --jpeg-quality=92 --pixel-format=yuv420p --x264-preset=medium) + ;; + inspect) + RENDER_ARGS+=(--codec=h264 --crf=14 --video-image-format=png --pixel-format=yuv420p --x264-preset=slow) + ;; + standard) + RENDER_ARGS+=(--codec=h264 --crf=18 --jpeg-quality=96 --pixel-format=yuv420p --x264-preset=slow) + ;; + *) + echo "error: unsupported fidelity profile: $FIDELITY" >&2 + exit 1 + ;; +esac + +# Stage clips into public/ +for clip in "${CLIPS[@]}"; do + base=$(basename "$clip") + source_clip="$clip" + staged_base="$base" + + if [[ "$clip" == *.cast ]]; then + staged_base="${base%.cast}.mp4" + source_clip="${WORK_DIR}/${staged_base}" + convert_cast_clip "$clip" "$source_clip" "$FIDELITY" "$SPEED" + fi + + cp "$source_clip" "${REMOTION_DIR}/public/${staged_base}" + STAGED+=("${REMOTION_DIR}/public/${staged_base}") + STAGED_BASES+=("$staged_base") + STAGED_SOURCES+=("$source_clip") +done + +PROPS="$(rewrite_props_clips "$PROPS" "${STAGED_BASES[@]}")" + +# Auto-detect clipDuration if not set in props (uses first clip) +HAS_DURATION=$(echo "$PROPS" | python3 -c "import sys,json; d=json.load(sys.stdin); print('yes' if d.get('clipDuration') else 'no')" 2>/dev/null || echo "no") +if [[ "$HAS_DURATION" == "no" && ${#STAGED_SOURCES[@]} -gt 0 ]]; then + DUR=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "${STAGED_SOURCES[0]}" 2>/dev/null | head -1) + if [[ -n "$DUR" ]]; then + PROPS=$(echo "$PROPS" | python3 -c " +import sys, json +d = json.load(sys.stdin) +d['clipDuration'] = round(float('${DUR}'), 2) +json.dump(d, sys.stdout) +") + echo "auto-detected clipDuration: ${DUR}s" >&2 + fi +fi + +# Render +cd "$REMOTION_DIR" +npx remotion render Showcase --props="$PROPS" "${RENDER_ARGS[@]}" "$OUTPUT" 2>&1 + +echo "$OUTPUT" diff --git a/plugins/droid-control/scripts/windows/vm-ctl.sh b/plugins/droid-control/scripts/windows/vm-ctl.sh new file mode 100755 index 0000000..4a5089f --- /dev/null +++ b/plugins/droid-control/scripts/windows/vm-ctl.sh @@ -0,0 +1,301 @@ +#!/usr/bin/env bash +# vm-ctl.sh — self-contained VM control for agents +# All input via virsh send-key. Nothing touches the host compositor. +# +# Required env vars: +# DROID_VM_NAME — libvirt domain name +# DROID_VM_SSH_KEY — path to SSH private key for the VM +# DROID_VM_SSH_USER — SSH username in the VM +# +# Optional: +# DROID_VM_CONN — libvirt connection URI (default: qemu:///system) +# DROID_VM_CAPTURE — remote capture dir (default: C:/capture) +# +# Usage: vm-ctl.sh [args] + +set -euo pipefail + +CONN="${DROID_VM_CONN:-qemu:///system}" +NAME="${DROID_VM_NAME:?vm-ctl: set DROID_VM_NAME (libvirt domain name)}" +SSH_KEY="${DROID_VM_SSH_KEY:?vm-ctl: set DROID_VM_SSH_KEY (path to SSH key)}" +SSH_USER="${DROID_VM_SSH_USER:?vm-ctl: set DROID_VM_SSH_USER (e.g. droid)}" +VM_CAPTURE_DIR="${DROID_VM_CAPTURE:-C:/capture}" + +_ssh() { + local ip + ip=$(virsh --connect "$CONN" domifaddr "$NAME" 2>/dev/null | awk '/ipv4/{print $4}' | cut -d/ -f1) + [[ -z "$ip" ]] && { echo "vm-ctl: can't resolve VM IP" >&2; exit 1; } + ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + -o ConnectTimeout=5 "$SSH_USER@$ip" "$@" 2>/dev/null +} + +_scp_to() { + local src="$1" dst="$2" + local ip + ip=$(virsh --connect "$CONN" domifaddr "$NAME" 2>/dev/null | awk '/ipv4/{print $4}' | cut -d/ -f1) + scp -i "$SSH_KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + "$src" "$SSH_USER@$ip:$dst" 2>/dev/null +} + +# ── char → KEY_* ───────────────────────────────────────────────── +declare -A CHAR_KEYS=( + [a]=KEY_A [b]=KEY_B [c]=KEY_C [d]=KEY_D [e]=KEY_E + [f]=KEY_F [g]=KEY_G [h]=KEY_H [i]=KEY_I [j]=KEY_J + [k]=KEY_K [l]=KEY_L [m]=KEY_M [n]=KEY_N [o]=KEY_O + [p]=KEY_P [q]=KEY_Q [r]=KEY_R [s]=KEY_S [t]=KEY_T + [u]=KEY_U [v]=KEY_V [w]=KEY_W [x]=KEY_X [y]=KEY_Y + [z]=KEY_Z + [0]=KEY_0 [1]=KEY_1 [2]=KEY_2 [3]=KEY_3 [4]=KEY_4 + [5]=KEY_5 [6]=KEY_6 [7]=KEY_7 [8]=KEY_8 [9]=KEY_9 + [" "]=KEY_SPACE + [-]=KEY_MINUS [=]=KEY_EQUAL + ["["]=KEY_LEFTBRACE ["]"]=KEY_RIGHTBRACE + [\\]=KEY_BACKSLASH [";"]=KEY_SEMICOLON ["'"]=KEY_APOSTROPHE + [,]=KEY_COMMA [.]=KEY_DOT [/]=KEY_SLASH + ['`']=KEY_GRAVE +) + +# ── shifted chars ──────────────────────────────────────────────── +declare -A SHIFT_CHARS=( + ["!"]=KEY_1 ["@"]=KEY_2 ["#"]=KEY_3 ['$']=KEY_4 + ["%"]=KEY_5 ["^"]=KEY_6 ["&"]=KEY_7 ["*"]=KEY_8 + ["("]=KEY_9 [")"]=KEY_0 + [_]=KEY_MINUS ["+"]=KEY_EQUAL + ["{"]=KEY_LEFTBRACE ["}"]=KEY_RIGHTBRACE + ["|"]=KEY_BACKSLASH [":"]=KEY_SEMICOLON ['"']=KEY_APOSTROPHE + ["<"]=KEY_COMMA [">"]=KEY_DOT ["?"]=KEY_SLASH + ["~"]=KEY_GRAVE +) + +# ── named keys ─────────────────────────────────────────────────── +declare -A NAMED_KEYS=( + [enter]=KEY_ENTER [return]=KEY_ENTER + [esc]=KEY_ESC [escape]=KEY_ESC + [tab]=KEY_TAB [space]=KEY_SPACE + [backspace]=KEY_BACKSPACE [bs]=KEY_BACKSPACE + [delete]=KEY_DELETE [del]=KEY_DELETE + [insert]=KEY_INSERT [ins]=KEY_INSERT + [home]=KEY_HOME [end]=KEY_END + [pageup]=KEY_PAGEUP [pgup]=KEY_PAGEUP + [pagedown]=KEY_PAGEDOWN [pgdn]=KEY_PAGEDOWN + [up]=KEY_UP [down]=KEY_DOWN + [left]=KEY_LEFT [right]=KEY_RIGHT + [ctrl]=KEY_LEFTCTRL [lctrl]=KEY_LEFTCTRL [rctrl]=KEY_RIGHTCTRL + [alt]=KEY_LEFTALT [lalt]=KEY_LEFTALT [ralt]=KEY_RIGHTALT + [shift]=KEY_LEFTSHIFT [lshift]=KEY_LEFTSHIFT [rshift]=KEY_RIGHTSHIFT + [super]=KEY_LEFTMETA [win]=KEY_LEFTMETA [meta]=KEY_LEFTMETA + [f1]=KEY_F1 [f2]=KEY_F2 [f3]=KEY_F3 [f4]=KEY_F4 + [f5]=KEY_F5 [f6]=KEY_F6 [f7]=KEY_F7 [f8]=KEY_F8 + [f9]=KEY_F9 [f10]=KEY_F10 [f11]=KEY_F11 [f12]=KEY_F12 + [capslock]=KEY_CAPSLOCK [caps]=KEY_CAPSLOCK + [printscreen]=KEY_SYSRQ [prtsc]=KEY_SYSRQ + [scrolllock]=KEY_SCROLLLOCK [pause]=KEY_PAUSE +) + +# ── helpers ────────────────────────────────────────────────────── +send_key() { + virsh --connect "$CONN" send-key "$NAME" --codeset linux "$@" 2>/dev/null +} + +ensure_running() { + local state + state=$(virsh --connect "$CONN" domstate "$NAME" 2>/dev/null || true) + if [[ "$state" != "running" ]]; then + echo "vm-ctl: not running" >&2 + exit 1 + fi +} + +resolve_key() { + local name="${1,,}" + if [[ -n "${NAMED_KEYS[$name]+_}" ]]; then + echo "${NAMED_KEYS[$name]}" + elif [[ "$1" == KEY_* ]]; then + echo "$1" + elif [[ ${#1} -eq 1 && "$1" =~ [a-zA-Z] ]]; then + echo "KEY_${1^^}" + else + echo "vm-ctl: unknown key '$1'" >&2 + exit 1 + fi +} + +type_char() { + local ch="$1" + local lower="${ch,,}" + + if [[ "$ch" =~ ^[A-Z]$ ]]; then + send_key KEY_LEFTSHIFT "${CHAR_KEYS[$lower]}" + elif [[ -n "${CHAR_KEYS[$ch]+_}" ]]; then + send_key "${CHAR_KEYS[$ch]}" + elif [[ -n "${SHIFT_CHARS[$ch]+_}" ]]; then + send_key KEY_LEFTSHIFT "${SHIFT_CHARS[$ch]}" + else + echo "vm-ctl type: unsupported char '$ch'" >&2 + return 1 + fi +} + +type_text() { + local text="$1" + local i ch + for (( i=0; i<${#text}; i++ )); do + ch="${text:$i:1}" + type_char "$ch" || return 1 + sleep 0.05 + done +} + +# ── commands ───────────────────────────────────────────────────── +cmd_up() { virsh --connect "$CONN" start "$NAME" 2>/dev/null && echo "vm-ctl: started" || echo "vm-ctl: already running or failed" >&2; } +cmd_down() { virsh --connect "$CONN" shutdown "$NAME" 2>/dev/null && echo "vm-ctl: shutting down" || echo "vm-ctl: not running" >&2; } +cmd_kill() { virsh --connect "$CONN" destroy "$NAME" 2>/dev/null && echo "vm-ctl: force-stopped" || echo "vm-ctl: not running" >&2; } +cmd_reboot() { virsh --connect "$CONN" reboot "$NAME" 2>/dev/null && echo "vm-ctl: rebooting" || echo "vm-ctl: not running" >&2; } +cmd_status() { virsh --connect "$CONN" domstate "$NAME" 2>/dev/null || echo "vm-ctl: not found" >&2; } + +cmd_type() { + ensure_running + [[ -z "${1:-}" ]] && { echo "usage: vm-ctl.sh type " >&2; exit 1; } + type_text "$*" +} + +cmd_press() { + ensure_running + [[ -z "${1:-}" ]] && { echo "usage: vm-ctl.sh press [key ...]" >&2; exit 1; } + local keys=() + for tok in "$@"; do + keys+=("$(resolve_key "$tok")") + done + send_key "${keys[@]}" +} + +cmd_login() { + ensure_running + local pwd="${1:-factory}" + send_key KEY_ENTER + sleep 2 + type_text "$pwd" + sleep 0.3 + send_key KEY_ENTER + echo "vm-ctl: login sent" +} + +cmd_shot() { + ensure_running + local out="${1:-/tmp/vm-screenshot.png}" + virsh --connect "$CONN" screenshot "$NAME" "$out" >/dev/null 2>&1 \ + && echo "vm-ctl: $out" \ + || { echo "vm-ctl: screenshot failed" >&2; exit 1; } +} + +cmd_pwsh() { + ensure_running + send_key KEY_LEFTMETA + sleep 1.5 + type_text "powershell" + sleep 1 + send_key KEY_ENTER + echo "vm-ctl: opening PowerShell" +} + +cmd_wt() { + ensure_running + send_key KEY_LEFTMETA + sleep 1.5 + type_text "terminal" + sleep 1 + send_key KEY_ENTER + echo "vm-ctl: opening Windows Terminal" +} + +cmd_snap() { + local name="${1:-$(date +%Y%m%d-%H%M%S)}" + virsh --connect "$CONN" snapshot-create-as "$NAME" "$name" \ + && echo "vm-ctl: snapshot '$name' created" +} + +cmd_snaps() { virsh --connect "$CONN" snapshot-list "$NAME"; } + +cmd_restore() { + [[ -z "${1:-}" ]] && { echo "usage: vm-ctl.sh restore " >&2; exit 1; } + virsh --connect "$CONN" snapshot-revert "$NAME" "$1" \ + && echo "vm-ctl: reverted to '$1'" +} + +cmd_ip() { virsh --connect "$CONN" domifaddr "$NAME" 2>/dev/null || echo "vm-ctl: couldn't get IP" >&2; } + +cmd_ssh() { + ensure_running + if [[ $# -eq 0 ]]; then + echo "usage: vm-ctl.sh ssh " >&2 + echo " NOTE: SSH is for deployment/scripting ONLY." >&2 + echo " DO NOT use for TUI testing -- SSH PTY layer distorts input." >&2 + exit 1 + fi + _ssh "$@" +} + +cmd_deploy() { + ensure_running + local script_dir + script_dir="$(cd "$(dirname "$0")" && pwd)" + _scp_to "$script_dir/win-vt-dumper.ps1" "$VM_CAPTURE_DIR/" + _scp_to "$script_dir/win-key-dumper.ps1" "$VM_CAPTURE_DIR/" + echo "vm-ctl: deployed capture scripts to $VM_CAPTURE_DIR" +} + +cmd_run() { + ensure_running + [[ -z "${1:-}" ]] && { echo "usage: vm-ctl.sh run [args]" >&2; exit 1; } + local script="$1"; shift + _ssh "powershell -ExecutionPolicy Bypass -File $VM_CAPTURE_DIR/$script $*" +} + +cmd_keys() { + echo "Named keys for 'press':" + printf '%s\n' "${!NAMED_KEYS[@]}" | sort | column +} + +cmd_help() { + cat <<'EOF' +usage: vm-ctl.sh [args] + + Lifecycle: up | down | kill | reboot | status + Input: type | press [key ...] | login [pwd] + Capture: shot [path] | wait [secs] + Shortcuts: pwsh | wt + Snapshots: snap [name] | snaps | restore + Info: ip | keys | help + + SSH (deployment only -- NOT for TUI testing): + ssh run a command in the VM via SSH + deploy push capture scripts to C:\capture + run execute a PowerShell script in C:\capture +EOF +} + +# ── dispatch ───────────────────────────────────────────────────── +case "${1:-help}" in + up) cmd_up ;; + down) cmd_down ;; + kill) cmd_kill ;; + reboot) cmd_reboot ;; + status) cmd_status ;; + type) shift; cmd_type "$@" ;; + press) shift; cmd_press "$@" ;; + login) shift; cmd_login "$@" ;; + shot) shift; cmd_shot "$@" ;; + wait) sleep "${2:-1}" ;; + pwsh) cmd_pwsh ;; + wt) cmd_wt ;; + snap) shift; cmd_snap "$@" ;; + snaps) cmd_snaps ;; + restore) shift; cmd_restore "$@" ;; + ip) cmd_ip ;; + keys) cmd_keys ;; + ssh) shift; cmd_ssh "$@" ;; + deploy) cmd_deploy ;; + run) shift; cmd_run "$@" ;; + help|-h|--help) cmd_help ;; + *) echo "vm-ctl: unknown command '$1'" >&2; cmd_help >&2; exit 1 ;; +esac diff --git a/plugins/droid-control/scripts/windows/vm-record.sh b/plugins/droid-control/scripts/windows/vm-record.sh new file mode 100755 index 0000000..5e586b6 --- /dev/null +++ b/plugins/droid-control/scripts/windows/vm-record.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +# vm-record.sh — screen recorder for the Windows VM +# Agent-only tool. Polls virsh screenshot and encodes to MP4 via ffmpeg. +# +# Usage: +# vm-record.sh start [output.mp4] [fps] +# vm-record.sh stop + +set -euo pipefail + +_CONN="${DROID_VM_CONN:-qemu:///system}" +_NAME="${DROID_VM_NAME:?vm-record: set DROID_VM_NAME}" +_PIDFILE="/tmp/.vm-rec.pid" +_METADIR="/tmp/.vm-rec-meta" + +cmd_start() { + local out="${1:-/tmp/vm-recording.mp4}" + local fps="${2:-5}" + + if [[ -f "$_PIDFILE" ]] && kill -0 "$(cat "$_PIDFILE")" 2>/dev/null; then + echo "vm-record: already recording (use 'stop' first)" >&2 + exit 1 + fi + + local state + state=$(virsh --connect "$_CONN" domstate "$_NAME" 2>/dev/null) + if [[ "$state" != "running" ]]; then + echo "vm-record: VM not running" >&2 + exit 1 + fi + + local framedir + framedir=$(mktemp -d /tmp/vm-rec-XXXXXX) + mkdir -p "$_METADIR" + echo "$framedir" > "$_METADIR/framedir" + echo "$out" > "$_METADIR/output" + echo "$fps" > "$_METADIR/fps" + + echo "vm-record: started → $out (${fps} fps)" + + nohup bash -c " + n=0 + while true; do + frame=\$(printf '%s/frame-%06d.png' '$framedir' \"\$n\") + virsh --connect '$_CONN' screenshot '$_NAME' \"\$frame\" >/dev/null 2>&1 || true + (( n++ )) + sleep \$(echo 'scale=4; 1/$fps' | bc) + done + " >/dev/null 2>&1 & + echo "$!" > "$_PIDFILE" +} + +cmd_stop() { + if [[ ! -f "$_PIDFILE" ]] || ! kill -0 "$(cat "$_PIDFILE")" 2>/dev/null; then + echo "vm-record: no recording in progress" >&2 + exit 1 + fi + + kill "$(cat "$_PIDFILE")" 2>/dev/null + wait "$(cat "$_PIDFILE")" 2>/dev/null || true + rm -f "$_PIDFILE" + + local framedir out fps nframes + framedir=$(cat "$_METADIR/framedir") + out=$(cat "$_METADIR/output") + fps=$(cat "$_METADIR/fps") + nframes=$(find "$framedir" -name 'frame-*.png' 2>/dev/null | wc -l) + + if (( nframes == 0 )); then + echo "vm-record: no frames captured" >&2 + rm -rf "$framedir" "$_METADIR" + exit 1 + fi + + echo "vm-record: encoding $nframes frames → $out" + ffmpeg -y -framerate "$fps" \ + -i "$framedir/frame-%06d.png" \ + -c:v libx264 -pix_fmt yuv420p \ + -vf "pad=ceil(iw/2)*2:ceil(ih/2)*2" \ + "$out" >/dev/null 2>&1 + + local size + size=$(du -h "$out" | cut -f1) + echo "vm-record: $out ($size, $nframes frames)" + + rm -rf "$framedir" "$_METADIR" +} + +case "${1:-}" in + start) shift; cmd_start "$@" ;; + stop) cmd_stop ;; + *) + echo "usage: vm-record.sh start [output.mp4] [fps]" + echo " vm-record.sh stop" + exit 1 + ;; +esac diff --git a/plugins/droid-control/scripts/windows/win-key-dumper.ps1 b/plugins/droid-control/scripts/windows/win-key-dumper.ps1 new file mode 100644 index 0000000..4c8a89b --- /dev/null +++ b/plugins/droid-control/scripts/windows/win-key-dumper.ps1 @@ -0,0 +1,31 @@ +# win-key-dumper.ps1 — Win32 console key event dumper +# Uses $host.UI.RawUI.ReadKey for lossless key metadata. +# Shows VirtualKeyCode, ControlKeyState, Character for each keypress. +# Captures distinctions VT mode loses (e.g., Shift+Enter vs Enter). +# Press Ctrl+Q to exit. + +Write-Host "Key event dumper active. Press keys to inspect. Ctrl+Q to exit." -ForegroundColor Cyan +Write-Host "" + +$fmt = "{0,-6} {1,-10} {2,-20} {3,-35} {4}" +Write-Host ($fmt -f "Down", "Char", "VKey", "ControlKeyState", "CharHex") -ForegroundColor DarkGray +Write-Host ($fmt -f ("=" * 6), ("=" * 10), ("=" * 20), ("=" * 35), ("=" * 8)) -ForegroundColor DarkGray + +while ($true) { + $k = $host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown,IncludeKeyUp') + + $charCode = [int][char]$k.Character + $charHex = "U+{0:X4}" -f $charCode + $charDisplay = if ($charCode -ge 0x20 -and $charCode -le 0x7E) { $k.Character } else { "." } + + # Ctrl+Q keydown + if ($k.KeyDown -and $k.VirtualKeyCode -eq 81 -and + ($k.ControlKeyState -band 0x0008)) { break } # LEFT_CTRL_PRESSED + + if ($k.KeyDown) { + $line = $fmt -f $k.KeyDown, $charDisplay, $k.VirtualKeyCode, $k.ControlKeyState, $charHex + Write-Host $line + } +} + +Write-Host "`nDone." -ForegroundColor Cyan diff --git a/plugins/droid-control/scripts/windows/win-vt-dumper.ps1 b/plugins/droid-control/scripts/windows/win-vt-dumper.ps1 new file mode 100644 index 0000000..bb7162a --- /dev/null +++ b/plugins/droid-control/scripts/windows/win-vt-dumper.ps1 @@ -0,0 +1,70 @@ +# win-vt-dumper.ps1 — raw VT byte dumper for Windows console +# Enables ENABLE_VIRTUAL_TERMINAL_INPUT, reads stdin as raw bytes, hex-dumps. +# Shows exactly what escape sequences the console delivers for each keypress. +# Press Ctrl+Q to exit. + +Add-Type @" +using System; +using System.Runtime.InteropServices; +public static class WinConsole { + public const int STD_INPUT_HANDLE = -10; + public const uint ENABLE_PROCESSED_INPUT = 0x0001; + public const uint ENABLE_LINE_INPUT = 0x0002; + public const uint ENABLE_ECHO_INPUT = 0x0004; + public const uint ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200; + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern IntPtr GetStdHandle(int nStdHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool GetConsoleMode(IntPtr h, out uint mode); + + [DllImport("kernel32.dll", SetLastError = true)] + public static extern bool SetConsoleMode(IntPtr h, uint mode); +} +"@ + +$hIn = [WinConsole]::GetStdHandle([WinConsole]::STD_INPUT_HANDLE) +[uint32]$oldMode = 0 +if (-not [WinConsole]::GetConsoleMode($hIn, [ref]$oldMode)) { + Write-Error "Not a console handle"; exit 1 +} + +$newMode = ($oldMode -band (-bnot ( + [WinConsole]::ENABLE_PROCESSED_INPUT -bor + [WinConsole]::ENABLE_LINE_INPUT -bor + [WinConsole]::ENABLE_ECHO_INPUT +))) -bor [WinConsole]::ENABLE_VIRTUAL_TERMINAL_INPUT + +[Console]::TreatControlCAsInput = $true + +if (-not [WinConsole]::SetConsoleMode($hIn, $newMode)) { + Write-Error "VT input mode not supported"; exit 1 +} + +Write-Host "VT byte dumper active. Press keys to see hex. Ctrl+Q to exit." -ForegroundColor Cyan +Write-Host ("{0,-35} {1}" -f "HEX", "ASCII") -ForegroundColor DarkGray +Write-Host ("{0,-35} {1}" -f ("=" * 35), ("=" * 20)) -ForegroundColor DarkGray + +$stdin = [Console]::OpenStandardInput() +$buf = New-Object byte[] 64 + +try { + while ($true) { + $n = $stdin.Read($buf, 0, $buf.Length) + if ($n -le 0) { break } + + # Ctrl+Q = 0x11 + if ($n -eq 1 -and $buf[0] -eq 0x11) { break } + + $hex = ($buf[0..($n-1)] | ForEach-Object { $_.ToString("X2") }) -join " " + $ascii = -join ($buf[0..($n-1)] | ForEach-Object { + if ($_ -ge 0x20 -and $_ -le 0x7E) { [char]$_ } else { "." } + }) + "{0,-35} {1}" -f $hex, $ascii + } +} +finally { + [WinConsole]::SetConsoleMode($hIn, $oldMode) | Out-Null + Write-Host "`nRestored console mode." -ForegroundColor Cyan +} diff --git a/plugins/droid-control/skills/agent-browser/SKILL.md b/plugins/droid-control/skills/agent-browser/SKILL.md new file mode 100644 index 0000000..d50fd49 --- /dev/null +++ b/plugins/droid-control/skills/agent-browser/SKILL.md @@ -0,0 +1,325 @@ +--- +name: agent-browser +description: Background knowledge for droid-control workflows -- not invoked directly. Agent-browser driver mechanics for web page and Electron desktop app automation. +user-invocable: false +--- + +# Agent-Browser Driver + +The orchestrator routed you here. Use these mechanics to execute your plan. + +Control web pages and Electron desktop apps via the `agent-browser` CLI. Uses Playwright under the hood with a headless Chromium instance managed by a background daemon. + +## When to use + +- Automating web app flows (login, form fill, data extraction, visual QA) +- Driving Electron apps (VS Code, Slack, Discord, Figma, Notion, Spotify) +- Visual verification -- screenshots and annotated element overlays +- DOM-level assertions where terminal snapshots are irrelevant + +If the target is a terminal TUI, use **tuistory** or **true-input** instead. + +## Prerequisites + +```bash +agent-browser install # one-time: downloads bundled Chromium +``` + +For Electron apps, the target app must be launched with `--remote-debugging-port=`. + +## Core workflow + +Every interaction follows the same loop: + +```bash +agent-browser open +agent-browser snapshot -i # interactive elements only -> refs like @e1, @e2 +agent-browser click @e3 # interact using refs +agent-browser snapshot -i # re-snapshot (refs invalidate after navigation/DOM changes) +agent-browser close # always close when done +``` + +## Command chaining + +Commands share a persistent daemon, so `&&` chaining is safe: + +```bash +agent-browser open https://example.com && agent-browser wait --load networkidle && agent-browser snapshot -i +``` + +Chain when you don't need intermediate output. Run separately when you need to parse refs before acting. + +## Command reference + +### Navigation + +| Command | Purpose | +|---|---| +| `open ` | Navigate (auto-prepends `https://` if no protocol) | +| `back` / `forward` / `reload` | History navigation | +| `close` | Shut down browser session | +| `connect ` | Attach to a running browser/Electron app via CDP | + +### Snapshot (page analysis) + +| Command | Purpose | +|---|---| +| `snapshot` | Full accessibility tree | +| `snapshot -i` | Interactive elements only (recommended default) | +| `snapshot -i -C` | Include cursor-interactive elements (onclick divs) | +| `snapshot -c` | Compact output | +| `snapshot -d ` | Limit tree depth | +| `snapshot -s ""` | Scope to CSS selector | + +### Interactions (use @refs from snapshot) + +| Command | Purpose | +|---|---| +| `click @e1` | Click (`dblclick` for double-click) | +| `fill @e2 "text"` | Clear field and type | +| `type @e2 "text"` | Type without clearing | +| `press Enter` | Press key (combos: `Control+a`) | +| `keyboard type "text"` | Type at current focus (no ref needed) | +| `keyboard inserttext "text"` | Insert without key events (Electron custom inputs) | +| `hover @e1` | Hover | +| `check @e1` / `uncheck @e1` | Toggle checkbox | +| `select @e1 "value"` | Select dropdown option | +| `scroll down 500` | Scroll page (`--selector` for containers) | +| `scrollintoview @e1` | Scroll element into view | +| `drag @e1 @e2` | Drag and drop | +| `upload @e1 file.pdf` | Upload file | + +### Semantic locators (when refs are unreliable) + +```bash +agent-browser find role button click --name "Submit" +agent-browser find text "Sign In" click +agent-browser find label "Email" fill "user@test.com" +agent-browser find testid "submit-btn" click +``` + +### Get information + +| Command | Purpose | +|---|---| +| `get text @e1` | Element text (`get text body > page.txt` for full page) | +| `get html @e1` | innerHTML | +| `get value @e1` | Input value | +| `get attr @e1 href` | Element attribute | +| `get title` / `get url` | Page title / URL | +| `get count ".item"` | Count matching elements | + +### Check state + +```bash +agent-browser is visible @e1 +agent-browser is enabled @e1 +agent-browser is checked @e1 +``` + +### Wait + +| Command | Purpose | +|---|---| +| `wait @e1` | Wait for element | +| `wait 2000` | Wait milliseconds | +| `wait --text "Success"` | Wait for text | +| `wait --url "**/dashboard"` | Wait for URL pattern | +| `wait --load networkidle` | Wait for network idle (best for slow pages) | +| `wait --fn "window.ready"` | Wait for JS condition | + +### JavaScript (eval) + +```bash +agent-browser eval 'document.title' + +# Complex JS -- use --stdin to avoid shell quoting issues +agent-browser eval --stdin <<'EVALEOF' +JSON.stringify(Array.from(document.querySelectorAll("a")).map(a => a.href)) +EVALEOF +``` + +### Diff (compare page states) + +```bash +agent-browser diff snapshot # current vs last snapshot +agent-browser diff snapshot --baseline before.txt # current vs saved file +agent-browser diff screenshot --baseline before.png # visual pixel diff +agent-browser diff url # compare two pages +``` + +### Dialogs + +```bash +agent-browser dialog accept [text] # accept alert/confirm/prompt +agent-browser dialog dismiss # dismiss dialog +``` + +### Tabs & frames + +```bash +agent-browser tab # list tabs +agent-browser tab new [url] # new tab +agent-browser tab 2 # switch to tab by index +agent-browser tab close # close current tab +agent-browser frame "#iframe" # switch to iframe +agent-browser frame main # back to main frame +``` + +## Screenshots & recording + +```bash +agent-browser screenshot # save to temp directory +agent-browser screenshot path.png # save to specific path +agent-browser screenshot --full # full-page screenshot +agent-browser screenshot --annotate # annotated with numbered element labels +agent-browser pdf output.pdf # save as PDF +``` + +`--annotate` overlays numbered labels on interactive elements. Each label `[N]` maps to ref `@eN`, enabling both visual verification and immediate interaction. + +Video recording: + +```bash +agent-browser record start ./demo.webm +# ... perform actions ... +agent-browser record stop +agent-browser record restart ./take2.webm # stop current + start new +``` + +Recording creates a fresh context but preserves cookies/storage. Explore first, then start recording for smooth demos. + +## Ref lifecycle + +Refs (`@e1`, `@e2`, ...) are invalidated whenever the page changes. Always re-snapshot after: + +- Clicking links/buttons that navigate +- Form submissions +- Dynamic content loading (dropdowns, modals) + +```bash +agent-browser click @e5 # navigates +agent-browser snapshot -i # MUST re-snapshot +agent-browser click @e1 # use new refs +``` + +## Electron app automation + +Any Electron app supports `--remote-debugging-port` since it's built on Chromium. + +### Launch and connect + +```bash +# macOS +open -a "Slack" --args --remote-debugging-port=9222 + +# Linux +slack --remote-debugging-port=9222 + +# Then connect +sleep 3 +agent-browser connect 9222 +agent-browser snapshot -i +``` + +**The app must be quit first** if already running -- the flag only takes effect at launch. + +### Tab management in Electron + +Electron apps often have multiple windows/webviews: + +```bash +agent-browser tab # list targets +agent-browser tab 2 # switch by index +agent-browser tab --url "*settings*" # switch by URL pattern +``` + +### Electron troubleshooting + +| Problem | Fix | +|---|---| +| "Connection refused" | Ensure app was launched with `--remote-debugging-port`; quit and relaunch if already running | +| Connect fails after launch | `sleep 3` before connecting; app needs time to initialize | +| Elements missing from snapshot | Try `snapshot -i -C`; use `tab` to switch to the correct webview | +| Cannot type in fields | Use `keyboard type "text"` or `keyboard inserttext "text"` for custom input components | +| Dark mode lost | Set `AGENT_BROWSER_COLOR_SCHEME=dark` or use `--color-scheme dark` | + +## State persistence + +Save and restore cookies/localStorage across sessions: + +```bash +agent-browser open https://app.example.com/login +# ... login flow ... +agent-browser state save auth.json + +# Later: load saved state +agent-browser state load auth.json +agent-browser open https://app.example.com/dashboard +``` + +Auto-save/restore with named sessions: + +```bash +agent-browser --session-name myapp open https://app.example.com +# state auto-saved on close, auto-loaded on next launch with same --session-name +``` + +## Sessions + +The browser persists via a background daemon. One session is the default. + +```bash +agent-browser --session test1 open site-a.com +agent-browser --session test2 open site-b.com +agent-browser session list +agent-browser --session test1 close +agent-browser --session test2 close +``` + +Each `--session` spawns a separate Chromium process (~300 MB). Prefer navigating within a single session. Exception: controlling multiple Electron apps on different CDP ports. + +## Global options + +| Flag | Purpose | +|---|---| +| `--session ` | Isolated browser session | +| `--headed` | Show browser window | +| `--cdp ` | Connect via CDP | +| `--auto-connect` | Auto-discover running Chrome | +| `--proxy ` | Use proxy server | +| `--color-scheme dark` | Force dark/light mode | +| `--ignore-https-errors` | Accept self-signed certs | +| `--allow-file-access` | Enable `file://` URLs | +| `--json` | JSON output for parsing | + +## Debugging + +```bash +agent-browser --headed open example.com # visible browser +agent-browser console # view console messages +agent-browser errors # view page errors +agent-browser highlight @e1 # highlight element +``` + +## Gotchas + +- **Invisible-to-snapshot elements.** `contenteditable` divs and custom components may not appear in accessibility snapshots. Use `eval` to interact: + ```bash + agent-browser eval --stdin <<'EVALEOF' + const el = document.querySelector("[contenteditable]"); + el.focus(); + el.textContent = "hello"; + el.dispatchEvent(new Event('input', { bubbles: true })); + EVALEOF + ``` +- **Unstable class names.** Never hardcode CSS-in-JS class names (`sc-*`, `css-*`). Find elements by text content, `cursor: pointer` style, or `testid` instead. +- **SPA loading delays.** Single-page apps may take 5-10s to render after navigation. Double-wait: `wait --load networkidle` then `wait 5000`. +- **Flag ordering.** Global flags (`--headers`, `--session`, `--cdp`) must come **before** the subcommand: `agent-browser --headers '{}' open `. + +## Critical rules + +1. **Always take screenshots for visual QA.** Text snapshots miss layout, styling, alignment, and z-index issues. Use `screenshot --annotate` when you need both visual proof and element refs. +2. **One session by default.** Navigate between pages with `open ` instead of creating new sessions. +3. **Always close when done.** `agent-browser close` frees the Chromium process. +4. **Re-snapshot after every navigation.** Refs are invalidated. diff --git a/plugins/droid-control/skills/capture/SKILL.md b/plugins/droid-control/skills/capture/SKILL.md new file mode 100644 index 0000000..7d2c0a1 --- /dev/null +++ b/plugins/droid-control/skills/capture/SKILL.md @@ -0,0 +1,144 @@ +--- +name: capture +description: Background knowledge for droid-control workflows -- not invoked directly. Recording lifecycle for terminal and browser sessions. +user-invocable: false +--- + +# Capture + +The orchestrator routed you here. This atom owns the full recording lifecycle: launch a target, execute an interaction script, collect raw outputs. + +You should already have a **driver atom** loaded (tuistory, true-input, or agent-browser) and optionally a **target atom** (droid-cli). This atom layers the recording discipline on top. + +## Inputs + +The command that invoked you should have provided: + +- **Target**: what to launch and on which branch(es) +- **Interaction script**: the sequence of actions to perform +- **What to capture**: recordings (.cast/.mp4), screenshots, text snapshots, byte sequences +- **Keystroke logging**: whether to emit a keystroke TSV for later overlay + +## Recording lifecycle + +### 1. Pre-flight + +Before recording anything: + +- Terminal size is consistent across all sessions (`--cols 120 --rows 36`) +- Branch/worktree paths and env vars are correct +- Recording format matches the driver: `.cast` for tuistory, `.mp4` for true-input, screenshots for agent-browser +- If comparing branches, both sessions use identical terminal dimensions and launch parameters +- For `droid-dev` captures, `--repo-root` is **mandatory** — `tctl` will refuse to launch without it +- **Color env vars are set** (see below) + +```bash +TCTL=${DROID_PLUGIN_ROOT}/bin/tctl +# RUN_ID and RUN_DIR should already be set by the parent (see droid-control ground rule 5) +``` + +### 2. Launch and record + +**CRITICAL: tuistory's virtual PTY does not advertise color support by default.** Node.js apps (Ink/chalk) detect this and suppress ALL color escape codes, producing a monochrome recording. You **must** pass `FORCE_COLOR=3` and `COLORTERM=truecolor` to force full 24-bit color output. Without these, agg has nothing to theme and the video will look grey/desaturated regardless of the agg theme chosen. + +**Single branch:** +```bash +$TCTL launch "droid-dev" -s ${RUN_ID}-demo --backend tuistory \ + --repo-root /path/to/worktree \ + --cols 120 --rows 36 --record ${RUN_DIR}/demo.cast \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor +``` + +**Comparison (before/after):** +```bash +$TCTL launch "droid-dev" -s ${RUN_ID}-before --backend tuistory \ + --repo-root /path/to/baseline-worktree \ + --cols 120 --rows 36 --record ${RUN_DIR}/before.cast \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor + +$TCTL launch "droid-dev" -s ${RUN_ID}-after --backend tuistory \ + --repo-root /path/to/candidate-worktree \ + --cols 120 --rows 36 --record ${RUN_DIR}/after.cast \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor +``` + +**Browser:** +```bash +agent-browser open +agent-browser record start ${RUN_DIR}/demo.webm +``` + +### 3. Execute the interaction script + +Film for a viewer with no context. You are a director, not an operator. + +- **Record before setup** -- the baseline state is act 1. +- **Hold after state changes** -- 2-3 seconds so text is readable. Use `snapshot --trim` as natural verification beats. +- **Verify between steps** -- `wait` or `snapshot` to confirm state before proceeding. Don't blindly fire the next key. +- **Verification IS evidence.** A snapshot that shows nothing changed after pressing ESC proves the session is frozen. A snapshot that shows an error message proves the command was blocked. Always snapshot after actions where the *absence* of a response is the point -- the viewer needs to see it too. + +For comparison recordings, both branches run **identical interactions** -- only the behavior differs. + +### 4. Keystroke logging + +If the workflow requires keystroke overlay, emit a TSV file during recording. Since every interaction is scripted, the timing data is already known. + +Write each keystroke's timestamp (seconds from recording start) and a human-readable label: + +``` +0.5 droid --fork +1.2 Enter +2.8 Ctrl+C +4.0 Esc +``` + +Use readable key names (`Ctrl+C`, not `\x03`). Save alongside the recording (e.g., `/tmp/keys.tsv`). + +### 5. Close and verify raw outputs + +```bash +$TCTL -s demo close # finalizes the .cast / stops recording +``` + +Before handing off, confirm every expected output file exists and is non-empty: +- Recording files (.cast, .mp4, .webm) +- Screenshot files (.png) +- Keystroke TSV (if committed) +- Text snapshot logs (if needed for the report) + +## Evidence capture patterns + +| Proof type | How to capture | +|---|---| +| Functional behavior | Text snapshots: `$TCTL -s snapshot --trim` | +| Visual rendering | Screenshots: `$TCTL -s screenshot -o /tmp/proof-N.png` | +| Keyboard encoding | PTY bytes: `${DROID_PLUGIN_ROOT}/scripts/capture-terminal-bytes.py --backend --combo ` | +| Web/Electron | Screenshots: `agent-browser screenshot --annotate /tmp/proof-N.png` | +| Before/after | Run the same sequence on both branches at the same capture points | + +## Outputs + +Hand these to the **compose** stage: + +``` +## Capture outputs +- clips: [/tmp/before.cast, /tmp/after.cast] +- screenshots: [/tmp/proof-1.png, /tmp/proof-2.png] +- keys: /tmp/keys.tsv (if keystroke logging was requested) +- driver: tuistory | true-input | agent-browser +- terminal_size: 120x36 +``` + +## Recovery + +If a session gets stuck mid-recording: + +```bash +$TCTL -s press esc # bail out of stuck dialog +$TCTL -s snapshot --trim # check visible state +$TCTL -s close # hard reset +``` + +For browser: `agent-browser close`. + +Then re-launch and re-record. Partial recordings are not usable. diff --git a/plugins/droid-control/skills/compose/SKILL.md b/plugins/droid-control/skills/compose/SKILL.md new file mode 100644 index 0000000..3c2a06b --- /dev/null +++ b/plugins/droid-control/skills/compose/SKILL.md @@ -0,0 +1,318 @@ +--- +name: compose +description: Background knowledge for droid-control workflows -- not invoked directly. Video assembly via Remotion — title cards, layout, transitions, effects, and showcase polish. +user-invocable: false +--- + +# Compose + +This atom owns the full video assembly pipeline. You receive raw outputs from the **capture** stage and produce a single polished artifact. Follow the pipeline below step by step. + +## Inputs + +The command or capture stage should have provided a handoff with two sections: + +### Mechanical (structured) + +- **clips**: paths to raw recordings (.cast, .mp4, .webm, .png) +- **driver**: tuistory | true-input | agent-browser +- **layout**: `single` | `side-by-side` +- **labels**: text for each clip (e.g., "BEFORE (dev)", "AFTER (PR)") +- **speed**: multiplier (default 3x) +- **fidelity**: `auto` | `compact` | `standard` | `inspect` (optional; auto => side-by-side=inspect, single=standard) +- **title**: text for the title card +- **subtitle**: one-sentence summary +- **sections**: text banners for chapters `[{t, title}]` (optional) +- **keys**: keystroke events `[{t, label, dur?}]` (if overlay requested) +- **showcase**: preset name -- `macos`, `minimal`, `hero`, `presentation`, `factory`, `factory-hero` +- **effects tier**: `utilitarian` | `full` | `none` (see "Choosing effects at compose time" below) +- **output**: desired output path + +### Creative (natural language) + +Free-text guidance on what to emphasize: which moments to hold, what the title card should convey, whether phase cards are warranted, how to trim dead time. Use this -- along with the effects tier -- for editorial decisions, including choosing specific effects to apply. + +## Pipeline + +``` +1. Build props → construct the Showcase JSON props +2. Render → render-showcase.sh (converts .cast, stages clips, renders, cleans up) +3. Finalize → verify and output +``` + +Remotion handles all compositing in a single pass — title cards, transitions, window chrome, backgrounds, keystroke overlays, spotlights, particles, noise, and color grading are all automatic. You construct the props JSON; the engine does the rest. + +## Remotion project & helper script + +```bash +REMOTION_DIR=${DROID_PLUGIN_ROOT}/remotion +RENDER=${DROID_PLUGIN_ROOT}/scripts/render-showcase.sh +``` + +## Showcase mode vs Demo mode + +Both use the same Remotion pipeline but target different visual registers. + +| | Showcase | Demo | +|---|---|---| +| **Goal** | Cinematic, high-polish marketing material | Clear, utilitarian before/after comparison | +| **Preset** | `factory`, `factory-hero`, or `hero` | `macos`, `minimal`, or `presentation` | +| **Effects tier** | **Full** -- spotlight, zoom, callout, keystroke overlay. Go all out. | **Utilitarian** -- zoom for readability, keystroke overlay for user actions | +| **Audience** | External — landing pages, social, marketing | Internal — PR reviews, docs, QA | + +**Decision rule**: If the video will be seen outside the eng team, use Showcase mode. If it's for a PR description, internal demo, or documentation embed, use Demo mode. The visual polish layers (warm glow, particles, color grade, motion blur) are always present but their intensity is palette-driven — Factory presets produce rich cinematic warmth while Catppuccin presets stay subtle and cool. + +### Choosing effects at compose time + +The command stage committed an **effects tier** (utilitarian, full, or none). Now that you have actual recordings, choose specific effects: + +- **Utilitarian**: Add zoom effects for any small or hard-to-read text. Add keystroke overlay if user actions were captured. Skip spotlight and callout unless something is genuinely hard to find on screen. +- **Full**: Use the full palette. Spotlight the key proof points. Zoom into details. Add callout annotations where the UI isn't self-explanatory. Layer keystroke overlay throughout. Aim for cinematic -- the viewer should feel guided, not left to scan. +- **None**: Pass `"effects": []` in props. Keystroke overlay is still allowed if committed separately. + +## Step 1: Choose fidelity and pacing + +`render-showcase.sh` auto-selects `inspect` for side-by-side and `standard` for single-clip layouts when `fidelity` is omitted or set to `auto`. + +| Fidelity | Default output size | Remotion encode | Polish overlays | Best for | +|---|---|---|---|---| +| `compact` | 1920x1080 | H.264 CRF 21, JPEG frames | full grain + grade | Small embeds | +| `standard` | 1920x1080 | H.264 CRF 18, JPEG frames | reduced grain + grade | Single-panel demos | +| `inspect` | 2560x1440 | H.264 CRF 14, PNG frames | minimal grain + grade | Side-by-side comparisons / tiny text | + +### .cast conversion behavior + +`render-showcase.sh` converts `.cast` inputs through `agg -> gif -> ffmpeg -> mp4` before Remotion render, using the asciicast's own cols/rows and fixed font metrics so element positions remain stable across fidelity profiles. + +**CRITICAL: `agg` replaces ALL 16 ANSI colors with its theme palette.** The render script uses a custom Droid CLI theme. If you manually run `agg`, never omit `--theme` and never use built-in themes like `monokai` or `dracula`. + +The `--theme` flag accepts a comma-separated hex string (no `#` prefix): `bg,fg,color0..color7` (10 values) or `bg,fg,color0..color7,color8..color15` (18 values for bright variants). + +Note: `tuistory` recordings of the Droid CLI typically emit NO color escape codes -- the CLI uses Ink's direct rendering which doesn't produce standard ANSI SGR sequences in the cast output. The theme's `bg` (first value: `181818`) and `fg` (second value: `e0d0c0`, warm white) are the only colors that will affect the output. The warm-white fg avoids the cold blue-grey look of default themes. + +For other terminals that DO emit ANSI color codes, build the full theme string from the terminal's actual color settings. + +**Pacing**: Target the final video duration, not a speed factor. A blind multiplier either makes text illegible or leaves dead air. + +| Demo type | Target duration | Why | +|---|---|---| +| Single feature, 3-5 steps | 30-45s | Viewer watches the whole thing in one breath | +| Before/after comparison, side-by-side | 45-75s | Each panel needs time to land; frozen-vs-active contrasts need a beat | +| Multi-phase or complex flow | 60-120s | Phase cards give the viewer reset points; rushing defeats the purpose | + +Set the `speed` prop to hit the target: if the raw recording is 3 minutes and the target is 60s, use `"speed": 3`. If it's already 40s raw, use `"speed": 1` or trim dead time instead. **Trim first, speed second** -- cut LLM thinking pauses, build waits, and network delays from the `.cast` with `asciinema cut` or by splitting segments, then apply a gentle speed-up only if still over target. + +**Keystroke timing adjustment**: If a keystroke list was emitted during capture, its timestamps are in raw recording time. When you apply a speed multiplier, you **must** divide every timestamp by the speed factor before passing it to the Remotion props. A keystroke at raw `t=6.0s` in a 3x video should appear at `t=2.0s`. + +### Non-.cast clips + +`.mp4`, `.webm`, and `.png` clips are passed through to Remotion unchanged except for staging into `public/`. Re-encode non-`.cast` clips manually only if their pixel format or dimensions are invalid. + +### Duration checkpoint (mandatory, before proceeding) + +Check whether the planned speed factor produces a final duration within the pacing table's target range: + +``` +final_duration = clip_duration / speed_factor +``` + +| If final_duration is... | Action | +|---|---| +| Within the target range | Proceed | +| Below the minimum | **Reduce the speed factor** until the target is met. If even at 1x the clip is below the minimum, the recording is too short — return to **capture** and add more interaction steps. | +| Above the maximum | Trim dead time first (`asciinema cut`), then increase the `speed` prop | + +This checkpoint is not optional. A video that lands outside the target range fails verification. + +## Step 2: Build props + +Save the `showcaseSchema` JSON to a temp file: + +```bash +DEMO_TMP="$(mktemp -d /tmp/droid-demo-XXXXXX)" +PROPS="${DEMO_TMP}/showcase-props.json" + +cat > "$PROPS" << 'EOF' +{ + "clips": ["before.cast", "after.cast"], + "layout": "side-by-side", + "fidelity": "auto", + "labels": ["BEFORE (dev)", "AFTER (PR #11621)"], + "speed": 3, + "title": "PR #11621 — Prevent session freezes", + "subtitle": "Bash Mode blocks interactive commands and supports ESC cancellation", + "preset": "factory", + "keys": [ + {"t": 2.0, "label": "vim"}, + {"t": 5.5, "label": "sleep 100"}, + {"t": 8.0, "label": "Esc"} + ], + "sections": [ + {"t": 2.0, "title": "Testing basic echo"}, + {"t": 10.0, "title": "Testing long loop"} + ], + "effects": [], + "speedNote": "3x speed", + "windowTitle": "droid demo" +} +EOF +``` + +Use a run-scoped props path like `$PROPS`; do not reuse a global `/tmp/showcase-props.json` across rerenders or concurrent demos. + +**CRITICAL: `clipDuration` handling.** The render script auto-detects clip duration via ffprobe when `clipDuration` is omitted from the props. If you set it manually, it **must** match the actual clip duration or you get blank frames (too long) or truncation (too short). When in doubt, omit it and let the script auto-detect. + +### Props reference + +| Prop | Type | Required | Description | +|---|---|---|---| +| `clips` | `string[]` | yes | Filenames (basenames only — the render script handles staging) | +| `layout` | `"single" \| "side-by-side"` | yes | Composition layout | +| `labels` | `string[]` | yes | Labels for each clip (visible in side-by-side; pass `[]` for single) | +| `fidelity` | `"compact" \| "standard" \| "inspect"` | no | Output quality/compression profile. Omit for auto-selection by layout. | +| `speed` | `number` | no | Playback speed for `.cast -> agg` conversion. | +| `title` | `string` | yes | Title card heading | +| `subtitle` | `string` | yes | Title card subheading | +| `preset` | preset name | yes | Visual preset — see table below | +| `keys` | `Keystroke[]` | yes | Keystroke overlay events (pass `[]` for none) | +| `sections` | `Section[]` | no | Section banners to mark chapters (pass `[]` for none) | +| `effects` | `Effect[]` | yes | Effect timeline (pass `[]` for none) | +| `clipDuration` | `number` | no | Clip duration in seconds. **Auto-detected by render script if omitted.** | +| `speedNote` | `string` | no | Shown on title card (e.g., `"3x speed"`) | +| `windowTitle` | `string` | no | Text in the window title bar | +| `width` | `number` | no | Output width (default: 2560 for inspect, else 1920) | +| `height` | `number` | no | Output height (default: 1440 for inspect, else 1080) | + +### Preset quick reference + +| Preset | Look | Palette | Best for | +|---|---|---|---| +| `factory` | Warm black bg, traffic lights, 12px radius, 80px margin | Factory (warm) | Official Factory content | +| `factory-hero` | Same as factory + gradient bg | Factory (warm) | Factory landing pages, social | +| `hero` | Gradient bg, generous margins, prominent shadow | Catppuccin (cool) | Non-Factory marketing | +| `macos` | Dark bg, traffic lights, clean frame | Catppuccin (cool) | General-purpose demos | +| `presentation` | Black bg, generous margins | Catppuccin (cool) | Slide decks, talks | +| `minimal` | No window bar, tiny radius, tight margins | Catppuccin (cool) | Docs embeds, inline clips | + +### Keystroke schema + +``` +{ t: number, label: string, dur?: number } +``` + +- `t`: Time in seconds relative to clip start (not title card). Adjust for speed factor. +- `label`: Display text (e.g., `"Ctrl+C"`, `"vim main.rs"`) +- `dur`: Display duration in seconds (default: 1.2s). Auto-cut when next keystroke starts. + +### Section schema + +```json +{ "t": 2.0, "title": "Testing basic echo" } +``` + +- `t`: Time in seconds relative to clip start (not title card). Adjust for speed factor. +- `title`: Display text for the section header. Remains visible until the next section starts. + +### Effect types + +| Effect | Props | Description | +|---|---|---| +| `fade-in` | `t`, `dur` | Fade from black | +| `fade-out` | `t`, `dur` | Fade to black | +| `zoom` | `t`, `dur`, `to: {x,y,w,h}` | Directed zoom to a target region (30% in, 40% hold, 30% out) | +| `spotlight` | `t`, `dur`, `on: {x,y,w,h}`, `dim?` | Dim everything except a region (`dim`: 0–1, default 0.6) | +| `callout` | `t`, `dur`, `text`, `at: {x,y}` | Text overlay anchored to a point | + +Regions use percentage strings (e.g., `"25%"`) relative to the video dimensions. + +### When to use effects + +| Effect | Use when… | Don't use when… | +|---|---|---| +| `spotlight` | Drawing attention to a specific region (error, status change) | The whole frame is relevant | +| `zoom` | Small text or detail that's hard to read at full scale | Content is already legible | +| `callout` | Annotating something the viewer might not recognize | The UI is self-explanatory | +| Keystroke overlay | Showing user actions (typing, key presses) | No user interaction in the clip | + +**Less is more.** One well-timed spotlight has more impact than five overlapping effects. + +## Step 3: Render + +Use the render script — it handles clip staging, duration detection, rendering, and cleanup: + +```bash +RENDER=${DROID_PLUGIN_ROOT}/scripts/render-showcase.sh + +# Basic render +$RENDER --props "$PROPS" --output /tmp/demo.mp4 \ + /tmp/before.cast /tmp/after.cast + +# Or with inline props (useful for simple cases) +$RENDER --props-inline '{"clips":["clip.mp4"],"layout":"single","labels":[],"title":"Demo","subtitle":"Test","preset":"macos","keys":[],"effects":[]}' \ + --output /tmp/demo.mp4 /tmp/clip.mp4 +``` + +The script: +1. Converts `.cast` inputs to `.mp4` using the selected fidelity profile +2. Copies clip files into `${REMOTION_DIR}/public/` +3. Auto-detects `clipDuration` via ffprobe if missing from props +4. Runs `npx remotion render Showcase` with profile-specific encode flags +5. Cleans up staged and generated clips + +**Quick frame check** (sanity-check layout before full render): + +```bash +cd ${REMOTION_DIR} +npx remotion still Showcase --props="$(cat "$PROPS")" --frame=30 --scale=0.5 /tmp/check.png +``` + +**Render time**: Expect ~1-3 minutes for a 30-60s video at 1920x1080. Set worker timeouts accordingly (5 minutes is safe). + +## Step 4: Finalize + +Check the result: + +```bash +ffprobe -v quiet -print_format json -show_format -show_streams /tmp/demo.mp4 +``` + +Confirm: +- Resolution is 1920x1080 (or matches the expected output) +- Duration is reasonable (not 0s, not hours) +- File size is manageable (under 5 MB for GitHub embeds, 25 MB hard limit) +- Pixel format is yuv420p (universal playback) + +## Outputs + +Hand to the **verify** stage: + +``` +## Compose outputs +- video: /tmp/demo-pr-11621.mp4 +- resolution: 1920x1080 +- duration: 42s +- size: 3.2 MB +- preset: factory +- keystrokes: 3 events overlaid +- effects: 1 spotlight +- engine: remotion +``` + +## Screenshot-only artifacts (proofs, QA) + +Not every deliverable is a video. For proof and QA workflows, compose may just organize screenshots and snapshots: + +### Annotated screenshot set + +```bash +ffmpeg -y -i before.png -i after.png \ + -filter_complex " + [0:v]scale=960:-1[left]; + [1:v]scale=960:-1[right]; + [left][right]hstack=inputs=2[out]" \ + -map "[out]" comparison.png +``` + +### Markdown report with embedded evidence + +For text-based deliverables, organize the evidence into a structured report rather than a video. The verify stage handles this. diff --git a/plugins/droid-control/skills/droid-cli/SKILL.md b/plugins/droid-control/skills/droid-cli/SKILL.md new file mode 100644 index 0000000..76f67a6 --- /dev/null +++ b/plugins/droid-control/skills/droid-cli/SKILL.md @@ -0,0 +1,138 @@ +--- +name: droid-cli +description: Background knowledge for droid-control workflows -- not invoked directly. Droid CLI target patterns, shortcuts, modes, and launch helpers. +user-invocable: false +--- + +# Droid CLI Target + +The orchestrator routed you here. Layer these target-specific patterns on top of the driver skill you already loaded. + +Droid-specific shortcuts, modes, and launch patterns. + +## Shortcuts + +| Action | Key chord | Result | +|---|---|---| +| Toggle Spec mode | `shift tab` | toggles Spec mode on/off | +| Cycle autonomy | `ctrl l` | Off > Low > Med > High > Off | +| Cycle models | `ctrl n` | cycles available models | +| Cycle reasoning | `tab` | High > none > Low > Medium > High | +| Cancel / close / stop | `esc` | stops streaming, closes overlays | +| Clear input | `ctrl u` | clears current line | +| Toggle bash mode | `!` on empty input | switches prompt between `>` and `$` | +| Help / shortcuts | `?` | opens keybinding help | +| Multiline input | `shift enter` | inserts newline without submitting | + +## Dialogs + +When a dialog shows `Use up/down to navigate...`: `up`/`down` moves the highlight, `enter` selects, `esc` closes. + +## Slash commands + +| Command | Purpose | +|---|---| +| `/help` | Show commands | +| `/settings` | Open settings menu | +| `/model` | Open model selector | +| `/clear` or `/new` | Start a new session | +| `/sessions` | Browse previous sessions | +| `/review` | Start AI code review | +| `/status` | Show current config | +| `/cost` | Show usage / cost | +| `/compress [prompt]` | Summarize and move to fresh session | + +## File mentions + +Type `@` to open file suggestions, filter by typing, `tab` to accept, `esc` to cancel: + +```bash +$TCTL -s demo type "review @" +$TCTL -s demo type "package.json" +$TCTL -s demo press tab +``` + +## Visual cues + +| State | What to look for | +|---|---| +| Spec mode on | input border shows `Spec` | +| Bash mode on | prompt is `$` | +| Idle / ready | prompt is `>` with no spinner | +| Dialog open | boxed menu + navigation hint | +| File suggestions | dropdown under input | +| Thinking | `Thinking...` and stop hint | + +## Launching Droid + +### How `droid-dev` works + +`droid-dev` is a thin bash shim at `~/.local/bin/droid-dev`. It runs `bun` against whichever checkout `DROID_DEV_REPO_ROOT` points to — it does NOT use a pre-compiled binary. This means: + +- **No per-branch builds.** One `npm run setup` (in any checkout) installs the shim. Switching branches is instant via `--repo-root`. +- **Prerequisite:** The target worktree must have `node_modules` installed (`npm install` at the repo root). If missing, the bun launch fails. +- **`tctl --repo-root`** sets `DROID_DEV_REPO_ROOT` automatically and pins the session to that worktree. + +### `droid-dev` + +`droid-dev` launches require `--repo-root` (or `--env DROID_DEV_REPO_ROOT=...`). `tctl` enforces this — launches without it will fail: + +```bash +# tuistory (default — virtual PTY) +$TCTL launch "droid-dev" -s demo --backend tuistory \ + --repo-root /path/to/worktree \ + --cols 120 --rows 36 + +# true-input (real terminal proof — headless Wayland compositor) +$TCTL launch "droid-dev" -s demo --backend true-input \ + --repo-root /path/to/worktree +``` + +### Feature branch / worktree + +For comparisons, launch separate sessions pointing at different worktrees: + +```bash +$TCTL launch "droid-dev" -s before --backend tuistory \ + --repo-root /path/to/baseline-worktree \ + --cols 120 --rows 36 --record /tmp/before.cast \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor + +$TCTL launch "droid-dev" -s after --backend tuistory \ + --repo-root /path/to/candidate-worktree \ + --cols 120 --rows 36 --record /tmp/after.cast \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor +``` + +### Comparison setup (before/after demos) + +For before/after comparisons, you need two worktree paths — one for the baseline and one for the candidate. + +1. **Find existing worktrees**: `git worktree list` in any checkout. The main clone (often on `dev` or `main`) is a valid baseline. +2. **Create if needed**: `git worktree add /tmp/baseline-worktree dev` (or the relevant base branch). +3. **Ensure `node_modules`**: Run `npm install` in any worktree that lacks it. This is the only setup needed — no `npm run setup` or CLI build per branch. +4. **Launch with `--repo-root`**: Each `tctl launch` pins to one worktree. + +### Environment safety + +`tctl`'s runner script launches `droid-dev` via `bash -lc`, which loads a clean login shell. Stale `FACTORY_*` env vars from parent processes are typically overridden by the runner. If you suspect env contamination, pass explicit overrides with `--env`. + +## Exec mode + +Non-interactive single-shot execution: + +```bash +droid exec "analyze this file" +droid exec --auto medium "run the tests" +``` + +## Logging + +Enable debug logging by passing the log file path via `--env`: + +```bash +$TCTL launch "droid-dev" -s demo --backend tuistory \ + --repo-root /path/to/worktree \ + --env FACTORY_LOG_FILE=/tmp/droid-test.log +tail -f /tmp/droid-test.log +``` diff --git a/plugins/droid-control/skills/droid-control/SKILL.md b/plugins/droid-control/skills/droid-control/SKILL.md new file mode 100644 index 0000000..cb0be05 --- /dev/null +++ b/plugins/droid-control/skills/droid-control/SKILL.md @@ -0,0 +1,178 @@ +--- +name: droid-control +description: Control terminal TUIs and web/Electron apps for testing, demos, QA, and computer-use tasks. Use when you need to automate a CLI, drive a browser, record a demo, or capture proof artifacts. +--- + +# Droid Control + +Automate terminals and browsers. Three routing decisions, then atoms guide you the rest of the way. + +## Ground rules + +1. **Real apps, real environments.** Non-deterministic behavior (LLM responses, network latency, variable output) is expected. Handle it with `wait` / `wait-idle`. Never substitute fixtures or mocked data. +2. **Commit to execution.** Once you've chosen a driver, run the plan. If something fails mid-run, recover and retry -- don't re-evaluate the approach. +3. **Atoms are self-contained.** Load one and follow its mechanics. No cross-referencing needed. +4. **`tctl` is the ONLY way to launch recorded sessions.** `tctl` manages recording by wrapping `asciinema rec` around the PTY — raw `tuistory` has no recording capability and never will. Never call `tuistory launch` directly; unknown flags crash `tuistory-relay`. Always resolve `TCTL` to its absolute filesystem path before use, especially when delegating to workers (they don't inherit `${DROID_PLUGIN_ROOT}`). +5. **Isolate every run.** Multiple droids may be filming simultaneously on the same machine. Session names and output paths share a global namespace (`/tmp/tctl-sessions/`). At the start of every workflow, generate a run ID (`RUN_ID=$(date +%s)-$$` or similar) and use it as a prefix for all session names and a scoped temp directory for all output files: + ```bash + RUN_ID="$(date +%s)-$$" + RUN_DIR="$(mktemp -d /tmp/droid-run-${RUN_ID}-XXXXXX)" + # Session names: -s ${RUN_ID}-before, -s ${RUN_ID}-after + # Output paths: ${RUN_DIR}/before.cast, ${RUN_DIR}/after.cast + ``` + Never use bare session names like `-s demo`, `-s before`, `-s after` — they will collide with concurrent runs. + +## Routing + +Three independent lookups. Do all three, then load the union of skills they produce. + +### 1. Target route — what are you driving? + +| Target | Load these skills | +|---|---| +| Droid CLI (`droid-dev`, `droid exec`) | **droid-cli** + tuistory backend via `${DROID_PLUGIN_ROOT}/bin/tctl` | +| Droid CLI (real terminal proof) | **true-input** + **droid-cli** | +| Other terminal TUI | tuistory backend via `${DROID_PLUGIN_ROOT}/bin/tctl` | +| Other terminal TUI (real terminal proof) | **true-input** | +| Web page or Electron app | **agent-browser** | +| Raw terminal byte sequences | **true-input** + **pty-capture** | + +**tuistory** is the default for terminal work. Use **true-input** only when you need real terminal rendering evidence. + +### 2. Stage route — what does the workflow need? + +Every workflow passes through stages. Load the atoms for each stage you'll use. + +| Stage | Skill | When to load | +|---|---|---| +| Capture | **capture** | Always -- every workflow records or captures something | +| Compose | **compose** | When the deliverable is a produced artifact (video, annotated screenshots, comparison image) | +| Verify | **verify** | Always -- every deliverable gets checked against commitments | + +### 3. Artifact route — does compose need polish tools? + +Only relevant when **compose** is loaded. + +| Artifact need | Also load | +|---|---| +| Showcase polish (window chrome, branded frame, cinematic background) | **showcase** | +| Effects and keystroke overlays | (compose handles this — they're fields in the Remotion props JSON) | + +## Workflow shape + +``` +Command (intent + commitments) + → Target route (load driver atoms) + → Capture (record / screenshot / byte-capture) + → Compose (assemble deliverable, if needed) + → Verify (check against commitments) + → Report +``` + +Commands declare **what** to produce. Atoms own **how**. + +## Delegation + +The parent agent plans and orchestrates. Mechanical work runs in **worker subagents** via the Task tool. This keeps the parent's context clean and enables parallelism. + +### What to delegate + +| Task | Delegate? | Why | +|---|---|---| +| **Capture before branch** | YES — `run_in_background=true` | Independent from after branch; run both in parallel | +| **Capture after branch** | YES — `run_in_background=true` | Independent from before branch | +| **Remotion render** | YES | Needs only props JSON, clip paths, output path. Runs `render-showcase.sh` (handles .cast conversion, fidelity profiles, duration detection, cleanup) | +| Planning, interaction scripting | NO — parent | Requires PR context and editorial judgment | +| Layout and prop construction | NO — parent | Requires editorial decisions about effects, timing, labels | +| Verification | NO — parent | Requires commitment context | +| Single ffprobe / file-existence check | NO — inline | Too trivial for subagent overhead | + +### How to delegate + +**Step 0: Resolve paths and generate a run ID.** Workers don't inherit `${DROID_PLUGIN_ROOT}`. Resolve once, paste everywhere: + +```bash +TCTL="$(realpath "${DROID_PLUGIN_ROOT}/bin/tctl")" +RENDER="$(realpath "${DROID_PLUGIN_ROOT}/scripts/render-showcase.sh")" +RUN_ID="$(date +%s)-$$" +RUN_DIR="$(mktemp -d /tmp/droid-run-${RUN_ID}-XXXXXX)" +``` + +Use `${RUN_DIR}` for all output files (recordings, props, rendered video). Use `${RUN_ID}-` as a prefix for all session names. Never use bare names like `-s before` or hardcoded paths like `/tmp/before.cast`. + +Give workers **exact commands** with the resolved absolute paths — not abstract instructions, not `tuistory`, not `${DROID_PLUGIN_ROOT}`. The parent does the thinking; the worker executes: + +``` +Task prompt for a capture worker: + "Run these commands in order. Report the output file path and any errors. + 1. /abs/path/to/bin/tctl launch "droid-dev" -s 1712345678-42-before --backend tuistory \ + --repo-root /abs/path/to/baseline/worktree \ + --cols 120 --rows 36 --record /tmp/droid-run-1712345678-42-xxxx/before.cast \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor + 2. /abs/path/to/bin/tctl -s 1712345678-42-before wait ">" --timeout 15000 + 3. /abs/path/to/bin/tctl -s 1712345678-42-before type "hello world" + 4. /abs/path/to/bin/tctl -s 1712345678-42-before press enter + 5. /abs/path/to/bin/tctl -s 1712345678-42-before wait-idle + 6. /abs/path/to/bin/tctl -s 1712345678-42-before close" +``` + +``` +Task prompt for a Remotion render worker: + "Run this command. Report the output file path and any errors. + /abs/path/to/scripts/render-showcase.sh \ + --props /tmp/droid-run-1712345678-42-xxxx/showcase-props.json \ + --output /tmp/droid-run-1712345678-42-xxxx/demo.mp4 \ + /tmp/droid-run-1712345678-42-xxxx/before.cast /tmp/droid-run-1712345678-42-xxxx/after.cast" +``` + +### Parallel capture pattern + +For before/after comparison demos, launch both capture workers simultaneously: + +``` +1. Parent constructs the interaction script (identical for both branches) +2. Launch worker A: capture the baseline/reference branch with `--repo-root` set to that worktree +3. Launch worker B: capture the candidate/change branch with `--repo-root` set to that worktree +4. Wait for both to complete (TaskOutput) +5. Collect .cast paths from results +6. Continue to compose +``` + +## Shared tooling + +Terminal drivers use the unified `tctl` wrapper. agent-browser has its own CLI and does not use `tctl`. + +Drivers can be combined in one workflow — e.g., `tctl` for a CLI and `agent-browser` for a web UI it interacts with. + +## Prerequisites + +| Stage | Platform | Required | Optional | +|---|---|---|---| +| tuistory | All | `tuistory`, `asciinema`, `agg` | `tmux` | +| true-input | Linux/Wayland | `cage`, `wtype`, Wayland terminal, `/dev/dri/*` | `grim`, `wf-recorder` | +| true-input | Windows (KVM) | `libvirt`, `qemu`, KVM VM with SPICE + SSH, `DROID_VM_*` env vars | `virt-manager` | +| true-input | macOS (QEMU) | `qemu`, `socat`, macOS VM with SSH, `DROID_MAC_*` env vars | — | +| agent-browser | All | `agent-browser` (+ `agent-browser install`) | — | +| compose | All | `ffmpeg`, `ffprobe`, `agg` | — | +| showcase | All | Node.js (>= 18), Chrome/Chromium | — | + +### Install commands + +```bash +# tuistory driver + recording +npm install -g tuistory # virtual PTY driver +pip install asciinema # terminal recording (tctl wraps this) +cargo install --git https://github.com/asciinema/agg # .cast -> .gif converter (compose needs this) + +# true-input driver (Linux/Wayland) +sudo apt-get install -y cage wtype # required: headless compositor + keystroke injection +sudo apt-get install -y grim wf-recorder # optional: screenshots + video recording + +# agent-browser driver +agent-browser install # one-time: downloads bundled Chromium + +# compose + showcase (video rendering) +sudo apt-get install -y ffmpeg # video processing (includes ffprobe) +cd ${DROID_PLUGIN_ROOT}/remotion && npm install # Remotion dependencies +# Chrome or Chromium must be installed for Remotion rendering +``` diff --git a/plugins/droid-control/skills/pty-capture/SKILL.md b/plugins/droid-control/skills/pty-capture/SKILL.md new file mode 100644 index 0000000..dba1499 --- /dev/null +++ b/plugins/droid-control/skills/pty-capture/SKILL.md @@ -0,0 +1,31 @@ +--- +name: pty-capture +description: Background knowledge for droid-control workflows -- not invoked directly. Capture ground-truth byte sequences from real terminal emulators. +user-invocable: false +--- + +# PTY Byte Capture + +The orchestrator routed you here. Use these mechanics to execute your plan. + +Capture the exact bytes a real terminal emits for a given keystroke. Use this when the question is "what sequence does terminal X send for key Y?" rather than "does the UI look right?" + +## Platform support + +| Platform | Status | Read | +|---|---|---| +| Linux / Wayland | Implemented | [platforms/linux.md](platforms/linux.md) | +| Windows (KVM) | Implemented | [platforms/windows.md](platforms/windows.md) | +| macOS (QEMU) | Implemented | [platforms/macos.md](platforms/macos.md) | + +**Read the platform file for your target OS.** Each contains the capture architecture, prerequisites, usage pattern, and platform-specific notes. + +## Known dead ends + +- **Xvfb + xdotool**: bypasses real keyboard processing entirely +- **uinput + Xvfb**: Xvfb does not consume kernel input devices +- **SSH PTY for keystroke injection**: distorts the input encoding; SSH is only for output capture or deployment + +## Follow-on + +Feed captured bytes into terminal compatibility fixtures and replay tests in `apps/cli`. diff --git a/plugins/droid-control/skills/pty-capture/platforms/linux.md b/plugins/droid-control/skills/pty-capture/platforms/linux.md new file mode 100644 index 0000000..ecf113f --- /dev/null +++ b/plugins/droid-control/skills/pty-capture/platforms/linux.md @@ -0,0 +1,53 @@ +# PTY Byte Capture: Linux / Wayland + +## Quick start + +```bash +# Full matrix across all installed terminals +${DROID_PLUGIN_ROOT}/scripts/capture-terminal-bytes.py --format table + +# Single terminal + combo +${DROID_PLUGIN_ROOT}/scripts/capture-terminal-bytes.py --backend ghostty --combo shift-enter --format json +``` + +Supported backends: `ghostty`, `kitty`, `alacritty`. +Supported combos: `enter`, `shift-enter`, `ctrl-l`, `escape`, `shift-tab`. + +## Architecture + +``` +wtype -> cage (headless compositor) -> terminal emulator -> PTY child -> hex output +``` + +The child process (`pty-hex-dumper.py`) switches stdin to raw mode, prints `READY`, then dumps each byte as space-separated hex pairs. + +## Prerequisites + +```bash +sudo apt-get install -y cage wtype +ls /dev/dri/ # must be non-empty +``` + +Plus a Wayland terminal (`ghostty`, `kitty`, or `alacritty`). + +## Manual capture (escape hatch) + +When the scripts are insufficient, use a zero-buffered PTY child directly: + +```bash +TCTL=${DROID_PLUGIN_ROOT}/bin/tctl + +$TCTL launch "${DROID_PLUGIN_ROOT}/scripts/pty-hex-dumper.py --ready" \ + -s capture --backend ghostty +$TCTL -s capture wait "READY" --timeout 10000 +$TCTL -s capture press shift enter +sleep 0.5 +$TCTL -s capture snapshot --trim +$TCTL -s capture close +``` + +Or use a one-liner Perl child: + +```bash +perl -e '$|=1; while(sysread(STDIN,$b,1)){printf "%02x ",ord($b)}' +``` diff --git a/plugins/droid-control/skills/pty-capture/platforms/macos.md b/plugins/droid-control/skills/pty-capture/platforms/macos.md new file mode 100644 index 0000000..95735e3 --- /dev/null +++ b/plugins/droid-control/skills/pty-capture/platforms/macos.md @@ -0,0 +1,64 @@ +# PTY Byte Capture: macOS (QEMU) + +Capture terminal byte sequences from a macOS VM by running a capture script inside the guest over SSH, then injecting keystrokes via QEMU monitor `sendkey`. + +## Architecture + +``` +mac-ctl.sh press → QEMU monitor sendkey → virtual USB kbd → macOS HID → Terminal.app + | + capture script (SSH) + | + hex output +``` + +The capture script runs inside the macOS VM via SSH. Keystrokes arrive through the real HID path (QEMU virtual keyboard), not through the SSH PTY -- so captured bytes reflect what macOS Terminal.app actually delivers. + +## Prerequisites + +The macOS VM must have: +- SSH enabled (System Settings → General → Sharing → Remote Login) +- Python 3 installed (ships with macOS, or via `brew install python3`) + +## Usage + +```bash +MACCTL=${DROID_PLUGIN_ROOT}/scripts/macos/mac-ctl.sh + +# Start a hex dumper inside the VM via SSH (runs in background) +$MACCTL ssh 'python3 -c " +import sys, tty, termios +fd = sys.stdin.fileno() +old = termios.tcgetattr(fd) +tty.setraw(fd) +sys.stdout.write(\"READY\n\") +sys.stdout.flush() +try: + while True: + b = sys.stdin.buffer.read(1) + if not b: break + sys.stdout.write(f\"{b[0]:02x} \") + sys.stdout.flush() + if b[0] == 0x11: break # Ctrl+Q exits +finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old) +"' & +CAPTURE_PID=$! +sleep 2 + +# Now inject keystrokes via QEMU monitor (true HID path) +$MACCTL press shift ret +sleep 0.5 + +# Kill the capture session +kill $CAPTURE_PID 2>/dev/null + +# For visual proof, take a screenshot +$MACCTL shot /tmp/macos-capture.png +``` + +## Important notes + +- The SSH session provides the **output channel** only. The actual keystrokes travel through QEMU's virtual USB keyboard → macOS HID → Terminal.app's PTY. +- This captures what Terminal.app delivers to its PTY child, which may differ from what iTerm2 or Ghostty delivers for the same key. +- For Ghostty or iTerm2 capture, open those apps via `mac-ctl.sh spotlight` instead of using the SSH terminal. diff --git a/plugins/droid-control/skills/pty-capture/platforms/windows.md b/plugins/droid-control/skills/pty-capture/platforms/windows.md new file mode 100644 index 0000000..1d331c7 --- /dev/null +++ b/plugins/droid-control/skills/pty-capture/platforms/windows.md @@ -0,0 +1,30 @@ +# PTY Byte Capture: Windows (KVM) + +Two PowerShell scripts in `${DROID_PLUGIN_ROOT}/scripts/windows/` get deployed to the VM via `vm-ctl.sh deploy`: + +| Script | Captures | Use when | +|---|---|---| +| `win-key-dumper.ps1` | Win32 `ReadKey` events: VirtualKeyCode, ControlKeyState, CharHex | You need lossless key metadata (e.g., Shift+Enter vs Enter) | +| `win-vt-dumper.ps1` | Raw VT bytes with `ENABLE_VIRTUAL_TERMINAL_INPUT` | You want the exact escape sequences the console delivers | + +Both exit with Ctrl+Q. + +## Usage + +```bash +VMCTL=${DROID_PLUGIN_ROOT}/scripts/windows/vm-ctl.sh +$VMCTL deploy # push scripts to VM +$VMCTL pwsh && sleep 4 +$VMCTL type "powershell -ExecutionPolicy Bypass -File C:\capture\win-key-dumper.ps1" +$VMCTL press enter && sleep 3 +$VMCTL press shift enter # test keystroke via virsh send-key (true HID) +sleep 0.5 +$VMCTL shot /tmp/key-events.png # screenshot the captured events +$VMCTL press ctrl q # exit the dumper +``` + +## VT mode vs Win32 API + +**Important**: VT mode (`ENABLE_VIRTUAL_TERMINAL_INPUT`) cannot distinguish Shift+Enter from Enter -- both produce `0D`. Use `win-key-dumper.ps1` (Win32 `ReadKey` API) when modifier discrimination matters. + +The Win32 API preserves VirtualKeyCode + exact modifier state (left/right Ctrl/Alt/Shift). Shift+Enter shows as VKey=13 + ShiftPressed, which is distinct from plain Enter. diff --git a/plugins/droid-control/skills/showcase/SKILL.md b/plugins/droid-control/skills/showcase/SKILL.md new file mode 100644 index 0000000..d454ecb --- /dev/null +++ b/plugins/droid-control/skills/showcase/SKILL.md @@ -0,0 +1,103 @@ +--- +name: showcase +description: Background knowledge for droid-control workflows -- not invoked directly. Visual polish for videos via Remotion-powered window chrome, animations, and branded backgrounds. +user-invocable: false +--- + +# Showcase Polish + +This atom describes the visual polish system. It is invoked by the **compose** atom — you should not need to invoke it directly. Load it when you need to understand what the presets look like and how the cinematic layers work. + +## What you control + +You control the visual output by choosing a **preset** and passing **props**. Everything else is automatic — the Remotion components handle all cinematic layers internally based on the preset and palette. + +## Presets + +Each preset configures window chrome, spacing, background style, and palette selection. + +| Preset | Look | Best for | +|---|---|---| +| `factory` | Warm black bg with amber radial glow, traffic-light dots, 12px radius, generous margins. Rich cinematic warmth. | Official Factory content | +| `factory-hero` | Same as `factory` + gradient background. Maximum cinematic punch. | Factory landing pages, social media | +| `hero` | Cool gradient bg, large margins, prominent shadow. | Non-Factory marketing, third-party | +| `macos` | Clean dark bg, traffic lights, subtle shadow. Professional but understated. | General-purpose demos, README heroes | +| `presentation` | Black bg, generous margins. Designed to look good on slides. | Talks, slide decks | +| `minimal` | No window bar, tiny radius, tight margins. Barely-there frame. | Docs embeds, inline clips | + +### What each preset automatically includes + +**Factory / factory-hero presets** (warm palette): +- Warm radial background vignette with amber glow blobs that intensify over video duration +- Warm-tinted box shadow with faint accent glow halo +- Warm color grade overlay (amber tint) +- Floating particles in Factory Orange + +**All other presets** (cool Catppuccin palette): +- Cool-toned solid or gradient background +- Neutral box shadow +- Subtle cool color grade overlay +- Floating particles in accent blue + +**All presets** include: floating particles, noise texture overlay, color grade, motion blur title→content transition, animated window entrance, staggered panel entrance (side-by-side). + +## Visual palettes + +Palette is auto-selected based on preset. Factory/factory-hero use the warm palette; everything else uses cool. + +### Factory (warm) + +| Token | Hex | Role | +|---|---|---| +| bg | `#0a0804` | Warm near-black | +| surface | `#18120e` | Terminal content bg | +| accent | `#EE6018` | Factory Orange | +| text | `#f0e8e0` | Warm white | +| muted | `#948781` | De-emphasized text | + +### Catppuccin (cool) + +| Token | Hex | Role | +|---|---|---| +| bg | `#0d1117` | Cool dark | +| surface | `#181818` | Content bg | +| accent | `#89b4fa` | Blue accent | +| text | `#cdd6f4` | Cool white | +| muted | `#6c7086` | De-emphasized text | + +## Operational notes + +**Render time**: ~1-3 minutes for a 30-60s video at 1920x1080. Set worker timeouts to 5 minutes. + +**Common failure modes**: +- `clipDuration` mismatch: video has blank frames at end or truncates early. The `render-showcase.sh` script auto-detects duration via ffprobe — prefer using it over manual `npx remotion render`. +- Missing clips in `public/`: render fails with "Could not read file." The render script handles staging automatically. +- Missing npm dependencies: run `cd ${REMOTION_DIR} && npm install` if rendering fails on first use. + +**Debugging layout**: Use `npx remotion still Showcase --props='...' --frame=30 --scale=0.5 /tmp/check.png` to render a single frame and inspect it visually before committing to a full render. + +**Cleanup**: The `render-showcase.sh` script removes staged clips from `public/` after rendering. If you run `npx remotion render` directly, clean up `public/` manually. + +## Rendering + +Use the render script from **compose** — see compose/SKILL.md Step 3 for full usage: + +```bash +RENDER=${DROID_PLUGIN_ROOT}/scripts/render-showcase.sh + +$RENDER --props /tmp/props.json --output /tmp/showcase.mp4 /tmp/clip.mp4 +``` + +## Advanced: GlitchTitle + +A stylized glitch title card component exists at `src/components/GlitchTitle.tsx` for edgy/hacker-aesthetic intros (pixel-decay effect with scattered blocks assembling into text). **This is NOT wired into the default Showcase composition.** Using it requires writing a custom Remotion composition. Only pursue this if specifically requested — the standard TitleCard handles all normal use cases. + +## Prerequisites + +- **Node.js** (>= 18) +- **Chrome / Chromium** (Remotion uses headless Chrome) +- **ffmpeg** and **ffprobe** (Remotion uses these under the hood) + +```bash +cd ${DROID_PLUGIN_ROOT}/remotion && npm install +``` diff --git a/plugins/droid-control/skills/true-input/SKILL.md b/plugins/droid-control/skills/true-input/SKILL.md new file mode 100644 index 0000000..ca799d9 --- /dev/null +++ b/plugins/droid-control/skills/true-input/SKILL.md @@ -0,0 +1,45 @@ +--- +name: true-input +description: Background knowledge for droid-control workflows -- not invoked directly. True-input driver mechanics for real terminal emulator automation via headless Wayland compositor. +user-invocable: false +--- + +# True-Input Driver + +The orchestrator routed you here. Use these mechanics to execute your plan. + +Drive a real terminal emulator, injecting keystrokes through the platform's native HID input path. This proves exactly what bytes the terminal emits -- no synthetic injection, no PTY distortion. + +## When to use + +- Proving that a terminal really sends the sequence you expect (e.g., Ghostty's `Shift+Enter`) +- Recording demos that reflect actual terminal rendering +- Validating that Droid handles a keystroke correctly end-to-end in a specific terminal + +If you don't need real terminal proof, use **tuistory** -- it's faster and more deterministic. + +## Platform support + +| Platform | Status | Driver | Read | +|---|---|---|---| +| Linux / Wayland | Implemented | `cage` + `wtype` + any Wayland terminal | [platforms/linux.md](platforms/linux.md) | +| macOS (QEMU) | Implemented | QEMU monitor `sendkey` to a macOS VM | [platforms/macos.md](platforms/macos.md) | +| Windows (KVM) | Implemented | `virsh send-key` to a KVM/QEMU VM | [platforms/windows.md](platforms/windows.md) | + +**Read the platform file for your target OS.** Each contains prerequisites, core pattern, command reference, encoding reference, recording, troubleshooting, and recovery -- specific to that platform. + +## Key differences from tuistory + +| Concern | tuistory | true-input | +|---|---|---| +| Snapshot source | Virtual screen buffer | Scrubbed PTY log (Linux) or screenshot (VM platforms) | +| Wait mechanism | Event-driven (screen redraws) | Log polling (Linux) or sleep-based (VMs) | +| Recording | Must wrap launch (`--record`) | Can start/stop any time | +| Keyboard encoding | Synthetic (bypasses terminal) | Real terminal encoding path | + +## Known dead ends + +- **Xvfb + xdotool**: bypasses real keyboard processing entirely +- **uinput + Xvfb**: Xvfb does not consume kernel input devices +- **SSH for TUI testing**: PTY layer distorts input encoding; use SSH only for deployment +- **Raw `asciinema rec`**: true-input records via `wf-recorder` (Wayland screen capture), not asciinema. Use `tctl --record` or `tctl record start/stop`. Calling `asciinema rec` directly has no access to the compositor and produces nothing useful. diff --git a/plugins/droid-control/skills/true-input/platforms/linux.md b/plugins/droid-control/skills/true-input/platforms/linux.md new file mode 100644 index 0000000..a75d274 --- /dev/null +++ b/plugins/droid-control/skills/true-input/platforms/linux.md @@ -0,0 +1,103 @@ +# True-Input: Linux / Wayland + +## Prerequisites + +```bash +sudo apt-get install -y cage wtype # required +sudo apt-get install -y grim wf-recorder # optional: screenshots / video +ls /dev/dri/ # must be non-empty +``` + +At least one Wayland terminal: `ghostty`, `kitty`, or `alacritty`. + +## Architecture + +`tctl` launches a headless Wayland compositor (`cage`) with an isolated per-session runtime directory, opens a real terminal emulator inside it, injects keystrokes via `wtype`, and monitors the PTY log stream via `script`. All socket/runtime management is handled by `tctl`. + +## Core pattern + +```bash +TCTL=${DROID_PLUGIN_ROOT}/bin/tctl + +$TCTL launch "droid-dev" -s proof --backend true-input --cols 120 --rows 36 +$TCTL -s proof wait ">" --timeout 15000 +$TCTL -s proof type "hello" +$TCTL -s proof press shift enter +$TCTL -s proof snapshot --trim +$TCTL -s proof screenshot -o /tmp/proof.png +$TCTL -s proof close +``` + +Auto-detection order: ghostty > kitty > alacritty. Force a specific terminal: + +```bash +$TCTL launch "droid-dev" -s proof --backend ghostty +``` + +## Command reference (via tctl) + +| Command | Purpose | +|---|---| +| `launch -s --backend true-input` | Start session in headless compositor | +| `type ` | Send literal text via wtype | +| `press [keys...]` | Send key chord (supports modifiers) | +| `wait ` | Block until text or `/regex/` in PTY log | +| `wait-idle` | Block until PTY log stabilizes | +| `snapshot [--trim]` | Print scrubbed PTY log output | +| `screenshot [-o ]` | Capture compositor PNG | +| `record start ` | Start wf-recorder video | +| `record stop` | Stop video recording | +| `close` | Tear down compositor + terminal | + +## Recording + +Recording can start and stop independently of the session: + +```bash +$TCTL -s proof record start /tmp/proof.mp4 +# ... interact ... +$TCTL -s proof record stop +``` + +Or record from launch via `--record`: + +```bash +$TCTL launch "droid-dev" -s proof --backend true-input --record /tmp/proof.mp4 +``` + +## Terminal encoding reference + +### Ghostty + +Falls back to xterm `modifyOtherKeys` (Kitty keyboard protocol disabled due to arrow-key issues): + +| Key | Bytes | +|---|---| +| `Enter` | `\r` | +| `Shift+Enter` | `\x1b[27;2;13~` | + +### Kitty + +CSI-u protocol by default: + +| Key | Bytes | +|---|---| +| `Enter` | `\r` | +| `Shift+Enter` | `\x1b[13;2u` | +| keypad `Enter` | `\x1b[57414u` | + +### VS Code / Cursor / Windsurf + +Integrated terminals need explicit keybinding setup. Droid's helper writes `workbench.action.terminal.sendSequence` bindings that emit `\\\r\n` for `Shift+Enter`. + +### tmux + +tmux may rewrite sequences when relaying. With extended keys enabled, `Shift+Enter` typically appears as `\x1b[27;2;13~` regardless of the inner terminal's native encoding. + +## Recovery + +```bash +$TCTL -s proof press esc +$TCTL -s proof snapshot --trim +$TCTL -s proof close +``` diff --git a/plugins/droid-control/skills/true-input/platforms/macos.md b/plugins/droid-control/skills/true-input/platforms/macos.md new file mode 100644 index 0000000..b33d09e --- /dev/null +++ b/plugins/droid-control/skills/true-input/platforms/macos.md @@ -0,0 +1,130 @@ +# True-Input: macOS via QEMU Monitor + +Drive a macOS VM's terminal through QEMU HMP `sendkey` -- keystrokes enter the VM's virtual USB keyboard, hitting macOS's HID subsystem the same way a physical keyboard would. No host compositor leakage, no SSH PTY distortion. + +## Architecture + +``` +mac-ctl.sh type / press + | + v + socat → QEMU monitor socket → sendkey → QEMU virtual USB keyboard (HID) + | + v + macOS HID subsystem → Terminal.app / iTerm2 / Ghostty +``` + +## Prerequisites + +Host packages: `qemu-system-x86_64` (or `aarch64`), `socat`, `ffmpeg` or ImageMagick (for PNG conversion). +VM: QEMU macOS guest with UEFI, user-mode networking with SSH port forward. +The VM must be started with `MONITOR_SOCKET` env var pointing to a unix socket so `mac-ctl.sh` can connect. + +Required env vars: + +```bash +export DROID_MAC_MONITOR="" # QEMU monitor unix socket +export DROID_MAC_SSH_HOST="" # SSH config host alias +``` + +Optional env vars: + +```bash +export DROID_MAC_BOOT_SCRIPT="" # for `up` command +export DROID_MAC_SHOT_DIR="/tmp" +``` + +## Core pattern + +```bash +MACCTL=${DROID_PLUGIN_ROOT}/scripts/macos/mac-ctl.sh + +$MACCTL up # start VM (needs DROID_MAC_BOOT_SCRIPT) +$MACCTL wait-boot 120 # poll SSH until ready +$MACCTL terminal # open Terminal.app via Spotlight +sleep 3 +$MACCTL type "echo hello world" +$MACCTL press enter +sleep 2 +$MACCTL shot /tmp/result.png # screendump via QEMU monitor → PNG +``` + +## Command reference (via mac-ctl.sh) + +| Command | Purpose | +|---|---| +| `up` | Start VM via boot script (requires `DROID_MAC_BOOT_SCRIPT`) | +| `down` | Graceful shutdown via SSH `sudo shutdown -h now` | +| `kill` | Force-quit QEMU process via monitor `quit` | +| `status` | Check VM state via monitor socket | +| `wait-boot [timeout]` | Poll SSH until macOS is ready (default 120s) | +| `type ` | Type literal text, char by char via QEMU `sendkey` | +| `press [key ...]` | Send key or chord (e.g., `press cmd c`, `press shift enter`) | +| `shot [path]` | Screenshot via `screendump` → PPM → PNG | +| `terminal` | Open Terminal.app via Spotlight (Cmd+Space) | +| `iterm` | Open iTerm2 via Spotlight | +| `spotlight [query]` | Open Spotlight, optionally type + launch a query | +| `ssh ` | Run command via SSH (deployment only -- **not** for TUI testing) | + +### macOS-specific keys + +`cmd` / `command` maps to `meta_l` (the macOS Command key). Chords work as expected: + +```bash +$MACCTL press cmd c # Copy +$MACCTL press cmd shift 3 # Screenshot (macOS native) +$MACCTL press cmd space # Spotlight +$MACCTL press cmd q # Quit foreground app +``` + +## Key differences from other platforms + +| Concern | Linux (Wayland) | Windows (KVM/virsh) | macOS (QEMU monitor) | +|---|---|---|---| +| Input path | `wtype` → compositor → terminal | `virsh send-key` → QEMU HID | `sendkey` via monitor socket → QEMU HID | +| Snapshot | Scrubbed PTY log | `virsh screenshot` (PNG) | `screendump` (PPM → PNG) | +| Wait mechanism | PTY log polling | Sleep-based | SSH polling (`wait-boot`) + sleep-based | +| Recording | `wf-recorder` | Poll-based screenshot → ffmpeg | Poll-based screenshot → ffmpeg | +| Session isolation | Per-session Wayland runtime dir | Full VM isolation | Full VM isolation | +| App launcher | N/A (direct command) | Start menu (`win` key) | Spotlight (`cmd+space`) | + +## Recording + +Poll-based recording via `screendump`: + +```bash +MACREC=${DROID_PLUGIN_ROOT}/scripts/macos/mac-record.sh + +$MACREC start /tmp/demo.mp4 5 # 5 fps +# ... interact with mac-ctl.sh ... +$MACREC stop # encodes PPM frames to MP4 +``` + +## Troubleshooting + +| Problem | Fix | +|---|---| +| Monitor socket not found | VM isn't running, or was started without `MONITOR_SOCKET` env var | +| SSH times out on boot | macOS VMs under QEMU take 60-120s to boot. Increase `wait-boot` timeout | +| `screendump` is black | VM may be asleep. `mac-ctl.sh press space` to wake | +| Keystrokes not registering | Check that QEMU was started with `-device usb-kbd` (not evdev passthrough) | +| PPM not converting to PNG | Install ImageMagick (`magick`) or `ffmpeg` on the host | + +## Recovery + +```bash +MACCTL=${DROID_PLUGIN_ROOT}/scripts/macos/mac-ctl.sh +$MACCTL press esc +$MACCTL shot /tmp/state.png +$MACCTL kill # hard-kill QEMU process +$MACCTL up # cold restart +``` + +## Future: native macOS (no VM) + +For bare-metal Macs (not VMs), the QEMU monitor approach isn't available. Potential paths: +- **Wawona** (native macOS Wayland compositor) + `wtype` + `waypipe` -- would reuse Linux plumbing +- **AppleScript** / Accessibility API injection into Terminal.app / iTerm2 +- **cliclick** for mouse/keyboard automation + +These are not yet implemented. Use this QEMU approach for VM-based macOS testing. diff --git a/plugins/droid-control/skills/true-input/platforms/windows.md b/plugins/droid-control/skills/true-input/platforms/windows.md new file mode 100644 index 0000000..a3ec71c --- /dev/null +++ b/plugins/droid-control/skills/true-input/platforms/windows.md @@ -0,0 +1,118 @@ +# True-Input: Windows via KVM/QEMU + +Drive a Windows VM's console through `virsh send-key` -- keystrokes enter the VM's virtual HID device, hitting the Windows Console Subsystem the same way a physical keyboard would. No host compositor leakage, no SSH PTY distortion. + +## Architecture + +``` +vm-ctl.sh type / press + | + v + virsh send-key ──> QEMU virtual keyboard (HID) + | + v + Windows Console Subsystem ──> PowerShell / TUI app +``` + +## Prerequisites + +Host packages: `qemu-full`, `libvirt`, `virt-manager`, `swtpm`, `dnsmasq`. +VM: KVM/QEMU with UEFI + TPM 2.0, SPICE display, NAT networking, SSH enabled. + +Required env vars: + +```bash +export DROID_VM_NAME="" # libvirt domain name +export DROID_VM_SSH_KEY="" # SSH key for deployment +export DROID_VM_SSH_USER="" # SSH user in the VM +``` + +## Core pattern + +```bash +VMCTL=${DROID_PLUGIN_ROOT}/scripts/windows/vm-ctl.sh + +$VMCTL up # start the VM +sleep 15 # wait for boot +$VMCTL login # dismiss lock screen +sleep 5 +$VMCTL pwsh # open PowerShell via Start menu +sleep 4 +$VMCTL type "Get-Process | Select-Object -First 5" +$VMCTL press enter +sleep 2 +$VMCTL shot /tmp/result.png # screenshot via virsh +``` + +## Command reference (via vm-ctl.sh) + +| Command | Purpose | +|---|---| +| `up` / `down` / `kill` / `reboot` / `status` | VM lifecycle | +| `type ` | Type literal text, char by char via virsh send-key | +| `press [keys...]` | Send key or chord (e.g., `press ctrl c`, `press shift enter`) | +| `login [password]` | Dismiss lock screen + type password | +| `shot [path]` | Screenshot via `virsh screenshot` | +| `pwsh` / `wt` | Open PowerShell / Windows Terminal via Start menu | +| `snap [name]` / `snaps` / `restore ` | VM snapshot management | +| `ssh ` | Run command via SSH (deployment only -- **not** for TUI testing) | +| `deploy` | Push capture scripts to VM via SCP | + +## Key differences from Linux true-input + +| Concern | Linux (Wayland) | Windows (KVM) | +|---|---|---| +| Input path | `wtype` → Wayland compositor → terminal | `virsh send-key` → QEMU HID → Windows console | +| Snapshot | Scrubbed PTY log | `virsh screenshot` (PNG of display) | +| Wait mechanism | PTY log polling | Sleep-based (no PTY log access) | +| Recording | `wf-recorder` (compositor video) | Poll-based screenshot → ffmpeg concat | +| Session isolation | Per-session Wayland runtime dir | Full VM isolation | + +## Recording + +Poll-based recording via `virsh screenshot`: + +```bash +VMREC=${DROID_PLUGIN_ROOT}/scripts/windows/vm-record.sh + +$VMREC start /tmp/demo.mp4 5 # 5 fps +# ... interact with vm-ctl.sh ... +$VMREC stop # encodes to MP4 +``` + +## Terminal encoding reference + +### VT mode (`ENABLE_VIRTUAL_TERMINAL_INPUT`) + +| Key | Bytes | Note | +|---|---|---| +| Enter | `0D` | | +| Shift+Enter | `0D` | Indistinguishable from Enter in VT mode | +| Ctrl+C | `03` | With processed input off | +| Tab | `09` | | +| Shift+Tab | `1B 5B 5A` | | +| Arrows | `1B 5B A/B/C/D` | | + +Modifier codes: 2=Shift, 3=Alt, 5=Ctrl, 6=Shift+Ctrl + +### Win32 console API (`ReadKey` / `ReadConsoleInput`) + +Lossless. Preserves VirtualKeyCode + exact modifier state (left/right Ctrl/Alt/Shift). Shift+Enter is VKey=13 + ShiftPressed (distinct from plain Enter). Use `win-key-dumper.ps1` for this level of detail. + +## Troubleshooting + +| Problem | Fix | +|---|---| +| VM won't get DHCP | Check `firewall_backend = "iptables"` in `/etc/libvirt/network.conf`, restart libvirtd | +| Keystrokes hitting host | You're using ydotool. Use `vm-ctl.sh press` (virsh send-key) instead | +| Screenshot is black | VM may be asleep. `vm-ctl.sh press space` to wake | +| SSH connection refused | sshd may have stopped. Use `vm-ctl.sh type` to run `Start-Service sshd` | + +## Recovery + +```bash +VMCTL=${DROID_PLUGIN_ROOT}/scripts/windows/vm-ctl.sh +$VMCTL press esc +$VMCTL shot /tmp/state.png +$VMCTL restore # hard reset to known state +``` diff --git a/plugins/droid-control/skills/tuistory/SKILL.md b/plugins/droid-control/skills/tuistory/SKILL.md new file mode 100644 index 0000000..df98521 --- /dev/null +++ b/plugins/droid-control/skills/tuistory/SKILL.md @@ -0,0 +1,134 @@ +--- +name: tuistory +description: Background knowledge for droid-control workflows -- not invoked directly. Tuistory driver mechanics for terminal TUI automation via virtual PTY. +user-invocable: false +--- + +# Tuistory Driver + +The orchestrator routed you here. Use these mechanics to execute your plan. + +Launch a target command in a virtual PTY with Playwright-style CLI for typing, pressing keys, waiting, snapshotting, and recording. + +## When to use + +- Routine TUI automation and regression checks +- Deterministic `wait` / `wait-idle` against the virtual screen buffer +- Text snapshots of exactly what the user would see +- Any scenario where you do **not** need to prove real terminal keyboard encoding + +If you need to prove what Ghostty or Kitty actually emits for a given keystroke, use **true-input** instead. + +## Prerequisites + +```bash +npm install -g tuistory # or: bun add -g tuistory +``` + +Optional: `tmux` (scrollback flows), `asciinema` (recordings), `agg` (`.cast` to `.gif`). + +## Core pattern + +```bash +TCTL=${DROID_PLUGIN_ROOT}/bin/tctl + +$TCTL launch "droid-dev" -s demo --backend tuistory \ + --repo-root /path/to/worktree \ + --cols 120 --rows 36 \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor +$TCTL -s demo wait ">" --timeout 15000 +$TCTL -s demo type "hello" +$TCTL -s demo press enter +$TCTL -s demo snapshot --trim +$TCTL -s demo close +``` + +**Note:** `--repo-root` is mandatory for `droid-dev` launches — `tctl` enforces it. + +**Always pass `--env FORCE_COLOR=3 --env COLORTERM=truecolor`** when launching. The virtual PTY doesn't advertise color support, so Node.js apps (Ink/chalk) suppress all color escape codes without these. + +## Command reference (via tctl) + +| Command | Purpose | +|---|---| +| `launch -s --backend tuistory` | Start a tuistory session | +| `type ` | Send literal text | +| `press [keys...]` | Send key chord (e.g., `press shift enter`) | +| `wait ` | Block until text or `/regex/` appears | +| `wait-idle` | Block until output stabilizes | +| `snapshot [--trim]` | Print cleaned text (`--trim` strips trailing blanks) | +| `close` | Tear down session | + +Launch options: `--cols `, `--rows `, `--cwd `, `--env KEY=VALUE`, `--record `. + +## Recording + +Pass `--record` at launch. `tctl` wraps `asciinema rec` around the PTY, so recording **must** be set at launch time (raw `tuistory` cannot record): + +```bash +$TCTL launch "droid-dev" -s demo --backend tuistory \ + --repo-root /path/to/worktree \ + --cols 120 --rows 36 --record /tmp/demo.cast \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor +# ... interact ... +$TCTL -s demo close # finalizes the .cast +``` + +Before/after comparison -- launch two sessions against different worktrees: + +```bash +$TCTL launch "droid-dev" -s before --backend tuistory \ + --repo-root /path/to/baseline-worktree \ + --cols 120 --rows 36 --record /tmp/before.cast \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor + +$TCTL launch "droid-dev" -s after --backend tuistory \ + --repo-root /path/to/candidate-worktree \ + --cols 120 --rows 36 --record /tmp/after.cast \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor +``` + +Playback: `asciinema play /tmp/demo.cast` + +## tmux (scrollback flows only) + +Only needed when the demo requires terminal-emulator scrollback and the app uses the standard buffer (not alternate screen). True-input sessions rarely need tmux because the real terminal owns native scrollback. + +Use `--tmux` at launch. `tctl` wraps the command in tmux, starts tmux with `TERM=xterm-256color`, and preconfigures `default-terminal=tmux-256color`, `terminal-features=...,xterm-256color:RGB`, `terminal-overrides=...,xterm-256color:Tc` (fallback for tmux < 3.2), `COLORTERM=truecolor` (via `set-environment`), `escape-time=50`, and `mode-keys=vi`. + +Copy-mode: `ctrl-b [` to enter, `g g` top, `shift-g` bottom, `ctrl-u`/`ctrl-d` half-page, `/` search, `q` to exit (not `esc`). + +Launch with tmux: + +```bash +$TCTL launch "droid-dev" -s demo --backend tuistory --tmux \ + --repo-root /path/to/worktree \ + --cols 120 --rows 36 --record /tmp/demo.cast \ + --env FORCE_COLOR=3 --env COLORTERM=truecolor +``` + +When recording includes tmux redraws, asciinema must wrap tmux, not the reverse. + +## Known dead ends + +- **Raw `asciinema rec`**: Do not call `asciinema rec` directly. `tctl --record` wraps `asciinema rec` around the PTY so that tuistory-relay still owns the session and interactive TUIs (Ink/React) receive stdin correctly. Calling `asciinema rec` manually bypasses this wiring — stdin forwarding breaks, typed keys echo on the outer PTY instead of reaching the child, and tuistory commands (`wait`, `snapshot`, `close`) cannot find the session. +- **Raw `tuistory launch` with tctl flags**: `tuistory` has no `--record`, `--backend`, `--repo-root`, or `--env` flags. Passing them crashes `tuistory-relay`. Use `tctl` for all launches. + +## Recovery + +```bash +$TCTL -s demo press esc # bail out of a stuck dialog +$TCTL -s demo snapshot --trim # check visible state +$TCTL -s demo close # hard reset +``` + +## Escape hatch: raw tuistory (last resort) + +If `tctl` itself is broken or unavailable, you can fall back to raw `tuistory` for non-recording sessions only. Raw `tuistory` accepts only `--cols`, `--rows`, and `-s` — no other flags. Do not pass `--record`, `--backend`, `--repo-root`, `--env`, or `--tmux`. + +```bash +tuistory launch "my-tui-app" -s demo --cols 120 --rows 36 +tuistory -s demo wait ">" --timeout 15000 +tuistory -s demo snapshot --trim +tuistory -s demo close +``` diff --git a/plugins/droid-control/skills/verify/SKILL.md b/plugins/droid-control/skills/verify/SKILL.md new file mode 100644 index 0000000..dcabb19 --- /dev/null +++ b/plugins/droid-control/skills/verify/SKILL.md @@ -0,0 +1,109 @@ +--- +name: verify +description: Background knowledge for droid-control workflows -- not invoked directly. Deliverable verification against commitments. +user-invocable: false +--- + +# Verify + +The orchestrator routed you here. This atom checks the final deliverable against the commitments made at the start of the workflow. + +## Inputs + +You receive: + +1. **Commitments** from the command's parse step -- the promises made about what the deliverable would contain +2. **Compose outputs** -- the finished artifact(s) and their metadata + +## Video deliverables + +### Technical checks + +Run `ffprobe` on the final .mp4: + +```bash +ffprobe -v quiet -print_format json -show_format -show_streams