diff --git a/.brain/BRAIN.md b/.brain/BRAIN.md new file mode 100644 index 0000000..d726d2b --- /dev/null +++ b/.brain/BRAIN.md @@ -0,0 +1,91 @@ +# `.brain/` — agent workflow rules + +This repo has a per-project second brain at `.brain/`. Hooks in +`.claude/settings.json` drive it automatically. Your job during a session: +keep the brain useful for the *next* session. + +## At session start (already automatic) + +The `SessionStart` hook runs `.brain/scripts/assemble-context.sh`, which +generates `.brain/_CONTEXT.md` and injects it into context. It contains: +- Pointers to CKIS (`_MEMORY.md`, `_overview.md`, architecture spec) +- Last 3 session summaries +- Open decisions and bugs +- Top of `GRAPH_REPORT.md` (if Graphify has run) + +You don't need to read `_CONTEXT.md` again — it's already in context. + +## During the session — what's auto-captured + +The `PostToolUse` hook runs `.brain/scripts/log-tool-event.sh` after every +Bash call and silently appends one line to `.brain/sessions/_active.md` +**only** for these objectively important events: + +- `npm run build` / `npm test` / `npm run lint` → success or failure (with last + ~8 lines of output on failure, so the error is preserved next session) +- `git commit` → SHA + message + diffstat + +The `UserPromptSubmit` hook runs `.brain/scripts/log-compact.sh` on every user +prompt but only acts on `/compact` commands — everything else is ignored. When +detected, it writes a timestamp breadcrumb to `.brain/.compact-triggers`. + +Everything else (file edits, reads, other shell commands, regular prompts) is +**not logged** — that would be noise. The Stop hook merges `_active.md` into +the final session log under `## Iterations`. This means: every build, every +test, every commit you make in a session is permanently in the brain — +searchable, visible to the next session, and impossible to lose to a `/clear`. + +When `/compact` runs, the Stop hook extracts the full summary from the session +JSONL transcript and writes it to `.brain/sessions/compacts/-compact.md`. +The session file gets a `## Compactions` section with a pointer and a 200-char +excerpt — the full summary is never auto-inlined to keep `_CONTEXT.md` lean. + +## During the session + +When the work warrants it, write to: + +- **`.brain/decisions/YYYY-MM-DD-.md`** — for any decision Eduardo + makes about Korvex Web (architecture, dependency, deploy, scope). Use the + CKIS decision-log format. See `.brain/decisions/README.md`. +- **`.brain/bugs/YYYY-MM-DD-.md`** — when a bug is found *and* fixed, + capture the lesson (root cause, why it happened, how to prevent it). The + patch lives in the commit; this file is for the *why*. + +Important decisions also get a one-line cross-post to +`~/Documents/Second Brain/00-inbox/_MEMORY.md` Open Decisions. + +## Before ending the session + +The Stop hook fills in the objective metadata automatically: +- Iterations (every build/test/lint/commit, with timestamps) +- Commits made, files changed, duration, branch state + +Your only job is the **narrative `## Summary`** — and only when it adds +value. If the session was a routine commit-and-iterate cycle, the iteration +log already tells the story. Fill in the Summary when: + +- A non-obvious decision was made mid-session +- A bug had a *why* worth remembering (root cause, not patch) +- Something is unfinished and the next session needs to know + +Format: 2–4 bullets max. What was done · What was decided · What's next. +Skip it if the iteration log + commits speak for themselves. + +## CKIS bridge — when to escalate + +| Situation | Goes to | +| --- | --- | +| Routine code change, bug fix, refactor | Just commit. No brain entry needed. | +| Decision about *this* project | `.brain/decisions/` | +| Bug worth a postmortem | `.brain/bugs/` | +| Strategic / cross-project / personal | CKIS `_MEMORY.md` + `02-projects/korvex/_overview.md` | +| Pattern reusable across projects | CKIS `03-knowledge/permanent-notes/` | + +When in doubt: project decision → `.brain/`. Strategic → CKIS. + +## Hard rules + +- Never modify `.brain/_CONTEXT.md` by hand — it's regenerated each session. +- Never delete files in `.brain/decisions/` or `.brain/bugs/` — supersede instead. +- `.brain/sessions/` is gitignored (personal); `decisions/` and `bugs/` are committed. +- Graphify rebuilds `.brain/graph/` on every commit (post-commit hook). Don't edit it. diff --git a/.brain/README.md b/.brain/README.md new file mode 100644 index 0000000..28a887b --- /dev/null +++ b/.brain/README.md @@ -0,0 +1,52 @@ +--- +type: brain-readme +project: korvex-web +created: 2026-05-03 +modified: 2026-05-03 +tags: [brain, ckis-bridge] +--- + +# `.brain/` — Per-project second brain + +Project-level memory layer for Claude Code sessions. Bridges this repo to the +global CKIS vault at `~/Documents/Second Brain/`. + +## What lives here + +| Path | Purpose | Committed? | +| ---------------- | ------------------------------------------------ | --------------- | +| `_CONTEXT.md` | Auto-assembled session start context | No (regenerable) | +| `decisions/` | Decision logs (CKIS format — see skill card) | **Yes** | +| `bugs/` | Bug → fix narratives | **Yes** | +| `sessions/` | Per-session summaries written by Stop hook | No (personal) | +| `graph/` | Graphify output (`graph.json`, `GRAPH_REPORT.md`, vault/) | No (regenerable) | +| `scripts/` | Hooks + assembler scripts | **Yes** | +| `config.sh` | Per-project config (CKIS paths, project slug) | **Yes** | + +## How it works + +1. **SessionStart hook** runs `scripts/assemble-context.sh`: + - Concatenates latest 3 session summaries + open decisions + active bugs + - Pulls god-nodes section from `graph/GRAPH_REPORT.md` if Graphify has run + - Adds pointers to CKIS `_MEMORY.md` and project `_overview.md` + - Writes the result to `_CONTEXT.md` and emits it as session context. + +2. **Stop hook** runs `scripts/log-session.sh`: + - Records git diff, commits, branch, duration vs. session start. + - Creates `sessions/YYYY-MM-DD-HHMM-session.md` with a "Summary" section to fill in. + +3. **Graphify** rebuilds `graph/` automatically on every git commit + (via `graphify hook install`) and is symlinked from the CKIS vault under + `02-projects/korvex/graph/` so the graph view shows up alongside the + curated overview. + +## Bridge to CKIS + +- Strategic / cross-project state → `~/Documents/Second Brain/00-inbox/_MEMORY.md` +- Project overview (curated) → `~/Documents/Second Brain/02-projects/korvex/_overview.md` +- Architecture spec → `~/Documents/Second Brain/03-knowledge/permanent-notes/per-project-second-brain.md` + +## See also + +- `00-system/ckis/06-decision-execution-and-review-protocol.md` — decision-log format +- `.claude/skills/ckis-decision-log/` — skill that writes here diff --git a/.brain/bugs/README.md b/.brain/bugs/README.md new file mode 100644 index 0000000..d723e9b --- /dev/null +++ b/.brain/bugs/README.md @@ -0,0 +1,30 @@ +--- +type: index +created: 2026-05-03 +tags: [bugs, brain] +--- + +# Bugs + +Bug → fix narratives for `korvex-web`. Capture the *why*, not just the patch +(the patch is in the commit; this folder is for the lesson). + +## File naming + +`YYYY-MM-DD-.md`. + +## Required frontmatter + +```yaml +--- +type: bug +project: korvex +status: open | fixed | wontfix +date: YYYY-MM-DD +severity: low | medium | high +related-commits: [, ...] +tags: [bug, korvex] +--- +``` + +Bugs with `status: open` are surfaced in `_CONTEXT.md` at session start. diff --git a/.brain/config.sh b/.brain/config.sh new file mode 100644 index 0000000..201a7d4 --- /dev/null +++ b/.brain/config.sh @@ -0,0 +1,29 @@ +# Per-project .brain/ configuration +# Sourced by every script in .brain/scripts/. + +# Project identity +PROJECT_SLUG="streamnet-cli" +PROJECT_NAME="StreamNet CLI" + +# CKIS vault paths (absolute — adjust per machine if vault relocates) +CKIS_VAULT="$HOME/Documents/Second Brain" +CKIS_MEMORY="$CKIS_VAULT/00-inbox/_MEMORY.md" +CKIS_PROJECT_OVERVIEW="$CKIS_VAULT/02-projects/$PROJECT_SLUG/_overview.md" +CKIS_ARCHITECTURE_NOTE="$CKIS_VAULT/03-knowledge/permanent-notes/per-project-second-brain.md" + +# Brain paths (relative to repo root) +BRAIN_DIR=".brain" +SESSIONS_DIR="$BRAIN_DIR/sessions" +DECISIONS_DIR="$BRAIN_DIR/decisions" +BUGS_DIR="$BRAIN_DIR/bugs" +GRAPH_DIR="$BRAIN_DIR/graph" +CONTEXT_FILE="$BRAIN_DIR/_CONTEXT.md" +SESSION_STATE="$BRAIN_DIR/.session-state" + +# How many recent session summaries to inline in _CONTEXT.md +RECENT_SESSIONS_LIMIT=3 + +# Dev Brain vault (Obsidian — code graph + wiki layer, separate from CKIS) +DEV_BRAIN_VAULT="$HOME/Documents/Dev Brain" +# Rebuild --obsidian vault every N commits (expensive: one .md per node) +OBSIDIAN_GRAPH_CADENCE=10 diff --git a/.brain/decisions/README.md b/.brain/decisions/README.md new file mode 100644 index 0000000..bc9d5c1 --- /dev/null +++ b/.brain/decisions/README.md @@ -0,0 +1,32 @@ +--- +type: index +created: 2026-05-03 +tags: [decisions, brain] +--- + +# Decisions + +Decision logs for `korvex-web`. CKIS format — see +`~/Documents/Second Brain/00-system/ckis/06-decision-execution-and-review-protocol.md`. + +## File naming + +`YYYY-MM-DD-.md` — one decision per file. + +## Required frontmatter + +```yaml +--- +type: decision +project: korvex +status: proposed | adopted | superseded +date: YYYY-MM-DD +reversal-cost: low | medium | high +review-by: YYYY-MM-DD or empty +tags: [decision, korvex] +--- +``` + +Decisions with `status: proposed` are surfaced in `_CONTEXT.md` at session start +and in CKIS `_MEMORY.md` Open Decisions. Important decisions are cross-posted +to `_MEMORY.md` as one-line entries pointing back here. diff --git a/.brain/graph/.gitkeep b/.brain/graph/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.brain/scripts/assemble-context.sh b/.brain/scripts/assemble-context.sh new file mode 100755 index 0000000..d524902 --- /dev/null +++ b/.brain/scripts/assemble-context.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +# assemble-context.sh — SessionStart hook +# +# Builds .brain/_CONTEXT.md from: +# - latest N session summaries +# - open decisions (status: proposed) +# - open bugs (status: open) +# - god-nodes section from .brain/graph/GRAPH_REPORT.md (if Graphify ran) +# - pointers to CKIS vault +# +# Records session start state to .brain/.session-state for the Stop hook. +# Emits the assembled context to stdout (Claude Code injects it into the session). + +set -euo pipefail + +# Resolve repo root from this script's location. +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +# shellcheck disable=SC1091 +source "$REPO_ROOT/.brain/config.sh" + +NOW="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +NOW_LOCAL="$(date +"%Y-%m-%d %H:%M %Z")" +HEAD_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "no-git")" +BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "no-git")" + +# Record session-start state for the Stop hook. +mkdir -p "$BRAIN_DIR" +cat > "$SESSION_STATE" </dev/null || true +fi + +# Rotate orphaned compact-triggers (from a session that never reached Stop). +COMPACT_TRIGGERS="$BRAIN_DIR/.compact-triggers" +if [ -f "$COMPACT_TRIGGERS" ]; then + mkdir -p "$SESSIONS_DIR" + mv "$COMPACT_TRIGGERS" "$SESSIONS_DIR/_orphaned-compacts-$(date +%Y-%m-%d-%H%M).log" 2>/dev/null || true +fi + +# Sync GRAPH_REPORT.md into the CKIS vault (catch-up in case post-commit hook missed it). +bash "$REPO_ROOT/.brain/scripts/sync-graph-to-vault.sh" >/dev/null 2>&1 || true + +# Build _CONTEXT.md. +{ + echo "---" + echo "type: project-context" + echo "project: $PROJECT_SLUG" + echo "generated: $NOW" + echo "branch: $BRANCH" + echo "head: $HEAD_SHA" + echo "tags: [context, brain, $PROJECT_SLUG]" + echo "---" + echo + echo "# $PROJECT_NAME — Session Context" + echo + echo "> Auto-generated by \`.brain/scripts/assemble-context.sh\` at session start." + echo "> Do not hand-edit. Update sources in \`.brain/decisions/\`, \`.brain/bugs/\`, or \`.brain/sessions/\`." + echo + echo "**Branch:** \`$BRANCH\` · **HEAD:** \`$HEAD_SHA\` · **Started:** $NOW_LOCAL" + echo + echo "━━━" + echo + echo "## CKIS pointers" + echo + echo "- Live business state → \`$CKIS_MEMORY\`" + echo "- Project overview (curated) → \`$CKIS_PROJECT_OVERVIEW\`" + echo "- Architecture spec → \`$CKIS_ARCHITECTURE_NOTE\`" + echo + echo "━━━" + echo + echo "## Recent sessions (last $RECENT_SESSIONS_LIMIT)" + echo + if compgen -G "$SESSIONS_DIR/*.md" > /dev/null; then + # Newest first, take N. + mapfile -t recent < <(ls -1t "$SESSIONS_DIR"/*.md 2>/dev/null | head -n "$RECENT_SESSIONS_LIMIT") + for f in "${recent[@]}"; do + echo "### $(basename "$f" .md)" + echo + cat "$f" + echo + echo "---" + echo + done + else + echo "_No prior sessions logged yet._" + echo + fi + + echo "━━━" + echo + echo "## Open decisions" + echo + if compgen -G "$DECISIONS_DIR/*.md" > /dev/null; then + found=0 + for f in "$DECISIONS_DIR"/*.md; do + [ "$(basename "$f")" = "README.md" ] && continue + # Match `status: proposed` in frontmatter. + if awk '/^---$/{c++} c==1 && /^status:[[:space:]]*proposed/{print; exit}' "$f" | grep -q proposed; then + title="$(awk '/^# /{sub(/^# /,""); print; exit}' "$f")" + [ -z "$title" ] && title="$(basename "$f" .md)" + echo "- [$title]($f)" + found=1 + fi + done + [ "$found" = "0" ] && echo "_No open decisions._" + else + echo "_No decisions logged yet._" + fi + echo + + echo "━━━" + echo + echo "## Open bugs" + echo + if compgen -G "$BUGS_DIR/*.md" > /dev/null; then + found=0 + for f in "$BUGS_DIR"/*.md; do + [ "$(basename "$f")" = "README.md" ] && continue + if awk '/^---$/{c++} c==1 && /^status:[[:space:]]*open/{print; exit}' "$f" | grep -q open; then + title="$(awk '/^# /{sub(/^# /,""); print; exit}' "$f")" + [ -z "$title" ] && title="$(basename "$f" .md)" + echo "- [$title]($f)" + found=1 + fi + done + [ "$found" = "0" ] && echo "_No open bugs._" + else + echo "_No bugs logged yet._" + fi + echo + + echo "━━━" + echo + echo "## Code graph (Graphify)" + echo + REPORT="$GRAPH_DIR/GRAPH_REPORT.md" + if [ -f "$REPORT" ]; then + # Inline only the "God nodes" or "Surprising connections" section, capped. + awk ' + /^## (God nodes|Surprising connections|Suggested questions)/ { keep=1; print; next } + keep && /^## / { keep=0 } + keep { print } + ' "$REPORT" | head -n 80 + echo + echo "_Full graph report: \`$REPORT\`_" + else + echo "_Graphify has not run yet. Run: \`graphify .\` then \`graphify hook install\`._" + fi + echo +} > "$CONTEXT_FILE" + +# Emit to stdout so the SessionStart hook injects it as context. +cat "$CONTEXT_FILE" diff --git a/.brain/scripts/lib/compact-routing.sh b/.brain/scripts/lib/compact-routing.sh new file mode 100755 index 0000000..e07b671 --- /dev/null +++ b/.brain/scripts/lib/compact-routing.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# compact-routing.sh — shared helpers for routing compact summaries to Dev Brain. +# Source-only. No side effects on load. + +# route_compact_to_dev_brain +# Copies a compact .md to $DEV_BRAIN_VAULT/sessions/compacts//. +# Idempotent: skips if destination already exists with same content. +# Fail-safe: any error is swallowed; never affects caller exit status. +route_compact_to_dev_brain() { + local src="$1" project="$2" ts="$3" sid="${4:-unknown}" + local vault="${DEV_BRAIN_VAULT:-$HOME/Documents/Dev Brain}" + + [ -f "$src" ] || return 0 + [ -d "$vault" ] || return 0 + + local dest_dir="$vault/sessions/compacts/$project" + mkdir -p "$dest_dir" 2>/dev/null || return 0 + + local base + base="$(basename "$src")" + local dest="$dest_dir/$base" + + # Idempotency: skip if destination already exists with identical content. + if [ -f "$dest" ] && cmp -s "$src" "$dest" 2>/dev/null; then + return 0 + fi + + # Build dest content: source file + wikilinks footer (for Obsidian graph connectivity). + local tmp="$dest.tmp.$$" + { + cat "$src" + # Inject footer only if not already present. + if ! grep -q "\[\[wiki/$project\]\]" "$src" 2>/dev/null; then + echo "" + echo "---" + printf '[[wiki/%s]] · [[sessions/index]]\n' "$project" + fi + } > "$tmp" 2>/dev/null || return 0 + mv -f "$tmp" "$dest" 2>/dev/null || { rm -f "$tmp"; return 0; } + + echo "[brain] Compact routed → $dest" >&2 + return 0 +} + +# route_all_session_compacts +# Reads the COMPACTS_TMP ledger (ts|path|excerpt) and routes each entry. +route_all_session_compacts() { + local tmp="$1" project="$2" sid="${3:-unknown}" + [ -f "$tmp" ] || return 0 + while IFS='|' read -r ts path excerpt; do + [ -n "$path" ] && route_compact_to_dev_brain "$path" "$project" "$ts" "$sid" + done < "$tmp" +} diff --git a/.brain/scripts/log-compact.sh b/.brain/scripts/log-compact.sh new file mode 100755 index 0000000..b0cdd7b --- /dev/null +++ b/.brain/scripts/log-compact.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash +# log-compact.sh — UserPromptSubmit hook +# +# Fires when the user submits "/compact" or "/compact ". +# At this moment, the NEW compact hasn't been generated yet — but any PRIOR +# compact from this session IS already in the transcript. We eagerly extract +# the most recent prior compact and route it to Dev Brain immediately, so +# long-running sessions don't accumulate un-mirrored compacts until session end. +# +# The Stop hook (log-session.sh) is the final catch-all — this is best-effort +# acceleration. Both use the same idempotent route_compact_to_dev_brain helper. +# +# Fail-safe: any error → silent no-op. UserPromptSubmit must not emit stdout. + +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +source "$REPO_ROOT/.brain/config.sh" 2>/dev/null || exit 0 + +PAYLOAD="" +[ ! -t 0 ] && PAYLOAD="$(cat)" +[ -z "$PAYLOAD" ] && exit 0 + +command -v jq >/dev/null 2>&1 || exit 0 + +PROMPT="$(echo "$PAYLOAD" | jq -r '.prompt // empty' 2>/dev/null || echo "")" +case "$PROMPT" in + "/compact"|"/compact "*) ;; + *) exit 0 ;; +esac + +TRANSCRIPT_PATH="$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null || echo "")" +SESSION_ID="$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null || echo "")" +[ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ] || exit 0 + +mkdir -p "$SESSIONS_DIR/compacts" 2>/dev/null || exit 0 + +# Extract the most recent prior compact summary (if any). +LAST_COMPACT="$(jq -cs ' + def textify: + if type == "array" then + [ .[] | select(.type == "text") | .text ] | join("\n\n") + elif type == "string" then . + else "" end; + + [ .[] + | select(.isCompactSummary == true) + | {ts: (.timestamp // "unknown"), + content: (.message.content | textify)} + | select(.content != "") + ] | last // empty +' "$TRANSCRIPT_PATH" 2>/dev/null)" + +[ -z "$LAST_COMPACT" ] || [ "$LAST_COMPACT" = "null" ] && exit 0 + +TS="$(echo "$LAST_COMPACT" | jq -r '.ts')" +CONTENT="$(echo "$LAST_COMPACT" | jq -r '.content')" +SLUG="$(date -u -d "$TS" +"%Y-%m-%d-%H%M" 2>/dev/null || date -u +"%Y-%m-%d-%H%M")" +OUT_FILE="$SESSIONS_DIR/compacts/${SLUG}-compact.md" + +# Write compact file only if it doesn't already exist (idempotent). +if [ ! -f "$OUT_FILE" ]; then + { + echo "---" + echo "type: compact-summary" + echo "project: $PROJECT_SLUG" + echo "session-id: ${SESSION_ID:-unknown}" + echo "compacted-at: $TS" + echo "source: log-compact.sh (eager)" + echo "tags: [compact, $PROJECT_SLUG]" + echo "---" + echo + echo "# Compact Summary — $TS" + echo + echo "$CONTENT" + } > "$OUT_FILE" 2>/dev/null +fi + +# Route to Dev Brain. +if [ -f "$REPO_ROOT/.brain/scripts/lib/compact-routing.sh" ]; then + # shellcheck disable=SC1091 + source "$REPO_ROOT/.brain/scripts/lib/compact-routing.sh" + route_compact_to_dev_brain "$OUT_FILE" "$PROJECT_SLUG" "$TS" "${SESSION_ID:-unknown}" +fi 2>/dev/null || true + +# No stdout — UserPromptSubmit must not inject context. +exit 0 diff --git a/.brain/scripts/log-session.sh b/.brain/scripts/log-session.sh new file mode 100755 index 0000000..f08e701 --- /dev/null +++ b/.brain/scripts/log-session.sh @@ -0,0 +1,297 @@ +#!/usr/bin/env bash +# log-session.sh — Stop hook +# +# Captures objective session metadata at end of session: +# - timestamp + duration +# - branch + git diff vs. session start +# - commits made during the session +# - /compact summaries extracted from JSONL transcript +# +# Writes .brain/sessions/YYYY-MM-DD-HHMM-session.md. +# Compact summaries go to .brain/sessions/compacts/ (separate files, pointer in session). + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +# shellcheck disable=SC1091 +source "$REPO_ROOT/.brain/config.sh" + +# Read stdin payload if present (Claude Code Stop hook sends JSON). +PAYLOAD="" +if [ ! -t 0 ]; then + PAYLOAD="$(cat)" +fi + +SESSION_ID="" +TRANSCRIPT_PATH="" +if [ -n "$PAYLOAD" ] && command -v jq >/dev/null 2>&1; then + SESSION_ID="$(echo "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null || true)" + TRANSCRIPT_PATH="$(echo "$PAYLOAD" | jq -r '.transcript_path // empty' 2>/dev/null || true)" +fi + +NOW_UTC="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +NOW_LOCAL="$(date +"%Y-%m-%d %H:%M %Z")" +DATE_TAG="$(date +"%Y-%m-%d-%H%M")" +BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "no-git")" +HEAD_SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "no-git")" + +# Load session-start state if recorded. +START_SHA="" +START_LOCAL="" +SESSION_START_UTC="" +DURATION="" +if [ -f "$SESSION_STATE" ]; then + # shellcheck disable=SC1090 + source "$SESSION_STATE" + START_SHA="${SESSION_START_SHA:-}" + START_LOCAL="${SESSION_START_LOCAL:-}" + SESSION_START_UTC="${SESSION_START_UTC:-}" + if [ -n "${SESSION_START_UTC:-}" ]; then + start_epoch="$(date -u -d "$SESSION_START_UTC" +%s 2>/dev/null || echo 0)" + end_epoch="$(date -u +%s)" + if [ "$start_epoch" -gt 0 ]; then + delta=$((end_epoch - start_epoch)) + DURATION="$((delta / 60)) min" + fi + fi +fi + +# Compute git activity during the session. +FILES_CHANGED="" +COMMITS_MADE="" +DIFFSTAT="" +if [ -n "$START_SHA" ] && [ "$START_SHA" != "$HEAD_SHA" ]; then + FILES_CHANGED="$(git diff --name-only "$START_SHA" HEAD 2>/dev/null || true)" + COMMITS_MADE="$(git log --oneline "$START_SHA..HEAD" 2>/dev/null || true)" + DIFFSTAT="$(git diff --stat "$START_SHA" HEAD 2>/dev/null | tail -n 1 || true)" +fi +WORKING_TREE="$(git status --short 2>/dev/null || true)" + +# Pull in iterations captured during the session by log-tool-event.sh. +ACTIVE_LOG="$SESSIONS_DIR/_active.md" +ITERATIONS="" +if [ -f "$ACTIVE_LOG" ]; then + ITERATIONS="$(grep -v '^" + echo + echo "## Iterations" + echo + echo "_Auto-captured by \`log-tool-event.sh\` during the session: builds, tests, lint, commits._" + echo + if [ -n "$ITERATIONS" ]; then + echo "$ITERATIONS" + else + echo "_No build/test/lint/commit events recorded._" + fi + echo + echo "## Compactions" + echo + if [ "${#COMPACT_LINES[@]}" -gt 0 ]; then + for line in "${COMPACT_LINES[@]}"; do + IFS='|' read -r ts file excerpt <<< "$line" + echo "- **$ts** → \`$file\`" + echo " > ${excerpt}…" + done + else + echo "_No /compact during this session._" + fi + echo + echo "## Commits made" + echo + if [ -n "$COMMITS_MADE" ]; then + echo '```' + echo "$COMMITS_MADE" + echo '```' + else + echo "_No commits made during this session._" + fi + echo + echo "## Files changed" + echo + if [ -n "$FILES_CHANGED" ]; then + echo '```' + echo "$FILES_CHANGED" + echo '```' + [ -n "$DIFFSTAT" ] && echo "**Diffstat:** $DIFFSTAT" + else + echo "_No tracked files changed via commits._" + fi + echo + echo "## Working tree at end" + echo + if [ -n "$WORKING_TREE" ]; then + echo '```' + echo "$WORKING_TREE" + echo '```' + else + echo "_Clean._" + fi + echo +} > "$OUT" + +# Cleanup transient session state. +rm -f "$SESSION_STATE" +rm -f "$ACTIVE_LOG" +rm -f "$COMPACTS_TMP" + +echo "[brain] Session logged → $OUT" >&2 + +# ── Dev Brain session index ─────────────────────────────────────────────────── +# Append a pointer to Dev Brain so any agent can query session history. +# Fails silently — Dev Brain indexing must never break the primary stop hook. +{ + DEV_BRAIN_VAULT="${DEV_BRAIN_VAULT:-$HOME/Documents/Dev Brain}" + if [ -d "$DEV_BRAIN_VAULT" ]; then + IDX="$DEV_BRAIN_VAULT/sessions/index.md" + # Hybrid summary: compact → commit → last assistant turn → diffstat → no-summary. + # All tiers routed through _sb_sanitize to keep the pipe-delimited index format intact. + _sb_sanitize() { + tr '\n\r\t|' ' ' | tr -s ' ' | sed 's/^ *//;s/ *$//' | head -c 120 + } + + SUMMARY_LINE="" + + # Tier 1: compact excerpt (LLM-distilled, highest signal when present) + if [ "${#COMPACT_LINES[@]}" -gt 0 ]; then + SUMMARY_LINE="$(printf '%s' "${COMPACT_LINES[0]}" | awk -F'|' '{for(i=3;i<=NF;i++)printf "%s%s",$i,(i/dev/null | _sb_sanitize)" + fi + + # Tier 4: diffstat as structural fallback + if [ -z "$SUMMARY_LINE" ] && [ -n "$DIFFSTAT" ]; then + SUMMARY_LINE="$(printf '%s' "$DIFFSTAT" | _sb_sanitize)" + fi + + SUMMARY_LINE="${SUMMARY_LINE:-no-summary}" + # Append one-liner to global index (pipe-separated for grep/awk) + echo "${NOW_UTC} | ${PROJECT_SLUG} | ${DURATION:-unknown} | ${HEAD_SHA} | ${SUMMARY_LINE} | ${OUT}" >> "$IDX" + echo "[brain] Session indexed → $IDX" >&2 + fi +} 2>/dev/null || true diff --git a/.brain/scripts/log-tool-event.sh b/.brain/scripts/log-tool-event.sh new file mode 100755 index 0000000..5d4aa29 --- /dev/null +++ b/.brain/scripts/log-tool-event.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# log-tool-event.sh — PostToolUse hook +# +# Captures objectively important Bash command outcomes (builds, tests, lint, +# commits) and appends them to the active session log: +# .brain/sessions/_active.md +# +# All other tool calls (Edit, Read, Write, etc.) and unrelated Bash commands +# are ignored. The Stop hook merges _active.md into the final session log. +# +# Fail-safe: any parse error or missing field results in a silent no-op so +# the user's session is never disrupted. + +set -uo pipefail # no -e: never break the user's session + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +# shellcheck disable=SC1091 +source "$REPO_ROOT/.brain/config.sh" 2>/dev/null || exit 0 + +ACTIVE_LOG="$SESSIONS_DIR/_active.md" + +# Read JSON payload from stdin (Claude Code provides tool_name, tool_input, tool_response). +PAYLOAD="" +if [ ! -t 0 ]; then + PAYLOAD="$(cat)" +fi +[ -z "$PAYLOAD" ] && exit 0 + +# jq is required to parse the payload — degrade gracefully if missing. +command -v jq >/dev/null 2>&1 || exit 0 + +TOOL_NAME=$(echo "$PAYLOAD" | jq -r '.tool_name // empty' 2>/dev/null || echo "") +[ "$TOOL_NAME" != "Bash" ] && exit 0 + +CMD=$(echo "$PAYLOAD" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "") +[ -z "$CMD" ] && exit 0 + +# Filter: only log build / test / lint / commit. Everything else is noise. +EVENT_KIND="" +case "$CMD" in + *"npm run build"*|*"yarn build"*|*"pnpm build"*) EVENT_KIND="build" ;; + *"npm run test"*|*"npm test"*|*"yarn test"*|*"pnpm test"*) EVENT_KIND="test" ;; + *"npm run lint"*|*"yarn lint"*|*"pnpm lint"*) EVENT_KIND="lint" ;; + *"git commit"*) EVENT_KIND="commit" ;; + *) exit 0 ;; +esac + +# Extract result fields (defensive — schema may vary). +EXIT_CODE=$(echo "$PAYLOAD" | jq -r ' + .tool_response.exit_code // + .tool_response.exitCode // + .tool_response.returncode // + 0 +' 2>/dev/null || echo "0") + +OUTPUT=$(echo "$PAYLOAD" | jq -r ' + .tool_response.output // + .tool_response.stdout // + .tool_response.content // + empty +' 2>/dev/null || echo "") + +# Init active log if missing. +mkdir -p "$SESSIONS_DIR" +if [ ! -f "$ACTIVE_LOG" ]; then + { + echo "" + echo + } > "$ACTIVE_LOG" +fi + +NOW="$(date +"%H:%M")" +STATUS="✅" +[ "$EXIT_CODE" != "0" ] && STATUS="❌" + +case "$EVENT_KIND" in + commit) + if [ "$EXIT_CODE" = "0" ]; then + SHA="$(git rev-parse --short HEAD 2>/dev/null || echo "?")" + MSG="$(git log -1 --pretty=%s 2>/dev/null | head -c 100 || echo "")" + FILES="$(git show --stat --format= HEAD 2>/dev/null | tail -n 1 | tr -s ' ' || echo "")" + { + echo "- [${NOW}] **commit** \`${SHA}\` · ${MSG}" + [ -n "$FILES" ] && echo " - ${FILES}" + } >> "$ACTIVE_LOG" + else + echo "- [${NOW}] ❌ **commit failed** (exit ${EXIT_CODE})" >> "$ACTIVE_LOG" + fi + ;; + build|test|lint) + # Truncate command for readability. + CMD_SHORT="$(echo "$CMD" | head -c 80 | tr '\n' ' ')" + if [ "$EXIT_CODE" = "0" ]; then + echo "- [${NOW}] ${STATUS} **${EVENT_KIND}** \`${CMD_SHORT}\`" >> "$ACTIVE_LOG" + else + # Capture last 8 lines of output on failure — that's where the error usually is. + TAIL="$(echo "$OUTPUT" | tail -n 8)" + { + echo "- [${NOW}] ${STATUS} **${EVENT_KIND} FAILED** \`${CMD_SHORT}\` (exit ${EXIT_CODE})" + if [ -n "$TAIL" ]; then + echo ' ```' + echo "$TAIL" | sed 's/^/ /' + echo ' ```' + fi + } >> "$ACTIVE_LOG" + fi + ;; +esac + +exit 0 diff --git a/.brain/scripts/register-to-dev-brain.sh b/.brain/scripts/register-to-dev-brain.sh new file mode 100755 index 0000000..144a3af --- /dev/null +++ b/.brain/scripts/register-to-dev-brain.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# register-to-dev-brain.sh — Register this project in Dev Brain's registry. +# Run once per project (idempotent). Updates projects.json + AGENT_README.md. +set -uo pipefail +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +source "$REPO_ROOT/.brain/config.sh" 2>/dev/null +bash "$HOME/Documents/Dev Brain/.scripts/register-project.sh" \ + "$PROJECT_SLUG" \ + "$PROJECT_NAME" \ + "$REPO_ROOT" diff --git a/.brain/scripts/sync-graph-to-vault.sh b/.brain/scripts/sync-graph-to-vault.sh new file mode 100755 index 0000000..cb3e821 --- /dev/null +++ b/.brain/scripts/sync-graph-to-vault.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# sync-graph-to-vault.sh — Copies GRAPH_REPORT.md into the CKIS vault. +# +# Called from: +# - assemble-context.sh (SessionStart) — catch-up on every session open +# - post-commit.brain git hook — low-latency sync after each commit +# +# Wraps the report in CKIS-standard frontmatter so Obsidian indexes it +# correctly. Skips if the file is already identical to avoid mtime churn. + +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +source "$REPO_ROOT/.brain/config.sh" 2>/dev/null || exit 0 + +SRC="$REPO_ROOT/$GRAPH_DIR/GRAPH_REPORT.md" +DEST_DIR="$CKIS_VAULT/02-projects/$PROJECT_SLUG" +DEST="$DEST_DIR/graph-report.md" + +[ -f "$SRC" ] || exit 0 +[ -d "$DEST_DIR" ] || exit 0 # vault not mounted on this machine — no-op + +NOW="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" +TMP="$(mktemp)" + +{ + echo "---" + echo "type: graph-report" + echo "project: $PROJECT_SLUG" + echo "source: \"$SRC\"" + echo "generated: $NOW" + echo "auto: true" + echo "tags: [graph, $PROJECT_SLUG, auto-generated]" + echo "---" + echo + echo "> Auto-synced from \`.brain/graph/GRAPH_REPORT.md\` — do not hand-edit." + echo + cat "$SRC" +} > "$TMP" + +# Skip write if content is identical (avoids triggering Obsidian re-index). +if [ -f "$DEST" ] && cmp -s "$TMP" "$DEST"; then + rm -f "$TMP" + exit 0 +fi + +mv "$TMP" "$DEST" +echo "[brain] graph-report synced → $DEST" >&2 diff --git a/.brain/scripts/sync-obsidian-graph.sh b/.brain/scripts/sync-obsidian-graph.sh new file mode 100755 index 0000000..157e188 --- /dev/null +++ b/.brain/scripts/sync-obsidian-graph.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# sync-obsidian-graph.sh — regenerates Graphify Obsidian notes for this project. +# +# Reads .brain/graph/graph.json and writes one .md file per code node into +# ~/Documents/Dev Brain/code-graph// using the graphify.export Python API +# (the graphify CLI's `update` command does not expose --obsidian; the flag only +# exists in the Claude skill's full pipeline, so we call the Python API directly). +# +# Called from: +# - post-commit.brain (cadence-gated: every OBSIDIAN_GRAPH_CADENCE commits) +# - Manually: bash .brain/scripts/sync-obsidian-graph.sh + +set -uo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$REPO_ROOT" + +source "$REPO_ROOT/.brain/config.sh" 2>/dev/null || exit 0 + +DEV_BRAIN_VAULT="${DEV_BRAIN_VAULT:-$HOME/Documents/Dev Brain}" +OBS_DIR="$DEV_BRAIN_VAULT/code-graph/$PROJECT_SLUG" +GRAPH_JSON="$REPO_ROOT/$GRAPH_DIR/graph.json" + +[ -d "$DEV_BRAIN_VAULT" ] || { echo "[brain] Dev Brain vault not found at $DEV_BRAIN_VAULT — skipping" >&2; exit 0; } +[ -f "$GRAPH_JSON" ] || { echo "[brain] graph.json not found — run graphify update . first" >&2; exit 0; } + +mkdir -p "$OBS_DIR" + +python3 - "$GRAPH_JSON" "$OBS_DIR" <<'PYEOF' +import sys, json, warnings +import networkx as nx +from pathlib import Path + +graph_path, obs_dir = Path(sys.argv[1]), sys.argv[2] + +with warnings.catch_warnings(): + warnings.simplefilter("ignore") + G = nx.node_link_graph(json.loads(graph_path.read_text()), edges="links") + +communities = {} +for node, data in G.nodes(data=True): + c = data.get("community", 0) + communities.setdefault(c, []).append(node) + +from graphify.export import to_obsidian +n = to_obsidian(G, communities, obs_dir) +print(f"[brain] {n} Obsidian notes written to {obs_dir}", file=sys.stderr) +PYEOF + +# ── Build Dev Brain wiki page for this project ──────────────────────────────── +BUILD_WIKI="$DEV_BRAIN_VAULT/.scripts/build-wiki-page.sh" +if [ -x "$BUILD_WIKI" ]; then + bash "$BUILD_WIKI" "$PROJECT_SLUG" 2>/dev/null || true +fi diff --git a/.brain/sessions/.gitkeep b/.brain/sessions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..1f38971 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,67 @@ +{ + "permissions": { + "allow": [ + "Bash(npm run build)", + "Bash(npm run dev)", + "Bash(npm run lint)", + "Bash(git *)", + "Bash(bash .brain/scripts/*)", + "Bash(graphify *)", + "Bash(bash .brain/scripts/sync-obsidian-graph.sh)" + ] + }, + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash -c 'cd \"$(git rev-parse --show-toplevel 2>/dev/null)\" && bash .brain/scripts/assemble-context.sh'" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "bash -c 'cd \"$(git rev-parse --show-toplevel 2>/dev/null)\" && bash .brain/scripts/log-tool-event.sh'" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "bash -c 'cd \"$(git rev-parse --show-toplevel 2>/dev/null)\" && bash .brain/scripts/log-compact.sh'" + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash -c 'cd \"$(git rev-parse --show-toplevel 2>/dev/null)\" && bash .brain/scripts/log-session.sh'" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "CMD=$(python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('tool_input',d).get('command',''))\" 2>/dev/null || true); case \"$CMD\" in *grep*|*rg\\ *|*ripgrep*|*find\\ *|*fd\\ *|*ack\\ *|*ag\\ *) [ -f graphify-out/graph.json ] && echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"additionalContext\":\"graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files.\"}}' || true ;; esac" + } + ] + } + ] + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f13022..e0fd21a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,23 +6,62 @@ on: pull_request: branches: [main] +permissions: {} + jobs: test: name: test (${{ matrix.os }}, node ${{ matrix.node }}) runs-on: ${{ matrix.os }} + permissions: + contents: read strategy: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] node: [20, 22] steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: node-version: ${{ matrix.node }} cache: npm - - run: npm ci - - run: npm run lint - - run: npm run typecheck - - run: npm run build - - run: npm test + + - name: Install dependencies (zero-trust) + run: npm ci --ignore-scripts + + # WebTorrent's native deps (utp-native, bufferutil, utf-8-validate, + # node-datachannel, fs-native-extensions) ship pre-built binaries via + # prebuild-install. Rebuild only if pre-built binary is missing. + # These are whitelisted: all are source-available and provide WebRTC/UDP primitives. + - name: Rebuild native WebTorrent dependencies + # Force bash: on Windows the default shell is pwsh, where `2>/dev/null` + # is parsed as Out-File to `D:\dev\null` and fails the step. + shell: bash + run: | + npm rebuild utp-native bufferutil utf-8-validate 2>/dev/null || true + npm rebuild node-datachannel fs-native-extensions 2>/dev/null || true + env: + npm_config_ignore_scripts: 'false' + + # ip SSRF (GHSA-2p57-rm9w-gvfp) is patched via package.json overrides (ip@2.0.1 in lockfile). + # npm audit still reports HIGH on webtorrent/bittorrent-tracker/torrent-discovery because + # the advisory tracks the dependent chain, not the patched root package — this is a known + # npm false positive when using overrides. Auditing at --audit-level=critical until + # webtorrent upstream releases with ip>=2.0.1 declared in their own package.json. + - name: Dependency security audit + run: npm audit --audit-level=critical + + - name: Lint + run: npm run lint + + - name: Type check + run: npm run typecheck + + - name: Build + run: npm run build + + - name: Test + run: npm test diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 1778ce4..d70af89 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -8,20 +8,24 @@ on: schedule: - cron: '0 6 * * 1' +permissions: {} + jobs: analyze: name: Analyze runs-on: ubuntu-latest - # CodeQL requires GitHub Advanced Security (GHAS). Skip on private repos - # until the repo is made public — the workflow stays here for that transition. if: github.event.repository.private == false permissions: actions: read contents: read security-events: write steps: - - uses: actions/checkout@v4 - - uses: github/codeql-action/init@v3 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - uses: github/codeql-action/init@d77b13a0df3134d64a457ea9003f600b09fa1c8a # v3 with: languages: javascript-typescript - - uses: github/codeql-action/analyze@v3 + + - uses: github/codeql-action/analyze@d77b13a0df3134d64a457ea9003f600b09fa1c8a # v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4fc6ef5..b9b43b0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,29 +4,59 @@ on: push: tags: ['v*'] -permissions: - contents: write - id-token: write +permissions: {} jobs: npm-publish: runs-on: ubuntu-latest + permissions: + contents: write + id-token: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: - node-version: 20 + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' registry-url: https://registry.npmjs.org cache: npm - - run: npm ci - - run: npm test - - run: npm run build - - run: npm publish --provenance --access public + + - name: Install dependencies (zero-trust) + run: npm ci --ignore-scripts + + - name: Rebuild native WebTorrent dependencies + # Force bash: on Windows the default shell is pwsh, where `2>/dev/null` + # is parsed as Out-File to `D:\dev\null` and fails the step. + shell: bash + run: | + npm rebuild utp-native bufferutil utf-8-validate 2>/dev/null || true + npm rebuild node-datachannel fs-native-extensions 2>/dev/null || true + env: + npm_config_ignore_scripts: 'false' + + # See ci.yml note: ip patched via overrides, audit-level=critical until webtorrent upstream fix + - name: Dependency security audit + run: npm audit --audit-level=critical + + - name: Type check + Test + Build + run: npm run typecheck && npm test && npm run build + + - name: Create GitHub Release + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 + with: + generate_release_notes: true + + - name: Publish to npm + run: npm publish --provenance --access public --ignore-scripts env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} binaries: needs: npm-publish + permissions: + contents: write strategy: fail-fast: false matrix: @@ -39,14 +69,30 @@ jobs: target: win-x64 runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 with: - node-version: 20 + node-version: '22' cache: npm - - run: npm ci - - run: npm run build - # Node SEA (Single Executable Application) bundling is wired up in v0.6. - # Placeholder step keeps the matrix valid until then. + + - name: Install dependencies (zero-trust) + run: npm ci --ignore-scripts + + - name: Rebuild native WebTorrent dependencies + # Force bash: on Windows the default shell is pwsh, where `2>/dev/null` + # is parsed as Out-File to `D:\dev\null` and fails the step. + shell: bash + run: | + npm rebuild utp-native bufferutil utf-8-validate 2>/dev/null || true + npm rebuild node-datachannel fs-native-extensions 2>/dev/null || true + env: + npm_config_ignore_scripts: 'false' + + - name: Build + run: npm run build + - name: Package standalone binary run: echo "SEA packaging for ${{ matrix.target }} lands in v0.6" diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 0000000..0bdf5d8 --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,30 @@ +name: Security Audit + +on: + schedule: + - cron: '0 9 * * 1' # Every Monday 09:00 UTC + workflow_dispatch: # Manual trigger + +permissions: {} + +jobs: + audit: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: '22' + + - name: Install dependencies (zero-trust) + run: npm ci --ignore-scripts + + # Weekly audit catches moderate/low that are non-blocking in CI. + # Fail on moderate or above — gives 7 days to assess before next push. + - name: Full dependency audit + run: npm audit --audit-level=moderate diff --git a/.gitignore b/.gitignore index 6c15563..2e17efb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,15 @@ coverage/ # Planning context is private — stripped before the public flip. docs/planning/ + +# .brain/ — per-project second brain (runtime artifacts only; scripts/config committed) +# decisions/, bugs/, scripts/, BRAIN.md, README.md, config.sh are versioned. +.brain/_CONTEXT.md +.brain/.session-state +.brain/.compact-triggers +.brain/sessions/* +!.brain/sessions/.gitkeep +.brain/graph/* +!.brain/graph/.gitkeep +# graphify-out is a symlink → .brain/graph (regenerable, not committed) +graphify-out diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..7947ff5 --- /dev/null +++ b/.npmrc @@ -0,0 +1,19 @@ +# ── Security lockdown ───────────────────────────────────────────────────── +# Block lifecycle scripts (postinstall/preinstall) during npm install/ci. +# Prevents supply chain attacks from executing arbitrary code on install. +# NOTE: npm run build/test/etc. still work — this only blocks INSTALL hooks. +ignore-scripts=true + +# Pin to the official registry — prevents registry confusion / substitution attacks +registry=https://registry.npmjs.org/ + +# Save exact versions (no ^ or ~ ranges) when adding new packages +save-exact=true + +# Enforce lockfile — fail if package-lock.json is missing or out of sync +# (enforced via npm ci in CI; locally this is a reminder) +package-lock=true + +# Disable noisy output +fund=false + diff --git a/CHANGELOG.md b/CHANGELOG.md index 10dd953..35d6b0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,35 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] -Nothing yet. +## [1.0.0] — 2026-06-04 + +First stable release: the complete terminal pipeline — search → in-process +WebTorrent stream → native VLC → hash-based subtitles — plus a full-download +mode and a standalone subtitle command. Public, semver-stable command surface +and `--json` envelope contract. + +### Added + +- Subtitle pipeline: OpenSubtitles/VLSub `moviehash` (size + first/last 64 KiB + checksum), OpenSubtitles REST v1 client (hash + text search, ranked download), + and a `subs ` command that writes `..srt` beside the video. +- `download ` command — full torrent download to the configured + directory with progress, plus automatic subtitle fetch for non-MKV files. +- `stream`/`play`: non-MKV streams now best-effort fetch subtitles by title and + pass `--sub-file` to VLC (never fails the stream on a subtitle error). +- `doctor`: download-directory write check and an advisory (warn-only) + OpenSubtitles API-key check that does not flip the exit code. + +### Fixed + +- Flag arity: optional/default-wrapped flags (`z.string().optional()`, + `z.number().optional()`) were misclassified as boolean, so value-taking flags + like `--container`, `--quality`, `--indexer`, `--sub-lang`, `--query`, `--out` + silently swallowed no argument ("too many arguments"). The registry now + unwraps Optional/Default/Nullable to the underlying type for both flag arity + and numeric coercion. +- Agent-mode subprocess tests (`config get`) given an explicit 15s timeout so + Node startup under parallel test load no longer trips the 5s default. ## [0.1.0] — 2026-06-02 @@ -38,5 +66,6 @@ Nothing yet. - CodeQL workflow: guard with `if: github.event.repository.private == false` to prevent spurious failures on private repos without GitHub Advanced Security. -[Unreleased]: https://github.com/aedneth/streamnet-cli/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/aedneth/streamnet-cli/compare/v1.0.0...HEAD +[1.0.0]: https://github.com/aedneth/streamnet-cli/compare/v0.1.0...v1.0.0 [0.1.0]: https://github.com/aedneth/streamnet-cli/releases/tag/v0.1.0 diff --git a/README.md b/README.md index 98d23ef..0293370 100644 --- a/README.md +++ b/README.md @@ -69,10 +69,17 @@ streamnet doctor # verifies VLC, webtorrent, Node, network streamnet search "Blade Runner 2049" # health-ranked results streamnet play "Blade Runner 2049" --yes # search + best result + stream streamnet stream "magnet:?xt=urn:btih:..." # stream a specific torrent +streamnet download "magnet:?xt=urn:btih:..." # full download + auto-subs (non-MKV) +streamnet subs ~/Videos/Movie.mp4 --lang es,en # fetch subtitles by file hash streamnet config list # view configuration streamnet manifest # machine-readable command catalog ``` +> Subtitles need a free OpenSubtitles API key: +> `streamnet config set opensubtitles.apiKey ` (key from +> ). MKV files use their embedded track +> and skip the lookup automatically. + ### Agent / scripting examples ```bash @@ -142,13 +149,13 @@ streamnet config get opensubtitles.apiKey # secrets are redacted on display ## Roadmap -| Version | Status | Highlights | -| ------- | ------ | ---------- | -| **v0.1.0** | ✅ shipped | Search (torrents-csv + YTS), WebTorrent engine, native VLC spawn, setup/doctor, agent-native `--json` / exit codes / manifest | -| **v0.2.0** | planned | OpenSubtitles hash-based subtitle fetch + VLC injection; MCP server (`streamnet mcp`) | -| **v0.3.0** | planned | Real-debrid / Premiumize resolver; additional indexers (1337x, RARBG mirrors) | -| **v0.4.0** | planned | Watch history + resume; `streamnet library` catalog; shell completions | -| **v1.0.0** | future | Stable public API, binary releases, Homebrew tap, Scoop bucket | +| Version | Status | Highlights | +| ---------- | ---------- | -------------------------------------------------------------------------------------------------------------------------------- | +| **v0.1.0** | ✅ shipped | Search (torrents-csv + YTS), WebTorrent engine, native VLC spawn, setup/doctor, agent-native `--json` / exit codes / manifest | +| **v1.0.0** | ✅ shipped | First stable release — full pipeline: `play`/`stream`/`download`/`subs`, OpenSubtitles hash-based subtitles + VLC injection, stable `--json` contract | +| **v1.1.0** | planned | MCP server (`streamnet mcp`); Real-debrid / Premiumize resolver; additional indexers (1337x, RARBG mirrors) | +| **v1.2.0** | planned | Watch history + resume; `streamnet library` catalog; shell completions | +| **future** | planned | Standalone binary releases (SEA), Homebrew tap, Scoop bucket | ## Contributing diff --git a/SECURITY.md b/SECURITY.md index 6281c66..8ef98d0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,40 +1,38 @@ # Security Policy -## Supported versions +## Supported Versions -StreamNet CLI is pre-1.0. Security fixes are applied to the latest released -minor version. Once 1.0.0 ships, the latest minor will be supported. +| Version | Supported | +|---------|-----------| +| latest | ✅ | +| < latest | ❌ — update to latest | -| Version | Supported | -| ---------- | --------- | -| latest 0.x | ✅ | -| older 0.x | ❌ | +## Reporting a Vulnerability -## Reporting a vulnerability +**Do not open a public GitHub issue for security vulnerabilities.** -**Please do not open a public GitHub issue for security vulnerabilities.** +Email: eduardoa.borjas@gmail.com -Report privately via one of: +Include: +- Description of the vulnerability +- Steps to reproduce +- Potential impact +- Any suggested fix (optional) -- GitHub's **private vulnerability reporting** (Security → Report a vulnerability) -- Email: **eduardoa.borjas@gmail.com** with subject `[streamnet-cli security]` +You will receive a response within 48 hours. If confirmed, a patch will be released within 7 days. -Please include: +## Supply Chain Security -- a description of the issue and its impact, -- steps to reproduce or a proof of concept, -- affected version(s) and platform. +This project implements zero-trust npm security: -You can expect an acknowledgement within **5 business days** and a status update -within **15 business days**. Coordinated disclosure is appreciated — we'll agree -a disclosure timeline with you once the issue is confirmed. +- **`ignore-scripts=true`** in `.npmrc` — blocks all postinstall/preinstall lifecycle scripts during `npm install`/`npm ci`. Prevents supply chain attacks via compromised transitive dependencies. +- **Explicit native module whitelist** — only named, reviewed native modules (listed in CI) are allowed to compile. All others are blocked. +- **Pinned GitHub Actions** — all Actions are pinned to a specific commit SHA, not a mutable tag. This prevents compromised Action tags from injecting malicious steps. +- **`npm publish --provenance`** — every published release includes a signed SLSA attestation linking the package to the exact GitHub Actions run that built it. Verify with: `npm audit signatures @` +- **`npm ci` in all CI jobs** — never `npm install`. Enforces exact cryptographic hash matching against `package-lock.json`. +- **Minimum permissions** — each CI job declares only the permissions it needs. Default is `permissions: {}` (deny all). +- **Weekly automated audit** — the Security Audit workflow runs every Monday at 09:00 UTC and fails on any moderate or higher vulnerability. -## Scope notes +## Known Mitigations -StreamNet spawns native VLC and runs a local HTTP stream server bound to -`127.0.0.1`. Reports involving local privilege escalation, the stream server, the -VLC IPC interface, subtitle handling, or indexer response parsing are in scope. - -StreamNet does not host or distribute content; it searches third-party indexers -and streams via the BitTorrent network. Legal/abuse concerns about specific -content are out of scope for this security policy. +Any known vulnerability mitigations (e.g., transitive dependency overrides) are documented in the relevant CI workflow files with inline comments. diff --git a/package-lock.json b/package-lock.json index cd87979..54fe762 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,12 +26,12 @@ "@types/node": "^20.16.0", "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", - "@vitest/coverage-v8": "^2.1.4", + "@vitest/coverage-v8": "^4.1.8", "eslint": "^9.14.0", "prettier": "^3.3.3", "tsup": "^8.3.5", "typescript": "^5.5.4", - "vitest": "^2.1.4" + "vitest": "^4.1.8" }, "engines": { "node": ">=20" @@ -40,20 +40,6 @@ "webtorrent": "^2.5.1" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -105,11 +91,48 @@ } }, "node_modules/@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } }, "node_modules/@esbuild/aix-ppc64": { "version": "0.27.7", @@ -822,34 +845,6 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", - "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -889,35 +884,39 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", "dev": true, "license": "MIT", "optional": true, - "engines": { - "node": ">=14" + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", - "cpu": [ - "arm" - ], + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "funding": { + "url": "https://github.com/sponsors/Boshen" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -926,12 +925,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -940,12 +942,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -954,188 +959,460 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ - "arm64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ - "x64" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "freebsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ - "arm" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ - "arm64" + "s390x" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ - "loong64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ - "ppc64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ - "ppc64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ - "riscv64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ - "riscv64" + "x64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" - ] + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", "cpu": [ - "s390x" + "arm" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "linux" + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { @@ -1286,6 +1563,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@thaunknown/simple-peer": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/@thaunknown/simple-peer/-/simple-peer-10.1.1.tgz", @@ -1351,6 +1635,35 @@ "node": ">=0.2.6" } }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1609,31 +1922,29 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.9.tgz", - "integrity": "sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.8.tgz", + "integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", - "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.7", + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.8", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.12", - "magicast": "^0.3.5", - "std-env": "^3.8.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^1.2.0" + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "2.1.9", - "vitest": "2.1.9" + "@vitest/browser": "4.1.8", + "vitest": "4.1.8" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -1642,38 +1953,40 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -1685,84 +1998,68 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1842,19 +2139,6 @@ "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1895,6 +2179,18 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz", + "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, "node_modules/b4a": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", @@ -2453,18 +2749,11 @@ } }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -2486,16 +2775,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16" - } - }, "node_modules/cheerio": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", @@ -2693,6 +2972,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cpus": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cpus/-/cpus-1.0.3.tgz", @@ -2852,16 +3138,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -3005,8 +3281,8 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "devOptional": true, "license": "Apache-2.0", - "optional": true, "engines": { "node": ">=8" } @@ -3066,20 +3342,6 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, "node_modules/encoding-sniffer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", @@ -3123,9 +3385,9 @@ "optional": true }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, @@ -3674,23 +3936,6 @@ "dev": true, "license": "ISC" }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -3822,28 +4067,6 @@ "license": "MIT", "optional": true }, - "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3857,39 +4080,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", @@ -4133,16 +4323,6 @@ "license": "MIT", "optional": true }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4223,21 +4403,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -4252,22 +4417,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/join-async-iterator": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/join-async-iterator/-/join-async-iterator-1.1.1.tgz", @@ -4285,6 +4434,13 @@ "node": ">=10" } }, + "node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/js-yaml": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", @@ -4405,123 +4561,370 @@ "node": ">= 0.8.0" } }, - "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==", + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, "engines": { - "node": ">=14" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/limiter": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", - "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", - "optional": true - }, - "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==", - "dev": true, - "license": "MIT" - }, - "node_modules/load-ip-set": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/load-ip-set/-/load-ip-set-3.0.2.tgz", - "integrity": "sha512-UD1GM3CLlkC3b0gAKIxd+6SFJb1WQttWyYhwvjdWjGpJKzu32HnaSMfWtUtVgRtFY+K5vgrvecuVQLRxx5Ojag==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" ], - "license": "MIT", + "dev": true, + "license": "MPL-2.0", "optional": true, - "dependencies": { - "cross-fetch-ponyfill": "^1.0.1", - "ip-set": "^3.0.0", - "netmask": "^2.0.1", - "once": "^1.4.0", - "queue-microtask": "^1.2.3", - "split": "^1.0.1" - }, + "os": [ + "android" + ], "engines": { - "node": ">=12.20.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/load-tsconfig": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", - "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" + "node": ">= 12.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" - }, - "node_modules/lru": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lru/-/lru-3.1.0.tgz", - "integrity": "sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==", - "license": "MIT", + "license": "MPL-2.0", "optional": true, - "dependencies": { - "inherits": "^2.0.1" - }, + "os": [ + "linux" + ], "engines": { - "node": ">= 0.4.0" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "ISC" + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==", + "optional": true + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-ip-set": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/load-ip-set/-/load-ip-set-3.0.2.tgz", + "integrity": "sha512-UD1GM3CLlkC3b0gAKIxd+6SFJb1WQttWyYhwvjdWjGpJKzu32HnaSMfWtUtVgRtFY+K5vgrvecuVQLRxx5Ojag==", + "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", + "optional": true, + "dependencies": { + "cross-fetch-ponyfill": "^1.0.1", + "ip-set": "^3.0.0", + "netmask": "^2.0.1", + "once": "^1.4.0", + "queue-microtask": "^1.2.3", + "split": "^1.0.1" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lru/-/lru-3.1.0.tgz", + "integrity": "sha512-5OUtoiVIGU4VXBOshidmtOsvBIvcQR6FD/RzWSvaeHyxCGB+PCUCu+52lqMfdc0h/2CLvHhZS4TwUmMQrrMbBQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.1" + }, + "engines": { + "node": ">= 0.4.0" + } }, "node_modules/lt_donthave": { "version": "2.0.7", @@ -4562,15 +4965,15 @@ } }, "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" } }, "node_modules/magnet-uri": { @@ -4700,16 +5103,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/minipass": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=16 || 14 >=14.17" - } - }, "node_modules/mkdirp-classic": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", @@ -4928,6 +5321,17 @@ "node": ">=0.10.0" } }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -5032,13 +5436,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5167,23 +5564,6 @@ "node": ">=8" } }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -5191,16 +5571,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5590,6 +5960,40 @@ "node": ">=4" } }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, "node_modules/rollup": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", @@ -5926,9 +6330,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, @@ -5956,132 +6360,28 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, + "node_modules/string2compact": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/string2compact/-/string2compact-2.0.2.tgz", + "integrity": "sha512-ZUUIyrS+sSj84gR6fFoz/boiqCptyw37/hMxIehReuf6ekpJOlwSXhTfEasqc5t3QucBXi2ghwLlHOSC8hjEaw==", "license": "MIT", + "optional": true, "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "addr-to-ip-port": "^2.0.0", + "ipaddr.js": "2.0.1" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12.20.0" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "node_modules/string2compact/node_modules/ipaddr.js": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", + "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, + "optional": true, "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string2compact": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/string2compact/-/string2compact-2.0.2.tgz", - "integrity": "sha512-ZUUIyrS+sSj84gR6fFoz/boiqCptyw37/hMxIehReuf6ekpJOlwSXhTfEasqc5t3QucBXi2ghwLlHOSC8hjEaw==", - "license": "MIT", - "optional": true, - "dependencies": { - "addr-to-ip-port": "^2.0.0", - "ipaddr.js": "2.0.1" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/string2compact/node_modules/ipaddr.js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz", - "integrity": "sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/strip-ansi": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", - "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" + "node": ">= 10" } }, "node_modules/strip-final-newline": { @@ -6207,21 +6507,6 @@ "streamx": "^2.12.5" } }, - "node_modules/test-exclude": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", - "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^10.2.2" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/text-decoder": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", @@ -6314,30 +6599,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -6432,6 +6697,14 @@ "dev": true, "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==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/tsup": { "version": "8.5.1", "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", @@ -6706,21 +6979,23 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -6729,23 +7004,33 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "less": { + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -6762,582 +7047,155 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" }, "bin": { - "vite-node": "vite-node.mjs" + "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } } }, - "node_modules/vite-node/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "license": "MIT", "optional": true, - "os": [ - "android" - ], "engines": { - "node": ">=12" + "node": ">= 8" } }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/webrtc-polyfill": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/webrtc-polyfill/-/webrtc-polyfill-1.2.1.tgz", + "integrity": "sha512-B52Rwxu7wzhLhANMRBys8W1wXAi9LwVzLfWWyueSQZmjpgojzX5p5g3m/fcZMFDtB+JZLnOt0Ud9u9FOZakg1Q==", "license": "MIT", "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "node-datachannel": "^0.32.3" + }, "engines": { - "node": ">=12" + "node": ">=16.0.0" } }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/webrtc-polyfill": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/webrtc-polyfill/-/webrtc-polyfill-1.2.1.tgz", - "integrity": "sha512-B52Rwxu7wzhLhANMRBys8W1wXAi9LwVzLfWWyueSQZmjpgojzX5p5g3m/fcZMFDtB+JZLnOt0Ud9u9FOZakg1Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "node-datachannel": "^0.32.3" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/webtorrent": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/webtorrent/-/webtorrent-2.8.5.tgz", - "integrity": "sha512-oIjpuBrypApJ+RCZ8RRaHEncVSkt2cd25/I4Trb2sk9nlaEF92Dg1u8BCwqA4eJR7wIZQM95GyO7Wo4QTbrUUA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } + "node_modules/webtorrent": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/webtorrent/-/webtorrent-2.8.5.tgz", + "integrity": "sha512-oIjpuBrypApJ+RCZ8RRaHEncVSkt2cd25/I4Trb2sk9nlaEF92Dg1u8BCwqA4eJR7wIZQM95GyO7Wo4QTbrUUA==", + "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", "optional": true, @@ -7460,101 +7318,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index 461c062..6fc0624 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "streamnet-cli", - "version": "0.1.0", + "version": "1.0.0", "description": "Torrent search + in-process WebTorrent streaming to native VLC with hash-based subtitles. Agent-native by design.", "type": "module", "license": "AGPL-3.0-or-later", @@ -73,11 +73,14 @@ "@types/node": "^20.16.0", "@typescript-eslint/eslint-plugin": "^8.13.0", "@typescript-eslint/parser": "^8.13.0", - "@vitest/coverage-v8": "^2.1.4", + "@vitest/coverage-v8": "^4.1.8", "eslint": "^9.14.0", "prettier": "^3.3.3", "tsup": "^8.3.5", "typescript": "^5.5.4", - "vitest": "^2.1.4" + "vitest": "^4.1.8" + }, + "overrides": { + "ip": ">=2.0.1" } } diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 59f33fe..3b40895 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -9,19 +9,19 @@ export interface DoctorResult { } export async function doctorHandler( - _ctx: CommandContext, + ctx: CommandContext, _input: Record, ): Promise { - const checks = await runAllChecks(); + const checks = await runAllChecks(ctx.config); const allOk = checks.every((c) => c.ok); return { checks, allOk }; } export function renderDoctor(data: DoctorResult, _output: OutputContext): void { for (const check of data.checks) { - const icon = check.ok ? pc.green('✔') : pc.red('✖'); + const icon = !check.ok ? pc.red('✖') : check.warn ? pc.yellow('⚠') : pc.green('✔'); process.stdout.write(` ${icon} ${check.name.padEnd(14)} ${check.message}\n`); - if (!check.ok && check.hint) { + if ((!check.ok || check.warn) && check.hint) { process.stdout.write(` ${pc.dim(check.hint)}\n`); } } diff --git a/src/commands/download.ts b/src/commands/download.ts new file mode 100644 index 0000000..9fb89bc --- /dev/null +++ b/src/commands/download.ts @@ -0,0 +1,86 @@ +import { readFileSync, mkdirSync } from 'node:fs'; +import type { CommandContext } from '../registry/types.js'; +import type { OutputContext } from '../agent/output.js'; +import { downloadTorrent } from '../core/torrent/engine.js'; +import { isMkv } from '../core/torrent/select.js'; +import { fetchSubtitle, osConfigFrom } from '../core/subtitles/fetch.js'; +import { ExitCode, fail } from '../agent/exit.js'; +import { formatBytes } from '../util/format.js'; + +export interface DownloadInput { + source: string; + out?: string; + fileIndex?: number; + noSubs?: boolean; +} + +export interface DownloadCmdResult { + filePath: string; + fileName: string; + sizeBytes: number; + subtitlePath?: string; +} + +export async function downloadHandler( + ctx: CommandContext, + input: DownloadInput, +): Promise { + let source = input.source; + if (source === '-') { + source = readFileSync('/dev/stdin', 'utf8').trim(); + if (!source) fail(ExitCode.USAGE, 'No source provided via stdin.'); + } + + const outDir = input.out ?? ctx.config.downloadDir; + try { + mkdirSync(outDir, { recursive: true }); + } catch (err) { + fail(ExitCode.NETWORK, `Cannot create download directory ${outDir}: ${String(err)}`); + } + + ctx.output.info(`Downloading to ${outDir}…`); + + const result = await downloadTorrent({ + source, + outDir, + fileIndex: input.fileIndex, + preferredContainers: ctx.config.preferredContainers, + onProgress: (p) => { + ctx.output.progress({ + progress: (p.progress * 100).toFixed(1) + '%', + peers: p.peers, + speed: formatBytes(p.downloadSpeed) + '/s', + }); + }, + }); + + ctx.output.success(`Downloaded: ${result.filePath} (${formatBytes(result.sizeBytes)})`); + + // Auto subtitle search for non-MKV (best-effort; never fails the download). + let subtitlePath: string | undefined; + const haveKey = Boolean(ctx.config.opensubtitles.apiKey); + if (!input.noSubs && !isMkv(result.fileName) && haveKey) { + try { + const sub = await fetchSubtitle({ + cfg: osConfigFrom(ctx.config), + videoPath: result.filePath, + }); + subtitlePath = sub.path; + ctx.output.success(`Subtitle (${sub.language}) → ${sub.path}`); + } catch (err) { + ctx.output.warn(`Subtitle search failed: ${(err as Error).message}`); + } + } + + return { + filePath: result.filePath, + fileName: result.fileName, + sizeBytes: result.sizeBytes, + subtitlePath, + }; +} + +export function renderDownload(data: DownloadCmdResult, output: OutputContext): void { + output.success(`Saved ${data.fileName} → ${data.filePath}`); + if (data.subtitlePath) output.info(`Subtitle: ${data.subtitlePath}`); +} diff --git a/src/commands/stream.ts b/src/commands/stream.ts index f00c327..d11096b 100644 --- a/src/commands/stream.ts +++ b/src/commands/stream.ts @@ -1,4 +1,5 @@ import { readFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; import type { CommandContext } from '../registry/types.js'; import type { OutputContext } from '../agent/output.js'; import { startStream } from '../core/torrent/engine.js'; @@ -6,6 +7,11 @@ import { isMkv } from '../core/torrent/select.js'; import { spawnVlc, waitForVlc } from '../core/player/vlc.js'; import { ExitCode, fail } from '../agent/exit.js'; import { formatBytes } from '../util/format.js'; +import { + fetchSubtitle, + osConfigFrom, + queryFromFileName, +} from '../core/subtitles/fetch.js'; export interface StreamInput { source: string; @@ -57,11 +63,37 @@ export async function streamHandler( try { let subtitleFile: string | undefined; - const skipSubs = input.noSubs || isMkv(info.fileName); + const haveKey = Boolean(ctx.config.opensubtitles.apiKey); + const skipSubs = input.noSubs || isMkv(info.fileName) || !haveKey; + + if (input.noSubs) { + // explicitly disabled + } else if (isMkv(info.fileName)) { + ctx.output.info('MKV detected — using embedded subtitles, skipping search.'); + } else if (!haveKey) { + ctx.output.warn( + 'Non-MKV file but no OpenSubtitles key — playing without subtitles.', + ); + } if (!skipSubs) { - // Subtitle search happens in v0.3; for now inform the user - ctx.output.info('Non-MKV file detected. Subtitle search will be added in v0.3.'); + // Best-effort: a live stream has no complete local file, so search by title. + // Never fail the stream over a subtitle problem. + try { + const langs = input.subLang ? [input.subLang] : undefined; + const sub = await fetchSubtitle({ + cfg: osConfigFrom(ctx.config), + query: queryFromFileName(info.fileName), + languages: langs, + outDir: tmpdir(), + }); + subtitleFile = sub.path; + ctx.output.success(`Subtitle (${sub.language}) → ${sub.path}`); + } catch (err) { + ctx.output.warn( + `Subtitle search failed, continuing without: ${(err as Error).message}`, + ); + } } const vlcProc = spawnVlc({ diff --git a/src/commands/subs.ts b/src/commands/subs.ts new file mode 100644 index 0000000..a771200 --- /dev/null +++ b/src/commands/subs.ts @@ -0,0 +1,69 @@ +import { existsSync } from 'node:fs'; +import type { CommandContext } from '../registry/types.js'; +import type { OutputContext } from '../agent/output.js'; +import { fetchSubtitle, osConfigFrom } from '../core/subtitles/fetch.js'; +import { ExitCode, fail } from '../agent/exit.js'; + +export interface SubsInput { + file: string; + lang?: string; + query?: string; +} + +export interface SubsResult { + subtitlePath: string; + language: string; + matchedByHash: boolean; + source: string; +} + +export async function subsHandler( + ctx: CommandContext, + input: SubsInput, +): Promise { + const file = input.file?.trim(); + if (!file) { + fail(ExitCode.USAGE, '`subs` requires a video file path (or use --query).'); + } + + const isLocalFile = existsSync(file); + if (!isLocalFile && !input.query) { + fail( + ExitCode.USAGE, + `File not found: ${file}`, + 'Pass an existing video file, or use --query "Title Year" for a text search.', + ); + } + + const languages = input.lang + ? input.lang + .split(',') + .map((l) => l.trim()) + .filter(Boolean) + : undefined; + + ctx.output.info( + isLocalFile + ? `Searching subtitles by file hash: ${file}` + : `Searching subtitles: ${input.query}`, + ); + + const result = await fetchSubtitle({ + cfg: osConfigFrom(ctx.config), + videoPath: isLocalFile ? file : undefined, + query: input.query, + languages, + }); + + return { + subtitlePath: result.path, + language: result.language, + matchedByHash: result.matchedByHash, + source: result.fileName, + }; +} + +export function renderSubs(data: SubsResult, output: OutputContext): void { + const how = data.matchedByHash ? 'hash match' : 'text match'; + output.success(`Subtitle (${data.language}, ${how}) → ${data.subtitlePath}`); +} diff --git a/src/core/setup/checks.ts b/src/core/setup/checks.ts index e152ee1..9b90745 100644 --- a/src/core/setup/checks.ts +++ b/src/core/setup/checks.ts @@ -1,12 +1,19 @@ +import { mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; import { findVlc } from '../player/detect.js'; import { httpGetText } from '../../util/http.js'; import { logger } from '../../util/logger.js'; +import { downloadsDir } from '../../config/paths.js'; +import type { Config } from '../../config/schema.js'; export interface CheckResult { name: string; + /** Contributes to `allOk` / the doctor exit code. A `warn` is reported with ok=true. */ ok: boolean; message: string; hint?: string; + /** Advisory-only: surfaced as a warning but does not fail doctor. */ + warn?: boolean; } async function checkVlc(): Promise { @@ -64,12 +71,45 @@ async function checkWebtorrent(): Promise { } } -export async function runAllChecks(): Promise { +async function checkDownloadDir(dir: string): Promise { + try { + mkdirSync(dir, { recursive: true }); + const probe = join(dir, `.streamnet-write-test-${process.pid}`); + writeFileSync(probe, 'ok'); + rmSync(probe, { force: true }); + return { name: 'downloadDir', ok: true, message: `Writable: ${dir}` }; + } catch { + return { + name: 'downloadDir', + ok: false, + message: `Download directory not writable: ${dir}`, + hint: 'Set a writable path: streamnet config set downloadDir ', + }; + } +} + +async function checkOpenSubtitles(config?: Config): Promise { + if (config?.opensubtitles.apiKey) { + return { name: 'opensubtitles', ok: true, message: 'API key configured' }; + } + return { + name: 'opensubtitles', + ok: true, + warn: true, + message: 'No API key — subtitle search disabled (optional)', + hint: 'Free key at https://www.opensubtitles.com/consumers, then: streamnet config set opensubtitles.apiKey ', + }; +} + +export async function runAllChecks(config?: Config): Promise { + const downloadDir = config?.downloadDir ?? downloadsDir(); const results = await Promise.allSettled([ checkNode(), checkVlc(), checkWebtorrent(), checkNetwork(), + checkDownloadDir(downloadDir), + checkOpenSubtitles(config), ]); return results.map((r) => { diff --git a/src/core/subtitles/fetch.ts b/src/core/subtitles/fetch.ts new file mode 100644 index 0000000..77a4fe1 --- /dev/null +++ b/src/core/subtitles/fetch.ts @@ -0,0 +1,114 @@ +import { writeFile } from 'node:fs/promises'; +import { existsSync } from 'node:fs'; +import { basename, dirname, extname, join } from 'node:path'; +import { ExitCode, fail } from '../../agent/exit.js'; +import { movieHash } from './hash.js'; +import { + searchSubtitles, + downloadSubtitle, + type OpenSubtitlesConfig, +} from './opensubtitles.js'; +import { logger } from '../../util/logger.js'; + +export interface FetchSubtitleOptions { + cfg: OpenSubtitlesConfig; + /** Local video path — when present (and large enough) a moviehash search is used. */ + videoPath?: string; + /** Text query — used as a fallback, or as the primary signal for live streams. */ + query?: string; + languages?: string[]; + /** Directory to write the .srt into. Defaults to the video's directory or cwd. */ + outDir?: string; +} + +export interface FetchedSubtitle { + path: string; + language: string; + fileName: string; + matchedByHash: boolean; +} + +/** Strip extension and common scene/torrent noise to make a usable text query. */ +export function queryFromFileName(name: string): string { + return basename(name, extname(name)) + .replace(/[._]+/g, ' ') + .replace( + /\b(1080p|720p|2160p|4k|x264|x265|hevc|web-?dl|bluray|hdtv|aac|ac3)\b/gi, + ' ', + ) + .replace(/[[(].*?[\])]/g, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +/** + * Locate and download the best subtitle, writing it next to the video as + * `..srt` (a path VLC auto-discovers). Prefers a moviehash match on + * the local file and falls back to a text query. + * + * @throws SUBS_NOT_FOUND when nothing matches; AUTH when the key is missing/rejected. + */ +export async function fetchSubtitle( + opts: FetchSubtitleOptions, +): Promise { + let moviehash: string | undefined; + if (opts.videoPath && existsSync(opts.videoPath)) { + try { + moviehash = (await movieHash(opts.videoPath)).hash; + } catch (err) { + // File too small or unreadable — fall back to the text query. + logger.debug(`moviehash skipped: ${String(err)}`); + } + } + + const query = + opts.query ?? (opts.videoPath ? queryFromFileName(opts.videoPath) : undefined); + + if (!moviehash && !query) { + fail(ExitCode.USAGE, 'Subtitle search needs a local file or a --query.'); + } + + const matches = await searchSubtitles(opts.cfg, { + moviehash, + query, + languages: opts.languages, + }); + + if (matches.length === 0) { + fail( + ExitCode.SUBS_NOT_FOUND, + `No subtitles found${query ? ` for "${query}"` : ''}.`, + 'Try a different --lang, or pass --query with the exact title and year.', + ); + } + + const best = matches[0]!; + const { content, fileName } = await downloadSubtitle(opts.cfg, best.fileId); + + const outDir = + opts.outDir ?? (opts.videoPath ? dirname(opts.videoPath) : process.cwd()); + const base = opts.videoPath + ? basename(opts.videoPath, extname(opts.videoPath)) + : basename(fileName, extname(fileName)); + const outPath = join(outDir, `${base}.${best.language}.srt`); + + await writeFile(outPath, content, 'utf8'); + + return { + path: outPath, + language: best.language, + fileName, + matchedByHash: best.hashMatch, + }; +} + +/** Build the OpenSubtitles client config from the resolved app config. */ +export function osConfigFrom(config: { + opensubtitles: { apiKey?: string }; + subtitleLanguages: string[]; +}): OpenSubtitlesConfig { + return { + apiKey: config.opensubtitles.apiKey, + languages: config.subtitleLanguages, + }; +} diff --git a/src/core/subtitles/hash.ts b/src/core/subtitles/hash.ts new file mode 100644 index 0000000..6dbc0c4 --- /dev/null +++ b/src/core/subtitles/hash.ts @@ -0,0 +1,66 @@ +import { open, stat } from 'node:fs/promises'; + +/** + * OpenSubtitles / VLSub "moviehash". + * + * The hash is a 64-bit value: the file size plus the 64-bit (little-endian) + * checksum of the first 64 KiB and the last 64 KiB of the file, all summed with + * unsigned 64-bit wraparound. It is the most reliable way to match a subtitle to + * a video because it depends on the bytes, not a fuzzy title match. + * + * Reference: https://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes + * + * Implementation reads only the two 64 KiB windows, never the whole file, so it + * is cheap even for multi-gigabyte videos. + */ +const CHUNK_SIZE = 64 * 1024; // 64 KiB +const U64_MASK = (1n << 64n) - 1n; + +/** Minimum file size OpenSubtitles considers hashable (two non-overlapping chunks). */ +export const MIN_HASHABLE_BYTES = 2 * CHUNK_SIZE; + +/** Sum every little-endian uint64 in a buffer into `acc`, with uint64 wraparound. */ +function sumQwords(buf: Buffer, acc: bigint): bigint { + let sum = acc; + // Only whole 8-byte words contribute (matches the reference implementation). + const end = buf.length - (buf.length % 8); + for (let i = 0; i < end; i += 8) { + sum = (sum + buf.readBigUInt64LE(i)) & U64_MASK; + } + return sum; +} + +/** + * Compute the moviehash for a file on disk. + * + * @returns lowercase 16-char hex string (zero-padded). + * @throws if the file is smaller than {@link MIN_HASHABLE_BYTES}; callers should + * fall back to a title/query search in that case. + */ +export async function movieHash( + filePath: string, +): Promise<{ hash: string; size: number }> { + const { size } = await stat(filePath); + if (size < MIN_HASHABLE_BYTES) { + throw new Error( + `File too small to hash (${size} bytes; need >= ${MIN_HASHABLE_BYTES}).`, + ); + } + + const fh = await open(filePath, 'r'); + try { + let hash = BigInt(size) & U64_MASK; + + const head = Buffer.alloc(CHUNK_SIZE); + await fh.read(head, 0, CHUNK_SIZE, 0); + hash = sumQwords(head, hash); + + const tail = Buffer.alloc(CHUNK_SIZE); + await fh.read(tail, 0, CHUNK_SIZE, size - CHUNK_SIZE); + hash = sumQwords(tail, hash); + + return { hash: hash.toString(16).padStart(16, '0'), size }; + } finally { + await fh.close(); + } +} diff --git a/src/core/subtitles/opensubtitles.ts b/src/core/subtitles/opensubtitles.ts new file mode 100644 index 0000000..9b0ff2a --- /dev/null +++ b/src/core/subtitles/opensubtitles.ts @@ -0,0 +1,180 @@ +import { ExitCode, StreamNetError, fail } from '../../agent/exit.js'; + +/** + * Minimal OpenSubtitles REST v1 client. + * + * Only the two endpoints StreamNet needs: subtitle search (by moviehash or text) + * and download-link resolution. Auth is an Api-Key header — never hardcoded, it + * comes from `config.opensubtitles.apiKey`. The base URL is overridable via + * STREAMNET_OPENSUBTITLES_URL so tests can point at a mock. + * + * API docs: https://opensubtitles.stoplight.io/docs/opensubtitles-api + */ +const BASE_URL = + process.env.STREAMNET_OPENSUBTITLES_URL ?? 'https://api.opensubtitles.com/api/v1'; +const DEFAULT_UA = 'streamnet-cli/1.0'; + +export interface OpenSubtitlesConfig { + apiKey?: string; + /** Language preference order, e.g. ['es', 'en']. */ + languages?: string[]; + userAgent?: string; +} + +export interface SubtitleMatch { + fileId: number; + fileName: string; + language: string; + /** True when the result matched by file hash rather than text — far more reliable. */ + hashMatch: boolean; + downloadCount: number; + release?: string; +} + +export interface SubtitleSearchParams { + moviehash?: string; + query?: string; + /** Overrides the config language preference for this call. */ + languages?: string[]; +} + +interface OsFile { + file_id: number; + file_name?: string; +} +interface OsAttributes { + language?: string; + download_count?: number; + moviehash_match?: boolean; + release?: string; + files?: OsFile[]; +} +interface OsSearchResponse { + data?: { attributes?: OsAttributes }[]; +} +interface OsDownloadResponse { + link?: string; + file_name?: string; +} + +function headers(cfg: OpenSubtitlesConfig, json = false): Record { + if (!cfg.apiKey) { + fail( + ExitCode.AUTH, + 'OpenSubtitles API key not configured.', + 'Get a free key at https://www.opensubtitles.com/consumers and run: streamnet config set opensubtitles.apiKey ', + ); + } + const h: Record = { + 'Api-Key': cfg.apiKey, + 'User-Agent': cfg.userAgent ?? DEFAULT_UA, + Accept: 'application/json', + }; + if (json) h['Content-Type'] = 'application/json'; + return h; +} + +/** The init type of the global fetch, avoiding a direct reference to the DOM lib name. */ +type FetchInit = NonNullable[1]>; + +async function osFetch(url: string, init: FetchInit): Promise { + let res: Response; + try { + res = await fetch(url, init); + } catch (err) { + if (err instanceof StreamNetError) throw err; + fail(ExitCode.NETWORK, `OpenSubtitles request failed: ${String(err)}`); + } + if (res.status === 401 || res.status === 403) { + fail(ExitCode.AUTH, `OpenSubtitles rejected the API key (HTTP ${res.status}).`); + } + if (res.status === 429) { + fail(ExitCode.AUTH, 'OpenSubtitles rate limit exceeded. Try again later.'); + } + if (!res.ok) { + fail(ExitCode.NETWORK, `OpenSubtitles HTTP ${res.status} for ${url}`); + } + return res.json(); +} + +/** + * Search for subtitles by moviehash and/or text query. Results are sorted so the + * best candidate is first: hash matches beat text matches, then by the caller's + * language preference, then by download count. + */ +export async function searchSubtitles( + cfg: OpenSubtitlesConfig, + params: SubtitleSearchParams, +): Promise { + const langs = (params.languages ?? cfg.languages ?? ['en']).map((l) => l.toLowerCase()); + const qp = new URLSearchParams(); + if (params.moviehash) qp.set('moviehash', params.moviehash.toLowerCase()); + if (params.query) qp.set('query', params.query); + if (langs.length) qp.set('languages', langs.join(',')); + + const body = (await osFetch(`${BASE_URL}/subtitles?${qp.toString()}`, { + method: 'GET', + headers: headers(cfg), + })) as OsSearchResponse; + + const matches: SubtitleMatch[] = []; + for (const entry of body.data ?? []) { + const a = entry.attributes ?? {}; + const file = a.files?.[0]; + if (!file?.file_id) continue; + matches.push({ + fileId: file.file_id, + fileName: file.file_name ?? `${params.query ?? 'subtitle'}.srt`, + language: (a.language ?? 'unknown').toLowerCase(), + hashMatch: Boolean(a.moviehash_match), + downloadCount: a.download_count ?? 0, + release: a.release, + }); + } + + const langRank = (l: string): number => { + const i = langs.indexOf(l); + return i === -1 ? langs.length : i; + }; + matches.sort((x, y) => { + if (x.hashMatch !== y.hashMatch) return x.hashMatch ? -1 : 1; + const lr = langRank(x.language) - langRank(y.language); + if (lr !== 0) return lr; + return y.downloadCount - x.downloadCount; + }); + + return matches; +} + +/** + * Resolve a download link for a subtitle file and fetch its contents. Returns the + * raw subtitle text plus the server-provided filename. + */ +export async function downloadSubtitle( + cfg: OpenSubtitlesConfig, + fileId: number, +): Promise<{ content: string; fileName: string }> { + const dl = (await osFetch(`${BASE_URL}/download`, { + method: 'POST', + headers: headers(cfg, true), + body: JSON.stringify({ file_id: fileId }), + })) as OsDownloadResponse; + + if (!dl.link) { + fail(ExitCode.SUBS_NOT_FOUND, 'OpenSubtitles did not return a download link.'); + } + + let res: Response; + try { + res = await fetch(dl.link); + } catch (err) { + fail(ExitCode.NETWORK, `Failed to download subtitle file: ${String(err)}`); + } + if (!res.ok) { + fail(ExitCode.NETWORK, `Subtitle download failed (HTTP ${res.status}).`); + } + return { + content: await res.text(), + fileName: dl.file_name ?? `subtitle-${fileId}.srt`, + }; +} diff --git a/src/core/torrent/engine.ts b/src/core/torrent/engine.ts index 07f859f..4ef9984 100644 --- a/src/core/torrent/engine.ts +++ b/src/core/torrent/engine.ts @@ -1,3 +1,4 @@ +import { join } from 'node:path'; import { ExitCode, StreamNetError, fail } from '../../agent/exit.js'; import { logger } from '../../util/logger.js'; import { selectVideoFile } from './select.js'; @@ -36,17 +37,25 @@ export interface StreamOptions { * commands that don't need streaming (search, config, doctor) don't pay the load * cost and so the module can be absent in test environments. */ -export async function startStream(opts: StreamOptions): Promise { - let WebTorrentCtor: new () => WebTorrentInstance; +/** + * Dynamically load the WebTorrent constructor. It's a heavy optional dependency, + * so it is imported lazily and absent-tolerant (commands that don't stream, and + * test environments, don't pay for it). + */ +async function loadWebTorrent(): Promise WebTorrentInstance> { try { const mod = (await import('webtorrent')) as { default: new () => WebTorrentInstance }; - WebTorrentCtor = mod.default; + return mod.default; } catch { fail( ExitCode.DEP_MISSING, 'webtorrent is not installed. Run `npm install webtorrent` or reinstall streamnet.', ); } +} + +export async function startStream(opts: StreamOptions): Promise { + const WebTorrentCtor = await loadWebTorrent(); return new Promise((resolve, reject) => { const client = new WebTorrentCtor(); @@ -137,10 +146,111 @@ export async function startStream(opts: StreamOptions): Promise void; + signal?: AbortSignal; + /** How long to wait for torrent metadata before giving up (ms). Default 30 s. */ + metadataTimeoutMs?: number; +} + +export interface DownloadResult { + /** Absolute path to the primary (selected) video file on disk. */ + filePath: string; + fileName: string; + fileIndex: number; + sizeBytes: number; +} + +/** + * Download a torrent fully to disk and resolve once complete. Files persist after + * the client is destroyed because they are written under `opts.outDir`. + */ +export async function downloadTorrent(opts: DownloadOptions): Promise { + const WebTorrentCtor = await loadWebTorrent(); + + return new Promise((resolve, reject) => { + const client = new WebTorrentCtor(); + const metaTimeout = opts.metadataTimeoutMs ?? 30_000; + + const timer = setTimeout(() => { + client.destroy(); + reject( + new StreamNetError( + ExitCode.TORRENT_UNPLAYABLE, + 'Torrent metadata timed out — no peers responded.', + ), + ); + }, metaTimeout); + + if (opts.signal) { + opts.signal.addEventListener( + 'abort', + () => { + clearTimeout(timer); + client.destroy(); + reject(new StreamNetError(ExitCode.TORRENT_UNPLAYABLE, 'Download aborted.')); + }, + { once: true }, + ); + } + + client.add(opts.source, { path: opts.outDir }, (torrent: TorrentHandle) => { + clearTimeout(timer); + logger.info(`Downloading: ${torrent.name} (${torrent.files.length} files)`); + + const fileList = torrent.files.map((f: TorrentFileHandle) => ({ + name: f.name, + sizeBytes: f.length, + })); + const fileIndex = + opts.fileIndex ?? selectVideoFile(fileList, opts.preferredContainers); + const file = torrent.files[fileIndex]; + if (!file) { + client.destroy(); + reject( + new StreamNetError( + ExitCode.TORRENT_UNPLAYABLE, + `File index ${fileIndex} not found in torrent.`, + ), + ); + return; + } + + torrent.on('download', () => { + opts.onProgress?.({ + progress: torrent.progress, + peers: torrent.numPeers, + downloadSpeed: torrent.downloadSpeed, + }); + }); + + torrent.on('done', () => { + const filePath = join(opts.outDir, file.path ?? file.name); + client.destroy(); + resolve({ filePath, fileName: file.name, fileIndex, sizeBytes: file.length }); + }); + }); + + client.on('error', (err: Error) => { + clearTimeout(timer); + reject(err); + }); + }); +} + // Minimal type stubs (avoids @types/webtorrent dependency) interface TorrentFileHandle { name: string; length: number; + /** Relative path of the file within the torrent (used for on-disk location). */ + path?: string; } interface TorrentHandle { name: string; @@ -151,7 +261,11 @@ interface TorrentHandle { on(event: string, cb: () => void): void; } interface WebTorrentInstance { - add(src: string, cb: (t: TorrentHandle) => void): void; + add( + src: string, + optsOrCb: { path?: string } | ((t: TorrentHandle) => void), + cb?: (t: TorrentHandle) => void, + ): void; on(event: string, cb: (e: Error) => void): void; destroy(): void; } diff --git a/src/registry/build.ts b/src/registry/build.ts index d8e4f11..ff182e9 100644 --- a/src/registry/build.ts +++ b/src/registry/build.ts @@ -1,4 +1,5 @@ import { Command } from 'commander'; +import type { z } from 'zod'; import type { CommandSpec, CommandContext } from './types.js'; import { ExitCode, StreamNetError, fail } from '../agent/exit.js'; @@ -24,9 +25,9 @@ export function buildCommand(spec: CommandSpec, ctx: () => CommandContext): Comm for (const flag of spec.flags ?? []) { const short = flag.short ? `-${flag.short}, ` : ''; - const isBoolean = - flag.schema._def?.typeName === 'ZodBoolean' || - flag.schema._def?.typeName === 'ZodOptional'; + // Only true booleans are value-less. Optional/default-wrapped strings and + // numbers (e.g. `z.string().optional()`) still take a argument. + const isBoolean = unwrappedTypeName(flag.schema) === 'ZodBoolean'; const syntax = isBoolean ? `${short}--${flag.long}` : `${short}--${flag.long} `; @@ -95,12 +96,13 @@ function buildInput( for (const flag of spec.flags ?? []) { const envVal = flag.env ? process.env[flag.env] : undefined; const cliVal = opts[camel(flag.long)]; - const typeName = flag.schema._def?.typeName as string | undefined; + const typeName = unwrappedTypeName(flag.schema); if (envVal !== undefined) { input[camel(flag.long)] = coerceEnv(envVal, typeName); } else if (cliVal !== undefined) { // Coerce string values for numeric flags (Commander always gives strings) - input[camel(flag.long)] = typeName === 'ZodNumber' ? coerceNum(cliVal, flag.long) : cliVal; + input[camel(flag.long)] = + typeName === 'ZodNumber' ? coerceNum(cliVal, flag.long) : cliVal; } else if (flag.default !== undefined) { input[camel(flag.long)] = flag.default; } @@ -118,10 +120,26 @@ function coerceEnv(val: string, typeName: string | undefined): unknown { function coerceNum(val: unknown, flagName: string): number { const n = Number(val); - if (Number.isNaN(n)) fail(ExitCode.USAGE, `--${flagName} requires a numeric value, got: ${String(val)}`); + if (Number.isNaN(n)) + fail(ExitCode.USAGE, `--${flagName} requires a numeric value, got: ${String(val)}`); return n; } +/** + * Resolve the underlying Zod type name, unwrapping Optional/Default/Nullable + * wrappers. `z.string().optional()` reports `ZodString`, not `ZodOptional` — so + * flag-arity and numeric-coercion decisions look at the real value type. + */ +export function unwrappedTypeName(schema: z.ZodTypeAny): string | undefined { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let s: any = schema; + const wrappers = new Set(['ZodOptional', 'ZodDefault', 'ZodNullable']); + while (s?._def && wrappers.has(s._def.typeName)) { + s = s._def.innerType; + } + return s?._def?.typeName as string | undefined; +} + function camel(s: string): string { return s.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); } diff --git a/src/registry/index.ts b/src/registry/index.ts index a2df787..fde22af 100644 --- a/src/registry/index.ts +++ b/src/registry/index.ts @@ -7,6 +7,8 @@ import { playHandler, renderPlay } from '../commands/play.js'; import { setupHandler } from '../commands/setup.js'; import { doctorHandler, renderDoctor, type DoctorResult } from '../commands/doctor.js'; import { configHandler, renderConfig } from '../commands/config.js'; +import { subsHandler, renderSubs } from '../commands/subs.js'; +import { downloadHandler, renderDownload } from '../commands/download.js'; const COMMON_EXIT_CODES = [ { code: ExitCode.OK, meaning: 'Success' }, @@ -245,6 +247,92 @@ export const COMMAND_SPECS: CommandSpec[] = [ render: renderConfig as unknown as CommandSpec['render'], }, + { + id: 'subs', + summary: 'Find and download subtitles for a local video (by file hash).', + description: + 'Computes the OpenSubtitles/VLSub moviehash of a local file and downloads the ' + + 'best matching subtitle as `..srt` beside it. Falls back to a text ' + + 'query. Requires an OpenSubtitles API key in config.', + args: [ + { + name: 'file', + description: 'Path to a local video file (or any path when using --query)', + required: true, + }, + ], + flags: [ + { + long: 'lang', + description: 'Comma-separated language codes, preference order (e.g. es,en)', + schema: z.string().optional(), + }, + { + long: 'query', + description: 'Force a text search (Title Year) instead of/after a hash match', + schema: z.string().optional(), + }, + ], + exitCodes: [ + ...COMMON_EXIT_CODES, + { code: ExitCode.SUBS_NOT_FOUND, meaning: 'No matching subtitles found' }, + { code: ExitCode.AUTH, meaning: 'OpenSubtitles API key missing/rejected' }, + { code: ExitCode.NETWORK, meaning: 'OpenSubtitles unreachable' }, + ], + examples: [ + 'streamnet subs ~/Videos/Movie.mp4', + 'streamnet subs ~/Videos/Movie.mp4 --lang es,en', + 'streamnet subs movie --query "Dune Part Two 2024" --json', + ], + handler: subsHandler as unknown as CommandSpec['handler'], + render: renderSubs as unknown as CommandSpec['render'], + }, + + { + id: 'download', + summary: 'Download a torrent to disk (with subtitle auto-fetch for non-MKV).', + description: + 'Full download to the configured download directory, with progress. On ' + + 'completion, non-MKV files trigger a subtitle search automatically (unless --no-subs).', + args: [ + { + name: 'source', + description: 'Magnet link, .torrent URL, or infohash. Use - to read from stdin.', + required: true, + }, + ], + flags: [ + { + long: 'out', + description: 'Output directory (overrides config.downloadDir)', + schema: z.string().optional(), + }, + { + long: 'file-index', + description: 'Force a specific file index within the torrent', + schema: z.number().optional(), + }, + { + long: 'no-subs', + description: 'Skip the post-download subtitle search', + schema: z.boolean(), + default: false, + }, + ], + exitCodes: [ + ...COMMON_EXIT_CODES, + { code: ExitCode.DEP_MISSING, meaning: 'webtorrent not installed' }, + { code: ExitCode.TORRENT_UNPLAYABLE, meaning: 'No peers / metadata timeout' }, + { code: ExitCode.NETWORK, meaning: 'Write failure or download error' }, + ], + examples: [ + 'streamnet download "magnet:?xt=urn:btih:..."', + 'streamnet download "magnet:?xt=urn:btih:..." --out ~/Videos --json', + ], + handler: downloadHandler as unknown as CommandSpec['handler'], + render: renderDownload as unknown as CommandSpec['render'], + }, + { id: 'manifest', summary: 'Emit the machine-readable command manifest (agent discovery).', diff --git a/test/__snapshots__/manifest.test.ts.snap b/test/__snapshots__/manifest.test.ts.snap index 47bf512..89abba1 100644 --- a/test/__snapshots__/manifest.test.ts.snap +++ b/test/__snapshots__/manifest.test.ts.snap @@ -62,6 +62,27 @@ exports[`manifest > is a stable, serializable shape (snapshot of command ids + f "flags": [], "id": "config", }, + { + "args": [ + "file", + ], + "flags": [ + "lang", + "query", + ], + "id": "subs", + }, + { + "args": [ + "source", + ], + "flags": [ + "file-index", + "no-subs", + "out", + ], + "id": "download", + }, { "args": [], "flags": [], diff --git a/test/agent-mode.test.ts b/test/agent-mode.test.ts index 7b618dd..7dc8149 100644 --- a/test/agent-mode.test.ts +++ b/test/agent-mode.test.ts @@ -33,7 +33,8 @@ async function run(args: string[]): Promise<{ stdout: string; code: number }> { } maybe('agent mode (subprocess, no TTY)', () => { - it('config get emits exactly one JSON envelope on stdout', async () => { + // subprocess spawn + Node startup can exceed the 5s default under parallel load + it('config get emits exactly one JSON envelope on stdout', { timeout: 15_000 }, async () => { const { stdout, code } = await run(['config', 'get', 'minSeeders', '--json']); const lines = stdout.trim().split('\n'); expect(lines).toHaveLength(1); @@ -45,7 +46,7 @@ maybe('agent mode (subprocess, no TTY)', () => { expect(code).toBe(0); }); - it('returns USAGE (2) for a missing required argument', async () => { + it('returns USAGE (2) for a missing required argument', { timeout: 15_000 }, async () => { const { stdout, code } = await run(['config', 'get', '--json']); const env = JSON.parse(stdout.trim()); expect(env.ok).toBe(false); diff --git a/test/download.test.ts b/test/download.test.ts new file mode 100644 index 0000000..a0dfdcb --- /dev/null +++ b/test/download.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { join } from 'node:path'; + +// Mock the heavy optional dependency with a torrent that completes immediately. +vi.mock('webtorrent', () => { + class FakeTorrent { + name = 'Movie'; + files = [ + { name: 'sample.mp4', length: 50, path: 'Movie/sample.mp4' }, + { name: 'Movie.mp4', length: 5000, path: 'Movie/Movie.mp4' }, + ]; + progress = 1; + numPeers = 7; + downloadSpeed = 0; + on(event: string, cb: () => void): void { + if (event === 'done') setTimeout(cb, 0); + } + } + class FakeClient { + add(_src: string, _opts: { path?: string }, cb: (t: FakeTorrent) => void): void { + cb(new FakeTorrent()); + } + on(): void {} + destroy(): void {} + } + return { default: FakeClient }; +}); + +import { downloadTorrent } from '../src/core/torrent/engine.js'; + +afterEach(() => vi.restoreAllMocks()); + +describe('downloadTorrent', () => { + it('selects the largest video file and resolves its on-disk path', async () => { + const out = await downloadTorrent({ + source: 'magnet:?xt=urn:btih:abc', + outDir: '/tmp/dl', + }); + expect(out.fileName).toBe('Movie.mp4'); // larger than sample.mp4 + expect(out.sizeBytes).toBe(5000); + expect(out.filePath).toBe(join('/tmp/dl', 'Movie/Movie.mp4')); + }); +}); diff --git a/test/flag-arity.test.ts b/test/flag-arity.test.ts new file mode 100644 index 0000000..8b7a8c9 --- /dev/null +++ b/test/flag-arity.test.ts @@ -0,0 +1,25 @@ +import { describe, it, expect } from 'vitest'; +import { z } from 'zod'; +import { unwrappedTypeName } from '../src/registry/build.js'; + +/** + * Regression guard: optional/default-wrapped flags must report their underlying + * value type, not the wrapper. A previous bug classified every `ZodOptional` as + * boolean, which made `--container `, `--query`, `--out`, etc. swallow no + * argument and pushed the value into positionals ("too many arguments"). + */ +describe('unwrappedTypeName', () => { + it('unwraps optional strings to ZodString (value-taking, not boolean)', () => { + expect(unwrappedTypeName(z.string().optional())).toBe('ZodString'); + }); + + it('unwraps default numbers to ZodNumber (so numeric coercion applies)', () => { + expect(unwrappedTypeName(z.number().default(25))).toBe('ZodNumber'); + expect(unwrappedTypeName(z.number().optional())).toBe('ZodNumber'); + }); + + it('keeps real booleans as ZodBoolean (value-less flags)', () => { + expect(unwrappedTypeName(z.boolean())).toBe('ZodBoolean'); + expect(unwrappedTypeName(z.boolean().default(false))).toBe('ZodBoolean'); + }); +}); diff --git a/test/manifest.test.ts b/test/manifest.test.ts index b07efee..ddfcb84 100644 --- a/test/manifest.test.ts +++ b/test/manifest.test.ts @@ -13,6 +13,8 @@ describe('manifest', () => { expect(ids).toContain('setup'); expect(ids).toContain('doctor'); expect(ids).toContain('config'); + expect(ids).toContain('subs'); + expect(ids).toContain('download'); }); it('documents the full exit code table', () => { diff --git a/test/opensubtitles.test.ts b/test/opensubtitles.test.ts new file mode 100644 index 0000000..5ba925c --- /dev/null +++ b/test/opensubtitles.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { + searchSubtitles, + downloadSubtitle, + type OpenSubtitlesConfig, +} from '../src/core/subtitles/opensubtitles.js'; +import { StreamNetError, ExitCode } from '../src/agent/exit.js'; + +const cfg: OpenSubtitlesConfig = { apiKey: 'test-key', languages: ['es', 'en'] }; + +function jsonResponse(body: unknown, status = 200): Response { + return { + ok: status >= 200 && status < 300, + status, + json: async () => body, + text: async () => (typeof body === 'string' ? body : JSON.stringify(body)), + } as unknown as Response; +} + +afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllGlobals(); +}); + +describe('searchSubtitles', () => { + it('ranks hash matches first, then language preference, then downloads', async () => { + const fetchMock = vi.fn(async (_url: string, _init?: unknown) => + jsonResponse({ + data: [ + { + attributes: { + language: 'en', + download_count: 999, + moviehash_match: false, + files: [{ file_id: 1, file_name: 'en-text.srt' }], + }, + }, + { + attributes: { + language: 'en', + download_count: 5, + moviehash_match: true, + files: [{ file_id: 2, file_name: 'en-hash.srt' }], + }, + }, + { + attributes: { + language: 'es', + download_count: 5, + moviehash_match: true, + files: [{ file_id: 3, file_name: 'es-hash.srt' }], + }, + }, + ], + }), + ); + vi.stubGlobal('fetch', fetchMock); + + const out = await searchSubtitles(cfg, { moviehash: 'ABC123', query: 'movie' }); + // es-hash (hashMatch + preferred lang) → en-hash (hashMatch) → en-text + expect(out.map((m) => m.fileId)).toEqual([3, 2, 1]); + + const calledUrl = String(fetchMock.mock.calls[0]![0]); + expect(calledUrl).toContain('moviehash=abc123'); + expect(calledUrl).toContain('languages=es%2Cen'); + }); + + it('throws AUTH when no API key is configured', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => jsonResponse({ data: [] })), + ); + await expect(searchSubtitles({}, { query: 'x' })).rejects.toMatchObject({ + code: ExitCode.AUTH, + }); + }); + + it('throws AUTH on HTTP 401', async () => { + vi.stubGlobal( + 'fetch', + vi.fn(async () => jsonResponse({}, 401)), + ); + const err = await searchSubtitles(cfg, { query: 'x' }).catch((e) => e); + expect(err).toBeInstanceOf(StreamNetError); + expect((err as StreamNetError).code).toBe(ExitCode.AUTH); + }); +}); + +describe('downloadSubtitle', () => { + it('resolves the download link then fetches the subtitle text', async () => { + const fetchMock = vi + .fn() + .mockResolvedValueOnce( + jsonResponse({ link: 'https://cdn.example/sub.srt', file_name: 'movie.srt' }), + ) + .mockResolvedValueOnce(jsonResponse('1\n00:00:01,000 --> 00:00:02,000\nHola\n')); + vi.stubGlobal('fetch', fetchMock); + + const { content, fileName } = await downloadSubtitle(cfg, 42); + expect(fileName).toBe('movie.srt'); + expect(content).toContain('Hola'); + + // First call POSTs the file_id + const firstInit = fetchMock.mock.calls[0]![1] as { method?: string; body?: unknown }; + expect(firstInit.method).toBe('POST'); + expect(String(firstInit.body)).toContain('"file_id":42'); + }); +}); diff --git a/test/subtitles-hash.test.ts b/test/subtitles-hash.test.ts new file mode 100644 index 0000000..b70f0f9 --- /dev/null +++ b/test/subtitles-hash.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { movieHash, MIN_HASHABLE_BYTES } from '../src/core/subtitles/hash.js'; + +const tmpDirs: string[] = []; + +function makeFile(bytes: Buffer): string { + const dir = mkdtempSync(join(tmpdir(), 'snhash-')); + tmpDirs.push(dir); + const p = join(dir, 'video.mp4'); + writeFileSync(p, bytes); + return p; +} + +afterEach(() => { + while (tmpDirs.length) rmSync(tmpDirs.pop()!, { recursive: true, force: true }); +}); + +describe('movieHash', () => { + it('hashes an all-zero 256 KiB file to size-only (known vector)', async () => { + const buf = Buffer.alloc(256 * 1024); // all zeros → only file size contributes + const { hash, size } = await movieHash(makeFile(buf)); + expect(size).toBe(262144); + expect(hash).toBe('0000000000040000'); // 0x40000 == 262144 + }); + + it('sums the first and last 64 KiB qwords with the file size', async () => { + const buf = Buffer.alloc(MIN_HASHABLE_BYTES); // exactly 128 KiB + buf.writeBigUInt64LE(1n, 0); // first qword of the head window + buf.writeBigUInt64LE(2n, MIN_HASHABLE_BYTES - 8); // last qword of the tail window + const { hash } = await movieHash(makeFile(buf)); + // 0x20000 (131072) + 1 + 2 == 0x20003 + expect(hash).toBe('0000000000020003'); + }); + + it('throws for a file smaller than the minimum hashable size', async () => { + const buf = Buffer.alloc(1024); + await expect(movieHash(makeFile(buf))).rejects.toThrow(/too small/i); + }); +});