From 8d4d9d8750384c055ed9b4eb240ae12c40296dbc Mon Sep 17 00:00:00 2001 From: Jamie Brunton Date: Sun, 25 Jan 2026 21:05:54 +0000 Subject: [PATCH] refactor(swipe): split swipe screen modules Move swipe screen logic into hooks/services and extract state-specific UI components to keep files small while preserving existing behavior. Reference: US-002 --- .agents/ralph/PROMPT_build.md | 126 + .agents/ralph/README.md | 190 ++ .agents/ralph/agents.sh | 15 + .agents/ralph/config.sh | 25 + .agents/ralph/diagram.svg | 55 + .agents/ralph/log-activity.sh | 15 + .agents/ralph/loop.sh | 955 ++++++ .agents/ralph/ralph.webp | Bin 0 -> 275808 bytes .../ralph/references/CONTEXT_ENGINEERING.md | 126 + .agents/ralph/references/GUARDRAILS.md | 174 + .agents/tasks/prd-mobile-refactor.json | 142 + .codex/skills/commit/SKILL.md | 219 ++ .../commit/references/commit_examples.md | 292 ++ .codex/skills/dev-browser/SKILL.md | 211 ++ .codex/skills/dev-browser/bun.lock | 443 +++ .codex/skills/dev-browser/package-lock.json | 2988 +++++++++++++++++ .codex/skills/dev-browser/package.json | 31 + .../skills/dev-browser/references/scraping.md | 155 + .../skills/dev-browser/scripts/start-relay.ts | 32 + .../dev-browser/scripts/start-server.ts | 117 + .codex/skills/dev-browser/server.sh | 24 + .codex/skills/dev-browser/src/client.ts | 474 +++ .codex/skills/dev-browser/src/index.ts | 287 ++ .codex/skills/dev-browser/src/relay.ts | 731 ++++ .../src/snapshot/__tests__/snapshot.test.ts | 223 ++ .../src/snapshot/browser-script.ts | 877 +++++ .../skills/dev-browser/src/snapshot/index.ts | 14 + .../skills/dev-browser/src/snapshot/inject.ts | 13 + .codex/skills/dev-browser/src/types.ts | 34 + .codex/skills/dev-browser/tsconfig.json | 36 + .codex/skills/dev-browser/vitest.config.ts | 12 + .codex/skills/prd/SKILL.md | 235 ++ .ralph/activity.log | 6 + .ralph/guardrails.md | 20 + .../runs/run-20260125-204012-65223-iter-1.md | 34 + apps/mobile/android/app/build.gradle | 2 +- apps/mobile/app.json | 2 +- apps/mobile/app/(app)/swipe.tsx | 1676 +-------- .../ios/Sauci.xcodeproj/project.pbxproj | 8 +- .../mobile/src/features/swipe/SwipeScreen.tsx | 168 + .../swipe/components/SwipeBlockedState.tsx | 33 + .../swipe/components/SwipeCardStack.tsx | 79 + .../swipe/components/SwipeCaughtUpState.tsx | 68 + .../swipe/components/SwipeDailyLimitState.tsx | 229 ++ .../features/swipe/components/SwipeHeader.tsx | 139 + .../swipe/components/SwipeInfoStateLayout.tsx | 173 + .../swipe/components/SwipeLoadingState.tsx | 20 + .../swipe/components/SwipeNoPacksState.tsx | 31 + .../swipe/components/SwipeNoPartnerState.tsx | 35 + .../swipe/components/SwipeQuestionCard.tsx | 81 + .../swipe/components/SwipeUploadOverlay.tsx | 39 + .../swipe/hooks/useProgressShimmer.ts | 20 + .../features/swipe/hooks/useSwipeScreen.ts | 469 +++ .../features/swipe/services/swipeService.ts | 169 + apps/mobile/src/features/swipe/types.ts | 36 + 55 files changed, 11127 insertions(+), 1681 deletions(-) create mode 100644 .agents/ralph/PROMPT_build.md create mode 100644 .agents/ralph/README.md create mode 100644 .agents/ralph/agents.sh create mode 100644 .agents/ralph/config.sh create mode 100644 .agents/ralph/diagram.svg create mode 100755 .agents/ralph/log-activity.sh create mode 100755 .agents/ralph/loop.sh create mode 100644 .agents/ralph/ralph.webp create mode 100644 .agents/ralph/references/CONTEXT_ENGINEERING.md create mode 100644 .agents/ralph/references/GUARDRAILS.md create mode 100644 .agents/tasks/prd-mobile-refactor.json create mode 100644 .codex/skills/commit/SKILL.md create mode 100644 .codex/skills/commit/references/commit_examples.md create mode 100644 .codex/skills/dev-browser/SKILL.md create mode 100644 .codex/skills/dev-browser/bun.lock create mode 100644 .codex/skills/dev-browser/package-lock.json create mode 100644 .codex/skills/dev-browser/package.json create mode 100644 .codex/skills/dev-browser/references/scraping.md create mode 100644 .codex/skills/dev-browser/scripts/start-relay.ts create mode 100644 .codex/skills/dev-browser/scripts/start-server.ts create mode 100755 .codex/skills/dev-browser/server.sh create mode 100644 .codex/skills/dev-browser/src/client.ts create mode 100644 .codex/skills/dev-browser/src/index.ts create mode 100644 .codex/skills/dev-browser/src/relay.ts create mode 100644 .codex/skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts create mode 100644 .codex/skills/dev-browser/src/snapshot/browser-script.ts create mode 100644 .codex/skills/dev-browser/src/snapshot/index.ts create mode 100644 .codex/skills/dev-browser/src/snapshot/inject.ts create mode 100644 .codex/skills/dev-browser/src/types.ts create mode 100644 .codex/skills/dev-browser/tsconfig.json create mode 100644 .codex/skills/dev-browser/vitest.config.ts create mode 100644 .codex/skills/prd/SKILL.md create mode 100644 .ralph/guardrails.md create mode 100644 .ralph/runs/run-20260125-204012-65223-iter-1.md create mode 100644 apps/mobile/src/features/swipe/SwipeScreen.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeBlockedState.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeCardStack.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeCaughtUpState.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeDailyLimitState.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeHeader.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeInfoStateLayout.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeLoadingState.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeNoPacksState.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeNoPartnerState.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeQuestionCard.tsx create mode 100644 apps/mobile/src/features/swipe/components/SwipeUploadOverlay.tsx create mode 100644 apps/mobile/src/features/swipe/hooks/useProgressShimmer.ts create mode 100644 apps/mobile/src/features/swipe/hooks/useSwipeScreen.ts create mode 100644 apps/mobile/src/features/swipe/services/swipeService.ts create mode 100644 apps/mobile/src/features/swipe/types.ts diff --git a/.agents/ralph/PROMPT_build.md b/.agents/ralph/PROMPT_build.md new file mode 100644 index 0000000..138316b --- /dev/null +++ b/.agents/ralph/PROMPT_build.md @@ -0,0 +1,126 @@ +# Build + +You are an autonomous coding agent. Your task is to complete the work for exactly one story and record the outcome. + +## Paths +- PRD: {{PRD_PATH}} +- AGENTS (optional): {{AGENTS_PATH}} +- Progress Log: {{PROGRESS_PATH}} +- Guardrails: {{GUARDRAILS_PATH}} +- Guardrails Reference: {{GUARDRAILS_REF}} +- Context Reference: {{CONTEXT_REF}} +- Errors Log: {{ERRORS_LOG_PATH}} +- Activity Log: {{ACTIVITY_LOG_PATH}} +- Activity Logger: {{ACTIVITY_CMD}} +- No-commit: {{NO_COMMIT}} +- Repo Root: {{REPO_ROOT}} +- Run ID: {{RUN_ID}} +- Iteration: {{ITERATION}} +- Run Log: {{RUN_LOG_PATH}} +- Run Summary: {{RUN_META_PATH}} + +## Global Quality Gates (apply to every story) +{{QUALITY_GATES}} + +## Selected Story (Do not change scope) +ID: {{STORY_ID}} +Title: {{STORY_TITLE}} + +Story details: +{{STORY_BLOCK}} + +If the story details are empty or missing, STOP and report that the PRD story format could not be parsed. + +## Rules (Non-Negotiable) +- Implement **only** the work required to complete the selected story. +- Complete all tasks associated with this story (and only this story). +- Do NOT ask the user questions. +- Do NOT change unrelated code. +- Do NOT assume something is unimplemented — confirm by reading code. +- Implement completely; no placeholders or stubs. +- If No-commit is true, do NOT commit or push changes. +- Do NOT edit the PRD JSON (status is handled by the loop). +- All changes made during the run must be committed (including updates to progress/logs). + - Before committing, perform a final **security**, **performance**, and **regression** review of your changes. + +## Your Task (Do this in order) +1. Read {{GUARDRAILS_PATH}} before any code changes. +2. Read {{ERRORS_LOG_PATH}} for repeated failures to avoid. +3. Read {{PRD_PATH}} for global context (do not edit). +4. Fully audit and read all necessary files to understand the task end-to-end before implementing. Do not assume missing functionality. +5. If {{AGENTS_PATH}} exists, follow its build/test instructions. +6. Implement only the tasks that belong to {{STORY_ID}}. +7. Run verification commands listed in the story, the global quality gates, and in {{AGENTS_PATH}} (if required). +8. If the project has a build or dev workflow, run what applies: + - Build step (e.g., `npm run build`) if defined. + - Dev server (e.g., `npm run dev`, `wrangler dev`) if it is the normal validation path. + - Confirm no runtime/build errors in the console. +9. Perform a brief audit before committing: + - **Security:** check for obvious vulnerabilities or unsafe handling introduced by your changes. + - **Performance:** check for avoidable regressions (extra queries, heavy loops, unnecessary re-renders). + - **Regression:** verify existing behavior that could be impacted still works. +10. If No-commit is false, commit changes using the `$commit` skill. + - Stage everything: `git add -A` + - Confirm a clean working tree after commit: `git status --porcelain` should be empty. + - After committing, capture the commit hash and subject using: + `git show -s --format="%h %s" HEAD`. +11. Append a progress entry to {{PROGRESS_PATH}} with run/commit/test details (format below). + If No-commit is true, skip committing and note it in the progress entry. + +## Progress Entry Format (Append Only) +``` +## [Date/Time] - {{STORY_ID}}: {{STORY_TITLE}} +Thread: [codex exec session id if available, otherwise leave blank] +Run: {{RUN_ID}} (iteration {{ITERATION}}) +Run log: {{RUN_LOG_PATH}} +Run summary: {{RUN_META_PATH}} +- Guardrails reviewed: yes +- No-commit run: {{NO_COMMIT}} +- Commit: (or `none` + reason) +- Post-commit status: `clean` or list remaining files +- Verification: + - Command: -> PASS/FAIL + - Command: -> PASS/FAIL +- Files changed: + - + - +- What was implemented +- **Learnings for future iterations:** + - Patterns discovered + - Gotchas encountered + - Useful context +--- +``` + +## Completion Signal +Only output the completion signal when the **selected story** is fully complete and verified. +When the selected story is complete, output: +COMPLETE + +Otherwise, end normally without the signal. + +## Additional Guardrails +- When authoring documentation, capture the why (tests + implementation intent). +- If you learn how to run/build/test the project, update {{AGENTS_PATH}} briefly (operational only). +- Keep AGENTS operational only; progress notes belong in {{PROGRESS_PATH}}. +- If you hit repeated errors, log them in {{ERRORS_LOG_PATH}} and add a Sign to {{GUARDRAILS_PATH}} using {{GUARDRAILS_REF}} as the template. + +## Activity Logging (Required) +Log major actions to {{ACTIVITY_LOG_PATH}} using the helper: +``` +{{ACTIVITY_CMD}} "message" +``` +Log at least: +- Start of work on the story +- After major code changes +- After tests/verification +- After updating progress log + +## Browser Testing (Required for Frontend Stories) +If the selected story changes UI, you MUST verify it in the browser: +1. Load the `dev-browser` skill. +2. Navigate to the relevant page. +3. Verify the UI changes work as expected. +4. Take a screenshot if helpful for the progress log. + +A frontend story is NOT complete until browser verification passes. diff --git a/.agents/ralph/README.md b/.agents/ralph/README.md new file mode 100644 index 0000000..9837b12 --- /dev/null +++ b/.agents/ralph/README.md @@ -0,0 +1,190 @@ +# Ralph + +![Ralph](ralph.webp) + +Ralph is a minimal, file‑based agent loop for autonomous coding. Each iteration starts fresh, reads the same on‑disk state, and commits work for one story at a time. + +## How it works + +Ralph treats **files and git** as memory, not the model context: + +- **PRD (JSON)** defines stories, gates, and status +- **Loop** executes one story per iteration +- **State** persists in `.ralph/` + +![Ralph architecture](diagram.svg) + +## Global CLI (recommended) + +Install and run Ralph from anywhere: + +```bash +npm i -g @iannuttall/ralph +ralph prd # launches an interactive prompt +ralph build 1 # one Ralph run +``` + +### Template hierarchy + +Ralph will look for templates in this order: + +1. `.agents/ralph/` in the current project (if present) +2. Bundled defaults shipped with this repo + +State and logs always go to `.ralph/` in the project. + +### Install templates into a project (optional overrides) + +```bash +ralph install +``` + +This creates `.agents/ralph/` in the current repo so you can customize prompts and loop behavior. During install, you’ll be asked if you want to add the required skills. + +### Install required skills (optional) + +```bash +ralph install --skills +``` + +You’ll be prompted for agent (codex/claude/droid/opencode) and local vs global install. Skills installed: **commit**, **dev-browser**, **prd**. +If you skipped skills during `ralph install`, you can run `ralph install --skills` anytime. + +## Quick start (project) + +1) Create your PRD (JSON) or generate one: +``` +ralph prd +``` +Requires the **prd** skill (install via `ralph install --skills`). + +Example prompt text: +``` +A lightweight uptime monitor (Hono app), deployed on Cloudflare, with email alerts via AWS SES +``` + +Default output (agent chooses a short filename in `.agents/tasks/`): +``` +.agents/tasks/prd-.json +``` + +2) Run one build iteration: +``` +ralph build 1 # one Ralph run +``` + +No‑commit dry run: +``` +ralph build 1 --no-commit # one Ralph run +``` + +Override PRD output for `ralph prd`: +``` +ralph prd --out .agents/tasks/prd-api.json +``` +Optional human overview (generated from JSON): +``` +ralph overview +``` +This writes a tiny overview alongside the PRD: `prd-.overview.md`. + +PRD story status fields are updated automatically by the loop: +- `open` → selectable +- `in_progress` → locked by a running loop (with `startedAt`) +- `done` → completed (with `completedAt`) + +If a loop crashes and a story stays `in_progress`, you can set `STALE_SECONDS` in `.agents/ralph/config.sh` to allow Ralph to automatically reopen stalled stories. + +## Override PRD paths + +You can point Ralph at a different PRD JSON file via CLI flags: + +```bash +ralph build 1 --prd .agents/tasks/prd-api.json # one Ralph run +``` + +Optional progress override: + +```bash +ralph build 1 --progress .ralph/progress-api.md # one Ralph run +``` + +If multiple PRD JSON files exist in `.agents/tasks/` and you omit `--prd`, Ralph will prompt you to choose. + +Optional config file (if you installed templates): + +``` +.agents/ralph/config.sh +``` + +## Choose the agent runner + +Set `AGENT_CMD` in `.agents/ralph/config.sh` to switch agents: + +``` +AGENT_CMD="codex exec --yolo -" +AGENT_CMD="claude -p --dangerously-skip-permissions \"\$(cat {prompt})\"" +AGENT_CMD="droid exec --skip-permissions-unsafe -f {prompt}" +AGENT_CMD="opencode run \"$(cat {prompt})\"" +``` + +Or override per run: + +``` +ralph prd --agent=codex +ralph build 1 --agent=codex # one Ralph run +ralph build 1 --agent=claude # one Ralph run +ralph build 1 --agent=droid # one Ralph run +ralph build 1 --agent=opencode # one Ralph run +``` + +If the CLI isn’t installed, Ralph prints install hints: + +``` +codex -> npm i -g @openai/codex +claude -> curl -fsSL https://claude.ai/install.sh | bash +droid -> curl -fsSL https://app.factory.ai/cli | sh +opencode -> curl -fsSL https://opencode.ai/install.sh | bash +``` + +## State files (.ralph/) + +- `progress.md` — append‑only progress log +- `guardrails.md` — “Signs” (lessons learned) +- `activity.log` — activity + timing log +- `errors.log` — repeated failures and notes +- `runs/` — raw run logs + summaries + +## Notes + +- `.agents/ralph` is portable and can be copied between repos. +- `.ralph` is per‑project state. +- Use `{prompt}` in `AGENT_CMD` when agent needs a file path instead of stdin. +- Examples: see `examples/commands.md`. +- **OpenCode server mode**: For faster performance with OpenCode, run `opencode serve` in a separate terminal and uncomment the `AGENT_OPENCODE_CMD` lines in `.agents/ralph/agents.sh` to use `--attach http://localhost:4096`. This avoids cold boot on every run. + +## Tests + +Dry-run smoke tests (no agent required): + +```bash +npm test +``` + +Fast agent health check (real agent call, minimal output): + +```bash +npm run test:ping +``` + +Optional integration test (requires agents installed): + +```bash +RALPH_INTEGRATION=1 npm test +``` + +Full real-agent loop test: + +```bash +npm run test:real +``` diff --git a/.agents/ralph/agents.sh b/.agents/ralph/agents.sh new file mode 100644 index 0000000..497eb37 --- /dev/null +++ b/.agents/ralph/agents.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Default agent command templates (used by loop.sh and CLI). + +AGENT_CODEX_CMD="codex exec --yolo --skip-git-repo-check -" +AGENT_CODEX_INTERACTIVE_CMD="codex --yolo {prompt}" +AGENT_CLAUDE_CMD="claude -p --dangerously-skip-permissions \"\$(cat {prompt})\"" +AGENT_CLAUDE_INTERACTIVE_CMD="claude --dangerously-skip-permissions {prompt}" +AGENT_DROID_CMD="droid exec --skip-permissions-unsafe -f {prompt}" +AGENT_DROID_INTERACTIVE_CMD="droid --skip-permissions-unsafe {prompt}" +AGENT_OPENCODE_CMD="opencode run \"\$(cat {prompt})\"" +AGENT_OPENCODE_INTERACTIVE_CMD="opencode --prompt {prompt}" +# Uncomment to use server mode (faster, avoids cold boot): +# AGENT_OPENCODE_CMD="opencode run --attach http://localhost:4096 \"\$(cat {prompt})\"" +# AGENT_OPENCODE_INTERACTIVE_CMD="opencode --prompt {prompt} --attach http://localhost:4096" +DEFAULT_AGENT="codex" diff --git a/.agents/ralph/config.sh b/.agents/ralph/config.sh new file mode 100644 index 0000000..45541b4 --- /dev/null +++ b/.agents/ralph/config.sh @@ -0,0 +1,25 @@ +# Optional Ralph config overrides. +# All paths are relative to repo root unless absolute. +# Uncomment and edit as needed. + +# PRD_PATH=".agents/tasks/prd.json" +# PROGRESS_PATH=".ralph/progress.md" +# GUARDRAILS_PATH=".ralph/guardrails.md" +# ERRORS_LOG_PATH=".ralph/errors.log" +# ACTIVITY_LOG_PATH=".ralph/activity.log" +# TMP_DIR=".ralph/.tmp" +# RUNS_DIR=".ralph/runs" +# GUARDRAILS_REF=".agents/ralph/references/GUARDRAILS.md" +# CONTEXT_REF=".agents/ralph/references/CONTEXT_ENGINEERING.md" +# ACTIVITY_CMD=".agents/ralph/log-activity.sh" +# AGENT_CMD defaults are defined in agents.sh. Override here if needed. +# AGENT_CMD="codex exec --yolo --skip-git-repo-check -" +# PRD_AGENT_CMD defaults are defined in agents.sh (interactive). +# PRD_AGENT_CMD="codex --yolo --skip-git-repo-check {prompt}" +# AGENT_CMD="claude -p --dangerously-skip-permissions \"\$(cat {prompt})\"" +# AGENT_CMD="droid exec --skip-permissions-unsafe -f {prompt}" +# AGENTS_PATH="AGENTS.md" +# PROMPT_BUILD=".agents/ralph/PROMPT_build.md" +# NO_COMMIT=false +# MAX_ITERATIONS=25 +# STALE_SECONDS=0 diff --git a/.agents/ralph/diagram.svg b/.agents/ralph/diagram.svg new file mode 100644 index 0000000..c63ce3d --- /dev/null +++ b/.agents/ralph/diagram.svg @@ -0,0 +1,55 @@ + + + + + + + + ralph CLI + + + agent CLI: codex | claude | droid + + + + + + + + + + + + + + + + + .agents/ralph/ + local overrides (optional) + loop.sh + PROMPT_build.md + references/ + log-activity.sh + config.sh (optional) + + + .ralph/ + state + logs + errors.log + progress.md + guardrails.md + activity.log + runs/ + + + Local templates override bundled defaults. State persists between runs. + + diff --git a/.agents/ralph/log-activity.sh b/.agents/ralph/log-activity.sh new file mode 100755 index 0000000..1bf4671 --- /dev/null +++ b/.agents/ralph/log-activity.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)" +ACTIVITY_LOG="$ROOT_DIR/.ralph/activity.log" + +if [ $# -lt 1 ]; then + echo "Usage: $0 \"message\"" + exit 1 +fi + +mkdir -p "$(dirname "$ACTIVITY_LOG")" +TS=$(date '+%Y-%m-%d %H:%M:%S') +echo "[$TS] $*" >> "$ACTIVITY_LOG" diff --git a/.agents/ralph/loop.sh b/.agents/ralph/loop.sh new file mode 100755 index 0000000..74dab66 --- /dev/null +++ b/.agents/ralph/loop.sh @@ -0,0 +1,955 @@ +#!/bin/bash +# Ralph loop — simple, portable, single-agent +# Usage: +# ./.agents/ralph/loop.sh # build mode, default iterations +# ./.agents/ralph/loop.sh build # build mode +# ./.agents/ralph/loop.sh prd "request" # generate PRD via agent +# ./.agents/ralph/loop.sh 10 # build mode, 10 iterations +# ./.agents/ralph/loop.sh build 1 --no-commit + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "${RALPH_ROOT:-${SCRIPT_DIR}/../..}" && pwd)" +CONFIG_FILE="${SCRIPT_DIR}/config.sh" + +DEFAULT_PRD_PATH=".agents/tasks/prd.json" +DEFAULT_PROGRESS_PATH=".ralph/progress.md" +DEFAULT_AGENTS_PATH="AGENTS.md" +DEFAULT_PROMPT_BUILD=".agents/ralph/PROMPT_build.md" +DEFAULT_GUARDRAILS_PATH=".ralph/guardrails.md" +DEFAULT_ERRORS_LOG_PATH=".ralph/errors.log" +DEFAULT_ACTIVITY_LOG_PATH=".ralph/activity.log" +DEFAULT_TMP_DIR=".ralph/.tmp" +DEFAULT_RUNS_DIR=".ralph/runs" +DEFAULT_GUARDRAILS_REF=".agents/ralph/references/GUARDRAILS.md" +DEFAULT_CONTEXT_REF=".agents/ralph/references/CONTEXT_ENGINEERING.md" +DEFAULT_ACTIVITY_CMD=".agents/ralph/log-activity.sh" +if [[ -n "${RALPH_ROOT:-}" ]]; then + agents_path="$RALPH_ROOT/.agents/ralph/agents.sh" +else + agents_path="$SCRIPT_DIR/agents.sh" +fi +if [[ -f "$agents_path" ]]; then + # shellcheck source=/dev/null + source "$agents_path" +fi + +DEFAULT_MAX_ITERATIONS=25 +DEFAULT_NO_COMMIT=false +DEFAULT_STALE_SECONDS=0 +PRD_REQUEST_PATH="" +PRD_INLINE="" + +# Optional config overrides (simple shell vars) +if [ -f "$CONFIG_FILE" ]; then + # shellcheck source=/dev/null + . "$CONFIG_FILE" +fi + +DEFAULT_AGENT_NAME="${DEFAULT_AGENT:-codex}" +resolve_agent_cmd() { + local name="$1" + case "$name" in + claude) + echo "${AGENT_CLAUDE_CMD:-claude -p --dangerously-skip-permissions \"\$(cat {prompt})\"}" + ;; + droid) + echo "${AGENT_DROID_CMD:-droid exec --skip-permissions-unsafe -f {prompt}}" + ;; + codex|"") + echo "${AGENT_CODEX_CMD:-codex exec --yolo --skip-git-repo-check -}" + ;; + *) + echo "${AGENT_CODEX_CMD:-codex exec --yolo --skip-git-repo-check -}" + ;; + esac +} +DEFAULT_AGENT_CMD="$(resolve_agent_cmd "$DEFAULT_AGENT_NAME")" + +PRD_PATH="${PRD_PATH:-$DEFAULT_PRD_PATH}" +PROGRESS_PATH="${PROGRESS_PATH:-$DEFAULT_PROGRESS_PATH}" +AGENTS_PATH="${AGENTS_PATH:-$DEFAULT_AGENTS_PATH}" +PROMPT_BUILD="${PROMPT_BUILD:-$DEFAULT_PROMPT_BUILD}" +GUARDRAILS_PATH="${GUARDRAILS_PATH:-$DEFAULT_GUARDRAILS_PATH}" +ERRORS_LOG_PATH="${ERRORS_LOG_PATH:-$DEFAULT_ERRORS_LOG_PATH}" +ACTIVITY_LOG_PATH="${ACTIVITY_LOG_PATH:-$DEFAULT_ACTIVITY_LOG_PATH}" +TMP_DIR="${TMP_DIR:-$DEFAULT_TMP_DIR}" +RUNS_DIR="${RUNS_DIR:-$DEFAULT_RUNS_DIR}" +GUARDRAILS_REF="${GUARDRAILS_REF:-$DEFAULT_GUARDRAILS_REF}" +CONTEXT_REF="${CONTEXT_REF:-$DEFAULT_CONTEXT_REF}" +ACTIVITY_CMD="${ACTIVITY_CMD:-$DEFAULT_ACTIVITY_CMD}" +AGENT_CMD="${AGENT_CMD:-$DEFAULT_AGENT_CMD}" +MAX_ITERATIONS="${MAX_ITERATIONS:-$DEFAULT_MAX_ITERATIONS}" +NO_COMMIT="${NO_COMMIT:-$DEFAULT_NO_COMMIT}" +STALE_SECONDS="${STALE_SECONDS:-$DEFAULT_STALE_SECONDS}" + +abs_path() { + local p="$1" + if [[ "$p" = /* ]]; then + echo "$p" + else + echo "$ROOT_DIR/$p" + fi +} + +PRD_PATH="$(abs_path "$PRD_PATH")" +PROGRESS_PATH="$(abs_path "$PROGRESS_PATH")" +AGENTS_PATH="$(abs_path "$AGENTS_PATH")" +PROMPT_BUILD="$(abs_path "$PROMPT_BUILD")" +GUARDRAILS_PATH="$(abs_path "$GUARDRAILS_PATH")" +ERRORS_LOG_PATH="$(abs_path "$ERRORS_LOG_PATH")" +ACTIVITY_LOG_PATH="$(abs_path "$ACTIVITY_LOG_PATH")" +TMP_DIR="$(abs_path "$TMP_DIR")" +RUNS_DIR="$(abs_path "$RUNS_DIR")" +GUARDRAILS_REF="$(abs_path "$GUARDRAILS_REF")" +CONTEXT_REF="$(abs_path "$CONTEXT_REF")" +ACTIVITY_CMD="$(abs_path "$ACTIVITY_CMD")" + +require_agent() { + local agent_cmd="${1:-$AGENT_CMD}" + local agent_bin + agent_bin="${agent_cmd%% *}" + if [ -z "$agent_bin" ]; then + echo "AGENT_CMD is empty. Set it in config.sh." + exit 1 + fi + if ! command -v "$agent_bin" >/dev/null 2>&1; then + echo "Agent command not found: $agent_bin" + case "$agent_bin" in + codex) + echo "Install: npm i -g @openai/codex" + ;; + claude) + echo "Install: curl -fsSL https://claude.ai/install.sh | bash" + ;; + droid) + echo "Install: curl -fsSL https://app.factory.ai/cli | sh" + ;; + opencode) + echo "Install: curl -fsSL https://opencode.ai/install.sh | bash" + ;; + esac + echo "Then authenticate per the CLI's instructions." + exit 1 + fi +} + +run_agent() { + local prompt_file="$1" + if [[ "$AGENT_CMD" == *"{prompt}"* ]]; then + local escaped + escaped=$(printf '%q' "$prompt_file") + local cmd="${AGENT_CMD//\{prompt\}/$escaped}" + eval "$cmd" + else + cat "$prompt_file" | eval "$AGENT_CMD" + fi +} + +run_agent_inline() { + local prompt_file="$1" + local prompt_content + prompt_content="$(cat "$prompt_file")" + local escaped + escaped=$(printf "%s" "$prompt_content" | sed "s/'/'\\\\''/g") + local cmd="${PRD_AGENT_CMD:-$AGENT_CMD}" + if [[ "$cmd" == *"{prompt}"* ]]; then + cmd="${cmd//\{prompt\}/'$escaped'}" + else + cmd="$cmd '$escaped'" + fi + eval "$cmd" +} + +MODE="build" +while [ $# -gt 0 ]; do + case "$1" in + build|prd) + MODE="$1" + shift + ;; + --prompt) + PRD_REQUEST_PATH="$2" + shift 2 + ;; + --no-commit) + NO_COMMIT=true + shift + ;; + *) + if [ "$MODE" = "prd" ]; then + PRD_INLINE="${PRD_INLINE:+$PRD_INLINE }$1" + shift + elif [[ "$1" =~ ^[0-9]+$ ]]; then + MAX_ITERATIONS="$1" + shift + else + echo "Unknown arg: $1" + exit 1 + fi + ;; + esac +done + +PROMPT_FILE="$PROMPT_BUILD" + +if [ "$MODE" = "prd" ]; then + PRD_USE_INLINE=1 + if [ -z "${PRD_AGENT_CMD:-}" ]; then + PRD_AGENT_CMD="$AGENT_CMD" + PRD_USE_INLINE=0 + fi + if [ "${RALPH_DRY_RUN:-}" != "1" ]; then + require_agent "${PRD_AGENT_CMD:-$AGENT_CMD}" + fi + + if [[ "$PRD_PATH" == *.json ]]; then + mkdir -p "$(dirname "$PRD_PATH")" "$TMP_DIR" + else + mkdir -p "$PRD_PATH" "$TMP_DIR" + fi + + if [ -z "$PRD_REQUEST_PATH" ] && [ -n "$PRD_INLINE" ]; then + PRD_REQUEST_PATH="$TMP_DIR/prd-request-$(date +%Y%m%d-%H%M%S)-$$.txt" + printf '%s\n' "$PRD_INLINE" > "$PRD_REQUEST_PATH" + fi + + if [ -z "$PRD_REQUEST_PATH" ] || [ ! -f "$PRD_REQUEST_PATH" ]; then + echo "PRD request missing. Provide a prompt string or --prompt ." + exit 1 + fi + + if [ "${RALPH_DRY_RUN:-}" = "1" ]; then + if [[ "$PRD_PATH" == *.json ]]; then + if [ ! -f "$PRD_PATH" ]; then + { + echo '{' + echo ' "version": 1,' + echo ' "project": "ralph",' + echo ' "qualityGates": [],' + echo ' "stories": []' + echo '}' + } > "$PRD_PATH" + fi + fi + exit 0 + fi + + PRD_PROMPT_FILE="$TMP_DIR/prd-prompt-$(date +%Y%m%d-%H%M%S)-$$.md" + { + echo "You are an autonomous coding agent." + echo "Use the \$prd skill to create a Product Requirements Document in JSON." + if [[ "$PRD_PATH" == *.json ]]; then + echo "Save the PRD to: $PRD_PATH" + else + echo "Save the PRD as JSON in directory: $PRD_PATH" + echo "Filename rules: prd-.json using 1-3 meaningful words." + echo "Examples: prd-workout-tracker.json, prd-usage-billing.json" + fi + echo "Do NOT implement anything." + echo "After creating the PRD, end with:" + echo "PRD JSON saved to . Close this chat and run \`ralph build\`." + echo "" + echo "User request:" + cat "$PRD_REQUEST_PATH" + } > "$PRD_PROMPT_FILE" + + if [ "$PRD_USE_INLINE" -eq 1 ]; then + run_agent_inline "$PRD_PROMPT_FILE" + else + run_agent "$PRD_PROMPT_FILE" + fi + exit 0 +fi + +if [ "${RALPH_DRY_RUN:-}" != "1" ]; then + require_agent +fi + +if [ ! -f "$PROMPT_FILE" ]; then + echo "Prompt not found: $PROMPT_FILE" + exit 1 +fi + +if [ "$MODE" != "prd" ] && [ ! -f "$PRD_PATH" ]; then + echo "PRD not found: $PRD_PATH" + exit 1 +fi + +mkdir -p "$(dirname "$PROGRESS_PATH")" "$TMP_DIR" "$RUNS_DIR" + +if [ ! -f "$PROGRESS_PATH" ]; then + { + echo "# Progress Log" + echo "Started: $(date)" + echo "" + echo "## Codebase Patterns" + echo "- (add reusable patterns here)" + echo "" + echo "---" + } > "$PROGRESS_PATH" +fi + +if [ ! -f "$GUARDRAILS_PATH" ]; then + { + echo "# Guardrails (Signs)" + echo "" + echo "> Lessons learned from failures. Read before acting." + echo "" + echo "## Core Signs" + echo "" + echo "### Sign: Read Before Writing" + echo "- **Trigger**: Before modifying any file" + echo "- **Instruction**: Read the file first" + echo "- **Added after**: Core principle" + echo "" + echo "### Sign: Test Before Commit" + echo "- **Trigger**: Before committing changes" + echo "- **Instruction**: Run required tests and verify outputs" + echo "- **Added after**: Core principle" + echo "" + echo "---" + echo "" + echo "## Learned Signs" + echo "" + } > "$GUARDRAILS_PATH" +fi + +if [ ! -f "$ERRORS_LOG_PATH" ]; then + { + echo "# Error Log" + echo "" + echo "> Failures and repeated issues. Use this to add guardrails." + echo "" + } > "$ERRORS_LOG_PATH" +fi + +if [ ! -f "$ACTIVITY_LOG_PATH" ]; then + { + echo "# Activity Log" + echo "" + echo "## Run Summary" + echo "" + echo "## Events" + echo "" + } > "$ACTIVITY_LOG_PATH" +fi + +RUN_TAG="$(date +%Y%m%d-%H%M%S)-$$" + +render_prompt() { + local src="$1" + local dst="$2" + local story_meta="$3" + local story_block="$4" + local run_id="$5" + local iter="$6" + local run_log="$7" + local run_meta="$8" + python3 - "$src" "$dst" "$PRD_PATH" "$AGENTS_PATH" "$PROGRESS_PATH" "$ROOT_DIR" "$GUARDRAILS_PATH" "$ERRORS_LOG_PATH" "$ACTIVITY_LOG_PATH" "$GUARDRAILS_REF" "$CONTEXT_REF" "$ACTIVITY_CMD" "$NO_COMMIT" "$story_meta" "$story_block" "$run_id" "$iter" "$run_log" "$run_meta" <<'PY' +import sys +from pathlib import Path + +src = Path(sys.argv[1]).read_text() +prd, agents, progress, root = sys.argv[3:7] +guardrails = sys.argv[7] +errors_log = sys.argv[8] +activity_log = sys.argv[9] +guardrails_ref = sys.argv[10] +context_ref = sys.argv[11] +activity_cmd = sys.argv[12] +no_commit = sys.argv[13] +meta_path = sys.argv[14] if len(sys.argv) > 14 else "" +block_path = sys.argv[15] if len(sys.argv) > 15 else "" +run_id = sys.argv[16] if len(sys.argv) > 16 else "" +iteration = sys.argv[17] if len(sys.argv) > 17 else "" +run_log = sys.argv[18] if len(sys.argv) > 18 else "" +run_meta = sys.argv[19] if len(sys.argv) > 19 else "" +repl = { + "PRD_PATH": prd, + "AGENTS_PATH": agents, + "PROGRESS_PATH": progress, + "REPO_ROOT": root, + "GUARDRAILS_PATH": guardrails, + "ERRORS_LOG_PATH": errors_log, + "ACTIVITY_LOG_PATH": activity_log, + "GUARDRAILS_REF": guardrails_ref, + "CONTEXT_REF": context_ref, + "ACTIVITY_CMD": activity_cmd, + "NO_COMMIT": no_commit, + "RUN_ID": run_id, + "ITERATION": iteration, + "RUN_LOG_PATH": run_log, + "RUN_META_PATH": run_meta, +} +story = {"id": "", "title": "", "block": ""} +quality_gates = [] +if meta_path: + try: + import json + meta = json.loads(Path(meta_path).read_text()) + story["id"] = meta.get("id", "") or "" + story["title"] = meta.get("title", "") or "" + quality_gates = meta.get("quality_gates", []) or [] + except Exception: + pass +if block_path and Path(block_path).exists(): + story["block"] = Path(block_path).read_text() +repl["STORY_ID"] = story["id"] +repl["STORY_TITLE"] = story["title"] +repl["STORY_BLOCK"] = story["block"] +if quality_gates: + repl["QUALITY_GATES"] = "\n".join([f"- {g}" for g in quality_gates]) +else: + repl["QUALITY_GATES"] = "- (none)" +for k, v in repl.items(): + src = src.replace("{{" + k + "}}", v) +Path(sys.argv[2]).write_text(src) +PY +} + +select_story() { + local meta_out="$1" + local block_out="$2" + python3 - "$PRD_PATH" "$meta_out" "$block_out" "$STALE_SECONDS" <<'PY' +import json +import os +import sys +from pathlib import Path +from datetime import datetime, timezone +try: + import fcntl +except Exception: + fcntl = None + +prd_path = Path(sys.argv[1]) +meta_out = Path(sys.argv[2]) +block_out = Path(sys.argv[3]) +stale_seconds = 0 +if len(sys.argv) > 4: + try: + stale_seconds = int(sys.argv[4]) + except Exception: + stale_seconds = 0 + +if not prd_path.exists(): + meta_out.write_text(json.dumps({"ok": False, "error": "PRD not found"}, indent=2) + "\n") + block_out.write_text("") + sys.exit(0) + +def normalize_status(value): + if value is None: + return "open" + return str(value).strip().lower() + +def parse_ts(value): + if not value: + return None + text = str(value).strip() + if text.endswith("Z"): + text = text[:-1] + "+00:00" + try: + return datetime.fromisoformat(text) + except Exception: + return None + +def now_iso(): + return datetime.now(timezone.utc).isoformat() + +with prd_path.open("r+", encoding="utf-8") as fh: + if fcntl is not None: + fcntl.flock(fh.fileno(), fcntl.LOCK_EX) + try: + try: + data = json.load(fh) + except Exception as exc: + meta_out.write_text(json.dumps({"ok": False, "error": f"Invalid PRD JSON: {exc}"}, indent=2) + "\n") + block_out.write_text("") + sys.exit(0) + + stories = data.get("stories") if isinstance(data, dict) else None + if not isinstance(stories, list) or not stories: + meta_out.write_text(json.dumps({"ok": False, "error": "No stories found in PRD"}, indent=2) + "\n") + block_out.write_text("") + sys.exit(0) + + story_index = {s.get("id"): s for s in stories if isinstance(s, dict)} + + def is_done(story_id: str) -> bool: + target = story_index.get(story_id) + if not isinstance(target, dict): + return False + return normalize_status(target.get("status")) == "done" + + if stale_seconds > 0: + now = datetime.now(timezone.utc) + for story in stories: + if not isinstance(story, dict): + continue + if normalize_status(story.get("status")) != "in_progress": + continue + started = parse_ts(story.get("startedAt")) + if started is None or (now - started).total_seconds() > stale_seconds: + story["status"] = "open" + story["startedAt"] = None + story["completedAt"] = None + story["updatedAt"] = now_iso() + + candidate = None + for story in stories: + if not isinstance(story, dict): + continue + if normalize_status(story.get("status")) != "open": + continue + deps = story.get("dependsOn") or [] + if not isinstance(deps, list): + deps = [] + if all(is_done(dep) for dep in deps): + candidate = story + break + + remaining = sum( + 1 for story in stories + if isinstance(story, dict) and normalize_status(story.get("status")) != "done" + ) + + meta = { + "ok": True, + "total": len(stories), + "remaining": remaining, + "quality_gates": data.get("qualityGates", []) or [], + } + + if candidate: + candidate["status"] = "in_progress" + if not candidate.get("startedAt"): + candidate["startedAt"] = now_iso() + candidate["completedAt"] = None + candidate["updatedAt"] = now_iso() + meta.update({ + "id": candidate.get("id", ""), + "title": candidate.get("title", ""), + }) + + depends = candidate.get("dependsOn") or [] + if not isinstance(depends, list): + depends = [] + acceptance = candidate.get("acceptanceCriteria") or [] + if not isinstance(acceptance, list): + acceptance = [] + + description = candidate.get("description") or "" + block_lines = [] + block_lines.append(f"### {candidate.get('id', '')}: {candidate.get('title', '')}") + block_lines.append(f"Status: {candidate.get('status', 'open')}") + block_lines.append( + f"Depends on: {', '.join(depends) if depends else 'None'}" + ) + block_lines.append("") + block_lines.append("Description:") + block_lines.append(description if description else "(none)") + block_lines.append("") + block_lines.append("Acceptance Criteria:") + if acceptance: + block_lines.extend([f"- [ ] {item}" for item in acceptance]) + else: + block_lines.append("- (none)") + block_out.write_text("\n".join(block_lines).rstrip() + "\n") + else: + block_out.write_text("") + + fh.seek(0) + fh.truncate() + json.dump(data, fh, indent=2) + fh.write("\n") + fh.flush() + os.fsync(fh.fileno()) + finally: + if fcntl is not None: + fcntl.flock(fh.fileno(), fcntl.LOCK_UN) + +meta_out.write_text(json.dumps(meta, indent=2) + "\n") +PY +} + +remaining_stories() { + local meta_file="$1" + python3 - "$meta_file" <<'PY' +import json +import sys +from pathlib import Path + +data = json.loads(Path(sys.argv[1]).read_text()) +print(data.get("remaining", "unknown")) +PY +} + +remaining_from_prd() { + python3 - "$PRD_PATH" <<'PY' +import json +import sys +from pathlib import Path + +prd_path = Path(sys.argv[1]) +if not prd_path.exists(): + print("unknown") + sys.exit(0) + +try: + data = json.loads(prd_path.read_text()) +except Exception: + print("unknown") + sys.exit(0) + +stories = data.get("stories") if isinstance(data, dict) else None +if not isinstance(stories, list): + print("unknown") + sys.exit(0) + +def normalize_status(value): + if value is None: + return "open" + return str(value).strip().lower() + +remaining = sum( + 1 for story in stories + if isinstance(story, dict) and normalize_status(story.get("status")) != "done" +) +print(remaining) +PY +} + +story_field() { + local meta_file="$1" + local field="$2" + python3 - "$meta_file" "$field" <<'PY' +import json +import sys +from pathlib import Path + +data = json.loads(Path(sys.argv[1]).read_text()) +field = sys.argv[2] +print(data.get(field, "")) +PY +} + +update_story_status() { + local story_id="$1" + local new_status="$2" + python3 - "$PRD_PATH" "$story_id" "$new_status" <<'PY' +import json +import os +import sys +from pathlib import Path +from datetime import datetime, timezone +try: + import fcntl +except Exception: + fcntl = None + +prd_path = Path(sys.argv[1]) +story_id = sys.argv[2] +new_status = sys.argv[3] + +if not story_id: + sys.exit(0) + +if not prd_path.exists(): + sys.exit(0) + +def now_iso(): + return datetime.now(timezone.utc).isoformat() + +with prd_path.open("r+", encoding="utf-8") as fh: + if fcntl is not None: + fcntl.flock(fh.fileno(), fcntl.LOCK_EX) + try: + data = json.load(fh) + stories = data.get("stories") if isinstance(data, dict) else None + if not isinstance(stories, list): + sys.exit(0) + for story in stories: + if isinstance(story, dict) and story.get("id") == story_id: + story["status"] = new_status + story["updatedAt"] = now_iso() + if new_status == "in_progress": + if not story.get("startedAt"): + story["startedAt"] = now_iso() + story["completedAt"] = None + elif new_status == "done": + story["completedAt"] = now_iso() + if not story.get("startedAt"): + story["startedAt"] = now_iso() + elif new_status == "open": + story["startedAt"] = None + story["completedAt"] = None + break + fh.seek(0) + fh.truncate() + json.dump(data, fh, indent=2) + fh.write("\n") + fh.flush() + os.fsync(fh.fileno()) + finally: + if fcntl is not None: + fcntl.flock(fh.fileno(), fcntl.LOCK_UN) +PY +} + +log_activity() { + local message="$1" + local timestamp + timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[$timestamp] $message" >> "$ACTIVITY_LOG_PATH" +} + +log_error() { + local message="$1" + local timestamp + timestamp=$(date '+%Y-%m-%d %H:%M:%S') + echo "[$timestamp] $message" >> "$ERRORS_LOG_PATH" +} + +append_run_summary() { + local line="$1" + python3 - "$ACTIVITY_LOG_PATH" "$line" <<'PY' +import sys +from pathlib import Path + +path = Path(sys.argv[1]) +line = sys.argv[2] +text = path.read_text().splitlines() +out = [] +inserted = False +for l in text: + out.append(l) + if not inserted and l.strip() == "## Run Summary": + out.append(f"- {line}") + inserted = True +if not inserted: + out = [ + "# Activity Log", + "", + "## Run Summary", + f"- {line}", + "", + "## Events", + "", + ] + text +Path(path).write_text("\n".join(out).rstrip() + "\n") +PY +} + +write_run_meta() { + local path="$1" + local mode="$2" + local iter="$3" + local run_id="$4" + local story_id="$5" + local story_title="$6" + local started="$7" + local ended="$8" + local duration="$9" + local status="${10}" + local log_file="${11}" + local head_before="${12}" + local head_after="${13}" + local commit_list="${14}" + local changed_files="${15}" + local dirty_files="${16}" + { + echo "# Ralph Run Summary" + echo "" + echo "- Run ID: $run_id" + echo "- Iteration: $iter" + echo "- Mode: $mode" + if [ -n "$story_id" ]; then + echo "- Story: $story_id: $story_title" + fi + echo "- Started: $started" + echo "- Ended: $ended" + echo "- Duration: ${duration}s" + echo "- Status: $status" + echo "- Log: $log_file" + echo "" + echo "## Git" + echo "- Head (before): ${head_before:-unknown}" + echo "- Head (after): ${head_after:-unknown}" + echo "" + echo "### Commits" + if [ -n "$commit_list" ]; then + echo "$commit_list" + else + echo "- (none)" + fi + echo "" + echo "### Changed Files (commits)" + if [ -n "$changed_files" ]; then + echo "$changed_files" + else + echo "- (none)" + fi + echo "" + echo "### Uncommitted Changes" + if [ -n "$dirty_files" ]; then + echo "$dirty_files" + else + echo "- (clean)" + fi + echo "" + } > "$path" +} + +git_head() { + if git -C "$ROOT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + git -C "$ROOT_DIR" rev-parse HEAD 2>/dev/null || true + else + echo "" + fi +} + +git_commit_list() { + local before="$1" + local after="$2" + if [ -n "$before" ] && [ -n "$after" ] && [ "$before" != "$after" ]; then + git -C "$ROOT_DIR" log --oneline "$before..$after" | sed 's/^/- /' + else + echo "" + fi +} + +git_changed_files() { + local before="$1" + local after="$2" + if [ -n "$before" ] && [ -n "$after" ] && [ "$before" != "$after" ]; then + git -C "$ROOT_DIR" diff --name-only "$before" "$after" | sed 's/^/- /' + else + echo "" + fi +} + +git_dirty_files() { + if git -C "$ROOT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + git -C "$ROOT_DIR" status --porcelain | awk '{print "- " $2}' + else + echo "" + fi +} + +echo "Ralph mode: $MODE" +echo "Max iterations: $MAX_ITERATIONS" +echo "PRD: $PRD_PATH" +HAS_ERROR="false" + +for i in $(seq 1 "$MAX_ITERATIONS"); do + echo "" + echo "═══════════════════════════════════════════════════════" + echo " Ralph Iteration $i of $MAX_ITERATIONS" + echo "═══════════════════════════════════════════════════════" + + STORY_META="" + STORY_BLOCK="" + ITER_START=$(date +%s) + ITER_START_FMT=$(date '+%Y-%m-%d %H:%M:%S') + if [ "$MODE" = "build" ]; then + STORY_META="$TMP_DIR/story-$RUN_TAG-$i.json" + STORY_BLOCK="$TMP_DIR/story-$RUN_TAG-$i.md" + select_story "$STORY_META" "$STORY_BLOCK" + REMAINING="$(remaining_stories "$STORY_META")" + if [ "$REMAINING" = "unknown" ]; then + echo "Could not parse stories from PRD: $PRD_PATH" + exit 1 + fi + if [ "$REMAINING" = "0" ]; then + echo "No remaining stories." + exit 0 + fi + STORY_ID="$(story_field "$STORY_META" "id")" + STORY_TITLE="$(story_field "$STORY_META" "title")" + if [ -z "$STORY_ID" ]; then + echo "No actionable open stories (all blocked or in progress). Remaining: $REMAINING" + exit 0 + fi + fi + + HEAD_BEFORE="$(git_head)" + PROMPT_RENDERED="$TMP_DIR/prompt-$RUN_TAG-$i.md" + LOG_FILE="$RUNS_DIR/run-$RUN_TAG-iter-$i.log" + RUN_META="$RUNS_DIR/run-$RUN_TAG-iter-$i.md" + render_prompt "$PROMPT_FILE" "$PROMPT_RENDERED" "$STORY_META" "$STORY_BLOCK" "$RUN_TAG" "$i" "$LOG_FILE" "$RUN_META" + + if [ "$MODE" = "build" ] && [ -n "${STORY_ID:-}" ]; then + log_activity "ITERATION $i start (mode=$MODE story=$STORY_ID)" + else + log_activity "ITERATION $i start (mode=$MODE)" + fi + set +e + if [ "${RALPH_DRY_RUN:-}" = "1" ]; then + echo "[RALPH_DRY_RUN] Skipping agent execution." | tee "$LOG_FILE" + CMD_STATUS=0 + else + run_agent "$PROMPT_RENDERED" 2>&1 | tee "$LOG_FILE" + CMD_STATUS=$? + fi + set -e + if [ "$CMD_STATUS" -eq 130 ] || [ "$CMD_STATUS" -eq 143 ]; then + echo "Interrupted." + exit "$CMD_STATUS" + fi + ITER_END=$(date +%s) + ITER_END_FMT=$(date '+%Y-%m-%d %H:%M:%S') + ITER_DURATION=$((ITER_END - ITER_START)) + HEAD_AFTER="$(git_head)" + log_activity "ITERATION $i end (duration=${ITER_DURATION}s)" + if [ "$CMD_STATUS" -ne 0 ]; then + log_error "ITERATION $i command failed (status=$CMD_STATUS)" + HAS_ERROR="true" + fi + COMMIT_LIST="$(git_commit_list "$HEAD_BEFORE" "$HEAD_AFTER")" + CHANGED_FILES="$(git_changed_files "$HEAD_BEFORE" "$HEAD_AFTER")" + DIRTY_FILES="$(git_dirty_files)" + STATUS_LABEL="success" + if [ "$CMD_STATUS" -ne 0 ]; then + STATUS_LABEL="error" + fi + if [ "$MODE" = "build" ] && [ "$NO_COMMIT" = "false" ] && [ -n "$DIRTY_FILES" ]; then + log_error "ITERATION $i left uncommitted changes; review run summary at $RUN_META" + fi + write_run_meta "$RUN_META" "$MODE" "$i" "$RUN_TAG" "${STORY_ID:-}" "${STORY_TITLE:-}" "$ITER_START_FMT" "$ITER_END_FMT" "$ITER_DURATION" "$STATUS_LABEL" "$LOG_FILE" "$HEAD_BEFORE" "$HEAD_AFTER" "$COMMIT_LIST" "$CHANGED_FILES" "$DIRTY_FILES" + if [ "$MODE" = "build" ] && [ -n "${STORY_ID:-}" ]; then + append_run_summary "$(date '+%Y-%m-%d %H:%M:%S') | run=$RUN_TAG | iter=$i | mode=$MODE | story=$STORY_ID | duration=${ITER_DURATION}s | status=$STATUS_LABEL" + else + append_run_summary "$(date '+%Y-%m-%d %H:%M:%S') | run=$RUN_TAG | iter=$i | mode=$MODE | duration=${ITER_DURATION}s | status=$STATUS_LABEL" + fi + + if [ "$MODE" = "build" ]; then + if [ "$CMD_STATUS" -ne 0 ]; then + log_error "ITERATION $i exited non-zero; review $LOG_FILE" + update_story_status "$STORY_ID" "open" + echo "Iteration failed; story reset to open." + elif grep -q "COMPLETE" "$LOG_FILE"; then + update_story_status "$STORY_ID" "done" + echo "Completion signal received; story marked done." + else + update_story_status "$STORY_ID" "open" + echo "No completion signal; story reset to open." + fi + REMAINING="$(remaining_from_prd)" + echo "Iteration $i complete. Remaining stories: $REMAINING" + if [ "$REMAINING" = "0" ]; then + echo "No remaining stories." + exit 0 + fi + else + echo "Iteration $i complete." + fi + sleep 2 + +done + +echo "Reached max iterations ($MAX_ITERATIONS)." +if [ "$HAS_ERROR" = "true" ]; then + exit 1 +fi +exit 0 diff --git a/.agents/ralph/ralph.webp b/.agents/ralph/ralph.webp new file mode 100644 index 0000000000000000000000000000000000000000..124eb2d08f2e120c0e71b4de38dcea35f77f7303 GIT binary patch literal 275808 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%0DkiZ<2B4de8L`FQN; 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 literal 0 HcmV?d00001 diff --git a/.agents/ralph/references/CONTEXT_ENGINEERING.md b/.agents/ralph/references/CONTEXT_ENGINEERING.md new file mode 100644 index 0000000..5577f69 --- /dev/null +++ b/.agents/ralph/references/CONTEXT_ENGINEERING.md @@ -0,0 +1,126 @@ +# Context Engineering Reference + +This document explains the malloc/free metaphor for LLM context management that underlies the Ralph technique. + +## The malloc() Metaphor + +In traditional programming: +- `malloc()` allocates memory +- `free()` releases memory +- Memory leaks occur when you allocate without freeing + +In LLM context: +- Reading files, receiving responses, tool outputs = `malloc()` +- **There is no `free()`** - context cannot be released +- The only way to "free" is to start a new conversation + +## Why This Matters + +### Context Pollution + +When you work on multiple unrelated tasks in the same context: + +``` +Task 1: Build authentication → context contains auth code, JWT docs, security patterns +Task 2: Build UI components → context now ALSO contains auth stuff + +Result: LLM might suggest auth-related patterns when building UI + or mix concerns inappropriately +``` + +### Autoregressive Failure + +LLMs predict the next token based on ALL context. When context contains: +- Unrelated information +- Failed attempts +- Mixed concerns + +The model can "spiral" into wrong territory, generating increasingly off-base responses. + +### The Gutter Metaphor + +> "If the bowling ball is in the gutter, there's no saving it." + +Once context is polluted with failed attempts or mixed concerns, the model will keep referencing that pollution. Starting fresh is often faster than trying to correct course. + +## Context Health Indicators + +### 🟢 Healthy Context +- Single focused task +- Relevant files only +- Clear progress +- Under 60% capacity + +### 🟡 Warning Signs +- Multiple unrelated topics discussed +- Several failed attempts in history +- Approaching 80% capacity +- Repeated similar errors + +### 🔴 Critical / Gutter +- Mixed concerns throughout +- Circular failure patterns +- Over 90% capacity +- Model suggesting irrelevant solutions + +## Best Practices + +### 1. One Task Per Context + +Don't ask "fix the auth bug AND add the new feature". Do them in separate conversations. + +### 2. Fresh Start on Topic Change + +Finished auth? Start a new conversation for the next feature. + +### 3. Don't Redline + +Stay under 80% of context capacity. Quality degrades as you approach limits. + +### 4. Recognize the Gutter + +If you're seeing: +- Same error 3+ times +- Solutions that don't match the problem +- Circular suggestions + +Start fresh. Your progress is in the files. + +### 5. State in Files, Not Context + +Write progress to files. The next conversation can read them. Context is ephemeral; files are permanent. + +## Ralph's Approach + +The original Ralph technique (`while :; do cat PROMPT.md | agent ; done`) naturally implements these principles: + +1. **Each iteration is a fresh process** - Context is freed +2. **State persists in files** - Progress survives context resets +3. **Same prompt each time** - Focused, single-task context +4. **Failures inform guardrails** - Learning without context pollution + +This Cursor implementation aims to bring these benefits while working within Cursor's session model. + +## Measuring Context + +Rough estimates: +- 1 token ≈ 4 characters +- Average code file: 500-2000 tokens +- Large file: 5000+ tokens +- Conversation history: 100-500 tokens per exchange + +Track allocations in `.ralph/context-log.md` to stay aware. + +## When to Start Fresh + +**Definitely start fresh when:** +- Switching to unrelated task +- Context over 90% full +- Same error 3+ times +- Model suggestions are off-topic + +**Consider starting fresh when:** +- Context over 70% full +- Significant topic shift within task +- Feeling "stuck" +- Multiple failed approaches in history diff --git a/.agents/ralph/references/GUARDRAILS.md b/.agents/ralph/references/GUARDRAILS.md new file mode 100644 index 0000000..0620424 --- /dev/null +++ b/.agents/ralph/references/GUARDRAILS.md @@ -0,0 +1,174 @@ +# Guardrails Reference ("Signs") + +This document explains how to create and use guardrails in Ralph. + +## The Signs Metaphor + +From Geoffrey Huntley: + +> "Ralph is very good at making playgrounds, but he comes home bruised because he fell off the slide, so one then tunes Ralph by adding a sign next to the slide saying 'SLIDE DOWN, DON'T JUMP, LOOK AROUND,' and Ralph is more likely to look and see the sign." + +Signs are explicit instructions added to prevent known failure modes. + +## Anatomy of a Sign + +```markdown +### Sign: [Descriptive Name] +- **Trigger**: When this situation occurs +- **Instruction**: What to do instead +- **Added after**: When/why this was added +- **Example**: Concrete example if helpful +``` + +## Types of Signs + +### 1. Preventive Signs + +Stop problems before they happen: + +```markdown +### Sign: Validate Before Trust +- **Trigger**: When receiving external input +- **Instruction**: Always validate and sanitize input before using it +- **Added after**: Iteration 3 - SQL injection vulnerability +``` + +### 2. Corrective Signs + +Fix recurring mistakes: + +```markdown +### Sign: Check Return Values +- **Trigger**: When calling functions that can fail +- **Instruction**: Always check return values and handle errors +- **Added after**: Iteration 7 - Null pointer exception +``` + +### 3. Process Signs + +Enforce good practices: + +```markdown +### Sign: Test Before Commit +- **Trigger**: Before committing changes +- **Instruction**: Run the test suite and ensure all tests pass +- **Added after**: Iteration 2 - Broken tests committed +``` + +### 4. Architecture Signs + +Guide design decisions: + +```markdown +### Sign: Single Responsibility +- **Trigger**: When a function grows beyond 50 lines +- **Instruction**: Consider splitting into smaller, focused functions +- **Added after**: Iteration 12 - Unmaintainable god function +``` + +## When to Add Signs + +Add a sign when: + +1. **The same mistake happens twice** - Once is learning, twice is a pattern +2. **A subtle bug is found** - Prevent future occurrences +3. **A best practice is violated** - Reinforce good habits +4. **Context-specific knowledge is needed** - Project-specific conventions + +## Sign Lifecycle + +### Creation + +```markdown +### Sign: [New Sign] +- **Trigger**: [When it applies] +- **Instruction**: [What to do] +- **Added after**: Iteration N - [What happened] +``` + +### Refinement + +If a sign isn't working: +- Make the trigger more specific +- Make the instruction clearer +- Add examples + +### Retirement + +Signs can be removed when: +- The underlying issue is fixed at a deeper level +- The sign is no longer relevant +- The sign is causing more problems than it solves + +## Example Signs Library + +### Security + +```markdown +### Sign: Sanitize All Input +- **Trigger**: Any user-provided data +- **Instruction**: Use parameterized queries, escape HTML, validate types +- **Example**: `db.query("SELECT * FROM users WHERE id = ?", [userId])` +``` + +### Error Handling + +```markdown +### Sign: Graceful Degradation +- **Trigger**: External service calls +- **Instruction**: Always have a fallback for when services are unavailable +- **Example**: Cache results, provide default values, show friendly errors +``` + +### Testing + +```markdown +### Sign: Test the Unhappy Path +- **Trigger**: Writing tests for new functionality +- **Instruction**: Include tests for error cases, edge cases, and invalid input +``` + +### Code Quality + +```markdown +### Sign: Explain Why, Not What +- **Trigger**: Writing comments +- **Instruction**: Comments should explain reasoning, not describe obvious code +- **Example**: `// Using retry because API is flaky under load` not `// Call the API` +``` + +## Automatic Sign Detection + +The Ralph hooks can automatically detect some patterns and suggest signs: + +- **Thrashing**: Same file edited many times → "Step back and reconsider" +- **Repeated errors**: Same test failing → "Check the test assumptions" +- **Large changes**: Big diffs → "Consider smaller increments" + +These are logged in `.ralph/failures.md` and can be promoted to guardrails. + +## Using Signs Effectively + +### Do + +- Keep signs concise and actionable +- Include concrete examples +- Update signs when they're not working +- Remove outdated signs + +### Don't + +- Add signs for every minor issue +- Make signs too vague ("be careful") +- Ignore signs that keep triggering +- Let the guardrails file become overwhelming + +## Integration with Ralph + +Signs are: +1. Stored in `.ralph/guardrails.md` +2. Injected into context at the start of each iteration +3. Referenced when relevant situations arise +4. Updated based on observed failures + +The goal is a self-improving system where each failure makes future iterations smarter. diff --git a/.agents/tasks/prd-mobile-refactor.json b/.agents/tasks/prd-mobile-refactor.json new file mode 100644 index 0000000..d442428 --- /dev/null +++ b/.agents/tasks/prd-mobile-refactor.json @@ -0,0 +1,142 @@ +{ + "version": 1, + "project": "Mobile App File Size Refactor", + "overview": "Refactor the existing React Native (Expo) mobile app to keep most files within ~300-400 lines by splitting into smaller modules without changing functionality.", + "goals": [ + "Reduce oversized files to a target range of ~300-400 lines where practical", + "Improve maintainability by splitting files into clearer layers (ui/components, hooks, services, utils)", + "Preserve existing behavior and UI" + ], + "nonGoals": [ + "No functional changes", + "No UI changes", + "No new product features" + ], + "successMetrics": [ + "All files are within target size or documented as justified exceptions", + "Refactor passes existing tests, lint, and typecheck" + ], + "openQuestions": [ + "Which specific files currently exceed 400 lines and need priority refactors?", + "Are there any existing architectural constraints that limit splitting (e.g., circular deps)?" + ], + "stack": { + "framework": "React Native (Expo)", + "hosting": "App stores via Expo/EAS", + "database": "Existing app data layer (no changes)", + "auth": "Existing app auth (no changes)" + }, + "routes": [ + { + "path": "(existing)", + "name": "App Routes", + "purpose": "No route changes; refactor only" + } + ], + "uiNotes": [ + "No UI changes; keep visual output identical", + "Split view logic into smaller components without altering layout" + ], + "dataModel": [ + { + "entity": "N/A", + "fields": [] + } + ], + "importFormat": { + "description": "Not applicable", + "example": {} + }, + "rules": [ + "Target file size is ~300-400 lines; exceptions must include justification", + "Refactor should follow layer-based split: ui/components, hooks, services, utils", + "No behavior changes; identical inputs must yield identical outputs" + ], + "qualityGates": [ + "npm test", + "npm run lint", + "npm run typecheck" + ], + "stories": [ + { + "id": "US-001", + "title": "Inventory oversized files and refactor plan", + "status": "done", + "dependsOn": [], + "description": "As a developer, I want an inventory of oversized files and a planned split strategy so refactors are systematic and safe.", + "acceptanceCriteria": [ + "Produce a list of files > 400 lines with line counts", + "For each file, define target split into layers (ui/components, hooks, services, utils)", + "Example: a 900-line screen file -> screen container + UI subcomponents + hooks + service module", + "Negative case: a file under 300 lines is not split unless a clear modularization need is documented" + ], + "startedAt": "2026-01-25T20:40:12.238126+00:00", + "completedAt": "2026-01-25T20:47:05.362525+00:00", + "updatedAt": "2026-01-25T20:47:05.362425+00:00" + }, + { + "id": "US-002", + "title": "Refactor screen-level files into layer-based modules", + "status": "in_progress", + "dependsOn": [ + "US-001" + ], + "description": "As a developer, I want oversized screen files split into smaller modules so each file stays around 300-400 lines.", + "acceptanceCriteria": [ + "Split screen files into container + UI components + hooks + services as needed", + "Example: screen logic moves into `hooks/use.ts` and data calls into `services/.ts`", + "Negative case: moving a function that creates circular dependencies is rejected and alternative split is used", + "All screens render identically before and after refactor" + ], + "startedAt": "2026-01-25T20:47:07.436262+00:00", + "completedAt": null, + "updatedAt": "2026-01-25T20:47:07.436370+00:00" + }, + { + "id": "US-003", + "title": "Refactor oversized shared components", + "status": "open", + "dependsOn": [ + "US-001" + ], + "description": "As a developer, I want oversized shared components split into smaller pieces to improve clarity and reusability.", + "acceptanceCriteria": [ + "Split shared UI components into smaller subcomponents and helper modules", + "Example: extract nested UI blocks into `components//SubComponent.tsx`", + "Negative case: do not extract one-off UI fragments that would reduce cohesion", + "No change to public component API unless required for split" + ] + }, + { + "id": "US-004", + "title": "Refactor oversized hooks/services/utils", + "status": "open", + "dependsOn": [ + "US-001" + ], + "description": "As a developer, I want oversized logic files split into smaller, focused modules for readability.", + "acceptanceCriteria": [ + "Split large hooks into smaller hooks or helper functions in utils", + "Example: a 500-line hook becomes a main hook + `utils/-helpers.ts`", + "Negative case: do not split logic in a way that increases public API surface without justification", + "All previous exports remain available or are re-exported from the original entry point" + ] + }, + { + "id": "US-005", + "title": "Validate refactor quality gates", + "status": "open", + "dependsOn": [ + "US-002", + "US-003", + "US-004" + ], + "description": "As a developer, I want automated checks to pass to ensure the refactor preserved behavior.", + "acceptanceCriteria": [ + "Run `npm test`, `npm run lint`, and `npm run typecheck` successfully", + "Example: all tests pass without modifications to test expectations", + "Negative case: if a check fails, refactor is incomplete and must be fixed before completion" + ] + } + ] +} diff --git a/.codex/skills/commit/SKILL.md b/.codex/skills/commit/SKILL.md new file mode 100644 index 0000000..29d6d03 --- /dev/null +++ b/.codex/skills/commit/SKILL.md @@ -0,0 +1,219 @@ +--- +name: commit +description: Write conventional commit messages with type, scope, and subject when the user wants to commit changes or save work. +--- + +# Git Commit + +Creates git commits following Conventional Commits format with proper type, scope, and subject. + +## Quick Start + +```bash +# 1. Stage changes +git add # or: git add -A + +# 2. Create commit (branch commit format) +git commit -m "type(scope): subject + +Body explaining HOW and WHY. +Reference: Task X.Y, Req N" +``` + +## Commit Types + +### Regular Branch Commits (During Development) + +**Format**: `type(scope): subject` + +| Type | Purpose | +|------|---------| +| `feat` | New feature or functionality | +| `fix` | Bug fix or issue resolution | +| `refactor` | Code refactoring without behavior change | +| `perf` | Performance improvements | +| `test` | Test additions or modifications | +| `ci` | CI/CD configuration changes | +| `docs` | Documentation updates | +| `chore` | Maintenance, dependencies, tooling | +| `style` | Code formatting, linting (non-functional) | +| `security` | Security vulnerability fixes or hardening | + +### Scope (Required, kebab-case) + +Examples: `validation`, `auth`, `cookie-service`, `template`, `config`, `tests`, `api` + +### Subject Line Rules + +- Max 50 characters after colon +- Present tense imperative: add, implement, fix, improve, enhance, refactor, remove, prevent +- NO period at the end +- Specific and descriptive - state WHAT, not WHY + +## Core Workflow + +### 1. Review Changes + +```bash +git status +git diff --staged # if already staged +git diff # if not staged +``` + +### 2. Stage Files + +```bash +git add # preferred +# or +git add -A # all changes +``` + +**NEVER commit**: +- `.env`, `credentials.json`, secrets +- `node_modules/`, `__pycache__/`, `.venv/` +- Large binary files without explicit approval + +### 3. Create Commit + +**Simple change**: +```bash +git commit -m "fix(auth): use hmac.compare_digest for secure comparison" +``` + +**Complex change (with body)**: +```bash +git commit -m "$(cat <<'EOF' +feat(validation): add URLValidator with domain whitelist + +Implement URLValidator class supporting: +- Domain whitelist enforcement (youtube.com, youtu.be) +- Dangerous scheme blocking (javascript, data, file) +- URL parsing with embedded credentials handling + +Addresses Requirement 31: Input validation +Part of Task 5.1: Input Validation Utilities +EOF +)" +``` + +### 4. Verify Commit + +```bash +git log -1 --format="%h %s" +git show --stat HEAD +``` + +## Body Format (Recommended for Complex Changes) + +``` + +Explain HOW and WHY the change was made. +- Use bullet points for multiple items +- Wrap at 72 characters + +Reference: Task X.Y +Addresses: Req N +``` + +## Git Trailers + +| Trailer | Purpose | +|---------|---------| +| `Fixes #N` | Links and closes issue on merge | +| `Closes #N` | Same as Fixes | +| `Co-authored-by: Name ` | Credit co-contributors | + +Place trailers at end of body after blank line. See `references/commit_examples.md` for examples. + +## Breaking Changes + +For incompatible API/behavior changes, use `!` after scope OR `BREAKING CHANGE:` footer: + +``` +feat(api)!: change response format to JSON:API + +BREAKING CHANGE: Response envelope changed from `{ data }` to `{ data: { type, id, attributes } }`. +``` + +Triggers major version bump in semantic-release. + +## Merge Commits (PR Closure) + +For PRs, use extended description with sections: + +```bash +gh pr create --title "feat(security): implement input validation (Task 5)" --body "$(cat <<'EOF' +## Summary +- Input validation utilities (URLValidator, FormatValidator) +- Secure template processor with path traversal prevention +- API key authentication middleware + +## Task Breakdown +Task 5.1: Input Validation - URLValidator, FormatValidator +Task 5.2: Template Processing - Path traversal prevention +Task 5.3: API Key Auth - Multi-key support, excluded paths +Task 5.4: Security Tests - 102 path traversal tests + +## Requirements Covered +Req 7, Req 9, Req 31, Req 33 + +## Test Coverage +- All 473 tests passing +- Coverage: 93% +- Pre-commit checks: passing +EOF +)" +``` + +## Integration with Other Skills + +### From github-pr-review + +When fixing review comments, use this format: + +```bash +git commit -m "fix(scope): address review comment #ID + +Brief explanation of what was wrong and how it's fixed. +Addresses review comment #123456789." +``` + +### From github-pr-creation + +Before creating PR, ensure all commits follow this format. The PR skill will: +1. Analyze commits for proper format +2. Extract types for PR labels +3. Build PR description from commit bodies + +## Important Rules + +- **ALWAYS** include scope in parentheses +- **ALWAYS** use present tense imperative verb +- **NEVER** end subject with period +- **NEVER** commit secrets or credentials +- **NEVER** use generic messages ("update code", "fix bug", "changes") +- **NEVER** exceed 50 chars in subject line +- Group related changes -> single focused commit + +## Examples + +**Good**: +``` +feat(validation): add URLValidator with domain whitelist +fix(auth): use hmac.compare_digest for secure key comparison +refactor(template): consolidate filename sanitization logic +test(security): add 102 path traversal prevention tests +``` + +**Bad**: +``` +update validation code # no type, no scope, vague +feat: add stuff # missing scope, too vague +fix(auth): fix bug # circular, not specific +chore: make changes # missing scope, vague +feat(security): improve things. # has period, vague +``` + +## References + +- `references/commit_examples.md` - Extended examples by type diff --git a/.codex/skills/commit/references/commit_examples.md b/.codex/skills/commit/references/commit_examples.md new file mode 100644 index 0000000..6775cbb --- /dev/null +++ b/.codex/skills/commit/references/commit_examples.md @@ -0,0 +1,292 @@ +# Commit Examples by Type + +Extended examples for each commit type with body content. + +## feat - New Features + +``` +feat(validation): add URLValidator with domain whitelist + +Implement URLValidator class supporting: +- Domain whitelist enforcement (youtube.com, youtu.be, m.youtube.com) +- Dangerous scheme blocking (javascript, data, file) +- URL parsing with embedded credentials handling +- Port number validation (1-65535) + +Addresses Requirement 31: Input validation +Part of Task 5.1: Input Validation Utilities +``` + +``` +feat(api): add video metadata endpoint + +New GET /api/v1/videos/{id}/metadata endpoint: +- Returns title, duration, formats, thumbnails +- Supports format filtering via query params +- Implements caching with 5-minute TTL + +Part of Task 6.2: API Endpoints +``` + +## fix - Bug Fixes + +``` +fix(auth): use hmac.compare_digest for secure key comparison + +Replace direct string equality with hmac.compare_digest to prevent +timing attacks on API key validation. Ensures constant-time comparison +regardless of key length or content. + +Addresses security best practice for sensitive data comparison +``` + +``` +fix(download): handle network timeout during video fetch + +Add retry logic with exponential backoff for network failures: +- Max 3 attempts with delays [2, 4, 8] seconds +- Classify retriable errors (5xx, timeout, connection) +- Log each retry attempt with remaining count + +Fixes issue where downloads would fail silently on flaky connections +``` + +## refactor - Code Improvements + +``` +refactor(template): consolidate filename sanitization logic + +Extract common sanitization patterns into helper methods: +- Path traversal prevention (.., /, absolute paths) +- Special character removal (control chars, null bytes) +- Windows reserved name handling (CON, PRN, LPT1-9, etc) + +Improves code maintainability and reduces duplication +``` + +``` +refactor(providers): extract common yt-dlp options builder + +Move duplicated option building from get_info/download to +_build_base_options helper. Reduces code duplication and ensures +consistent option handling across all provider methods. + +No behavior change, pure refactoring +``` + +## test - Test Changes + +``` +test(security): add 102 path traversal prevention tests + +Comprehensive test coverage for TemplateProcessor including: +- Basic path traversal attempts (.., /) +- URL-encoded variants (%2e%2e, %2f) +- Unicode/UTF-8 bypass attempts +- Windows edge cases (backslashes, drive letters) + +Part of Task 5.4: Security Test Suite +``` + +``` +test(validation): add parametrized URL validation tests + +Add 25 test cases covering: +- Valid YouTube URL formats (watch, shorts, embed, youtu.be) +- Invalid domains (vimeo, dailymotion) +- Malformed URLs (no scheme, wrong port) +- Edge cases (trailing slashes, query params) + +Coverage for URLValidator: 98% +``` + +## perf - Performance + +``` +perf(cache): implement LRU eviction for metadata cache + +Replace dict-based cache with LRU implementation: +- Max 1000 entries with automatic eviction +- 40% memory reduction under high load +- Sub-millisecond lookup times maintained + +Addresses memory growth issue in long-running instances +``` + +## security - Security Fixes + +``` +security(cookie): validate cookie file integrity before use + +Add SHA256 checksum verification for cookie files: +- Compute hash on first load, store in memory +- Verify hash before each use +- Reject modified files with clear error message + +Prevents use of tampered cookie files +Addresses Requirement 33: Security validation +``` + +## ci - CI/CD Changes + +``` +ci(github): add security scanning to PR workflow + +Enable Bandit security scanner in GitHub Actions: +- Run on all Python files +- Fail on HIGH/CRITICAL findings +- Cache virtualenv for faster runs + +Part of Task 15.3: Basic security validation +``` + +## docs - Documentation + +``` +docs(api): add OpenAPI description for download endpoint + +Document /api/v1/download endpoint: +- Request body schema with format options +- Response codes (200, 400, 401, 404, 500) +- Example requests and responses + +Improves API documentation for consumers +``` + +## chore - Maintenance + +``` +chore(deps): update yt-dlp to 2024.12.06 + +Update yt-dlp from 2024.11.15 to 2024.12.06: +- Fixes YouTube throttling detection +- Adds support for new Instagram format +- Improves error messages for geo-blocked content + +No breaking changes expected +``` + +## style - Formatting + +``` +style(providers): apply black formatting to youtube.py + +Apply black formatter with 88 char line length. +No functional changes, formatting only. +``` + +## Merge Commit Examples + +### Feature Branch to Develop + +``` +Merge pull request #5 from fvadicamo/feature/input-validation-security + +feat(security): implement input validation and security (Task 5) + +Merges comprehensive security implementation (Task 5) into develop: +- Input validation utilities (URLValidator, FormatValidator, ParameterValidator) +- Secure template processor with path traversal prevention +- API key authentication middleware with multi-key support +- 473 tests with 93% coverage + +Task 5.1: Input Validation Utilities +- URLValidator: Domain whitelist (youtube.com, youtu.be), dangerous scheme blocking +- FormatValidator: yt-dlp format ID validation with regex and selectors +- ParameterValidator: Audio quality/format and language code validation + +Task 5.2: Template Processor +- Path traversal prevention (.., /, absolute paths, URL encoding) +- Filename sanitization (illegal chars, control chars, null bytes) +- Windows reserved names handling (CON, PRN, AUX, NUL, COM1-9, LPT1-9) +- Collision handling with numeric suffix, max length 200 chars + +Task 5.3: API Key Authentication +- APIKeyAuth class with multi-key support +- Excluded paths for health/doc endpoints +- Secure hashing for logging (SHA256 first 8 chars) +- FastAPI dependency injection integration + +Task 5.4: Security Tests +- 102 path traversal prevention tests with edge cases +- URL validation tests with malicious inputs +- API key authentication and credential tests +- Sensitive data redaction verification + +Requirements Covered: +- Req 7: Output template processing with security +- Req 9: API key authentication +- Req 31: Input validation +- Req 33: Security (secure comparison, log redaction) + +Test Coverage: +- All 473 tests passing +- Coverage: 93% (exceeds 80% minimum) +- Pre-commit checks: all passing +``` + +### Develop to Main (Release) + +``` +Merge pull request #10 from fvadicamo/develop + +release: v0.1.0 - MVP with YouTube provider + +First stable release with core functionality: +- YouTube video info, formats, download, audio extraction +- Cookie-based authentication for age-restricted content +- API key authentication +- Input validation and security hardening +- 500+ tests with 92% coverage + +Breaking Changes: None (initial release) + +Features: +- GET /api/v1/info - Video metadata +- GET /api/v1/formats - Available formats +- POST /api/v1/download - Video/audio download +- Cookie file support for authenticated requests + +Documentation: +- API documentation at /docs (Swagger UI) +- OpenAPI spec at /openapi.json +``` + +## Commits with Trailers + +### Single Issue +``` +fix(validation): prevent XSS in user input + +Escape HTML entities before rendering. + +Fixes #78 +``` + +### Multiple Issues + Co-author +``` +fix(auth): resolve session and token issues + +- Fix session expiry not triggering logout +- Fix token refresh race condition + +Fixes #101 +Fixes #103 +Co-authored-by: Bob +``` + +## Breaking Changes + +### With ! Notation +``` +feat(api)!: migrate to v2 endpoints + +BREAKING CHANGE: /api/v1/* endpoints removed. Update base URL to /api/v2/. +``` + +### Config Breaking Change +``` +chore(config)!: rename environment variables + +BREAKING CHANGE: DATABASE_URL -> APP_DATABASE_URL, API_KEY -> APP_API_KEY +``` diff --git a/.codex/skills/dev-browser/SKILL.md b/.codex/skills/dev-browser/SKILL.md new file mode 100644 index 0000000..ed0a028 --- /dev/null +++ b/.codex/skills/dev-browser/SKILL.md @@ -0,0 +1,211 @@ +--- +name: dev-browser +description: Browser automation with persistent page state. Use when users ask to navigate websites, fill forms, take screenshots, extract web data, test web apps, or automate browser workflows. Trigger phrases include "go to [url]", "click on", "fill out the form", "take a screenshot", "scrape", "automate", "test the website", "log into", or any browser interaction request. +--- + +# Dev Browser Skill + +Browser automation that maintains page state across script executions. Write small, focused scripts to accomplish tasks incrementally. Once you've proven out part of a workflow and there is repeated work to be done, you can write a script to do the repeated work in a single execution. + +## Choosing Your Approach + +- **Local/source-available sites**: Read the source code first to write selectors directly +- **Unknown page layouts**: Use `getAISnapshot()` to discover elements and `selectSnapshotRef()` to interact with them +- **Visual feedback**: Take screenshots to see what the user sees + +## Setup + +Two modes available. Ask the user if unclear which to use. + +### Standalone Mode (Default) + +Launches a new Chromium browser for fresh automation sessions. + +```bash +./skills/dev-browser/server.sh & +``` + +Add `--headless` flag if user requests it. **Wait for the `Ready` message before running scripts.** + +### Extension Mode + +Connects to user's existing Chrome browser. Use this when: + +- The user is already logged into sites and wants you to do things behind an authed experience that isn't local dev. +- The user asks you to use the extension + +**Important**: The core flow is still the same. You create named pages inside of their browser. + +**Start the relay server:** + +```bash +cd skills/dev-browser && npm i && npm run start-extension & +``` + +Wait for `Waiting for extension to connect...` followed by `Extension connected` in the console. To know that a client has connected and the browser is ready to be controlled. +**Workflow:** + +1. Scripts call `client.page("name")` just like the normal mode to create new pages / connect to existing ones. +2. Automation runs on the user's actual browser session + +If the extension hasn't connected yet, tell the user to launch and activate it. Download link: https://github.com/SawyerHood/dev-browser/releases + +## Writing Scripts + +> **Run all scripts from `skills/dev-browser/` directory.** The `@/` import alias requires this directory's config. + +Execute scripts inline using heredocs: + +```bash +cd skills/dev-browser && npx tsx <<'EOF' +import { connect, waitForPageLoad } from "@/client.js"; + +const client = await connect(); +// Create page with custom viewport size (optional) +const page = await client.page("example", { viewport: { width: 1920, height: 1080 } }); + +await page.goto("https://example.com"); +await waitForPageLoad(page); + +console.log({ title: await page.title(), url: page.url() }); +await client.disconnect(); +EOF +``` + +**Write to `tmp/` files only when** the script needs reuse, is complex, or user explicitly requests it. + +### Key Principles + +1. **Small scripts**: Each script does ONE thing (navigate, click, fill, check) +2. **Evaluate state**: Log/return state at the end to decide next steps +3. **Descriptive page names**: Use `"checkout"`, `"login"`, not `"main"` +4. **Disconnect to exit**: `await client.disconnect()` - pages persist on server +5. **Plain JS in evaluate**: `page.evaluate()` runs in browser - no TypeScript syntax + +## Workflow Loop + +Follow this pattern for complex tasks: + +1. **Write a script** to perform one action +2. **Run it** and observe the output +3. **Evaluate** - did it work? What's the current state? +4. **Decide** - is the task complete or do we need another script? +5. **Repeat** until task is done + +### No TypeScript in Browser Context + +Code passed to `page.evaluate()` runs in the browser, which doesn't understand TypeScript: + +```typescript +// ✅ Correct: plain JavaScript +const text = await page.evaluate(() => { + return document.body.innerText; +}); + +// ❌ Wrong: TypeScript syntax will fail at runtime +const text = await page.evaluate(() => { + const el: HTMLElement = document.body; // Type annotation breaks in browser! + return el.innerText; +}); +``` + +## Scraping Data + +For scraping large datasets, intercept and replay network requests rather than scrolling the DOM. See [references/scraping.md](references/scraping.md) for the complete guide covering request capture, schema discovery, and paginated API replay. + +## Client API + +```typescript +const client = await connect(); + +// Get or create named page (viewport only applies to new pages) +const page = await client.page("name"); +const pageWithSize = await client.page("name", { viewport: { width: 1920, height: 1080 } }); + +const pages = await client.list(); // List all page names +await client.close("name"); // Close a page +await client.disconnect(); // Disconnect (pages persist) + +// ARIA Snapshot methods +const snapshot = await client.getAISnapshot("name"); // Get accessibility tree +const element = await client.selectSnapshotRef("name", "e5"); // Get element by ref +``` + +The `page` object is a standard Playwright Page. + +## Waiting + +```typescript +import { waitForPageLoad } from "@/client.js"; + +await waitForPageLoad(page); // After navigation +await page.waitForSelector(".results"); // For specific elements +await page.waitForURL("**/success"); // For specific URL +``` + +## Inspecting Page State + +### Screenshots + +```typescript +await page.screenshot({ path: "tmp/screenshot.png" }); +await page.screenshot({ path: "tmp/full.png", fullPage: true }); +``` + +### ARIA Snapshot (Element Discovery) + +Use `getAISnapshot()` to discover page elements. Returns YAML-formatted accessibility tree: + +```yaml +- banner: + - link "Hacker News" [ref=e1] + - navigation: + - link "new" [ref=e2] +- main: + - list: + - listitem: + - link "Article Title" [ref=e8] + - link "328 comments" [ref=e9] +- contentinfo: + - textbox [ref=e10] + - /placeholder: "Search" +``` + +**Interpreting refs:** + +- `[ref=eN]` - Element reference for interaction (visible, clickable elements only) +- `[checked]`, `[disabled]`, `[expanded]` - Element states +- `[level=N]` - Heading level +- `/url:`, `/placeholder:` - Element properties + +**Interacting with refs:** + +```typescript +const snapshot = await client.getAISnapshot("hackernews"); +console.log(snapshot); // Find the ref you need + +const element = await client.selectSnapshotRef("hackernews", "e2"); +await element.click(); +``` + +## Error Recovery + +Page state persists after failures. Debug with: + +```bash +cd skills/dev-browser && npx tsx <<'EOF' +import { connect } from "@/client.js"; + +const client = await connect(); +const page = await client.page("hackernews"); + +await page.screenshot({ path: "tmp/debug.png" }); +console.log({ + url: page.url(), + title: await page.title(), + bodyText: await page.textContent("body").then((t) => t?.slice(0, 200)), +}); + +await client.disconnect(); +EOF +``` diff --git a/.codex/skills/dev-browser/bun.lock b/.codex/skills/dev-browser/bun.lock new file mode 100644 index 0000000..350c6c9 --- /dev/null +++ b/.codex/skills/dev-browser/bun.lock @@ -0,0 +1,443 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "dev-browser", + "dependencies": { + "express": "^4.21.0", + "playwright": "^1.49.0", + }, + "devDependencies": { + "@types/express": "^5.0.0", + "tsx": "^4.21.0", + "vitest": "^2.1.0", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.1", "", { "os": "android", "cpu": "arm" }, "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.1", "", { "os": "android", "cpu": "arm64" }, "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.1", "", { "os": "android", "cpu": "x64" }, "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.1", "", { "os": "linux", "cpu": "arm" }, "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.1", "", { "os": "linux", "cpu": "none" }, "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.1", "", { "os": "linux", "cpu": "x64" }, "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.1", "", { "os": "none", "cpu": "x64" }, "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.1", "", { "os": "win32", "cpu": "x64" }, "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.53.3", "", { "os": "android", "cpu": "arm" }, "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.53.3", "", { "os": "android", "cpu": "arm64" }, "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.53.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.53.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.53.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.53.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.53.3", "", { "os": "linux", "cpu": "arm" }, "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.53.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.53.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.53.3", "", { "os": "linux", "cpu": "none" }, "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.53.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.53.3", "", { "os": "linux", "cpu": "x64" }, "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.53.3", "", { "os": "none", "cpu": "arm64" }, "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.53.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.53.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.53.3", "", { "os": "win32", "cpu": "x64" }, "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.0", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], + + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + + "@vitest/expect": ["@vitest/expect@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "@vitest/utils": "2.1.9", "chai": "^5.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw=="], + + "@vitest/mocker": ["@vitest/mocker@2.1.9", "", { "dependencies": { "@vitest/spy": "2.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.12" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@2.1.9", "", { "dependencies": { "tinyrainbow": "^1.2.0" } }, "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ=="], + + "@vitest/runner": ["@vitest/runner@2.1.9", "", { "dependencies": { "@vitest/utils": "2.1.9", "pathe": "^1.1.2" } }, "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g=="], + + "@vitest/snapshot": ["@vitest/snapshot@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "magic-string": "^0.30.12", "pathe": "^1.1.2" } }, "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ=="], + + "@vitest/spy": ["@vitest/spy@2.1.9", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ=="], + + "@vitest/utils": ["@vitest/utils@2.1.9", "", { "dependencies": { "@vitest/pretty-format": "2.1.9", "loupe": "^3.1.2", "tinyrainbow": "^1.2.0" } }, "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], + + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "esbuild": ["esbuild@0.27.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.1", "@esbuild/android-arm": "0.27.1", "@esbuild/android-arm64": "0.27.1", "@esbuild/android-x64": "0.27.1", "@esbuild/darwin-arm64": "0.27.1", "@esbuild/darwin-x64": "0.27.1", "@esbuild/freebsd-arm64": "0.27.1", "@esbuild/freebsd-x64": "0.27.1", "@esbuild/linux-arm": "0.27.1", "@esbuild/linux-arm64": "0.27.1", "@esbuild/linux-ia32": "0.27.1", "@esbuild/linux-loong64": "0.27.1", "@esbuild/linux-mips64el": "0.27.1", "@esbuild/linux-ppc64": "0.27.1", "@esbuild/linux-riscv64": "0.27.1", "@esbuild/linux-s390x": "0.27.1", "@esbuild/linux-x64": "0.27.1", "@esbuild/netbsd-arm64": "0.27.1", "@esbuild/netbsd-x64": "0.27.1", "@esbuild/openbsd-arm64": "0.27.1", "@esbuild/openbsd-x64": "0.27.1", "@esbuild/openharmony-arm64": "0.27.1", "@esbuild/sunos-x64": "0.27.1", "@esbuild/win32-arm64": "0.27.1", "@esbuild/win32-ia32": "0.27.1", "@esbuild/win32-x64": "0.27.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="], + + "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + + "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "playwright": ["playwright@1.57.0", "", { "dependencies": { "playwright-core": "1.57.0" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw=="], + + "playwright-core": ["playwright-core@1.57.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "rollup": ["rollup@4.53.3", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.3", "@rollup/rollup-android-arm64": "4.53.3", "@rollup/rollup-darwin-arm64": "4.53.3", "@rollup/rollup-darwin-x64": "4.53.3", "@rollup/rollup-freebsd-arm64": "4.53.3", "@rollup/rollup-freebsd-x64": "4.53.3", "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", "@rollup/rollup-linux-arm-musleabihf": "4.53.3", "@rollup/rollup-linux-arm64-gnu": "4.53.3", "@rollup/rollup-linux-arm64-musl": "4.53.3", "@rollup/rollup-linux-loong64-gnu": "4.53.3", "@rollup/rollup-linux-ppc64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-gnu": "4.53.3", "@rollup/rollup-linux-riscv64-musl": "4.53.3", "@rollup/rollup-linux-s390x-gnu": "4.53.3", "@rollup/rollup-linux-x64-gnu": "4.53.3", "@rollup/rollup-linux-x64-musl": "4.53.3", "@rollup/rollup-openharmony-arm64": "4.53.3", "@rollup/rollup-win32-arm64-msvc": "4.53.3", "@rollup/rollup-win32-ia32-msvc": "4.53.3", "@rollup/rollup-win32-x64-gnu": "4.53.3", "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@0.19.1", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-p4rRk4f23ynFEfcD9LA0xRYngj+IyGiEYyqqOak8kaN0TvNmuxC2dcVeBn62GpCeR2CpWqyHCNScTP91QbAVFg=="], + + "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@1.2.0", "", {}, "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ=="], + + "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "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" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vite-node": ["vite-node@2.1.9", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.7", "es-module-lexer": "^1.5.4", "pathe": "^1.1.2", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA=="], + + "vitest": ["vitest@2.1.9", "", { "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" }, "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": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "playwright/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "serve-static/send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + + "vite/esbuild": ["esbuild@0.21.5", "", { "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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + + "serve-static/send/http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + } +} diff --git a/.codex/skills/dev-browser/package-lock.json b/.codex/skills/dev-browser/package-lock.json new file mode 100644 index 0000000..6e4aaa4 --- /dev/null +++ b/.codex/skills/dev-browser/package-lock.json @@ -0,0 +1,2988 @@ +{ + "name": "dev-browser", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dev-browser", + "version": "0.0.1", + "dependencies": { + "@hono/node-server": "^1.19.7", + "@hono/node-ws": "^1.2.0", + "express": "^4.21.0", + "hono": "^4.11.1", + "playwright": "^1.49.0" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "tsx": "^4.21.0", + "typescript": "^5.0.0", + "vitest": "^2.1.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.7", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.7.tgz", + "integrity": "sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==", + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@hono/node-ws": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@hono/node-ws/-/node-ws-1.2.0.tgz", + "integrity": "sha512-OBPQ8OSHBw29mj00wT/xGYtB6HY54j0fNSdVZ7gZM3TUeq0So11GXaWtFf1xWxQNfumKIsj0wRuLKWfVsO5GgQ==", + "license": "MIT", + "dependencies": { + "ws": "^8.17.0" + }, + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "@hono/node-server": "^1.11.1", + "hono": "^4.6.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz", + "integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz", + "integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz", + "integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz", + "integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz", + "integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz", + "integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz", + "integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz", + "integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz", + "integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz", + "integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz", + "integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz", + "integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz", + "integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz", + "integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz", + "integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz", + "integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz", + "integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz", + "integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz", + "integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz", + "integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz", + "integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz", + "integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz", + "integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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==", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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" + } + }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "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/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.1.tgz", + "integrity": "sha512-KsFcH0xxHes0J4zaQgWbYwmz3UPOOskdqZmItstUG93+Wk1ePBLkLGwbP9zlmh1BFUiL8Qp+Xfu9P7feJWpGNg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "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/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", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/playwright": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", + "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.57.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.57.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", + "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.54.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz", + "integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.54.0", + "@rollup/rollup-android-arm64": "4.54.0", + "@rollup/rollup-darwin-arm64": "4.54.0", + "@rollup/rollup-darwin-x64": "4.54.0", + "@rollup/rollup-freebsd-arm64": "4.54.0", + "@rollup/rollup-freebsd-x64": "4.54.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.54.0", + "@rollup/rollup-linux-arm-musleabihf": "4.54.0", + "@rollup/rollup-linux-arm64-gnu": "4.54.0", + "@rollup/rollup-linux-arm64-musl": "4.54.0", + "@rollup/rollup-linux-loong64-gnu": "4.54.0", + "@rollup/rollup-linux-ppc64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-gnu": "4.54.0", + "@rollup/rollup-linux-riscv64-musl": "4.54.0", + "@rollup/rollup-linux-s390x-gnu": "4.54.0", + "@rollup/rollup-linux-x64-gnu": "4.54.0", + "@rollup/rollup-linux-x64-musl": "4.54.0", + "@rollup/rollup-openharmony-arm64": "4.54.0", + "@rollup/rollup-win32-arm64-msvc": "4.54.0", + "@rollup/rollup-win32-ia32-msvc": "4.54.0", + "@rollup/rollup-win32-x64-gnu": "4.54.0", + "@rollup/rollup-win32-x64-msvc": "4.54.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "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" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "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" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vite-node/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "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, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "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/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/.codex/skills/dev-browser/package.json b/.codex/skills/dev-browser/package.json new file mode 100644 index 0000000..115869c --- /dev/null +++ b/.codex/skills/dev-browser/package.json @@ -0,0 +1,31 @@ +{ + "name": "dev-browser", + "version": "0.0.1", + "type": "module", + "imports": { + "@/*": "./src/*" + }, + "scripts": { + "start-server": "npx tsx scripts/start-server.ts", + "start-extension": "npx tsx scripts/start-relay.ts", + "dev": "npx tsx --watch src/index.ts", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@hono/node-server": "^1.19.7", + "@hono/node-ws": "^1.2.0", + "express": "^4.21.0", + "hono": "^4.11.1", + "playwright": "^1.49.0" + }, + "devDependencies": { + "@types/express": "^5.0.0", + "tsx": "^4.21.0", + "typescript": "^5.0.0", + "vitest": "^2.1.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "^4.0.0" + } +} diff --git a/.codex/skills/dev-browser/references/scraping.md b/.codex/skills/dev-browser/references/scraping.md new file mode 100644 index 0000000..a6e9b3c --- /dev/null +++ b/.codex/skills/dev-browser/references/scraping.md @@ -0,0 +1,155 @@ +# Data Scraping Guide + +For large datasets (followers, posts, search results), **intercept and replay network requests** rather than scrolling and parsing the DOM. This is faster, more reliable, and handles pagination automatically. + +## Why Not Scroll? + +Scrolling is slow, unreliable, and wastes time. APIs return structured data with pagination built in. Always prefer API replay. + +## Start Small, Then Scale + +**Don't try to automate everything at once.** Work incrementally: + +1. **Capture one request** - verify you're intercepting the right endpoint +2. **Inspect one response** - understand the schema before writing extraction code +3. **Extract a few items** - make sure your parsing logic works +4. **Then scale up** - add pagination loop only after the basics work + +This prevents wasting time debugging a complex script when the issue is a simple path like `data.user.timeline` vs `data.user.result.timeline`. + +## Step-by-Step Workflow + +### 1. Capture Request Details + +First, intercept a request to understand URL structure and required headers: + +```typescript +import { connect, waitForPageLoad } from "@/client.js"; +import * as fs from "node:fs"; + +const client = await connect(); +const page = await client.page("site"); + +let capturedRequest = null; +page.on("request", (request) => { + const url = request.url(); + // Look for API endpoints (adjust pattern for your target site) + if (url.includes("/api/") || url.includes("/graphql/")) { + capturedRequest = { + url: url, + headers: request.headers(), + method: request.method(), + }; + fs.writeFileSync("tmp/request-details.json", JSON.stringify(capturedRequest, null, 2)); + console.log("Captured request:", url.substring(0, 80) + "..."); + } +}); + +await page.goto("https://example.com/profile"); +await waitForPageLoad(page); +await page.waitForTimeout(3000); + +await client.disconnect(); +``` + +### 2. Capture Response to Understand Schema + +Save a raw response to inspect the data structure: + +```typescript +page.on("response", async (response) => { + const url = response.url(); + if (url.includes("UserTweets") || url.includes("/api/data")) { + const json = await response.json(); + fs.writeFileSync("tmp/api-response.json", JSON.stringify(json, null, 2)); + console.log("Captured response"); + } +}); +``` + +Then analyze the structure to find: + +- Where the data array lives (e.g., `data.user.result.timeline.instructions[].entries`) +- Where pagination cursors are (e.g., `cursor-bottom` entries) +- What fields you need to extract + +### 3. Replay API with Pagination + +Once you understand the schema, replay requests directly: + +```typescript +import { connect } from "@/client.js"; +import * as fs from "node:fs"; + +const client = await connect(); +const page = await client.page("site"); + +const results = new Map(); // Use Map for deduplication +const headers = JSON.parse(fs.readFileSync("tmp/request-details.json", "utf8")).headers; +const baseUrl = "https://example.com/api/data"; + +let cursor = null; +let hasMore = true; + +while (hasMore) { + // Build URL with pagination cursor + const params = { count: 20 }; + if (cursor) params.cursor = cursor; + const url = `${baseUrl}?params=${encodeURIComponent(JSON.stringify(params))}`; + + // Execute fetch in browser context (has auth cookies/headers) + const response = await page.evaluate( + async ({ url, headers }) => { + const res = await fetch(url, { headers }); + return res.json(); + }, + { url, headers } + ); + + // Extract data and cursor (adjust paths for your API) + const entries = response?.data?.entries || []; + for (const entry of entries) { + if (entry.type === "cursor-bottom") { + cursor = entry.value; + } else if (entry.id && !results.has(entry.id)) { + results.set(entry.id, { + id: entry.id, + text: entry.content, + timestamp: entry.created_at, + }); + } + } + + console.log(`Fetched page, total: ${results.size}`); + + // Check stop conditions + if (!cursor || entries.length === 0) hasMore = false; + + // Rate limiting - be respectful + await new Promise((r) => setTimeout(r, 500)); +} + +// Export results +const data = Array.from(results.values()); +fs.writeFileSync("tmp/results.json", JSON.stringify(data, null, 2)); +console.log(`Saved ${data.length} items`); + +await client.disconnect(); +``` + +## Key Patterns + +| Pattern | Description | +| ----------------------- | ------------------------------------------------------ | +| `page.on('request')` | Capture outgoing request URL + headers | +| `page.on('response')` | Capture response data to understand schema | +| `page.evaluate(fetch)` | Replay requests in browser context (inherits auth) | +| `Map` for deduplication | APIs often return overlapping data across pages | +| Cursor-based pagination | Look for `cursor`, `next_token`, `offset` in responses | + +## Tips + +- **Extension mode**: `page.context().cookies()` doesn't work - capture auth headers from intercepted requests instead +- **Rate limiting**: Add 500ms+ delays between requests to avoid blocks +- **Stop conditions**: Check for empty results, missing cursor, or reaching a date/ID threshold +- **GraphQL APIs**: URL params often include `variables` and `features` JSON objects - capture and reuse them diff --git a/.codex/skills/dev-browser/scripts/start-relay.ts b/.codex/skills/dev-browser/scripts/start-relay.ts new file mode 100644 index 0000000..0bc79e4 --- /dev/null +++ b/.codex/skills/dev-browser/scripts/start-relay.ts @@ -0,0 +1,32 @@ +/** + * Start the CDP relay server for Chrome extension mode + * + * Usage: npm run start-extension + */ + +import { serveRelay } from "@/relay.js"; + +const PORT = parseInt(process.env.PORT || "9222", 10); +const HOST = process.env.HOST || "127.0.0.1"; + +async function main() { + const server = await serveRelay({ + port: PORT, + host: HOST, + }); + + // Handle shutdown + const shutdown = async () => { + console.log("\nShutting down relay server..."); + await server.stop(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); +} + +main().catch((err) => { + console.error("Failed to start relay server:", err); + process.exit(1); +}); diff --git a/.codex/skills/dev-browser/scripts/start-server.ts b/.codex/skills/dev-browser/scripts/start-server.ts new file mode 100644 index 0000000..e130a27 --- /dev/null +++ b/.codex/skills/dev-browser/scripts/start-server.ts @@ -0,0 +1,117 @@ +import { serve } from "@/index.js"; +import { execSync } from "child_process"; +import { mkdirSync, existsSync, readdirSync } from "fs"; +import { join, dirname } from "path"; +import { fileURLToPath } from "url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const tmpDir = join(__dirname, "..", "tmp"); +const profileDir = join(__dirname, "..", "profiles"); + +// Create tmp and profile directories if they don't exist +console.log("Creating tmp directory..."); +mkdirSync(tmpDir, { recursive: true }); +console.log("Creating profiles directory..."); +mkdirSync(profileDir, { recursive: true }); + +// Install Playwright browsers if not already installed +console.log("Checking Playwright browser installation..."); + +function findPackageManager(): { name: string; command: string } | null { + const managers = [ + { name: "bun", command: "bunx playwright install chromium" }, + { name: "pnpm", command: "pnpm exec playwright install chromium" }, + { name: "npm", command: "npx playwright install chromium" }, + ]; + + for (const manager of managers) { + try { + execSync(`which ${manager.name}`, { stdio: "ignore" }); + return manager; + } catch { + // Package manager not found, try next + } + } + return null; +} + +function isChromiumInstalled(): boolean { + const homeDir = process.env.HOME || process.env.USERPROFILE || ""; + const playwrightCacheDir = join(homeDir, ".cache", "ms-playwright"); + + if (!existsSync(playwrightCacheDir)) { + return false; + } + + // Check for chromium directories (e.g., chromium-1148, chromium_headless_shell-1148) + try { + const entries = readdirSync(playwrightCacheDir); + return entries.some((entry) => entry.startsWith("chromium")); + } catch { + return false; + } +} + +try { + if (!isChromiumInstalled()) { + console.log("Playwright Chromium not found. Installing (this may take a minute)..."); + + const pm = findPackageManager(); + if (!pm) { + throw new Error("No package manager found (tried bun, pnpm, npm)"); + } + + console.log(`Using ${pm.name} to install Playwright...`); + execSync(pm.command, { stdio: "inherit" }); + console.log("Chromium installed successfully."); + } else { + console.log("Playwright Chromium already installed."); + } +} catch (error) { + console.error("Failed to install Playwright browsers:", error); + console.log("You may need to run: npx playwright install chromium"); +} + +// Check if server is already running +console.log("Checking for existing servers..."); +try { + const res = await fetch("http://localhost:9222", { + signal: AbortSignal.timeout(1000), + }); + if (res.ok) { + console.log("Server already running on port 9222"); + process.exit(0); + } +} catch { + // Server not running, continue to start +} + +// Clean up stale CDP port if HTTP server isn't running (crash recovery) +// This handles the case where Node crashed but Chrome is still running on 9223 +try { + const pid = execSync("lsof -ti:9223", { encoding: "utf-8" }).trim(); + if (pid) { + console.log(`Cleaning up stale Chrome process on CDP port 9223 (PID: ${pid})`); + execSync(`kill -9 ${pid}`); + } +} catch { + // No process on CDP port, which is expected +} + +console.log("Starting dev browser server..."); +const headless = process.env.HEADLESS === "true"; +const server = await serve({ + port: 9222, + headless, + profileDir, +}); + +console.log(`Dev browser server started`); +console.log(` WebSocket: ${server.wsEndpoint}`); +console.log(` Tmp directory: ${tmpDir}`); +console.log(` Profile directory: ${profileDir}`); +console.log(`\nReady`); +console.log(`\nPress Ctrl+C to stop`); + +// Keep the process running +await new Promise(() => {}); diff --git a/.codex/skills/dev-browser/server.sh b/.codex/skills/dev-browser/server.sh new file mode 100755 index 0000000..50369a4 --- /dev/null +++ b/.codex/skills/dev-browser/server.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Get the directory where this script is located +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +# Change to the script directory +cd "$SCRIPT_DIR" + +# Parse command line arguments +HEADLESS=false +while [[ "$#" -gt 0 ]]; do + case $1 in + --headless) HEADLESS=true ;; + *) echo "Unknown parameter: $1"; exit 1 ;; + esac + shift +done + +echo "Installing dependencies..." +npm install + +echo "Starting dev-browser server..." +export HEADLESS=$HEADLESS +npx tsx scripts/start-server.ts diff --git a/.codex/skills/dev-browser/src/client.ts b/.codex/skills/dev-browser/src/client.ts new file mode 100644 index 0000000..bfd0b56 --- /dev/null +++ b/.codex/skills/dev-browser/src/client.ts @@ -0,0 +1,474 @@ +import { chromium, type Browser, type Page, type ElementHandle } from "playwright"; +import type { + GetPageRequest, + GetPageResponse, + ListPagesResponse, + ServerInfoResponse, + ViewportSize, +} from "./types"; +import { getSnapshotScript } from "./snapshot/browser-script"; + +/** + * Options for waiting for page load + */ +export interface WaitForPageLoadOptions { + /** Maximum time to wait in ms (default: 10000) */ + timeout?: number; + /** How often to check page state in ms (default: 50) */ + pollInterval?: number; + /** Minimum time to wait even if page appears ready in ms (default: 100) */ + minimumWait?: number; + /** Wait for network to be idle (no pending requests) (default: true) */ + waitForNetworkIdle?: boolean; +} + +/** + * Result of waiting for page load + */ +export interface WaitForPageLoadResult { + /** Whether the page is considered loaded */ + success: boolean; + /** Document ready state when finished */ + readyState: string; + /** Number of pending network requests when finished */ + pendingRequests: number; + /** Time spent waiting in ms */ + waitTimeMs: number; + /** Whether timeout was reached */ + timedOut: boolean; +} + +interface PageLoadState { + documentReadyState: string; + documentLoading: boolean; + pendingRequests: PendingRequest[]; +} + +interface PendingRequest { + url: string; + loadingDurationMs: number; + resourceType: string; +} + +/** + * Wait for a page to finish loading using document.readyState and performance API. + * + * Uses browser-use's approach of: + * - Checking document.readyState for 'complete' + * - Monitoring pending network requests via Performance API + * - Filtering out ads, tracking, and non-critical resources + * - Graceful timeout handling (continues even if timeout reached) + */ +export async function waitForPageLoad( + page: Page, + options: WaitForPageLoadOptions = {} +): Promise { + const { + timeout = 10000, + pollInterval = 50, + minimumWait = 100, + waitForNetworkIdle = true, + } = options; + + const startTime = Date.now(); + let lastState: PageLoadState | null = null; + + // Wait minimum time first + if (minimumWait > 0) { + await new Promise((resolve) => setTimeout(resolve, minimumWait)); + } + + // Poll until ready or timeout + while (Date.now() - startTime < timeout) { + try { + lastState = await getPageLoadState(page); + + // Check if document is complete + const documentReady = lastState.documentReadyState === "complete"; + + // Check if network is idle (no pending critical requests) + const networkIdle = !waitForNetworkIdle || lastState.pendingRequests.length === 0; + + if (documentReady && networkIdle) { + return { + success: true, + readyState: lastState.documentReadyState, + pendingRequests: lastState.pendingRequests.length, + waitTimeMs: Date.now() - startTime, + timedOut: false, + }; + } + } catch { + // Page may be navigating, continue polling + } + + await new Promise((resolve) => setTimeout(resolve, pollInterval)); + } + + // Timeout reached - return current state + return { + success: false, + readyState: lastState?.documentReadyState ?? "unknown", + pendingRequests: lastState?.pendingRequests.length ?? 0, + waitTimeMs: Date.now() - startTime, + timedOut: true, + }; +} + +/** + * Get the current page load state including document ready state and pending requests. + * Filters out ads, tracking, and non-critical resources that shouldn't block loading. + */ +async function getPageLoadState(page: Page): Promise { + const result = await page.evaluate(() => { + // Access browser globals via globalThis for TypeScript compatibility + /* eslint-disable @typescript-eslint/no-explicit-any */ + const g = globalThis as { document?: any; performance?: any }; + /* eslint-enable @typescript-eslint/no-explicit-any */ + const perf = g.performance!; + const doc = g.document!; + + const now = perf.now(); + const resources = perf.getEntriesByType("resource"); + const pending: Array<{ url: string; loadingDurationMs: number; resourceType: string }> = []; + + // Common ad/tracking domains and patterns to filter out + const adPatterns = [ + "doubleclick.net", + "googlesyndication.com", + "googletagmanager.com", + "google-analytics.com", + "facebook.net", + "connect.facebook.net", + "analytics", + "ads", + "tracking", + "pixel", + "hotjar.com", + "clarity.ms", + "mixpanel.com", + "segment.com", + "newrelic.com", + "nr-data.net", + "/tracker/", + "/collector/", + "/beacon/", + "/telemetry/", + "/log/", + "/events/", + "/track.", + "/metrics/", + ]; + + // Non-critical resource types + const nonCriticalTypes = ["img", "image", "icon", "font"]; + + for (const entry of resources) { + // Resources with responseEnd === 0 are still loading + if (entry.responseEnd === 0) { + const url = entry.name; + + // Filter out ads and tracking + const isAd = adPatterns.some((pattern) => url.includes(pattern)); + if (isAd) continue; + + // Filter out data: URLs and very long URLs + if (url.startsWith("data:") || url.length > 500) continue; + + const loadingDuration = now - entry.startTime; + + // Skip requests loading > 10 seconds (likely stuck/polling) + if (loadingDuration > 10000) continue; + + const resourceType = entry.initiatorType || "unknown"; + + // Filter out non-critical resources loading > 3 seconds + if (nonCriticalTypes.includes(resourceType) && loadingDuration > 3000) continue; + + // Filter out image URLs even if type is unknown + const isImageUrl = /\.(jpg|jpeg|png|gif|webp|svg|ico)(\?|$)/i.test(url); + if (isImageUrl && loadingDuration > 3000) continue; + + pending.push({ + url, + loadingDurationMs: Math.round(loadingDuration), + resourceType, + }); + } + } + + return { + documentReadyState: doc.readyState, + documentLoading: doc.readyState !== "complete", + pendingRequests: pending, + }; + }); + + return result; +} + +/** Server mode information */ +export interface ServerInfo { + wsEndpoint: string; + mode: "launch" | "extension"; + extensionConnected?: boolean; +} + +/** + * Options for creating or getting a page + */ +export interface PageOptions { + /** Viewport size for new pages */ + viewport?: ViewportSize; +} + +export interface DevBrowserClient { + page: (name: string, options?: PageOptions) => Promise; + list: () => Promise; + close: (name: string) => Promise; + disconnect: () => Promise; + /** + * Get AI-friendly ARIA snapshot for a page. + * Returns YAML format with refs like [ref=e1], [ref=e2]. + * Refs are stored on window.__devBrowserRefs for cross-connection persistence. + */ + getAISnapshot: (name: string) => Promise; + /** + * Get an element handle by its ref from the last getAISnapshot call. + * Refs persist across Playwright connections. + */ + selectSnapshotRef: (name: string, ref: string) => Promise; + /** + * Get server information including mode and extension connection status. + */ + getServerInfo: () => Promise; +} + +export async function connect(serverUrl = "http://localhost:9222"): Promise { + let browser: Browser | null = null; + let wsEndpoint: string | null = null; + let connectingPromise: Promise | null = null; + + async function ensureConnected(): Promise { + // Return existing connection if still active + if (browser && browser.isConnected()) { + return browser; + } + + // If already connecting, wait for that connection (prevents race condition) + if (connectingPromise) { + return connectingPromise; + } + + // Start new connection with mutex + connectingPromise = (async () => { + try { + // Fetch wsEndpoint from server + const res = await fetch(serverUrl); + if (!res.ok) { + throw new Error(`Server returned ${res.status}: ${await res.text()}`); + } + const info = (await res.json()) as ServerInfoResponse; + wsEndpoint = info.wsEndpoint; + + // Connect to the browser via CDP + browser = await chromium.connectOverCDP(wsEndpoint); + return browser; + } finally { + connectingPromise = null; + } + })(); + + return connectingPromise; + } + + // Find page by CDP targetId - more reliable than JS globals + async function findPageByTargetId(b: Browser, targetId: string): Promise { + for (const context of b.contexts()) { + for (const page of context.pages()) { + let cdpSession; + try { + cdpSession = await context.newCDPSession(page); + const { targetInfo } = await cdpSession.send("Target.getTargetInfo"); + if (targetInfo.targetId === targetId) { + return page; + } + } catch (err) { + // Only ignore "target closed" errors, log unexpected ones + const msg = err instanceof Error ? err.message : String(err); + if (!msg.includes("Target closed") && !msg.includes("Session closed")) { + console.warn(`Unexpected error checking page target: ${msg}`); + } + } finally { + if (cdpSession) { + try { + await cdpSession.detach(); + } catch { + // Ignore detach errors - session may already be closed + } + } + } + } + } + return null; + } + + // Helper to get a page by name (used by multiple methods) + async function getPage(name: string, options?: PageOptions): Promise { + // Request the page from server (creates if doesn't exist) + const res = await fetch(`${serverUrl}/pages`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, viewport: options?.viewport } satisfies GetPageRequest), + }); + + if (!res.ok) { + throw new Error(`Failed to get page: ${await res.text()}`); + } + + const pageInfo = (await res.json()) as GetPageResponse & { url?: string }; + const { targetId } = pageInfo; + + // Connect to browser + const b = await ensureConnected(); + + // Check if we're in extension mode + const infoRes = await fetch(serverUrl); + const info = (await infoRes.json()) as { mode?: string }; + const isExtensionMode = info.mode === "extension"; + + if (isExtensionMode) { + // In extension mode, DON'T use findPageByTargetId as it corrupts page state + // Instead, find page by URL or use the only available page + const allPages = b.contexts().flatMap((ctx) => ctx.pages()); + + if (allPages.length === 0) { + throw new Error(`No pages available in browser`); + } + + if (allPages.length === 1) { + return allPages[0]!; + } + + // Multiple pages - try to match by URL if available + if (pageInfo.url) { + const matchingPage = allPages.find((p) => p.url() === pageInfo.url); + if (matchingPage) { + return matchingPage; + } + } + + // Fall back to first page + if (!allPages[0]) { + throw new Error(`No pages available in browser`); + } + return allPages[0]; + } + + // In launch mode, use the original targetId-based lookup + const page = await findPageByTargetId(b, targetId); + if (!page) { + throw new Error(`Page "${name}" not found in browser contexts`); + } + + return page; + } + + return { + page: getPage, + + async list(): Promise { + const res = await fetch(`${serverUrl}/pages`); + const data = (await res.json()) as ListPagesResponse; + return data.pages; + }, + + async close(name: string): Promise { + const res = await fetch(`${serverUrl}/pages/${encodeURIComponent(name)}`, { + method: "DELETE", + }); + + if (!res.ok) { + throw new Error(`Failed to close page: ${await res.text()}`); + } + }, + + async disconnect(): Promise { + // Just disconnect the CDP connection - pages persist on server + if (browser) { + await browser.close(); + browser = null; + } + }, + + async getAISnapshot(name: string): Promise { + // Get the page + const page = await getPage(name); + + // Inject the snapshot script and call getAISnapshot + const snapshotScript = getSnapshotScript(); + const snapshot = await page.evaluate((script: string) => { + // Inject script if not already present + // Note: page.evaluate runs in browser context where window exists + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const w = globalThis as any; + if (!w.__devBrowser_getAISnapshot) { + // eslint-disable-next-line no-eval + eval(script); + } + return w.__devBrowser_getAISnapshot(); + }, snapshotScript); + + return snapshot; + }, + + async selectSnapshotRef(name: string, ref: string): Promise { + // Get the page + const page = await getPage(name); + + // Find the element using the stored refs + const elementHandle = await page.evaluateHandle((refId: string) => { + // Note: page.evaluateHandle runs in browser context where globalThis is the window + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const w = globalThis as any; + const refs = w.__devBrowserRefs; + if (!refs) { + throw new Error("No snapshot refs found. Call getAISnapshot first."); + } + const element = refs[refId]; + if (!element) { + throw new Error( + `Ref "${refId}" not found. Available refs: ${Object.keys(refs).join(", ")}` + ); + } + return element; + }, ref); + + // Check if we got an element + const element = elementHandle.asElement(); + if (!element) { + await elementHandle.dispose(); + return null; + } + + return element; + }, + + async getServerInfo(): Promise { + const res = await fetch(serverUrl); + if (!res.ok) { + throw new Error(`Server returned ${res.status}: ${await res.text()}`); + } + const info = (await res.json()) as { + wsEndpoint: string; + mode?: string; + extensionConnected?: boolean; + }; + return { + wsEndpoint: info.wsEndpoint, + mode: (info.mode as "launch" | "extension") ?? "launch", + extensionConnected: info.extensionConnected, + }; + }, + }; +} diff --git a/.codex/skills/dev-browser/src/index.ts b/.codex/skills/dev-browser/src/index.ts new file mode 100644 index 0000000..22fc2e4 --- /dev/null +++ b/.codex/skills/dev-browser/src/index.ts @@ -0,0 +1,287 @@ +import express, { type Express, type Request, type Response } from "express"; +import { chromium, type BrowserContext, type Page } from "playwright"; +import { mkdirSync } from "fs"; +import { join } from "path"; +import type { Socket } from "net"; +import type { + ServeOptions, + GetPageRequest, + GetPageResponse, + ListPagesResponse, + ServerInfoResponse, +} from "./types"; + +export type { ServeOptions, GetPageResponse, ListPagesResponse, ServerInfoResponse }; + +export interface DevBrowserServer { + wsEndpoint: string; + port: number; + stop: () => Promise; +} + +// Helper to retry fetch with exponential backoff +async function fetchWithRetry( + url: string, + maxRetries = 5, + delayMs = 500 +): Promise { + let lastError: Error | null = null; + for (let i = 0; i < maxRetries; i++) { + try { + const res = await fetch(url); + if (res.ok) return res; + throw new Error(`HTTP ${res.status}: ${res.statusText}`); + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)); + if (i < maxRetries - 1) { + await new Promise((resolve) => setTimeout(resolve, delayMs * (i + 1))); + } + } + } + throw new Error(`Failed after ${maxRetries} retries: ${lastError?.message}`); +} + +// Helper to add timeout to promises +function withTimeout(promise: Promise, ms: number, message: string): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => + setTimeout(() => reject(new Error(`Timeout: ${message}`)), ms) + ), + ]); +} + +export async function serve(options: ServeOptions = {}): Promise { + const port = options.port ?? 9222; + const headless = options.headless ?? false; + const cdpPort = options.cdpPort ?? 9223; + const profileDir = options.profileDir; + + // Validate port numbers + if (port < 1 || port > 65535) { + throw new Error(`Invalid port: ${port}. Must be between 1 and 65535`); + } + if (cdpPort < 1 || cdpPort > 65535) { + throw new Error(`Invalid cdpPort: ${cdpPort}. Must be between 1 and 65535`); + } + if (port === cdpPort) { + throw new Error("port and cdpPort must be different"); + } + + // Determine user data directory for persistent context + const userDataDir = profileDir + ? join(profileDir, "browser-data") + : join(process.cwd(), ".browser-data"); + + // Create directory if it doesn't exist + mkdirSync(userDataDir, { recursive: true }); + console.log(`Using persistent browser profile: ${userDataDir}`); + + console.log("Launching browser with persistent context..."); + + // Launch persistent context - this persists cookies, localStorage, cache, etc. + const context: BrowserContext = await chromium.launchPersistentContext(userDataDir, { + headless, + args: [`--remote-debugging-port=${cdpPort}`], + }); + console.log("Browser launched with persistent profile..."); + + // Get the CDP WebSocket endpoint from Chrome's JSON API (with retry for slow startup) + const cdpResponse = await fetchWithRetry(`http://127.0.0.1:${cdpPort}/json/version`); + const cdpInfo = (await cdpResponse.json()) as { webSocketDebuggerUrl: string }; + const wsEndpoint = cdpInfo.webSocketDebuggerUrl; + console.log(`CDP WebSocket endpoint: ${wsEndpoint}`); + + // Registry entry type for page tracking + interface PageEntry { + page: Page; + targetId: string; + } + + // Registry: name -> PageEntry + const registry = new Map(); + + // Helper to get CDP targetId for a page + async function getTargetId(page: Page): Promise { + const cdpSession = await context.newCDPSession(page); + try { + const { targetInfo } = await cdpSession.send("Target.getTargetInfo"); + return targetInfo.targetId; + } finally { + await cdpSession.detach(); + } + } + + // Express server for page management + const app: Express = express(); + app.use(express.json()); + + // GET / - server info + app.get("/", (_req: Request, res: Response) => { + const response: ServerInfoResponse = { wsEndpoint }; + res.json(response); + }); + + // GET /pages - list all pages + app.get("/pages", (_req: Request, res: Response) => { + const response: ListPagesResponse = { + pages: Array.from(registry.keys()), + }; + res.json(response); + }); + + // POST /pages - get or create page + app.post("/pages", async (req: Request, res: Response) => { + const body = req.body as GetPageRequest; + const { name, viewport } = body; + + if (!name || typeof name !== "string") { + res.status(400).json({ error: "name is required and must be a string" }); + return; + } + + if (name.length === 0) { + res.status(400).json({ error: "name cannot be empty" }); + return; + } + + if (name.length > 256) { + res.status(400).json({ error: "name must be 256 characters or less" }); + return; + } + + // Check if page already exists + let entry = registry.get(name); + if (!entry) { + // Create new page in the persistent context (with timeout to prevent hangs) + const page = await withTimeout(context.newPage(), 30000, "Page creation timed out after 30s"); + + // Apply viewport if provided + if (viewport) { + await page.setViewportSize(viewport); + } + + const targetId = await getTargetId(page); + entry = { page, targetId }; + registry.set(name, entry); + + // Clean up registry when page is closed (e.g., user clicks X) + page.on("close", () => { + registry.delete(name); + }); + } + + const response: GetPageResponse = { wsEndpoint, name, targetId: entry.targetId }; + res.json(response); + }); + + // DELETE /pages/:name - close a page + app.delete("/pages/:name", async (req: Request<{ name: string }>, res: Response) => { + const name = decodeURIComponent(req.params.name); + const entry = registry.get(name); + + if (entry) { + await entry.page.close(); + registry.delete(name); + res.json({ success: true }); + return; + } + + res.status(404).json({ error: "page not found" }); + }); + + // Start the server + const server = app.listen(port, () => { + console.log(`HTTP API server running on port ${port}`); + }); + + // Track active connections for clean shutdown + const connections = new Set(); + server.on("connection", (socket: Socket) => { + connections.add(socket); + socket.on("close", () => connections.delete(socket)); + }); + + // Track if cleanup has been called to avoid double cleanup + let cleaningUp = false; + + // Cleanup function + const cleanup = async () => { + if (cleaningUp) return; + cleaningUp = true; + + console.log("\nShutting down..."); + + // Close all active HTTP connections + for (const socket of connections) { + socket.destroy(); + } + connections.clear(); + + // Close all pages + for (const entry of registry.values()) { + try { + await entry.page.close(); + } catch { + // Page might already be closed + } + } + registry.clear(); + + // Close context (this also closes the browser) + try { + await context.close(); + } catch { + // Context might already be closed + } + + server.close(); + console.log("Server stopped."); + }; + + // Synchronous cleanup for forced exits + const syncCleanup = () => { + try { + context.close(); + } catch { + // Best effort + } + }; + + // Signal handlers (consolidated to reduce duplication) + const signals = ["SIGINT", "SIGTERM", "SIGHUP"] as const; + + const signalHandler = async () => { + await cleanup(); + process.exit(0); + }; + + const errorHandler = async (err: unknown) => { + console.error("Unhandled error:", err); + await cleanup(); + process.exit(1); + }; + + // Register handlers + signals.forEach((sig) => process.on(sig, signalHandler)); + process.on("uncaughtException", errorHandler); + process.on("unhandledRejection", errorHandler); + process.on("exit", syncCleanup); + + // Helper to remove all handlers + const removeHandlers = () => { + signals.forEach((sig) => process.off(sig, signalHandler)); + process.off("uncaughtException", errorHandler); + process.off("unhandledRejection", errorHandler); + process.off("exit", syncCleanup); + }; + + return { + wsEndpoint, + port, + async stop() { + removeHandlers(); + await cleanup(); + }, + }; +} diff --git a/.codex/skills/dev-browser/src/relay.ts b/.codex/skills/dev-browser/src/relay.ts new file mode 100644 index 0000000..210f70d --- /dev/null +++ b/.codex/skills/dev-browser/src/relay.ts @@ -0,0 +1,731 @@ +/** + * CDP Relay Server for Chrome Extension mode + * + * This server acts as a bridge between Playwright clients and a Chrome extension. + * Instead of launching a browser, it waits for the extension to connect and + * forwards CDP commands/events between them. + */ + +import { Hono } from "hono"; +import { serve } from "@hono/node-server"; +import { createNodeWebSocket } from "@hono/node-ws"; +import type { WSContext } from "hono/ws"; + +// ============================================================================ +// Types +// ============================================================================ + +export interface RelayOptions { + port?: number; + host?: string; +} + +export interface RelayServer { + wsEndpoint: string; + port: number; + stop(): Promise; +} + +interface TargetInfo { + targetId: string; + type: string; + title: string; + url: string; + attached: boolean; +} + +interface ConnectedTarget { + sessionId: string; + targetId: string; + targetInfo: TargetInfo; +} + +interface PlaywrightClient { + id: string; + ws: WSContext; + knownTargets: Set; // targetIds this client has received attachedToTarget for +} + +// Message types for extension communication +interface ExtensionCommandMessage { + id: number; + method: "forwardCDPCommand"; + params: { + method: string; + params?: Record; + sessionId?: string; + }; +} + +interface ExtensionResponseMessage { + id: number; + result?: unknown; + error?: string; +} + +interface ExtensionEventMessage { + method: "forwardCDPEvent"; + params: { + method: string; + params?: Record; + sessionId?: string; + }; +} + +type ExtensionMessage = + | ExtensionResponseMessage + | ExtensionEventMessage + | { method: "log"; params: { level: string; args: string[] } }; + +// CDP message types +interface CDPCommand { + id: number; + method: string; + params?: Record; + sessionId?: string; +} + +interface CDPResponse { + id: number; + sessionId?: string; + result?: unknown; + error?: { message: string }; +} + +interface CDPEvent { + method: string; + sessionId?: string; + params?: Record; +} + +// ============================================================================ +// Relay Server Implementation +// ============================================================================ + +export async function serveRelay(options: RelayOptions = {}): Promise { + const port = options.port ?? 9222; + const host = options.host ?? "127.0.0.1"; + + // State + const connectedTargets = new Map(); + const namedPages = new Map(); // name -> sessionId + const playwrightClients = new Map(); + let extensionWs: WSContext | null = null; + + // Pending requests to extension + const extensionPendingRequests = new Map< + number, + { + resolve: (result: unknown) => void; + reject: (error: Error) => void; + } + >(); + let extensionMessageId = 0; + + // ============================================================================ + // Helper Functions + // ============================================================================ + + function log(...args: unknown[]) { + console.log("[relay]", ...args); + } + + function sendToPlaywright(message: CDPResponse | CDPEvent, clientId?: string) { + const messageStr = JSON.stringify(message); + + if (clientId) { + const client = playwrightClients.get(clientId); + if (client) { + client.ws.send(messageStr); + } + } else { + // Broadcast to all clients + for (const client of playwrightClients.values()) { + client.ws.send(messageStr); + } + } + } + + /** + * Send Target.attachedToTarget event with deduplication. + * Tracks which targets each client has seen to prevent "Duplicate target" errors. + */ + function sendAttachedToTarget( + target: ConnectedTarget, + clientId?: string, + waitingForDebugger = false + ) { + const event: CDPEvent = { + method: "Target.attachedToTarget", + params: { + sessionId: target.sessionId, + targetInfo: { ...target.targetInfo, attached: true }, + waitingForDebugger, + }, + }; + + if (clientId) { + const client = playwrightClients.get(clientId); + if (client && !client.knownTargets.has(target.targetId)) { + client.knownTargets.add(target.targetId); + client.ws.send(JSON.stringify(event)); + } + } else { + // Broadcast to all clients that don't know about this target yet + for (const client of playwrightClients.values()) { + if (!client.knownTargets.has(target.targetId)) { + client.knownTargets.add(target.targetId); + client.ws.send(JSON.stringify(event)); + } + } + } + } + + async function sendToExtension({ + method, + params, + timeout = 30000, + }: { + method: string; + params?: Record; + timeout?: number; + }): Promise { + if (!extensionWs) { + throw new Error("Extension not connected"); + } + + const id = ++extensionMessageId; + const message = { id, method, params }; + + extensionWs.send(JSON.stringify(message)); + + return new Promise((resolve, reject) => { + const timeoutId = setTimeout(() => { + extensionPendingRequests.delete(id); + reject(new Error(`Extension request timeout after ${timeout}ms: ${method}`)); + }, timeout); + + extensionPendingRequests.set(id, { + resolve: (result) => { + clearTimeout(timeoutId); + resolve(result); + }, + reject: (error) => { + clearTimeout(timeoutId); + reject(error); + }, + }); + }); + } + + async function routeCdpCommand({ + method, + params, + sessionId, + }: { + method: string; + params?: Record; + sessionId?: string; + }): Promise { + // Handle some CDP commands locally + switch (method) { + case "Browser.getVersion": + return { + protocolVersion: "1.3", + product: "Chrome/Extension-Bridge", + revision: "1.0.0", + userAgent: "dev-browser-relay/1.0.0", + jsVersion: "V8", + }; + + case "Browser.setDownloadBehavior": + return {}; + + case "Target.setAutoAttach": + if (sessionId) { + break; // Forward to extension for child frames + } + return {}; + + case "Target.setDiscoverTargets": + return {}; + + case "Target.attachToBrowserTarget": + // Browser-level session - return a fake session since we only proxy tabs + return { sessionId: "browser" }; + + case "Target.detachFromTarget": + // If detaching from our fake "browser" session, just return success + if (sessionId === "browser" || params?.sessionId === "browser") { + return {}; + } + // Otherwise forward to extension + break; + + case "Target.attachToTarget": { + const targetId = params?.targetId as string; + if (!targetId) { + throw new Error("targetId is required for Target.attachToTarget"); + } + + for (const target of connectedTargets.values()) { + if (target.targetId === targetId) { + return { sessionId: target.sessionId }; + } + } + + throw new Error(`Target ${targetId} not found in connected targets`); + } + + case "Target.getTargetInfo": { + const targetId = params?.targetId as string; + + if (targetId) { + for (const target of connectedTargets.values()) { + if (target.targetId === targetId) { + return { targetInfo: target.targetInfo }; + } + } + } + + if (sessionId) { + const target = connectedTargets.get(sessionId); + if (target) { + return { targetInfo: target.targetInfo }; + } + } + + // Return first target if no specific one requested + const firstTarget = Array.from(connectedTargets.values())[0]; + return { targetInfo: firstTarget?.targetInfo }; + } + + case "Target.getTargets": + return { + targetInfos: Array.from(connectedTargets.values()).map((t) => ({ + ...t.targetInfo, + attached: true, + })), + }; + + case "Target.createTarget": + case "Target.closeTarget": + // Forward to extension + return await sendToExtension({ + method: "forwardCDPCommand", + params: { method, params }, + }); + } + + // Forward all other commands to extension + return await sendToExtension({ + method: "forwardCDPCommand", + params: { sessionId, method, params }, + }); + } + + // ============================================================================ + // HTTP/WebSocket Server + // ============================================================================ + + const app = new Hono(); + const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app }); + + // Health check / server info + app.get("/", (c) => { + return c.json({ + wsEndpoint: `ws://${host}:${port}/cdp`, + extensionConnected: extensionWs !== null, + mode: "extension", + }); + }); + + // List named pages + app.get("/pages", (c) => { + return c.json({ + pages: Array.from(namedPages.keys()), + }); + }); + + // Get or create a named page + app.post("/pages", async (c) => { + const body = await c.req.json(); + const name = body.name as string; + + if (!name) { + return c.json({ error: "name is required" }, 400); + } + + // Check if page already exists by name + const existingSessionId = namedPages.get(name); + if (existingSessionId) { + const target = connectedTargets.get(existingSessionId); + if (target) { + // Activate the tab so it becomes the active tab + await sendToExtension({ + method: "forwardCDPCommand", + params: { + method: "Target.activateTarget", + params: { targetId: target.targetId }, + }, + }); + return c.json({ + wsEndpoint: `ws://${host}:${port}/cdp`, + name, + targetId: target.targetId, + url: target.targetInfo.url, + }); + } + // Session no longer valid, remove it + namedPages.delete(name); + } + + // Create a new tab + if (!extensionWs) { + return c.json({ error: "Extension not connected" }, 503); + } + + try { + const result = (await sendToExtension({ + method: "forwardCDPCommand", + params: { method: "Target.createTarget", params: { url: "about:blank" } }, + })) as { targetId: string }; + + // Wait for Target.attachedToTarget event to register the new target + await new Promise((resolve) => setTimeout(resolve, 200)); + + // Find and name the new target + for (const [sessionId, target] of connectedTargets) { + if (target.targetId === result.targetId) { + namedPages.set(name, sessionId); + // Activate the tab so it becomes the active tab + await sendToExtension({ + method: "forwardCDPCommand", + params: { + method: "Target.activateTarget", + params: { targetId: target.targetId }, + }, + }); + return c.json({ + wsEndpoint: `ws://${host}:${port}/cdp`, + name, + targetId: target.targetId, + url: target.targetInfo.url, + }); + } + } + + throw new Error("Target created but not found in registry"); + } catch (err) { + log("Error creating tab:", err); + return c.json({ error: (err as Error).message }, 500); + } + }); + + // Delete a named page (removes the name, doesn't close the tab) + app.delete("/pages/:name", (c) => { + const name = c.req.param("name"); + const deleted = namedPages.delete(name); + return c.json({ success: deleted }); + }); + + // ============================================================================ + // Playwright Client WebSocket + // ============================================================================ + + app.get( + "/cdp/:clientId?", + upgradeWebSocket((c) => { + const clientId = + c.req.param("clientId") || `client-${Date.now()}-${Math.random().toString(36).slice(2)}`; + + return { + onOpen(_event, ws) { + if (playwrightClients.has(clientId)) { + log(`Rejecting duplicate client ID: ${clientId}`); + ws.close(1000, "Client ID already connected"); + return; + } + + playwrightClients.set(clientId, { id: clientId, ws, knownTargets: new Set() }); + log(`Playwright client connected: ${clientId}`); + }, + + async onMessage(event, _ws) { + let message: CDPCommand; + + try { + message = JSON.parse(event.data.toString()); + } catch { + return; + } + + const { id, sessionId, method, params } = message; + + if (!extensionWs) { + sendToPlaywright( + { + id, + sessionId, + error: { message: "Extension not connected" }, + }, + clientId + ); + return; + } + + try { + const result = await routeCdpCommand({ method, params, sessionId }); + + // After Target.setAutoAttach, send attachedToTarget for existing targets + // Uses deduplication to prevent "Duplicate target" errors + if (method === "Target.setAutoAttach" && !sessionId) { + for (const target of connectedTargets.values()) { + sendAttachedToTarget(target, clientId); + } + } + + // After Target.setDiscoverTargets, send targetCreated events + if ( + method === "Target.setDiscoverTargets" && + (params as { discover?: boolean })?.discover + ) { + for (const target of connectedTargets.values()) { + sendToPlaywright( + { + method: "Target.targetCreated", + params: { + targetInfo: { ...target.targetInfo, attached: true }, + }, + }, + clientId + ); + } + } + + // After Target.attachToTarget, send attachedToTarget event (with deduplication) + if ( + method === "Target.attachToTarget" && + (result as { sessionId?: string })?.sessionId + ) { + const targetId = params?.targetId as string; + const target = Array.from(connectedTargets.values()).find( + (t) => t.targetId === targetId + ); + if (target) { + sendAttachedToTarget(target, clientId); + } + } + + sendToPlaywright({ id, sessionId, result }, clientId); + } catch (e) { + log("Error handling CDP command:", method, e); + sendToPlaywright( + { + id, + sessionId, + error: { message: (e as Error).message }, + }, + clientId + ); + } + }, + + onClose() { + playwrightClients.delete(clientId); + log(`Playwright client disconnected: ${clientId}`); + }, + + onError(event) { + log(`Playwright WebSocket error [${clientId}]:`, event); + }, + }; + }) + ); + + // ============================================================================ + // Extension WebSocket + // ============================================================================ + + app.get( + "/extension", + upgradeWebSocket(() => { + return { + onOpen(_event, ws) { + if (extensionWs) { + log("Closing existing extension connection"); + extensionWs.close(4001, "Extension Replaced"); + + // Clear state + connectedTargets.clear(); + namedPages.clear(); + for (const pending of extensionPendingRequests.values()) { + pending.reject(new Error("Extension connection replaced")); + } + extensionPendingRequests.clear(); + } + + extensionWs = ws; + log("Extension connected"); + }, + + async onMessage(event, ws) { + let message: ExtensionMessage; + + try { + message = JSON.parse(event.data.toString()); + } catch { + ws.close(1000, "Invalid JSON"); + return; + } + + // Handle response to our request + if ("id" in message && typeof message.id === "number") { + const pending = extensionPendingRequests.get(message.id); + if (!pending) { + log("Unexpected response with id:", message.id); + return; + } + + extensionPendingRequests.delete(message.id); + + if ((message as ExtensionResponseMessage).error) { + pending.reject(new Error((message as ExtensionResponseMessage).error)); + } else { + pending.resolve((message as ExtensionResponseMessage).result); + } + return; + } + + // Handle log messages + if ("method" in message && message.method === "log") { + const { level, args } = message.params; + console.log(`[extension:${level}]`, ...args); + return; + } + + // Handle CDP events from extension + if ("method" in message && message.method === "forwardCDPEvent") { + const eventMsg = message as ExtensionEventMessage; + const { method, params, sessionId } = eventMsg.params; + + // Handle target lifecycle events + if (method === "Target.attachedToTarget") { + const targetParams = params as { + sessionId: string; + targetInfo: TargetInfo; + }; + + const target: ConnectedTarget = { + sessionId: targetParams.sessionId, + targetId: targetParams.targetInfo.targetId, + targetInfo: targetParams.targetInfo, + }; + connectedTargets.set(targetParams.sessionId, target); + + log(`Target attached: ${targetParams.targetInfo.url} (${targetParams.sessionId})`); + + // Use deduplication helper - only sends to clients that don't know about this target + sendAttachedToTarget(target); + } else if (method === "Target.detachedFromTarget") { + const detachParams = params as { sessionId: string }; + connectedTargets.delete(detachParams.sessionId); + + // Also remove any name mapping + for (const [name, sid] of namedPages) { + if (sid === detachParams.sessionId) { + namedPages.delete(name); + break; + } + } + + log(`Target detached: ${detachParams.sessionId}`); + + sendToPlaywright({ + method: "Target.detachedFromTarget", + params: detachParams, + }); + } else if (method === "Target.targetInfoChanged") { + const infoParams = params as { targetInfo: TargetInfo }; + for (const target of connectedTargets.values()) { + if (target.targetId === infoParams.targetInfo.targetId) { + target.targetInfo = infoParams.targetInfo; + break; + } + } + + sendToPlaywright({ + method: "Target.targetInfoChanged", + params: infoParams, + }); + } else { + // Forward other CDP events to Playwright + sendToPlaywright({ + sessionId, + method, + params, + }); + } + } + }, + + onClose(_event, ws) { + if (extensionWs && extensionWs !== ws) { + log("Old extension connection closed"); + return; + } + + log("Extension disconnected"); + + for (const pending of extensionPendingRequests.values()) { + pending.reject(new Error("Extension connection closed")); + } + extensionPendingRequests.clear(); + + extensionWs = null; + connectedTargets.clear(); + namedPages.clear(); + + // Close all Playwright clients + for (const client of playwrightClients.values()) { + client.ws.close(1000, "Extension disconnected"); + } + playwrightClients.clear(); + }, + + onError(event) { + log("Extension WebSocket error:", event); + }, + }; + }) + ); + + // ============================================================================ + // Start Server + // ============================================================================ + + const server = serve({ fetch: app.fetch, port, hostname: host }); + injectWebSocket(server); + + const wsEndpoint = `ws://${host}:${port}/cdp`; + + log("CDP relay server started"); + log(` HTTP: http://${host}:${port}`); + log(` CDP endpoint: ${wsEndpoint}`); + log(` Extension endpoint: ws://${host}:${port}/extension`); + log(""); + log("Waiting for extension to connect..."); + + return { + wsEndpoint, + port, + async stop() { + for (const client of playwrightClients.values()) { + client.ws.close(1000, "Server stopped"); + } + playwrightClients.clear(); + extensionWs?.close(1000, "Server stopped"); + server.close(); + }, + }; +} diff --git a/.codex/skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts b/.codex/skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts new file mode 100644 index 0000000..8439fd7 --- /dev/null +++ b/.codex/skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts @@ -0,0 +1,223 @@ +import { chromium } from "playwright"; +import type { Browser, BrowserContext, Page } from "playwright"; +import { beforeAll, afterAll, beforeEach, afterEach, describe, test, expect } from "vitest"; +import { getSnapshotScript, clearSnapshotScriptCache } from "../browser-script"; + +let browser: Browser; +let context: BrowserContext; +let page: Page; + +beforeAll(async () => { + browser = await chromium.launch(); +}); + +afterAll(async () => { + await browser.close(); +}); + +beforeEach(async () => { + context = await browser.newContext(); + page = await context.newPage(); + clearSnapshotScriptCache(); // Start fresh for each test +}); + +afterEach(async () => { + await context.close(); +}); + +async function setContent(html: string): Promise { + await page.setContent(html, { waitUntil: "domcontentloaded" }); +} + +async function getSnapshot(): Promise { + const script = getSnapshotScript(); + return await page.evaluate((s: string) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const w = globalThis as any; + if (!w.__devBrowser_getAISnapshot) { + // eslint-disable-next-line no-eval + eval(s); + } + return w.__devBrowser_getAISnapshot(); + }, script); +} + +async function selectRef(ref: string): Promise { + return await page.evaluate((refId: string) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const w = globalThis as any; + const element = w.__devBrowser_selectSnapshotRef(refId); + return { + tagName: element.tagName, + textContent: element.textContent?.trim(), + }; + }, ref); +} + +describe("ARIA Snapshot", () => { + test("generates snapshot for simple page", async () => { + await setContent(` + + +

Hello World

+ + + + `); + + const snapshot = await getSnapshot(); + + expect(snapshot).toContain("heading"); + expect(snapshot).toContain("Hello World"); + expect(snapshot).toContain("button"); + expect(snapshot).toContain("Click me"); + }); + + test("assigns refs to interactive elements", async () => { + await setContent(` + + + + + + + `); + + const snapshot = await getSnapshot(); + + // Should have refs + expect(snapshot).toMatch(/\[ref=e\d+\]/); + }); + + test("refs persist on window.__devBrowserRefs", async () => { + await setContent(` + + + + + + `); + + await getSnapshot(); + + // Check that refs are stored + const hasRefs = await page.evaluate(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const w = globalThis as any; + return typeof w.__devBrowserRefs === "object" && Object.keys(w.__devBrowserRefs).length > 0; + }); + + expect(hasRefs).toBe(true); + }); + + test("selectSnapshotRef returns element for valid ref", async () => { + await setContent(` + + + + + + `); + + const snapshot = await getSnapshot(); + + // Extract a ref from the snapshot + const refMatch = snapshot.match(/\[ref=(e\d+)\]/); + expect(refMatch).toBeTruthy(); + expect(refMatch![1]).toBeDefined(); + const ref = refMatch![1] as string; + + // Select the element by ref + const result = (await selectRef(ref)) as { tagName: string; textContent: string }; + expect(result.tagName).toBe("BUTTON"); + expect(result.textContent).toBe("My Button"); + }); + + test("includes links with URLs", async () => { + await setContent(` + + +
Example Link + + + `); + + const snapshot = await getSnapshot(); + + expect(snapshot).toContain("link"); + expect(snapshot).toContain("Example Link"); + // URL should be included as a prop + expect(snapshot).toContain("/url:"); + }); + + test("includes form elements", async () => { + await setContent(` + + + + + + + + `); + + const snapshot = await getSnapshot(); + + expect(snapshot).toContain("textbox"); + expect(snapshot).toContain("checkbox"); + expect(snapshot).toContain("combobox"); + }); + + test("renders nested structure correctly", async () => { + await setContent(` + + + + + + `); + + const snapshot = await getSnapshot(); + + expect(snapshot).toContain("navigation"); + expect(snapshot).toContain("list"); + expect(snapshot).toContain("listitem"); + expect(snapshot).toContain("link"); + }); + + test("handles disabled elements", async () => { + await setContent(` + + + + + + `); + + const snapshot = await getSnapshot(); + + expect(snapshot).toContain("[disabled]"); + }); + + test("handles checked checkboxes", async () => { + await setContent(` + + + + + + `); + + const snapshot = await getSnapshot(); + + expect(snapshot).toContain("[checked]"); + }); +}); diff --git a/.codex/skills/dev-browser/src/snapshot/browser-script.ts b/.codex/skills/dev-browser/src/snapshot/browser-script.ts new file mode 100644 index 0000000..133e637 --- /dev/null +++ b/.codex/skills/dev-browser/src/snapshot/browser-script.ts @@ -0,0 +1,877 @@ +/** + * Browser-injectable snapshot script. + * + * This module provides the snapshot functionality as a string that can be + * injected into the browser via page.addScriptTag() or page.evaluate(). + * + * The approach is to read the compiled JavaScript at runtime and bundle it + * into a single script that exposes window.__devBrowser_getAISnapshot() and + * window.__devBrowser_selectSnapshotRef(). + */ + +import * as fs from "fs"; +import * as path from "path"; + +// Cache the bundled script +let cachedScript: string | null = null; + +/** + * Get the snapshot script that can be injected into the browser. + * Returns a self-contained JavaScript string that: + * 1. Defines all necessary functions (domUtils, roleUtils, yaml, ariaSnapshot) + * 2. Exposes window.__devBrowser_getAISnapshot() + * 3. Exposes window.__devBrowser_selectSnapshotRef() + */ +export function getSnapshotScript(): string { + if (cachedScript) return cachedScript; + + // Read the compiled JavaScript files + const snapshotDir = path.dirname(new URL(import.meta.url).pathname); + + // For now, we'll inline the functions directly + // In production, we could use a bundler like esbuild to create a single file + cachedScript = ` +(function() { + // Skip if already injected + if (window.__devBrowser_getAISnapshot) return; + + ${getDomUtilsCode()} + ${getYamlCode()} + ${getRoleUtilsCode()} + ${getAriaSnapshotCode()} + + // Expose main functions + window.__devBrowser_getAISnapshot = getAISnapshot; + window.__devBrowser_selectSnapshotRef = selectSnapshotRef; +})(); +`; + + return cachedScript; +} + +function getDomUtilsCode(): string { + return ` +// === domUtils === +let cacheStyle; +let cachesCounter = 0; + +function beginDOMCaches() { + ++cachesCounter; + cacheStyle = cacheStyle || new Map(); +} + +function endDOMCaches() { + if (!--cachesCounter) { + cacheStyle = undefined; + } +} + +function getElementComputedStyle(element, pseudo) { + const cache = cacheStyle; + const cacheKey = pseudo ? undefined : element; + if (cache && cacheKey && cache.has(cacheKey)) return cache.get(cacheKey); + const style = element.ownerDocument && element.ownerDocument.defaultView + ? element.ownerDocument.defaultView.getComputedStyle(element, pseudo) + : undefined; + if (cache && cacheKey) cache.set(cacheKey, style); + return style; +} + +function parentElementOrShadowHost(element) { + if (element.parentElement) return element.parentElement; + if (!element.parentNode) return; + if (element.parentNode.nodeType === 11 && element.parentNode.host) + return element.parentNode.host; +} + +function enclosingShadowRootOrDocument(element) { + let node = element; + while (node.parentNode) node = node.parentNode; + if (node.nodeType === 11 || node.nodeType === 9) + return node; +} + +function closestCrossShadow(element, css, scope) { + while (element) { + const closest = element.closest(css); + if (scope && closest !== scope && closest?.contains(scope)) return; + if (closest) return closest; + element = enclosingShadowHost(element); + } +} + +function enclosingShadowHost(element) { + while (element.parentElement) element = element.parentElement; + return parentElementOrShadowHost(element); +} + +function isElementStyleVisibilityVisible(element, style) { + style = style || getElementComputedStyle(element); + if (!style) return true; + if (style.visibility !== "visible") return false; + const detailsOrSummary = element.closest("details,summary"); + if (detailsOrSummary !== element && detailsOrSummary?.nodeName === "DETAILS" && !detailsOrSummary.open) + return false; + return true; +} + +function computeBox(element) { + const style = getElementComputedStyle(element); + if (!style) return { visible: true, inline: false }; + const cursor = style.cursor; + if (style.display === "contents") { + for (let child = element.firstChild; child; child = child.nextSibling) { + if (child.nodeType === 1 && isElementVisible(child)) + return { visible: true, inline: false, cursor }; + if (child.nodeType === 3 && isVisibleTextNode(child)) + return { visible: true, inline: true, cursor }; + } + return { visible: false, inline: false, cursor }; + } + if (!isElementStyleVisibilityVisible(element, style)) + return { cursor, visible: false, inline: false }; + const rect = element.getBoundingClientRect(); + return { rect, cursor, visible: rect.width > 0 && rect.height > 0, inline: style.display === "inline" }; +} + +function isElementVisible(element) { + return computeBox(element).visible; +} + +function isVisibleTextNode(node) { + const range = node.ownerDocument.createRange(); + range.selectNode(node); + const rect = range.getBoundingClientRect(); + return rect.width > 0 && rect.height > 0; +} + +function elementSafeTagName(element) { + const tagName = element.tagName; + if (typeof tagName === "string") return tagName.toUpperCase(); + if (element instanceof HTMLFormElement) return "FORM"; + return element.tagName.toUpperCase(); +} + +function normalizeWhiteSpace(text) { + return text.split("\\u00A0").map(chunk => + chunk.replace(/\\r\\n/g, "\\n").replace(/[\\u200b\\u00ad]/g, "").replace(/\\s\\s*/g, " ") + ).join("\\u00A0").trim(); +} +`; +} + +function getYamlCode(): string { + return ` +// === yaml === +function yamlEscapeKeyIfNeeded(str) { + if (!yamlStringNeedsQuotes(str)) return str; + return "'" + str.replace(/'/g, "''") + "'"; +} + +function yamlEscapeValueIfNeeded(str) { + if (!yamlStringNeedsQuotes(str)) return str; + return '"' + str.replace(/[\\\\"\x00-\\x1f\\x7f-\\x9f]/g, c => { + switch (c) { + case "\\\\": return "\\\\\\\\"; + case '"': return '\\\\"'; + case "\\b": return "\\\\b"; + case "\\f": return "\\\\f"; + case "\\n": return "\\\\n"; + case "\\r": return "\\\\r"; + case "\\t": return "\\\\t"; + default: + const code = c.charCodeAt(0); + return "\\\\x" + code.toString(16).padStart(2, "0"); + } + }) + '"'; +} + +function yamlStringNeedsQuotes(str) { + if (str.length === 0) return true; + if (/^\\s|\\s$/.test(str)) return true; + if (/[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f-\\x9f]/.test(str)) return true; + if (/^-/.test(str)) return true; + if (/[\\n:](\\s|$)/.test(str)) return true; + if (/\\s#/.test(str)) return true; + if (/[\\n\\r]/.test(str)) return true; + if (/^[&*\\],?!>|@"'#%]/.test(str)) return true; + if (/[{}\`]/.test(str)) return true; + if (/^\\[/.test(str)) return true; + if (!isNaN(Number(str)) || ["y","n","yes","no","true","false","on","off","null"].includes(str.toLowerCase())) return true; + return false; +} +`; +} + +function getRoleUtilsCode(): string { + return ` +// === roleUtils === +const validRoles = ["alert","alertdialog","application","article","banner","blockquote","button","caption","cell","checkbox","code","columnheader","combobox","complementary","contentinfo","definition","deletion","dialog","directory","document","emphasis","feed","figure","form","generic","grid","gridcell","group","heading","img","insertion","link","list","listbox","listitem","log","main","mark","marquee","math","meter","menu","menubar","menuitem","menuitemcheckbox","menuitemradio","navigation","none","note","option","paragraph","presentation","progressbar","radio","radiogroup","region","row","rowgroup","rowheader","scrollbar","search","searchbox","separator","slider","spinbutton","status","strong","subscript","superscript","switch","tab","table","tablist","tabpanel","term","textbox","time","timer","toolbar","tooltip","tree","treegrid","treeitem"]; + +let cacheAccessibleName; +let cacheIsHidden; +let cachePointerEvents; +let ariaCachesCounter = 0; + +function beginAriaCaches() { + beginDOMCaches(); + ++ariaCachesCounter; + cacheAccessibleName = cacheAccessibleName || new Map(); + cacheIsHidden = cacheIsHidden || new Map(); + cachePointerEvents = cachePointerEvents || new Map(); +} + +function endAriaCaches() { + if (!--ariaCachesCounter) { + cacheAccessibleName = undefined; + cacheIsHidden = undefined; + cachePointerEvents = undefined; + } + endDOMCaches(); +} + +function hasExplicitAccessibleName(e) { + return e.hasAttribute("aria-label") || e.hasAttribute("aria-labelledby"); +} + +const kAncestorPreventingLandmark = "article:not([role]), aside:not([role]), main:not([role]), nav:not([role]), section:not([role]), [role=article], [role=complementary], [role=main], [role=navigation], [role=region]"; + +const kGlobalAriaAttributes = [ + ["aria-atomic", undefined],["aria-busy", undefined],["aria-controls", undefined],["aria-current", undefined], + ["aria-describedby", undefined],["aria-details", undefined],["aria-dropeffect", undefined],["aria-flowto", undefined], + ["aria-grabbed", undefined],["aria-hidden", undefined],["aria-keyshortcuts", undefined], + ["aria-label", ["caption","code","deletion","emphasis","generic","insertion","paragraph","presentation","strong","subscript","superscript"]], + ["aria-labelledby", ["caption","code","deletion","emphasis","generic","insertion","paragraph","presentation","strong","subscript","superscript"]], + ["aria-live", undefined],["aria-owns", undefined],["aria-relevant", undefined],["aria-roledescription", ["generic"]] +]; + +function hasGlobalAriaAttribute(element, forRole) { + return kGlobalAriaAttributes.some(([attr, prohibited]) => !prohibited?.includes(forRole || "") && element.hasAttribute(attr)); +} + +function hasTabIndex(element) { + return !Number.isNaN(Number(String(element.getAttribute("tabindex")))); +} + +function isFocusable(element) { + return !isNativelyDisabled(element) && (isNativelyFocusable(element) || hasTabIndex(element)); +} + +function isNativelyFocusable(element) { + const tagName = elementSafeTagName(element); + if (["BUTTON","DETAILS","SELECT","TEXTAREA"].includes(tagName)) return true; + if (tagName === "A" || tagName === "AREA") return element.hasAttribute("href"); + if (tagName === "INPUT") return !element.hidden; + return false; +} + +function isNativelyDisabled(element) { + const isNativeFormControl = ["BUTTON","INPUT","SELECT","TEXTAREA","OPTION","OPTGROUP"].includes(elementSafeTagName(element)); + return isNativeFormControl && (element.hasAttribute("disabled") || belongsToDisabledFieldSet(element)); +} + +function belongsToDisabledFieldSet(element) { + const fieldSetElement = element?.closest("FIELDSET[DISABLED]"); + if (!fieldSetElement) return false; + const legendElement = fieldSetElement.querySelector(":scope > LEGEND"); + return !legendElement || !legendElement.contains(element); +} + +const inputTypeToRole = {button:"button",checkbox:"checkbox",image:"button",number:"spinbutton",radio:"radio",range:"slider",reset:"button",submit:"button"}; + +function getIdRefs(element, ref) { + if (!ref) return []; + const root = enclosingShadowRootOrDocument(element); + if (!root) return []; + try { + const ids = ref.split(" ").filter(id => !!id); + const result = []; + for (const id of ids) { + const firstElement = root.querySelector("#" + CSS.escape(id)); + if (firstElement && !result.includes(firstElement)) result.push(firstElement); + } + return result; + } catch { return []; } +} + +const kImplicitRoleByTagName = { + A: e => e.hasAttribute("href") ? "link" : null, + AREA: e => e.hasAttribute("href") ? "link" : null, + ARTICLE: () => "article", ASIDE: () => "complementary", BLOCKQUOTE: () => "blockquote", BUTTON: () => "button", + CAPTION: () => "caption", CODE: () => "code", DATALIST: () => "listbox", DD: () => "definition", + DEL: () => "deletion", DETAILS: () => "group", DFN: () => "term", DIALOG: () => "dialog", DT: () => "term", + EM: () => "emphasis", FIELDSET: () => "group", FIGURE: () => "figure", + FOOTER: e => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : "contentinfo", + FORM: e => hasExplicitAccessibleName(e) ? "form" : null, + H1: () => "heading", H2: () => "heading", H3: () => "heading", H4: () => "heading", H5: () => "heading", H6: () => "heading", + HEADER: e => closestCrossShadow(e, kAncestorPreventingLandmark) ? null : "banner", + HR: () => "separator", HTML: () => "document", + IMG: e => e.getAttribute("alt") === "" && !e.getAttribute("title") && !hasGlobalAriaAttribute(e) && !hasTabIndex(e) ? "presentation" : "img", + INPUT: e => { + const type = e.type.toLowerCase(); + if (type === "search") return e.hasAttribute("list") ? "combobox" : "searchbox"; + if (["email","tel","text","url",""].includes(type)) { + const list = getIdRefs(e, e.getAttribute("list"))[0]; + return list && elementSafeTagName(list) === "DATALIST" ? "combobox" : "textbox"; + } + if (type === "hidden") return null; + if (type === "file") return "button"; + return inputTypeToRole[type] || "textbox"; + }, + INS: () => "insertion", LI: () => "listitem", MAIN: () => "main", MARK: () => "mark", MATH: () => "math", + MENU: () => "list", METER: () => "meter", NAV: () => "navigation", OL: () => "list", OPTGROUP: () => "group", + OPTION: () => "option", OUTPUT: () => "status", P: () => "paragraph", PROGRESS: () => "progressbar", + SEARCH: () => "search", SECTION: e => hasExplicitAccessibleName(e) ? "region" : null, + SELECT: e => e.hasAttribute("multiple") || e.size > 1 ? "listbox" : "combobox", + STRONG: () => "strong", SUB: () => "subscript", SUP: () => "superscript", SVG: () => "img", + TABLE: () => "table", TBODY: () => "rowgroup", + TD: e => { const table = closestCrossShadow(e, "table"); const role = table ? getExplicitAriaRole(table) : ""; return role === "grid" || role === "treegrid" ? "gridcell" : "cell"; }, + TEXTAREA: () => "textbox", TFOOT: () => "rowgroup", + TH: e => { const scope = e.getAttribute("scope"); if (scope === "col" || scope === "colgroup") return "columnheader"; if (scope === "row" || scope === "rowgroup") return "rowheader"; return "columnheader"; }, + THEAD: () => "rowgroup", TIME: () => "time", TR: () => "row", UL: () => "list" +}; + +function getExplicitAriaRole(element) { + const roles = (element.getAttribute("role") || "").split(" ").map(role => role.trim()); + return roles.find(role => validRoles.includes(role)) || null; +} + +function getImplicitAriaRole(element) { + const fn = kImplicitRoleByTagName[elementSafeTagName(element)]; + return fn ? fn(element) : null; +} + +function hasPresentationConflictResolution(element, role) { + return hasGlobalAriaAttribute(element, role) || isFocusable(element); +} + +function getAriaRole(element) { + const explicitRole = getExplicitAriaRole(element); + if (!explicitRole) return getImplicitAriaRole(element); + if (explicitRole === "none" || explicitRole === "presentation") { + const implicitRole = getImplicitAriaRole(element); + if (hasPresentationConflictResolution(element, implicitRole)) return implicitRole; + } + return explicitRole; +} + +function getAriaBoolean(attr) { + return attr === null ? undefined : attr.toLowerCase() === "true"; +} + +function isElementIgnoredForAria(element) { + return ["STYLE","SCRIPT","NOSCRIPT","TEMPLATE"].includes(elementSafeTagName(element)); +} + +function isElementHiddenForAria(element) { + if (isElementIgnoredForAria(element)) return true; + const style = getElementComputedStyle(element); + const isSlot = element.nodeName === "SLOT"; + if (style?.display === "contents" && !isSlot) { + for (let child = element.firstChild; child; child = child.nextSibling) { + if (child.nodeType === 1 && !isElementHiddenForAria(child)) return false; + if (child.nodeType === 3 && isVisibleTextNode(child)) return false; + } + return true; + } + const isOptionInsideSelect = element.nodeName === "OPTION" && !!element.closest("select"); + if (!isOptionInsideSelect && !isSlot && !isElementStyleVisibilityVisible(element, style)) return true; + return belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element); +} + +function belongsToDisplayNoneOrAriaHiddenOrNonSlotted(element) { + let hidden = cacheIsHidden?.get(element); + if (hidden === undefined) { + hidden = false; + if (element.parentElement && element.parentElement.shadowRoot && !element.assignedSlot) hidden = true; + if (!hidden) { + const style = getElementComputedStyle(element); + hidden = !style || style.display === "none" || getAriaBoolean(element.getAttribute("aria-hidden")) === true; + } + if (!hidden) { + const parent = parentElementOrShadowHost(element); + if (parent) hidden = belongsToDisplayNoneOrAriaHiddenOrNonSlotted(parent); + } + cacheIsHidden?.set(element, hidden); + } + return hidden; +} + +function getAriaLabelledByElements(element) { + const ref = element.getAttribute("aria-labelledby"); + if (ref === null) return null; + const refs = getIdRefs(element, ref); + return refs.length ? refs : null; +} + +function getElementAccessibleName(element, includeHidden) { + let accessibleName = cacheAccessibleName?.get(element); + if (accessibleName === undefined) { + accessibleName = ""; + const elementProhibitsNaming = ["caption","code","definition","deletion","emphasis","generic","insertion","mark","paragraph","presentation","strong","subscript","suggestion","superscript","term","time"].includes(getAriaRole(element) || ""); + if (!elementProhibitsNaming) { + accessibleName = normalizeWhiteSpace(getTextAlternativeInternal(element, { includeHidden, visitedElements: new Set(), embeddedInTargetElement: "self" })); + } + cacheAccessibleName?.set(element, accessibleName); + } + return accessibleName; +} + +function getTextAlternativeInternal(element, options) { + if (options.visitedElements.has(element)) return ""; + const childOptions = { ...options, embeddedInTargetElement: options.embeddedInTargetElement === "self" ? "descendant" : options.embeddedInTargetElement }; + + if (!options.includeHidden) { + const isEmbeddedInHiddenReferenceTraversal = !!options.embeddedInLabelledBy?.hidden || !!options.embeddedInLabel?.hidden; + if (isElementIgnoredForAria(element) || (!isEmbeddedInHiddenReferenceTraversal && isElementHiddenForAria(element))) { + options.visitedElements.add(element); + return ""; + } + } + + const labelledBy = getAriaLabelledByElements(element); + if (!options.embeddedInLabelledBy) { + const accessibleName = (labelledBy || []).map(ref => getTextAlternativeInternal(ref, { ...options, embeddedInLabelledBy: { element: ref, hidden: isElementHiddenForAria(ref) }, embeddedInTargetElement: undefined, embeddedInLabel: undefined })).join(" "); + if (accessibleName) return accessibleName; + } + + const role = getAriaRole(element) || ""; + const tagName = elementSafeTagName(element); + + const ariaLabel = element.getAttribute("aria-label") || ""; + if (ariaLabel.trim()) { options.visitedElements.add(element); return ariaLabel; } + + if (!["presentation","none"].includes(role)) { + if (tagName === "INPUT" && ["button","submit","reset"].includes(element.type)) { + options.visitedElements.add(element); + const value = element.value || ""; + if (value.trim()) return value; + if (element.type === "submit") return "Submit"; + if (element.type === "reset") return "Reset"; + return element.getAttribute("title") || ""; + } + if (tagName === "INPUT" && element.type === "image") { + options.visitedElements.add(element); + const alt = element.getAttribute("alt") || ""; + if (alt.trim()) return alt; + const title = element.getAttribute("title") || ""; + if (title.trim()) return title; + return "Submit"; + } + if (tagName === "IMG") { + options.visitedElements.add(element); + const alt = element.getAttribute("alt") || ""; + if (alt.trim()) return alt; + return element.getAttribute("title") || ""; + } + if (!labelledBy && ["BUTTON","INPUT","TEXTAREA","SELECT"].includes(tagName)) { + const labels = element.labels; + if (labels?.length) { + options.visitedElements.add(element); + return [...labels].map(label => getTextAlternativeInternal(label, { ...options, embeddedInLabel: { element: label, hidden: isElementHiddenForAria(label) }, embeddedInLabelledBy: undefined, embeddedInTargetElement: undefined })).filter(name => !!name).join(" "); + } + } + } + + const allowsNameFromContent = ["button","cell","checkbox","columnheader","gridcell","heading","link","menuitem","menuitemcheckbox","menuitemradio","option","radio","row","rowheader","switch","tab","tooltip","treeitem"].includes(role); + if (allowsNameFromContent || !!options.embeddedInLabelledBy || !!options.embeddedInLabel) { + options.visitedElements.add(element); + const accessibleName = innerAccumulatedElementText(element, childOptions); + const maybeTrimmedAccessibleName = options.embeddedInTargetElement === "self" ? accessibleName.trim() : accessibleName; + if (maybeTrimmedAccessibleName) return accessibleName; + } + + if (!["presentation","none"].includes(role) || tagName === "IFRAME") { + options.visitedElements.add(element); + const title = element.getAttribute("title") || ""; + if (title.trim()) return title; + } + + options.visitedElements.add(element); + return ""; +} + +function innerAccumulatedElementText(element, options) { + const tokens = []; + const visit = (node, skipSlotted) => { + if (skipSlotted && node.assignedSlot) return; + if (node.nodeType === 1) { + const display = getElementComputedStyle(node)?.display || "inline"; + let token = getTextAlternativeInternal(node, options); + if (display !== "inline" || node.nodeName === "BR") token = " " + token + " "; + tokens.push(token); + } else if (node.nodeType === 3) { + tokens.push(node.textContent || ""); + } + }; + const assignedNodes = element.nodeName === "SLOT" ? element.assignedNodes() : []; + if (assignedNodes.length) { + for (const child of assignedNodes) visit(child, false); + } else { + for (let child = element.firstChild; child; child = child.nextSibling) visit(child, true); + if (element.shadowRoot) { + for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) visit(child, true); + } + } + return tokens.join(""); +} + +const kAriaCheckedRoles = ["checkbox","menuitemcheckbox","option","radio","switch","menuitemradio","treeitem"]; +function getAriaChecked(element) { + const tagName = elementSafeTagName(element); + if (tagName === "INPUT" && element.indeterminate) return "mixed"; + if (tagName === "INPUT" && ["checkbox","radio"].includes(element.type)) return element.checked; + if (kAriaCheckedRoles.includes(getAriaRole(element) || "")) { + const checked = element.getAttribute("aria-checked"); + if (checked === "true") return true; + if (checked === "mixed") return "mixed"; + return false; + } + return false; +} + +const kAriaDisabledRoles = ["application","button","composite","gridcell","group","input","link","menuitem","scrollbar","separator","tab","checkbox","columnheader","combobox","grid","listbox","menu","menubar","menuitemcheckbox","menuitemradio","option","radio","radiogroup","row","rowheader","searchbox","select","slider","spinbutton","switch","tablist","textbox","toolbar","tree","treegrid","treeitem"]; +function getAriaDisabled(element) { + return isNativelyDisabled(element) || hasExplicitAriaDisabled(element); +} +function hasExplicitAriaDisabled(element, isAncestor) { + if (!element) return false; + if (isAncestor || kAriaDisabledRoles.includes(getAriaRole(element) || "")) { + const attribute = (element.getAttribute("aria-disabled") || "").toLowerCase(); + if (attribute === "true") return true; + if (attribute === "false") return false; + return hasExplicitAriaDisabled(parentElementOrShadowHost(element), true); + } + return false; +} + +const kAriaExpandedRoles = ["application","button","checkbox","combobox","gridcell","link","listbox","menuitem","row","rowheader","tab","treeitem","columnheader","menuitemcheckbox","menuitemradio","switch"]; +function getAriaExpanded(element) { + if (elementSafeTagName(element) === "DETAILS") return element.open; + if (kAriaExpandedRoles.includes(getAriaRole(element) || "")) { + const expanded = element.getAttribute("aria-expanded"); + if (expanded === null) return undefined; + if (expanded === "true") return true; + return false; + } + return undefined; +} + +const kAriaLevelRoles = ["heading","listitem","row","treeitem"]; +function getAriaLevel(element) { + const native = {H1:1,H2:2,H3:3,H4:4,H5:5,H6:6}[elementSafeTagName(element)]; + if (native) return native; + if (kAriaLevelRoles.includes(getAriaRole(element) || "")) { + const attr = element.getAttribute("aria-level"); + const value = attr === null ? Number.NaN : Number(attr); + if (Number.isInteger(value) && value >= 1) return value; + } + return 0; +} + +const kAriaPressedRoles = ["button"]; +function getAriaPressed(element) { + if (kAriaPressedRoles.includes(getAriaRole(element) || "")) { + const pressed = element.getAttribute("aria-pressed"); + if (pressed === "true") return true; + if (pressed === "mixed") return "mixed"; + } + return false; +} + +const kAriaSelectedRoles = ["gridcell","option","row","tab","rowheader","columnheader","treeitem"]; +function getAriaSelected(element) { + if (elementSafeTagName(element) === "OPTION") return element.selected; + if (kAriaSelectedRoles.includes(getAriaRole(element) || "")) return getAriaBoolean(element.getAttribute("aria-selected")) === true; + return false; +} + +function receivesPointerEvents(element) { + const cache = cachePointerEvents; + let e = element; + let result; + const parents = []; + for (; e; e = parentElementOrShadowHost(e)) { + const cached = cache?.get(e); + if (cached !== undefined) { result = cached; break; } + parents.push(e); + const style = getElementComputedStyle(e); + if (!style) { result = true; break; } + const value = style.pointerEvents; + if (value) { result = value !== "none"; break; } + } + if (result === undefined) result = true; + for (const parent of parents) cache?.set(parent, result); + return result; +} + +function getCSSContent(element, pseudo) { + const style = getElementComputedStyle(element, pseudo); + if (!style) return undefined; + const contentValue = style.content; + if (!contentValue || contentValue === "none" || contentValue === "normal") return undefined; + if (style.display === "none" || style.visibility === "hidden") return undefined; + const match = contentValue.match(/^"(.*)"$/); + if (match) { + const content = match[1].replace(/\\\\"/g, '"'); + if (pseudo) { + const display = style.display || "inline"; + if (display !== "inline") return " " + content + " "; + } + return content; + } + return undefined; +} +`; +} + +function getAriaSnapshotCode(): string { + return ` +// === ariaSnapshot === +let lastRef = 0; + +function generateAriaTree(rootElement) { + const options = { visibility: "ariaOrVisible", refs: "interactable", refPrefix: "", includeGenericRole: true, renderActive: true, renderCursorPointer: true }; + const visited = new Set(); + const snapshot = { + root: { role: "fragment", name: "", children: [], element: rootElement, props: {}, box: computeBox(rootElement), receivesPointerEvents: true }, + elements: new Map(), + refs: new Map(), + iframeRefs: [] + }; + + const visit = (ariaNode, node, parentElementVisible) => { + if (visited.has(node)) return; + visited.add(node); + if (node.nodeType === Node.TEXT_NODE && node.nodeValue) { + if (!parentElementVisible) return; + const text = node.nodeValue; + if (ariaNode.role !== "textbox" && text) ariaNode.children.push(node.nodeValue || ""); + return; + } + if (node.nodeType !== Node.ELEMENT_NODE) return; + const element = node; + const isElementVisibleForAria = !isElementHiddenForAria(element); + let visible = isElementVisibleForAria; + if (options.visibility === "ariaOrVisible") visible = isElementVisibleForAria || isElementVisible(element); + if (options.visibility === "ariaAndVisible") visible = isElementVisibleForAria && isElementVisible(element); + if (options.visibility === "aria" && !visible) return; + const ariaChildren = []; + if (element.hasAttribute("aria-owns")) { + const ids = element.getAttribute("aria-owns").split(/\\s+/); + for (const id of ids) { + const ownedElement = rootElement.ownerDocument.getElementById(id); + if (ownedElement) ariaChildren.push(ownedElement); + } + } + const childAriaNode = visible ? toAriaNode(element, options) : null; + if (childAriaNode) { + if (childAriaNode.ref) { + snapshot.elements.set(childAriaNode.ref, element); + snapshot.refs.set(element, childAriaNode.ref); + if (childAriaNode.role === "iframe") snapshot.iframeRefs.push(childAriaNode.ref); + } + ariaNode.children.push(childAriaNode); + } + processElement(childAriaNode || ariaNode, element, ariaChildren, visible); + }; + + function processElement(ariaNode, element, ariaChildren, parentElementVisible) { + const display = getElementComputedStyle(element)?.display || "inline"; + const treatAsBlock = display !== "inline" || element.nodeName === "BR" ? " " : ""; + if (treatAsBlock) ariaNode.children.push(treatAsBlock); + ariaNode.children.push(getCSSContent(element, "::before") || ""); + const assignedNodes = element.nodeName === "SLOT" ? element.assignedNodes() : []; + if (assignedNodes.length) { + for (const child of assignedNodes) visit(ariaNode, child, parentElementVisible); + } else { + for (let child = element.firstChild; child; child = child.nextSibling) { + if (!child.assignedSlot) visit(ariaNode, child, parentElementVisible); + } + if (element.shadowRoot) { + for (let child = element.shadowRoot.firstChild; child; child = child.nextSibling) visit(ariaNode, child, parentElementVisible); + } + } + for (const child of ariaChildren) visit(ariaNode, child, parentElementVisible); + ariaNode.children.push(getCSSContent(element, "::after") || ""); + if (treatAsBlock) ariaNode.children.push(treatAsBlock); + if (ariaNode.children.length === 1 && ariaNode.name === ariaNode.children[0]) ariaNode.children = []; + if (ariaNode.role === "link" && element.hasAttribute("href")) ariaNode.props["url"] = element.getAttribute("href"); + if (ariaNode.role === "textbox" && element.hasAttribute("placeholder") && element.getAttribute("placeholder") !== ariaNode.name) ariaNode.props["placeholder"] = element.getAttribute("placeholder"); + } + + beginAriaCaches(); + try { visit(snapshot.root, rootElement, true); } + finally { endAriaCaches(); } + normalizeStringChildren(snapshot.root); + normalizeGenericRoles(snapshot.root); + return snapshot; +} + +function computeAriaRef(ariaNode, options) { + if (options.refs === "none") return; + if (options.refs === "interactable" && (!ariaNode.box.visible || !ariaNode.receivesPointerEvents)) return; + let ariaRef = ariaNode.element._ariaRef; + if (!ariaRef || ariaRef.role !== ariaNode.role || ariaRef.name !== ariaNode.name) { + ariaRef = { role: ariaNode.role, name: ariaNode.name, ref: (options.refPrefix || "") + "e" + (++lastRef) }; + ariaNode.element._ariaRef = ariaRef; + } + ariaNode.ref = ariaRef.ref; +} + +function toAriaNode(element, options) { + const active = element.ownerDocument.activeElement === element; + if (element.nodeName === "IFRAME") { + const ariaNode = { role: "iframe", name: "", children: [], props: {}, element, box: computeBox(element), receivesPointerEvents: true, active }; + computeAriaRef(ariaNode, options); + return ariaNode; + } + const defaultRole = options.includeGenericRole ? "generic" : null; + const role = getAriaRole(element) || defaultRole; + if (!role || role === "presentation" || role === "none") return null; + const name = normalizeWhiteSpace(getElementAccessibleName(element, false) || ""); + const receivesPointerEventsValue = receivesPointerEvents(element); + const box = computeBox(element); + if (role === "generic" && box.inline && element.childNodes.length === 1 && element.childNodes[0].nodeType === Node.TEXT_NODE) return null; + const result = { role, name, children: [], props: {}, element, box, receivesPointerEvents: receivesPointerEventsValue, active }; + computeAriaRef(result, options); + if (kAriaCheckedRoles.includes(role)) result.checked = getAriaChecked(element); + if (kAriaDisabledRoles.includes(role)) result.disabled = getAriaDisabled(element); + if (kAriaExpandedRoles.includes(role)) result.expanded = getAriaExpanded(element); + if (kAriaLevelRoles.includes(role)) result.level = getAriaLevel(element); + if (kAriaPressedRoles.includes(role)) result.pressed = getAriaPressed(element); + if (kAriaSelectedRoles.includes(role)) result.selected = getAriaSelected(element); + if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { + if (element.type !== "checkbox" && element.type !== "radio" && element.type !== "file") result.children = [element.value]; + } + return result; +} + +function normalizeGenericRoles(node) { + const normalizeChildren = (node) => { + const result = []; + for (const child of node.children || []) { + if (typeof child === "string") { result.push(child); continue; } + const normalized = normalizeChildren(child); + result.push(...normalized); + } + const removeSelf = node.role === "generic" && !node.name && result.length <= 1 && result.every(c => typeof c !== "string" && !!c.ref); + if (removeSelf) return result; + node.children = result; + return [node]; + }; + normalizeChildren(node); +} + +function normalizeStringChildren(rootA11yNode) { + const flushChildren = (buffer, normalizedChildren) => { + if (!buffer.length) return; + const text = normalizeWhiteSpace(buffer.join("")); + if (text) normalizedChildren.push(text); + buffer.length = 0; + }; + const visit = (ariaNode) => { + const normalizedChildren = []; + const buffer = []; + for (const child of ariaNode.children || []) { + if (typeof child === "string") { buffer.push(child); } + else { flushChildren(buffer, normalizedChildren); visit(child); normalizedChildren.push(child); } + } + flushChildren(buffer, normalizedChildren); + ariaNode.children = normalizedChildren.length ? normalizedChildren : []; + if (ariaNode.children.length === 1 && ariaNode.children[0] === ariaNode.name) ariaNode.children = []; + }; + visit(rootA11yNode); +} + +function hasPointerCursor(ariaNode) { return ariaNode.box.cursor === "pointer"; } + +function renderAriaTree(ariaSnapshot) { + const options = { visibility: "ariaOrVisible", refs: "interactable", refPrefix: "", includeGenericRole: true, renderActive: true, renderCursorPointer: true }; + const lines = []; + let nodesToRender = ariaSnapshot.root.role === "fragment" ? ariaSnapshot.root.children : [ariaSnapshot.root]; + + const visitText = (text, indent) => { + const escaped = yamlEscapeValueIfNeeded(text); + if (escaped) lines.push(indent + "- text: " + escaped); + }; + + const createKey = (ariaNode, renderCursorPointer) => { + let key = ariaNode.role; + if (ariaNode.name && ariaNode.name.length <= 900) { + const name = ariaNode.name; + if (name) { + const stringifiedName = name.startsWith("/") && name.endsWith("/") ? name : JSON.stringify(name); + key += " " + stringifiedName; + } + } + if (ariaNode.checked === "mixed") key += " [checked=mixed]"; + if (ariaNode.checked === true) key += " [checked]"; + if (ariaNode.disabled) key += " [disabled]"; + if (ariaNode.expanded) key += " [expanded]"; + if (ariaNode.active && options.renderActive) key += " [active]"; + if (ariaNode.level) key += " [level=" + ariaNode.level + "]"; + if (ariaNode.pressed === "mixed") key += " [pressed=mixed]"; + if (ariaNode.pressed === true) key += " [pressed]"; + if (ariaNode.selected === true) key += " [selected]"; + if (ariaNode.ref) { + key += " [ref=" + ariaNode.ref + "]"; + if (renderCursorPointer && hasPointerCursor(ariaNode)) key += " [cursor=pointer]"; + } + return key; + }; + + const getSingleInlinedTextChild = (ariaNode) => { + return ariaNode?.children.length === 1 && typeof ariaNode.children[0] === "string" && !Object.keys(ariaNode.props).length ? ariaNode.children[0] : undefined; + }; + + const visit = (ariaNode, indent, renderCursorPointer) => { + const escapedKey = indent + "- " + yamlEscapeKeyIfNeeded(createKey(ariaNode, renderCursorPointer)); + const singleInlinedTextChild = getSingleInlinedTextChild(ariaNode); + if (!ariaNode.children.length && !Object.keys(ariaNode.props).length) { + lines.push(escapedKey); + } else if (singleInlinedTextChild !== undefined) { + lines.push(escapedKey + ": " + yamlEscapeValueIfNeeded(singleInlinedTextChild)); + } else { + lines.push(escapedKey + ":"); + for (const [name, value] of Object.entries(ariaNode.props)) lines.push(indent + " - /" + name + ": " + yamlEscapeValueIfNeeded(value)); + const childIndent = indent + " "; + const inCursorPointer = !!ariaNode.ref && renderCursorPointer && hasPointerCursor(ariaNode); + for (const child of ariaNode.children) { + if (typeof child === "string") visitText(child, childIndent); + else visit(child, childIndent, renderCursorPointer && !inCursorPointer); + } + } + }; + + for (const nodeToRender of nodesToRender) { + if (typeof nodeToRender === "string") visitText(nodeToRender, ""); + else visit(nodeToRender, "", !!options.renderCursorPointer); + } + return lines.join("\\n"); +} + +function getAISnapshot() { + const snapshot = generateAriaTree(document.body); + const refsObject = {}; + for (const [ref, element] of snapshot.elements) refsObject[ref] = element; + window.__devBrowserRefs = refsObject; + return renderAriaTree(snapshot); +} + +function selectSnapshotRef(ref) { + const refs = window.__devBrowserRefs; + if (!refs) throw new Error("No snapshot refs found. Call getAISnapshot first."); + const element = refs[ref]; + if (!element) throw new Error('Ref "' + ref + '" not found. Available refs: ' + Object.keys(refs).join(", ")); + return element; +} +`; +} + +/** + * Clear the cached script (useful for development/testing) + */ +export function clearSnapshotScriptCache(): void { + cachedScript = null; +} diff --git a/.codex/skills/dev-browser/src/snapshot/index.ts b/.codex/skills/dev-browser/src/snapshot/index.ts new file mode 100644 index 0000000..d713f6b --- /dev/null +++ b/.codex/skills/dev-browser/src/snapshot/index.ts @@ -0,0 +1,14 @@ +/** + * ARIA Snapshot module for dev-browser. + * + * Provides Playwright-compatible ARIA snapshots with cross-connection ref persistence. + * Refs are stored on window.__devBrowserRefs and survive across Playwright reconnections. + * + * Usage: + * import { getSnapshotScript } from './snapshot'; + * const script = getSnapshotScript(); + * await page.evaluate(script); + * // Now window.__devBrowser_getAISnapshot() and window.__devBrowser_selectSnapshotRef(ref) are available + */ + +export { getSnapshotScript, clearSnapshotScriptCache } from "./browser-script"; diff --git a/.codex/skills/dev-browser/src/snapshot/inject.ts b/.codex/skills/dev-browser/src/snapshot/inject.ts new file mode 100644 index 0000000..2392221 --- /dev/null +++ b/.codex/skills/dev-browser/src/snapshot/inject.ts @@ -0,0 +1,13 @@ +/** + * Injectable snapshot script for browser context. + * + * This module provides the getSnapshotScript function that returns a + * self-contained JavaScript string for injection into browser contexts. + * + * The script is injected via page.evaluate() and exposes: + * - window.__devBrowser_getAISnapshot(): Returns ARIA snapshot YAML + * - window.__devBrowser_selectSnapshotRef(ref): Returns element for given ref + * - window.__devBrowserRefs: Map of ref -> Element (persists across connections) + */ + +export { getSnapshotScript, clearSnapshotScriptCache } from "./browser-script"; diff --git a/.codex/skills/dev-browser/src/types.ts b/.codex/skills/dev-browser/src/types.ts new file mode 100644 index 0000000..afbfcbb --- /dev/null +++ b/.codex/skills/dev-browser/src/types.ts @@ -0,0 +1,34 @@ +// API request/response types - shared between client and server + +export interface ServeOptions { + port?: number; + headless?: boolean; + cdpPort?: number; + /** Directory to store persistent browser profiles (cookies, localStorage, etc.) */ + profileDir?: string; +} + +export interface ViewportSize { + width: number; + height: number; +} + +export interface GetPageRequest { + name: string; + /** Optional viewport size for new pages */ + viewport?: ViewportSize; +} + +export interface GetPageResponse { + wsEndpoint: string; + name: string; + targetId: string; // CDP target ID for reliable page matching +} + +export interface ListPagesResponse { + pages: string[]; +} + +export interface ServerInfoResponse { + wsEndpoint: string; +} diff --git a/.codex/skills/dev-browser/tsconfig.json b/.codex/skills/dev-browser/tsconfig.json new file mode 100644 index 0000000..81471ee --- /dev/null +++ b/.codex/skills/dev-browser/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Path aliases + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + }, + "include": ["src/**/*", "scripts/**/*"] +} diff --git a/.codex/skills/dev-browser/vitest.config.ts b/.codex/skills/dev-browser/vitest.config.ts new file mode 100644 index 0000000..e2f469e --- /dev/null +++ b/.codex/skills/dev-browser/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", + include: ["src/**/*.test.ts"], + testTimeout: 60000, // Playwright tests can be slow + hookTimeout: 60000, + teardownTimeout: 60000, + }, +}); diff --git a/.codex/skills/prd/SKILL.md b/.codex/skills/prd/SKILL.md new file mode 100644 index 0000000..9ef1d01 --- /dev/null +++ b/.codex/skills/prd/SKILL.md @@ -0,0 +1,235 @@ +--- +name: prd +description: "Generate a Product Requirements Document (PRD) as JSON for Ralph. Triggers on: create a prd, write prd for, plan this feature, requirements for, spec out." +--- + +# PRD Generator (JSON Only) + +Create a structured JSON PRD that Ralph can execute deterministically. This PRD is the **single source of truth** for stories, gates, and status. + +--- + +## The Job + +1. Receive a feature description from the user +2. Ask **5–10** clarifying questions (with lettered options), in batches of up to 5 at a time (ask 5, wait for answers, then ask the next batch if needed). +3. **Always ask about quality gates** (commands that must pass) +4. Generate a **detailed** JSON PRD and save it to the provided path + +**Important:** Do NOT implement anything. Only generate JSON. + +--- + +## Step 1: Clarifying Questions + +Ask questions even if the prompt seems clear. The goal is to capture missing product details, stack choices, and UI/route structure so the PRD is implementable without guesswork. Focus on: + +- **Problem/Goal:** What problem does this solve? +- **Core Functionality:** What are the key actions? +- **Scope/Boundaries:** What should it NOT do? +- **Success Criteria:** How do we know it's done? +- **Stack + Environment:** frameworks, hosting, runtime, database, auth approach +- **UI + Routes:** key screens, navigation, route map, layout constraints +- **Data Model + Import Format:** entities, relationships, external data shape +- **Rules/Calculations:** business logic, progression rules, edge cases +- **Quality Gates:** tests, lint, typecheck, build/dev verification (REQUIRED) + +Always ask explicitly: +- **Is this a new project or an existing codebase?** + +### Format Questions Like This: + +``` +1. What is the primary goal of this feature? + A. Improve user onboarding experience + B. Increase user retention + C. Reduce support burden + D. Other: [please specify] + +2. Who is the target user? + A. New users only + B. Existing users only + C. All users + D. Admin users only + +3. What quality commands must pass for each story? + A. npm test + B. npm run lint + C. npm run typecheck + D. Other: [specify] + +Note: All example questions and options in this section are illustrative only. Do not copy them verbatim into the PRD unless the user explicitly chooses them. Derive the final quality gates and requirements from the user's answers and the feature context. + +4. What stack + hosting are we using (and any constraints)? + A. React + Vite (static hosting) + B. Next.js (Node/Edge) + C. TanStack Start (Cloudflare) + D. Other: [specify] + +5. What UI screens/routes are required? + A. Minimal (1–2 pages) + B. Basic app shell (dashboard + detail pages) + C. Full routing map (list all routes) + D. Other: [specify] +``` + +--- + +## Step 2: JSON Structure + +Output a JSON file with this shape (include detailed top-level fields so the PRD is fully self-contained): + +```json +{ + "version": 1, + "project": "Feature Name", + "overview": "Short problem + solution summary", + "goals": [ + "Goal 1", + "Goal 2" + ], + "nonGoals": [ + "Explicitly out of scope items" + ], + "successMetrics": [ + "How success is measured" + ], + "openQuestions": [ + "Remaining unknowns" + ], + "stack": { + "framework": "TanStack Start", + "hosting": "Cloudflare", + "database": "D1", + "auth": "describe approach" + }, + "routes": [ + { "path": "/", "name": "Home", "purpose": "..." } + ], + "uiNotes": [ + "Layout or component requirements" + ], + "dataModel": [ + { "entity": "Workout", "fields": ["id", "userId", "date", "notes"] } + ], + "importFormat": { + "description": "Expected JSON shape", + "example": { "programName": "..." } + }, + "rules": [ + "Key business rules / calculations" + ], + "qualityGates": ["npm run test:ping"], + "stories": [ + { + "id": "US-001", + "title": "Short story title", + "status": "open", + "dependsOn": [], + "description": "As a [user], I want [feature] so that [benefit].", + "acceptanceCriteria": [ + "Specific verifiable criterion", + "Another criterion" + ] + } + ] +} +``` + +### Rules +- **IDs**: Sequential (`US-001`, `US-002`, ...) +- **Status**: Always `"open"` for new stories +- **DependsOn**: Use IDs only; empty array if none +- **Quality Gates**: Only at the top-level `qualityGates` +- **Acceptance Criteria**: Verifiable, specific, testable +- **Every story must include**: at least 1 example + 1 negative case +- **UI stories**: include explicit routes, components, and UI states +- **New projects**: include initial setup stories (scaffold, env/config, local dev, deploy basics, **package installs**) +- **Dependencies**: any new package/library introduced must be called out with install commands in acceptance criteria (e.g., `npm install `), plus any required config or scripts. +- **Ordering**: if this is a new project, the **first story must be setup** (scaffold + installs + scripts + env/config). Migrations or data model work come after setup. + +--- + +## Output Requirements + +- Save the JSON to the exact path provided in the prompt (e.g., `.agents/tasks/prd-.json`) +- Output **only** the JSON file content (no Markdown PRD) +- Do not include extra commentary in the file + +After saving, tell the user: +`PRD JSON saved to . Close this chat and run \`ralph build\`.` + +If the prompt provides a **directory** (not a filename), choose a short filename: +- `prd-.json` where `` is 1–3 meaningful words (avoid filler like “i want to”). +- Examples: `prd-workout-tracker.json`, `prd-usage-billing.json` + +--- + +## Story Size (Critical) + +Each story must be completable in a **single Ralph iteration**. +If a story feels too large, split it into multiple smaller stories with dependencies. + +--- + +## Example Output (JSON) + +```json +{ + "version": 1, + "project": "Task Priority System", + "overview": "Add priority levels to tasks so users can focus on what matters most.", + "goals": [ + "Allow assigning priority (high/medium/low) to any task", + "Enable filtering by priority" + ], + "nonGoals": [ + "No automatic priority assignment" + ], + "successMetrics": [ + "Users can change priority in under 2 clicks" + ], + "openQuestions": [ + "Should priority affect ordering within a column?" + ], + "stack": { + "framework": "React", + "hosting": "Cloudflare Pages", + "database": "D1", + "auth": "single shared login" + }, + "routes": [ + { "path": "/tasks", "name": "Task List", "purpose": "View and filter tasks" }, + { "path": "/tasks/:id", "name": "Task Detail", "purpose": "Edit task priority" } + ], + "uiNotes": [ + "Priority badge colors: high=red, medium=yellow, low=gray" + ], + "dataModel": [ + { "entity": "Task", "fields": ["id", "title", "priority"] } + ], + "importFormat": { + "description": "Not applicable", + "example": {} + }, + "rules": [ + "Priority defaults to medium when not set" + ], + "qualityGates": ["npm run test:ping"], + "stories": [ + { + "id": "US-001", + "title": "Add priority field to database", + "status": "open", + "dependsOn": [], + "description": "As a developer, I want to store task priority so it persists across sessions.", + "acceptanceCriteria": [ + "Add priority column with default 'medium'", + "Example: creating a task without priority -> defaults to 'medium'", + "Negative case: invalid priority 'urgent' -> validation error", + "Migration runs successfully" + ] + } + ] +} +``` diff --git a/.ralph/activity.log b/.ralph/activity.log index 48bc0e7..4cc32b9 100644 --- a/.ralph/activity.log +++ b/.ralph/activity.log @@ -1,6 +1,7 @@ # Activity Log ## Run Summary +- 2026-01-25 20:47:05 | run=20260125-204012-65223 | iter=1 | mode=build | story=US-001 | duration=413s | status=success ## Events @@ -9,3 +10,8 @@ [2026-01-25 20:42:52] Added oversized file inventory and split plan [2026-01-25 20:43:19] Completed verification commands (test/lint/typecheck/build) [2026-01-25 20:46:15] Updated progress log for US-001 +[2026-01-25 20:47:05] ITERATION 1 end (duration=413s) +[2026-01-25 20:47:07] ITERATION 2 start (mode=build story=US-002) +[2026-01-25 20:47:25] Start US-002 work +[2026-01-25 20:59:27] Refactored swipe screen into feature modules (hooks/services/components) +[2026-01-25 21:02:54] Completed verification commands (test/lint/typecheck/build/web) diff --git a/.ralph/guardrails.md b/.ralph/guardrails.md new file mode 100644 index 0000000..e7b8dff --- /dev/null +++ b/.ralph/guardrails.md @@ -0,0 +1,20 @@ +# Guardrails (Signs) + +> Lessons learned from failures. Read before acting. + +## Core Signs + +### Sign: Read Before Writing +- **Trigger**: Before modifying any file +- **Instruction**: Read the file first +- **Added after**: Core principle + +### Sign: Test Before Commit +- **Trigger**: Before committing changes +- **Instruction**: Run required tests and verify outputs +- **Added after**: Core principle + +--- + +## Learned Signs + diff --git a/.ralph/runs/run-20260125-204012-65223-iter-1.md b/.ralph/runs/run-20260125-204012-65223-iter-1.md new file mode 100644 index 0000000..e7468a7 --- /dev/null +++ b/.ralph/runs/run-20260125-204012-65223-iter-1.md @@ -0,0 +1,34 @@ +# Ralph Run Summary + +- Run ID: 20260125-204012-65223 +- Iteration: 1 +- Mode: build +- Story: US-001: Inventory oversized files and refactor plan +- Started: 2026-01-25 20:40:12 +- Ended: 2026-01-25 20:47:05 +- Duration: 413s +- Status: success +- Log: /Users/jjbrunton/Projects/Sauci/.ralph/runs/run-20260125-204012-65223-iter-1.log + +## Git +- Head (before): 45499edc9a0b78094601f577c707da2374b1765d +- Head (after): 209dc652d2aaf04d31fe6dd187a28cf2da8ffd67 + +### Commits +- 209dc65 chore(ralph): update progress log +- 06dfc5b docs(inventory): add oversized file plan + +### Changed Files (commits) +- .ralph/activity.log +- .ralph/progress.md +- docs/oversized-file-inventory.md + +### Uncommitted Changes +- .ralph/activity.log +- apps/mobile/android/app/build.gradle +- apps/mobile/app.json +- apps/mobile/ios/Sauci.xcodeproj/project.pbxproj +- .agents/ +- .codex/ +- .ralph/guardrails.md + diff --git a/apps/mobile/android/app/build.gradle b/apps/mobile/android/app/build.gradle index 1e02b30..4424df0 100644 --- a/apps/mobile/android/app/build.gradle +++ b/apps/mobile/android/app/build.gradle @@ -93,7 +93,7 @@ android { applicationId 'com.sauci.app' minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 34 + versionCode 35 versionName "1.0.2" } signingConfigs { diff --git a/apps/mobile/app.json b/apps/mobile/app.json index eed5342..6a3ecff 100644 --- a/apps/mobile/app.json +++ b/apps/mobile/app.json @@ -177,7 +177,7 @@ "backgroundColor": "#1a1a2e" }, "package": "com.sauci.app", - "versionCode": 34, + "versionCode": 35, "googleServicesFile": "./google-services.json", "permissions": [ "RECEIVE_BOOT_COMPLETED", diff --git a/apps/mobile/app/(app)/swipe.tsx b/apps/mobile/app/(app)/swipe.tsx index b6e7743..ecee82b 100755 --- a/apps/mobile/app/(app)/swipe.tsx +++ b/apps/mobile/app/(app)/swipe.tsx @@ -1,1675 +1 @@ -import { useState, useEffect, useRef, useCallback } from "react"; -import { View, StyleSheet, Text, ActivityIndicator, Platform, TouchableOpacity } from "react-native"; -import { useLocalSearchParams, router, useFocusEffect } from "expo-router"; -import { Ionicons } from "@expo/vector-icons"; -import * as FileSystem from "expo-file-system"; -import { decode } from "base64-arraybuffer"; -import Animated, { - FadeIn, - FadeInUp, - useSharedValue, - useAnimatedStyle, - withRepeat, - withTiming, - interpolate, - Easing, -} from "react-native-reanimated"; -import { LinearGradient } from "expo-linear-gradient"; -import { supabase } from "../../src/lib/supabase"; -import { usePacksStore, useAuthStore } from "../../src/store"; -import { useAmbientOrbAnimation } from "../../src/hooks"; -import { skipQuestion, getSkippedQuestionIds } from "../../src/lib/skippedQuestions"; -import { invokeWithAuthRetry } from "../../src/lib/authErrorHandler"; -import { - QuestionCard, - QuestionCardText, - QuestionCardPhoto, - QuestionCardAudio, - QuestionCardWhoLikely -} from "../../src/components/questions"; -import { QuestionFeedbackModal } from "../../src/components/feedback"; -import { MatchConfetti } from "../../src/components/MatchConfetti"; -import { GradientBackground, GlassCard, GlassButton, DecorativeSeparator } from "../../src/components/ui"; -import { Paywall } from "../../src/components/paywall"; -import { Events } from "../../src/lib/analytics"; -import { colors, gradients, spacing, typography, radius, shadows, getCategoryColor } from "../../src/theme"; -import type { AnswerType } from "../../src/types"; - -// Response data types for different question types -interface TextResponseData { - type: 'text_answer'; - text: string; -} - -interface PhotoResponseData { - type: 'photo'; - media_path: string; -} - -interface AudioResponseData { - type: 'audio'; - media_path: string; - duration_seconds: number; -} - -interface WhoLikelyResponseData { - type: 'who_likely'; - chosen_user_id: string; -} - -type ResponseData = TextResponseData | PhotoResponseData | AudioResponseData | WhoLikelyResponseData; - -interface DailyLimitInfo { - responses_today: number; - limit_value: number; - remaining: number; - reset_at: string; - is_blocked: boolean; -} - -export default function SwipeScreen() { - const params = useLocalSearchParams(); - // Normalize packId - useLocalSearchParams can return string | string[] - const packId = Array.isArray(params.packId) ? params.packId[0] : params.packId; - // Mode can be 'pending' to show only questions partner has answered - const mode = Array.isArray(params.mode) ? params.mode[0] : params.mode; - // Start question ID - when user taps a specific pending question, start there - const startQuestionId = Array.isArray(params.startQuestionId) ? params.startQuestionId[0] : params.startQuestionId; - const [questions, setQuestions] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [currentIndex, setCurrentIndex] = useState(0); - const [feedbackQuestion, setFeedbackQuestion] = useState<{id: string, text: string} | null>(null); - const [isUploading, setIsUploading] = useState(false); - const [isBlocked, setIsBlocked] = useState(false); - const [gapInfo, setGapInfo] = useState<{ unanswered: number; threshold: number } | null>(null); - const [dailyLimitInfo, setDailyLimitInfo] = useState(null); - const [showPaywall, setShowPaywall] = useState(false); - const [packContext, setPackContext] = useState<{ name: string; icon: string } | null>(null); - const [showConfetti, setShowConfetti] = useState(false); - const { enabledPackIds, ensureEnabledPacksLoaded, packs } = usePacksStore(); - const { user, partner, couple } = useAuthStore(); - - // Supported question types - filter out types the UI doesn't support yet - const supportedTypes = ['swipe', 'text_answer', 'photo', 'audio', 'who_likely']; - - // Filter questions to only supported types (safety during rollout) - // Must be calculated early since it's used by effectiveTotal and throughout rendering - const filteredQuestions = questions.filter(q => - !q.question_type || supportedTypes.includes(q.question_type) - ); - const hasTrackedExhausted = useRef(false); - const fetchRequestId = useRef(0); - const lastFetchedMode = useRef(undefined); - - // Ambient orb breathing animations - const { orbStyle1, orbStyle2 } = useAmbientOrbAnimation(); - - // Progress bar shimmer animation - const shimmerPosition = useSharedValue(-1); - - // Countdown for daily limit reset - const [countdown, setCountdown] = useState(""); - - useEffect(() => { - if (!dailyLimitInfo?.is_blocked || !dailyLimitInfo.reset_at) { - setCountdown(""); - return; - } - - const updateCountdown = () => { - const now = new Date(); - const reset = new Date(dailyLimitInfo.reset_at); - const diff = reset.getTime() - now.getTime(); - - if (diff <= 0) { - setCountdown("00:00:00"); - // Refresh questions when timer expires - fetchQuestions(); - return; - } - - const hours = Math.floor(diff / (1000 * 60 * 60)); - const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); - const seconds = Math.floor((diff % (1000 * 60)) / 1000); - - setCountdown(`${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`); - }; - - updateCountdown(); - const interval = setInterval(updateCountdown, 1000); - - return () => clearInterval(interval); - }, [dailyLimitInfo?.is_blocked, dailyLimitInfo?.reset_at]); - - useEffect(() => { - // Progress bar shimmer sweep - 2.5 second cycle - shimmerPosition.value = withRepeat( - withTiming(2, { duration: 2500, easing: Easing.linear }), - -1, - false - ); - }, []); - - const shimmerStyle = useAnimatedStyle(() => ({ - transform: [ - { translateX: interpolate(shimmerPosition.value, [-1, 2], [-60, 220]) }, - ], - })); - - const isFirstMount = useRef(true); - - useEffect(() => { - // Reset state when pack or mode changes - setCurrentIndex(0); - setIsLoading(true); - setIsBlocked(false); - setGapInfo(null); - hasTrackedExhausted.current = false; - lastFetchedMode.current = mode; - - // Use lightweight ensureEnabledPacksLoaded instead of full fetchPacks - ensureEnabledPacksLoaded().then(() => { - if (mode === 'pending') { - fetchPendingQuestions(); - } else { - fetchQuestions(); - } - }); - }, [packId, mode, startQuestionId]); - - // Fetch pack context when packId is provided - useEffect(() => { - if (packId) { - const fetchPackContext = async () => { - const { data } = await supabase - .from('question_packs') - .select('name, icon') - .eq('id', packId) - .single(); - if (data) { - setPackContext({ name: data.name, icon: data.icon || 'layers' }); - } - }; - fetchPackContext(); - } else { - setPackContext(null); - } - }, [packId]); - - // Refresh questions when screen gains focus (handles partner answering while away) - useFocusEffect( - useCallback(() => { - // Skip on first mount since useEffect already fetches - if (isFirstMount.current) { - isFirstMount.current = false; - return; - } - // Skip if mode just changed (useEffect handles that) - if (lastFetchedMode.current !== mode) { - return; - } - // Silently refetch to get updated status - if (mode === 'pending') { - fetchPendingQuestions(); - } else { - fetchQuestions(); - } - }, [packId, mode]) - ); - - // Check if user is too far ahead of partner (called when questions empty or after swipe) - const checkAnswerGap = async (): Promise => { - if (!couple || !partner) { - setIsBlocked(false); - setGapInfo(null); - return false; - } - - try { - const { data, error } = await supabase.rpc('get_answer_gap_status'); - - if (error) { - console.error('Error checking answer gap:', error); - // Clear blocked state on error so user isn't stuck - setIsBlocked(false); - setGapInfo(null); - return false; - } - - if (data && data.length > 0) { - const result = data[0]; - setIsBlocked(result.is_blocked); - // Only set gap info if threshold is enabled (> 0) - if (result.threshold > 0) { - setGapInfo({ - unanswered: result.unanswered_by_partner, - threshold: result.threshold - }); - } else { - setGapInfo(null); - } - return result.is_blocked; - } - - // No data returned - clear blocked state - setIsBlocked(false); - setGapInfo(null); - return false; - } catch (error) { - console.error('Failed to check answer gap:', error); - // Clear blocked state on error so user isn't stuck - setIsBlocked(false); - setGapInfo(null); - return false; - } - }; - - // Check if user has reached daily response limit - const checkDailyLimit = async (): Promise => { - try { - const { data, error } = await supabase.rpc('get_daily_response_limit_status'); - - if (error) { - console.error('Error checking daily limit:', error); - setDailyLimitInfo(null); - return false; - } - - if (data && data.length > 0) { - const result = data[0]; - setDailyLimitInfo(result); - return result.is_blocked; - } - - setDailyLimitInfo(null); - return false; - } catch (error) { - console.error('Failed to check daily limit:', error); - setDailyLimitInfo(null); - return false; - } - }; - - // Calculate effective total questions (accounting for gap limit AND daily limit) - const effectiveTotal = (() => { - let total = filteredQuestions.length; - - // Apply gap limit if active - if (gapInfo && gapInfo.threshold > 0) { - const remaining = gapInfo.threshold - gapInfo.unanswered; - total = Math.min(total, currentIndex + Math.max(0, remaining)); - } - - // Apply daily limit if active - if (dailyLimitInfo && dailyLimitInfo.limit_value > 0 && !dailyLimitInfo.is_blocked) { - total = Math.min(total, currentIndex + dailyLimitInfo.remaining); - } - - return total; - })(); - - // Track when all questions are exhausted - useEffect(() => { - if (filteredQuestions.length > 0 && currentIndex >= filteredQuestions.length && !hasTrackedExhausted.current) { - hasTrackedExhausted.current = true; - Events.allQuestionsExhausted(); - } - }, [currentIndex, filteredQuestions.length]); - - // Filter out questions from disabled packs immediately when enabledPackIds changes - // Skip this filter in pending mode - user should answer ALL questions partner swiped on - useEffect(() => { - if (!packId && mode !== 'pending' && enabledPackIds.length > 0 && questions.length > 0) { - const enabledSet = new Set(enabledPackIds); - setQuestions(prev => { - const filtered = prev.filter(q => enabledSet.has(q.pack_id)); - // Adjust currentIndex if it's now beyond the filtered list - if (currentIndex >= filtered.length && filtered.length > 0) { - setCurrentIndex(filtered.length - 1); - } - return filtered; - }); - } - }, [enabledPackIds, mode]); - - - const fetchQuestions = async () => { - // Race protection: increment request ID and track current request - const currentRequestId = ++fetchRequestId.current; - - try { - const [{ data, error }, skippedIds] = await Promise.all([ - supabase.rpc("get_recommended_questions", { - target_pack_id: packId || null - }), - getSkippedQuestionIds() - ]); - - // Race guard: if a newer request started, discard this result - if (currentRequestId !== fetchRequestId.current) { - return; - } - - if (error) { - console.error("Error fetching recommended questions:", error); - throw error; - } - - // Filter out recently skipped questions - let filtered = (data || []).filter((q: any) => !skippedIds.has(q.id)); - - // If no unskipped questions remain, show skipped ones again - if (filtered.length === 0 && data && data.length > 0) { - filtered = data; - } - - // Always check answer gap and daily limit when user has partner and not viewing specific pack - if (partner && !packId) { - await Promise.all([checkAnswerGap(), checkDailyLimit()]); - } else { - // Reset blocked state if no partner or viewing specific pack - setIsBlocked(false); - setGapInfo(null); - setDailyLimitInfo(null); - } - - // Server provides ordering (daily seeded random with priority) - // Only update state if still the current request - if (currentRequestId === fetchRequestId.current) { - setQuestions(filtered); - } - } catch (error) { - if (currentRequestId === fetchRequestId.current) { - console.error(error); - } - } finally { - if (currentRequestId === fetchRequestId.current) { - setIsLoading(false); - } - } - }; - - // Fetch pending questions (partner answered, user hasn't) for "Your Turn" mode - const fetchPendingQuestions = async () => { - const currentRequestId = ++fetchRequestId.current; - const userId = useAuthStore.getState().user?.id; - const coupleId = useAuthStore.getState().user?.couple_id; - - if (!coupleId || !userId) { - if (currentRequestId === fetchRequestId.current) { - setQuestions([]); - setIsLoading(false); - } - return; - } - - try { - // Get partner's user_id - const { data: coupleProfiles } = await supabase - .from("profiles") - .select("id") - .eq("couple_id", coupleId) - .neq("id", userId); - - const partnerId = coupleProfiles?.[0]?.id; - if (!partnerId) { - if (currentRequestId === fetchRequestId.current) { - setQuestions([]); - setIsLoading(false); - } - return; - } - - // Get questions the current user has already answered - const { data: userResponses } = await supabase - .from("responses") - .select("question_id") - .eq("user_id", userId) - .eq("couple_id", coupleId); - - const answeredQuestionIds = new Set(userResponses?.map(r => r.question_id) ?? []); - - // Get partner's responses (questions they answered that user hasn't) - // Order by oldest first so user burns down the backlog - const { data: partnerResponses, error } = await supabase - .from("responses") - .select(` - id, - question_id, - created_at, - question:questions( - *, - pack:question_packs(id, name, icon) - ) - `) - .eq("user_id", partnerId) - .eq("couple_id", coupleId) - .order("created_at", { ascending: true }); // Oldest first - - if (currentRequestId !== fetchRequestId.current) return; - - if (error) { - console.error("Error fetching pending questions:", error); - throw error; - } - - // Filter to only questions user hasn't answered, excluding deleted questions - const pendingQuestions = (partnerResponses ?? []) - .filter(r => { - const question = r.question as any; - return !answeredQuestionIds.has(r.question_id) && question && !question.deleted_at; - }) - .map(r => { - const question = r.question as any; - return { - ...question, - pack_id: question.pack?.id, - pack_name: question.pack?.name, - pack_icon: question.pack?.icon, - }; - }); - - if (currentRequestId === fetchRequestId.current) { - // If user tapped a specific question, reorder so it's first - let orderedQuestions = pendingQuestions; - if (startQuestionId && pendingQuestions.length > 0) { - const startIndex = pendingQuestions.findIndex(q => q.id === startQuestionId); - if (startIndex !== -1) { - // Put tapped question first, then the rest in original order - const tappedQuestion = pendingQuestions[startIndex]; - const rest = pendingQuestions.filter((_, i) => i !== startIndex); - orderedQuestions = [tappedQuestion, ...rest]; - } - } - - setQuestions(orderedQuestions); - setCurrentIndex(0); // Always start at 0 now since we reordered - - // Don't check gap/daily limits in pending mode - user is just catching up - setIsBlocked(false); - setGapInfo(null); - setDailyLimitInfo(null); - } - } catch (error) { - if (currentRequestId === fetchRequestId.current) { - console.error("Failed to fetch pending questions:", error); - setQuestions([]); - } - } finally { - if (currentRequestId === fetchRequestId.current) { - setIsLoading(false); - } - } - }; - - // Upload media (photo/audio) to Supabase storage - const uploadResponseMedia = async ( - localUri: string, - questionId: string, - mediaType: 'photo' | 'audio' - ): Promise => { - if (!user?.id) return null; - - try { - const timestamp = Date.now(); - const extMatch = localUri.match(/\.(\w+)$/); - const ext = extMatch ? extMatch[1] : (mediaType === 'photo' ? 'jpg' : 'm4a'); - const fileName = `${user.id}/${questionId}_${timestamp}.${ext}`; - const contentType = mediaType === 'photo' - ? `image/${ext === 'jpg' ? 'jpeg' : ext}` - : `audio/${ext}`; - - let fileBody: ArrayBuffer | Blob; - - if (Platform.OS === 'web') { - const response = await fetch(localUri); - fileBody = await response.blob(); - } else { - const base64 = await FileSystem.readAsStringAsync(localUri, { - encoding: FileSystem.EncodingType.Base64, - }); - fileBody = decode(base64); - } - - const { error } = await supabase.storage - .from('response-media') - .upload(fileName, fileBody, { contentType }); - - if (error) { - console.error('Media upload error:', error); - return null; - } - - return fileName; - } catch (error) { - console.error('Media upload failed:', error); - return null; - } - }; - - // Handle answer submission from question cards (supports response_data for new question types) - const handleAnswer = async (questionId: string, answer: AnswerType | 'skip', responseData?: ResponseData) => { - const question = filteredQuestions[currentIndex]; - - // Handle skip - don't submit a response, just store it for later - if (answer === 'skip') { - setCurrentIndex(prev => prev + 1); - await skipQuestion(questionId); - Events.questionSkipped(); - return; - } - - try { - // Upload media if response contains photo or audio with local URI - let finalResponseData = responseData; - if (responseData) { - if (responseData.type === 'photo' && responseData.media_path) { - setIsUploading(true); - try { - const uploadedPath = await uploadResponseMedia(responseData.media_path, questionId, 'photo'); - if (uploadedPath) { - finalResponseData = { ...responseData, media_path: uploadedPath }; - } else { - console.error('Photo upload failed, skipping response_data'); - finalResponseData = undefined; - } - } finally { - setIsUploading(false); - } - } else if (responseData.type === 'audio' && responseData.media_path) { - setIsUploading(true); - try { - const uploadedPath = await uploadResponseMedia(responseData.media_path, questionId, 'audio'); - if (uploadedPath) { - finalResponseData = { ...responseData, media_path: uploadedPath }; - } else { - console.error('Audio upload failed, skipping response_data'); - finalResponseData = undefined; - } - } finally { - setIsUploading(false); - } - } - } - - const { data, error } = await invokeWithAuthRetry("submit-response", { - body: { - question_id: questionId, - answer, - response_data: finalResponseData, - }, - }); - - if (error) { - console.error("Submit response error:", error); - // Advance to next question even on error - setCurrentIndex(prev => prev + 1); - } else { - Events.questionAnswered(answer, question.pack_id); - - // Check if a match was created - const hasMatch = data?.match != null; - - if (hasMatch) { - // Show confetti celebration - delay advancing to next question - setShowConfetti(true); - // Confetti component will call onAnimationComplete which advances - } else { - // No match - advance immediately - setCurrentIndex(prev => prev + 1); - } - - // Skip limit checks in pending mode - user is just catching up - if (mode !== 'pending') { - // Increment local daily count if we have limit info - if (dailyLimitInfo && dailyLimitInfo.limit_value > 0) { - setDailyLimitInfo(prev => prev ? { - ...prev, - responses_today: prev.responses_today + 1, - remaining: Math.max(0, prev.remaining - 1), - is_blocked: prev.responses_today + 1 >= prev.limit_value - } : null); - } - - // Check both gap and daily limit - await Promise.all([checkAnswerGap(), checkDailyLimit()]); - } - } - } catch (error) { - console.error("Failed to submit response", error); - // Advance to next question even on error - setCurrentIndex(prev => prev + 1); - } - }; - - // Handle confetti animation completion - const handleConfettiComplete = useCallback(() => { - setShowConfetti(false); - setCurrentIndex(prev => prev + 1); - }, []); - - // Get pack info for a question - use packContext if viewing a specific pack, otherwise question's pack - const getQuestionPackInfo = (question: any) => { - // If viewing specific pack, use that context but try to enrich with color from store - if (packId) { - const pack = packs.find(p => p.id === packId); - const categoryColor = pack ? getCategoryColor(pack.category) : undefined; - - if (packContext) { - return { ...packContext, color: categoryColor }; - } - if (pack) { - return { name: pack.name, icon: pack.icon || 'layers', color: categoryColor }; - } - } - - // For pending mode or mixed mode, look up pack details - if (question.pack_id) { - const pack = packs.find(p => p.id === question.pack_id); - const categoryColor = pack ? getCategoryColor(pack.category) : undefined; - - // Pending questions might have name/icon embedded if pack not found in store - if (question.pack_name) { - return { - name: question.pack_name, - icon: question.pack_icon || 'layers', - color: categoryColor - }; - } - - if (pack) { - return { name: pack.name, icon: pack.icon || 'layers', color: categoryColor }; - } - } - - return null; - }; - - // Render the appropriate question card based on question_type - const renderQuestionCard = (question: any) => { - // For backwards compatibility: default to 'swipe' if no question_type - const questionType = question.question_type || 'swipe'; - const packInfo = getQuestionPackInfo(question); - - switch (questionType) { - case 'text_answer': - return ( - handleAnswer(question.id, answer, responseData)} - onReport={() => setFeedbackQuestion({ - id: question.id, - text: question.text - })} - /> - ); - case 'photo': - return ( - handleAnswer(question.id, answer, responseData)} - onReport={() => setFeedbackQuestion({ - id: question.id, - text: question.text - })} - /> - ); - case 'audio': - return ( - handleAnswer(question.id, answer, responseData)} - onReport={() => setFeedbackQuestion({ - id: question.id, - text: question.text - })} - /> - ); - case 'who_likely': - return ( - handleAnswer(question.id, 'yes', responseData)} - onSkip={() => handleAnswer(question.id, 'skip')} - onReport={() => setFeedbackQuestion({ - id: question.id, - text: question.text - })} - /> - ); - case 'swipe': - default: - // Standard swipe card for 'swipe' type and any unknown types (backwards compatibility) - return ( - handleAnswer(question.id, answer)} - onReport={() => setFeedbackQuestion({ - id: question.id, - text: question.text - })} - /> - ); - } - }; - - if (isLoading) { - return ( - - - - - - ); - } - - // Show "pair with partner" prompt when user doesn't have a partner yet - if (!partner) { - const PAIR_ACCENT = colors.premium.rose; - return ( - - - - {/* Icon */} - - - - - {/* Title section */} - {couple ? "ALMOST THERE" : "CONNECT"} - {couple ? "Waiting" : "Pair Up"} - - - - {/* Status badge */} - - {couple ? "INVITE SENT" : "MADE FOR TWO"} - - - {/* Description */} - - {couple - ? "Share your invite code so they can join you. Once paired, you'll both answer questions and discover what you agree on!" - : "Sauci is made for two! Connect with your partner to start answering questions together and discover what you have in common." - } - - - {/* Feature hints */} - - - - Private and secure - - - - Discover hidden desires - - - - Chat about your matches - - - - {/* Bottom teaser */} - {couple ? "Your partner is just a code away" : "Begin your intimate journey together"} - - router.push("/pairing")} - style={{ marginTop: spacing.lg }} - > - {couple ? "View Invite Code" : "Pair Now"} - - - - - ); - } - - // Show "daily limit reached" screen when user has exhausted their daily quota - if (dailyLimitInfo?.is_blocked) { - const LIMIT_ACCENT = colors.premium.gold; - return ( - - {/* Ambient Orbs - Gold/warm glow - Commented out for flat design - - - - - - - */} - - - - {/* Icon with glow effect */} - - - - - - - - {/* Title section */} - DAILY LIMIT REACHED - Take a Breather - - - - {/* Countdown timer - prominent display */} - - REFRESHES IN - - - {countdown} - - - - {/* Description */} - - You've answered {dailyLimitInfo.limit_value} questions today!{'\n'} - Let the anticipation build while you wait. - - - {/* Feature hints - vertical list */} - - - - Chat about your matches - - - - Fresh questions tomorrow - - - - Go unlimited with Premium - - - - {/* Premium upsell button */} - Want to keep exploring? - setShowPaywall(true)} - style={styles.premiumButton} - activeOpacity={0.85} - > - - - - Unlock Unlimited - - - - - - {/* Paywall Modal */} - setShowPaywall(false)} - onSuccess={() => { - setShowPaywall(false); - // Refresh to clear limit status - fetchQuestions(); - }} - /> - - ); - } - - // Show "waiting for partner" message when user is too far ahead - if (isBlocked && gapInfo) { - const WAITING_ACCENT = colors.premium.rose; - return ( - - - - {/* Icon */} - - - - - {/* Title section */} - PATIENCE - Waiting - - - - {/* Status badge */} - - {gapInfo.unanswered} UNANSWERED - - - {/* Description */} - - You're ahead by {gapInfo.unanswered} questions. Give your partner some time to catch up so you can discover matches together. - - - {/* Feature hints */} - - - - Matches happen together - - - - We'll notify your partner - - - - Check back anytime - - - - {/* Bottom teaser */} - Good things come to those who wait - - - Check Again - - - - - ); - } - - // Show "no packs enabled" message when not viewing a specific pack and no packs are enabled - if (!packId && enabledPackIds.length === 0) { - const NO_PACKS_ACCENT = colors.premium.rose; - return ( - - - - {/* Icon */} - - - - - {/* Title section */} - GET STARTED - Choose Packs - - - - {/* Status badge */} - - NO PACKS ENABLED - - - {/* Description */} - - Select the question packs that interest you and your partner. Each pack explores different aspects of your relationship. - - - {/* Feature hints */} - - - - From playful to passionate - - - - Safe space to explore - - - - New packs added regularly - - - - {/* Bottom teaser */} - Your journey of discovery awaits - - router.push("/")} - style={{ marginTop: spacing.lg }} - > - Browse Packs - - - - - ); - } - - if (currentIndex >= filteredQuestions.length) { - const CAUGHT_UP_ACCENT = colors.premium.rose; - const isPackMode = !!packId; - const isPendingMode = mode === 'pending'; - return ( - - - - {/* Icon */} - - - - - {/* Title section */} - - {isPendingMode ? 'QUEUE COMPLETE' : isPackMode ? 'PACK COMPLETE' : 'COMPLETE'} - - - {isPendingMode ? 'All Caught Up!' : isPackMode ? packContext?.name || 'Pack Complete' : 'All Caught Up'} - - - - - {/* Status badge */} - - - {isPendingMode ? 'YOUR TURN COMPLETE' : isPackMode ? 'ALL QUESTIONS ANSWERED' : "YOU'RE AHEAD"} - - - - {/* Description */} - - {isPendingMode - ? "You've answered all the questions your partner swiped on. Nice work! Check back later for more." - : isPackMode - ? "You've answered all questions in this pack. Head home to discover more!" - : "You've answered all available questions. New questions are added regularly, or explore different packs from home." - } - - - {/* Feature hints */} - - {isPendingMode ? ( - <> - - - Check your matches - - - - Chat about discoveries - - - - Keep exploring together - - - ) : ( - <> - - - New questions weekly - - - - Explore from home - - - - Chat about your matches - - - )} - - - {/* Bottom teaser */} - - {isPendingMode - ? 'Your partner will love that you caught up' - : isPackMode - ? 'Discover more ways to connect' - : 'More ways to connect are on the way'} - - - {isPendingMode ? ( - router.push("/(app)/matches")} - style={{ marginTop: spacing.lg }} - > - View Matches - - ) : isPackMode ? ( - router.push("/")} - style={{ marginTop: spacing.lg }} - > - Back to Home - - ) : ( - - Refresh Questions - - )} - - - - ); - } - - return ( - - {/* Ambient Orbs - Premium gold/rose - Commented out for flat design - - - - - - - */} - - - {/* Header */} - - - {/* Back button when playing specific pack or pending mode */} - {(packId || mode === 'pending') && ( - router.back()} style={styles.backButton}> - - - )} - - {/* Premium label */} - - {mode === 'pending' ? 'YOUR TURN' : packContext ? packContext.name.toUpperCase() : 'EXPLORE'} - - - {currentIndex + 1} of {effectiveTotal || filteredQuestions.length} - - - - {/* Shimmer sweep */} - - - - - - - - - - {/* Card Stack */} - - {/* Third card (deepest) */} - {filteredQuestions[currentIndex + 2] && ( - - )} - - {/* Second card */} - {filteredQuestions[currentIndex + 1] && ( - - )} - - {/* Active card - render appropriate component based on question_type */} - {renderQuestionCard(filteredQuestions[currentIndex])} - - - {/* Bottom spacing for tab bar */} - - - - {/* Question Feedback Modal */} - setFeedbackQuestion(null)} - questionId={feedbackQuestion?.id || ''} - questionText={feedbackQuestion?.text || ''} - /> - - {/* Match Confetti Animation */} - - - {/* Paywall Modal */} - setShowPaywall(false)} - onSuccess={() => { - setShowPaywall(false); - // Refresh to clear limit status - fetchQuestions(); - }} - /> - - {/* Upload spinner overlay */} - {isUploading && ( - - - - Uploading... - - - )} - - ); -} - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - centerContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - }, - // Ambient orbs - more visible - ambientOrb: { - position: 'absolute', - width: 300, - height: 300, - borderRadius: 150, - }, - orbTopRight: { - top: 60, - right: -40, - }, - orbBottomLeft: { - bottom: 180, - left: -40, - }, - orbGradient: { - width: '100%', - height: '100%', - borderRadius: 150, - }, - header: { - paddingTop: 60, - paddingHorizontal: spacing.lg, - paddingBottom: spacing.md, - }, - headerRow: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - }, - backButton: { - position: 'absolute', - left: 0, - padding: spacing.sm, - }, - progressContainer: { - alignItems: "center", - }, - progressLabel: { - ...typography.caption2, - fontWeight: '600', - letterSpacing: 3, - color: colors.premium.gold, - marginBottom: spacing.xs, - }, - progressText: { - ...typography.subhead, - color: colors.textSecondary, - marginBottom: spacing.sm, - }, - progressBar: { - width: 160, - height: 6, - backgroundColor: colors.glass.background, - borderRadius: 3, - overflow: "hidden", - }, - progressBarPremium: { - width: 140, - height: 4, - borderRadius: 2, - backgroundColor: 'rgba(212, 175, 55, 0.1)', - borderWidth: 1, - borderColor: 'rgba(212, 175, 55, 0.15)', - }, - progressFill: { - height: "100%", - borderRadius: 4, - overflow: "hidden", - }, - progressFillPremium: { - backgroundColor: 'rgba(212, 175, 55, 0.5)', - borderRadius: 2, - }, - progressGradient: { - flex: 1, - borderRadius: 4, - }, - progressShimmer: { - position: 'absolute', - width: 60, - height: '100%', - top: 0, - }, - cardContainer: { - flex: 1, - justifyContent: "center", - alignItems: "center", - }, - // Card stack - Premium styling (background cards) - stackCard: { - position: "absolute", - width: "85%", - height: "85%", - backgroundColor: '#0d0d1a', - borderRadius: radius.xl, - borderWidth: 1, - borderColor: 'rgba(212, 175, 55, 0.15)', - }, - stackCardSecond: { - transform: [{ scale: 0.95 }, { translateY: 12 }], - opacity: 0.4, - ...shadows.md, - }, - stackCardThird: { - transform: [{ scale: 0.90 }, { translateY: 24 }], - opacity: 0.2, - ...shadows.sm, - }, - bottomSpacer: { - height: Platform.OS === 'ios' ? 100 : 80, - }, - // Premium "waiting for partner" styles - waitingContent: { - width: '100%', - alignItems: 'center', - paddingHorizontal: spacing.md, - }, - waitingIconContainer: { - width: 72, - height: 72, - borderRadius: 36, - backgroundColor: 'rgba(232, 164, 174, 0.1)', - borderWidth: 1, - borderColor: 'rgba(232, 164, 174, 0.2)', - justifyContent: 'center', - alignItems: 'center', - marginBottom: spacing.xl, - }, - waitingLabel: { - ...typography.caption1, - fontWeight: '600', - letterSpacing: 3, - color: colors.premium.rose, - textAlign: 'center', - marginBottom: spacing.xs, - }, - waitingTitle: { - ...typography.largeTitle, - color: colors.text, - textAlign: 'center', - }, - waitingBadge: { - backgroundColor: 'rgba(232, 164, 174, 0.1)', - paddingHorizontal: spacing.lg, - paddingVertical: spacing.sm, - borderRadius: radius.full, - borderWidth: 1, - borderColor: 'rgba(232, 164, 174, 0.2)', - marginBottom: spacing.xl, - }, - waitingBadgeText: { - ...typography.caption2, - fontWeight: '600', - letterSpacing: 2, - color: colors.premium.rose, - }, - waitingDescription: { - ...typography.body, - color: colors.textSecondary, - textAlign: 'center', - marginBottom: spacing.xl, - paddingHorizontal: spacing.md, - }, - waitingFeatures: { - marginBottom: spacing.xl, - }, - waitingFeatureItem: { - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - marginBottom: spacing.sm, - }, - waitingFeatureText: { - ...typography.subhead, - color: colors.text, - marginLeft: spacing.sm, - }, - waitingTeaser: { - ...typography.footnote, - fontStyle: 'italic', - color: colors.textTertiary, - textAlign: 'center', - }, - // Caught up specific styles - caughtUpIconContainer: { - width: 72, - height: 72, - borderRadius: 36, - backgroundColor: 'rgba(232, 164, 174, 0.1)', - borderWidth: 1, - borderColor: 'rgba(232, 164, 174, 0.2)', - justifyContent: 'center', - alignItems: 'center', - marginBottom: spacing.xl, - }, - caughtUpLabel: { - ...typography.caption1, - fontWeight: '600', - letterSpacing: 3, - color: colors.premium.rose, - textAlign: 'center', - marginBottom: spacing.xs, - }, - // Daily limit screen styles - dailyLimitOrbTop: { - top: 80, - right: -60, - }, - dailyLimitOrbBottom: { - bottom: 200, - left: -80, - }, - dailyLimitIconWrapper: { - position: 'relative', - marginBottom: spacing.xl, - }, - dailyLimitIconGlow: { - position: 'absolute', - width: 120, - height: 120, - borderRadius: 60, - backgroundColor: 'rgba(212, 175, 55, 0.15)', - top: -24, - left: -24, - }, - dailyLimitIconContainer: { - width: 72, - height: 72, - borderRadius: 36, - backgroundColor: 'rgba(212, 175, 55, 0.1)', - borderWidth: 1.5, - borderColor: 'rgba(212, 175, 55, 0.3)', - justifyContent: 'center', - alignItems: 'center', - }, - dailyLimitLabel: { - ...typography.caption2, - fontWeight: '600', - letterSpacing: 2, - color: colors.premium.gold, - textAlign: 'center', - marginBottom: spacing.xs, - }, - dailyLimitTitle: { - ...typography.largeTitle, - color: colors.text, - textAlign: 'center', - }, - countdownContainer: { - alignItems: 'center', - marginBottom: spacing.xl, - }, - countdownLabel: { - ...typography.caption2, - letterSpacing: 1.5, - color: colors.textTertiary, - marginBottom: spacing.sm, - }, - countdownBadge: { - flexDirection: 'row', - alignItems: 'center', - backgroundColor: 'rgba(212, 175, 55, 0.1)', - paddingHorizontal: spacing.lg, - paddingVertical: spacing.md, - borderRadius: radius.lg, - borderWidth: 1, - borderColor: 'rgba(212, 175, 55, 0.25)', - gap: spacing.sm, - }, - countdownText: { - ...typography.title2, - fontWeight: '700', - color: colors.premium.gold, - fontVariant: ['tabular-nums'], - letterSpacing: 2, - }, - dailyLimitDescription: { - ...typography.body, - color: colors.textSecondary, - textAlign: 'center', - marginBottom: spacing.lg, - lineHeight: 24, - paddingHorizontal: spacing.md, - }, - premiumUpsellText: { - ...typography.footnote, - fontStyle: 'italic', - color: colors.textTertiary, - textAlign: 'center', - marginBottom: spacing.md, - }, - premiumButton: { - position: 'relative', - borderRadius: radius.lg, - overflow: 'hidden', - ...shadows.md, - }, - premiumButtonGradient: { - ...StyleSheet.absoluteFillObject, - }, - premiumButtonContent: { - flexDirection: 'row', - alignItems: 'center', - paddingVertical: spacing.md, - paddingHorizontal: spacing.xl, - gap: spacing.sm, - }, - premiumButtonText: { - ...typography.body, - fontWeight: '600', - color: '#000', - }, - // Upload overlay styles - uploadOverlay: { - ...StyleSheet.absoluteFillObject, - backgroundColor: 'rgba(0, 0, 0, 0.6)', - justifyContent: 'center', - alignItems: 'center', - zIndex: 100, - }, - uploadSpinnerContainer: { - backgroundColor: colors.backgroundLight, - paddingHorizontal: spacing.xl, - paddingVertical: spacing.lg, - borderRadius: radius.lg, - alignItems: 'center', - gap: spacing.md, - borderWidth: 1, - borderColor: 'rgba(212, 175, 55, 0.2)', - }, - uploadText: { - ...typography.subhead, - color: colors.text, - }, -}); +export { default } from "../../src/features/swipe/SwipeScreen"; diff --git a/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj b/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj index ca44296..c1c48e6 100644 --- a/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj +++ b/apps/mobile/ios/Sauci.xcodeproj/project.pbxproj @@ -432,11 +432,11 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app; - PRODUCT_NAME = Sauci; + PRODUCT_NAME = "Sauci"; SWIFT_OBJC_BRIDGING_HEADER = "Sauci/Sauci-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1"; VERSIONING_SYSTEM = "apple-generic"; DEVELOPMENT_TEAM = "BXVPBATDZY"; CODE_SIGN_IDENTITY = "Apple Development"; @@ -463,10 +463,10 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = com.sauci.app; - PRODUCT_NAME = Sauci; + PRODUCT_NAME = "Sauci"; SWIFT_OBJC_BRIDGING_HEADER = "Sauci/Sauci-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = 1; + TARGETED_DEVICE_FAMILY = "1"; VERSIONING_SYSTEM = "apple-generic"; DEVELOPMENT_TEAM = "BXVPBATDZY"; CODE_SIGN_IDENTITY = "Apple Development"; diff --git a/apps/mobile/src/features/swipe/SwipeScreen.tsx b/apps/mobile/src/features/swipe/SwipeScreen.tsx new file mode 100644 index 0000000..f7cf977 --- /dev/null +++ b/apps/mobile/src/features/swipe/SwipeScreen.tsx @@ -0,0 +1,168 @@ +import { Platform, StyleSheet, View } from "react-native"; +import { useRouter } from "expo-router"; + +import { GradientBackground } from "../../components/ui"; +import { QuestionFeedbackModal } from "../../components/feedback"; +import { MatchConfetti } from "../../components/MatchConfetti"; +import { Paywall } from "../../components/paywall"; +import { useSwipeScreen } from "./hooks/useSwipeScreen"; +import { SwipeCardStack } from "./components/SwipeCardStack"; +import { SwipeHeader } from "./components/SwipeHeader"; +import { SwipeUploadOverlay } from "./components/SwipeUploadOverlay"; +import { SwipeLoadingState } from "./components/SwipeLoadingState"; +import { SwipeNoPartnerState } from "./components/SwipeNoPartnerState"; +import { SwipeDailyLimitState } from "./components/SwipeDailyLimitState"; +import { SwipeBlockedState } from "./components/SwipeBlockedState"; +import { SwipeNoPacksState } from "./components/SwipeNoPacksState"; +import { SwipeCaughtUpState } from "./components/SwipeCaughtUpState"; + +const SwipeScreen = () => { + const router = useRouter(); + const { + packId, + mode, + filteredQuestions, + currentIndex, + feedbackQuestion, + isLoading, + isUploading, + isBlocked, + gapInfo, + dailyLimitInfo, + showPaywall, + packContext, + showConfetti, + countdown, + user, + partner, + couple, + enabledPackIds, + effectiveTotal, + fetchQuestions, + handleAnswer, + handleConfettiComplete, + getQuestionPackInfo, + checkAnswerGap, + setFeedbackQuestion, + setShowPaywall, + } = useSwipeScreen(); + + if (isLoading) { + return ; + } + + if (!partner) { + return ( + router.push("/pairing")} + /> + ); + } + + if (dailyLimitInfo?.is_blocked) { + return ( + setShowPaywall(true)} + onClosePaywall={() => setShowPaywall(false)} + onPaywallSuccess={() => { + setShowPaywall(false); + fetchQuestions(); + }} + /> + ); + } + + if (isBlocked && gapInfo) { + return ( + + ); + } + + if (!packId && enabledPackIds.length === 0) { + return ( + router.push("/")} /> + ); + } + + if (currentIndex >= filteredQuestions.length) { + return ( + router.push("/(app)/matches")} + onBackToHome={() => router.push("/")} + onRefresh={fetchQuestions} + /> + ); + } + + return ( + + + router.back()} + /> + + setFeedbackQuestion({ id: questionId, text: questionText })} + /> + + + + + setFeedbackQuestion(null)} + questionId={feedbackQuestion?.id || ''} + questionText={feedbackQuestion?.text || ''} + /> + + + + setShowPaywall(false)} + onSuccess={() => { + setShowPaywall(false); + fetchQuestions(); + }} + /> + + {isUploading && } + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + bottomSpacer: { + height: Platform.OS === 'ios' ? 100 : 80, + }, +}); + +export default SwipeScreen; diff --git a/apps/mobile/src/features/swipe/components/SwipeBlockedState.tsx b/apps/mobile/src/features/swipe/components/SwipeBlockedState.tsx new file mode 100644 index 0000000..c9085cb --- /dev/null +++ b/apps/mobile/src/features/swipe/components/SwipeBlockedState.tsx @@ -0,0 +1,33 @@ +import { colors } from "../../../theme"; +import { SwipeInfoStateLayout } from "./SwipeInfoStateLayout"; + +interface SwipeBlockedStateProps { + unansweredCount: number; + onCheckAgain: () => void; +} + +export const SwipeBlockedState = ({ unansweredCount, onCheckAgain }: SwipeBlockedStateProps) => { + const accent = colors.premium.rose; + + return ( + + ); +}; diff --git a/apps/mobile/src/features/swipe/components/SwipeCardStack.tsx b/apps/mobile/src/features/swipe/components/SwipeCardStack.tsx new file mode 100644 index 0000000..80849fe --- /dev/null +++ b/apps/mobile/src/features/swipe/components/SwipeCardStack.tsx @@ -0,0 +1,79 @@ +import { StyleSheet, View } from "react-native"; + +import { radius, shadows } from "../../../theme"; +import type { PackInfo } from "../types"; +import { SwipeQuestionCard } from "./SwipeQuestionCard"; +import type { AnswerType } from "../../../types"; +import type { ResponseData } from "../types"; + +interface SwipeCardStackProps { + questions: any[]; + currentIndex: number; + getPackInfo: (question: any) => PackInfo | null; + user: { id: string; name?: string | null; avatar_url?: string | null } | null; + partner: { id: string; name?: string | null; avatar_url?: string | null } | null; + onAnswer: (questionId: string, answer: AnswerType | 'skip', responseData?: ResponseData) => void; + onReport: (questionId: string, questionText: string) => void; +} + +export const SwipeCardStack = ({ + questions, + currentIndex, + getPackInfo, + user, + partner, + onAnswer, + onReport, +}: SwipeCardStackProps) => { + const activeQuestion = questions[currentIndex]; + + return ( + + {questions[currentIndex + 2] && ( + + )} + + {questions[currentIndex + 1] && ( + + )} + + {activeQuestion && ( + + )} + + ); +}; + +const styles = StyleSheet.create({ + cardContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + stackCard: { + position: "absolute", + width: "85%", + height: "85%", + backgroundColor: '#0d0d1a', + borderRadius: radius.xl, + borderWidth: 1, + borderColor: 'rgba(212, 175, 55, 0.15)', + }, + stackCardSecond: { + transform: [{ scale: 0.95 }, { translateY: 12 }], + opacity: 0.4, + ...shadows.md, + }, + stackCardThird: { + transform: [{ scale: 0.90 }, { translateY: 24 }], + opacity: 0.2, + ...shadows.sm, + }, +}); diff --git a/apps/mobile/src/features/swipe/components/SwipeCaughtUpState.tsx b/apps/mobile/src/features/swipe/components/SwipeCaughtUpState.tsx new file mode 100644 index 0000000..381e7e6 --- /dev/null +++ b/apps/mobile/src/features/swipe/components/SwipeCaughtUpState.tsx @@ -0,0 +1,68 @@ +import { colors } from "../../../theme"; +import { SwipeInfoStateLayout, type FeatureItem } from "./SwipeInfoStateLayout"; + +interface SwipeCaughtUpStateProps { + isPendingMode: boolean; + isPackMode: boolean; + packName?: string | null; + onViewMatches: () => void; + onBackToHome: () => void; + onRefresh: () => void; +} + +export const SwipeCaughtUpState = ({ + isPendingMode, + isPackMode, + packName, + onViewMatches, + onBackToHome, + onRefresh, +}: SwipeCaughtUpStateProps) => { + const accent = colors.premium.rose; + + const label = isPendingMode ? 'QUEUE COMPLETE' : isPackMode ? 'PACK COMPLETE' : 'COMPLETE'; + const title = isPendingMode ? 'All Caught Up!' : isPackMode ? packName || 'Pack Complete' : 'All Caught Up'; + const badgeText = isPendingMode ? 'YOUR TURN COMPLETE' : isPackMode ? 'ALL QUESTIONS ANSWERED' : "YOU'RE AHEAD"; + const description = isPendingMode + ? "You've answered all the questions your partner swiped on. Nice work! Check back later for more." + : isPackMode + ? "You've answered all questions in this pack. Head home to discover more!" + : "You've answered all available questions. New questions are added regularly, or explore different packs from home."; + const teaser = isPendingMode + ? 'Your partner will love that you caught up' + : isPackMode + ? 'Discover more ways to connect' + : 'More ways to connect are on the way'; + + const features: FeatureItem[] = isPendingMode + ? [ + { icon: 'heart', text: 'Check your matches', color: accent }, + { icon: 'chatbubbles-outline', text: 'Chat about discoveries', color: accent }, + { icon: 'sparkles', text: 'Keep exploring together', color: accent }, + ] + : [ + { icon: 'sparkles', text: 'New questions weekly', color: accent }, + { icon: 'home-outline', text: 'Explore from home', color: accent }, + { icon: 'chatbubbles-outline', text: 'Chat about your matches', color: accent }, + ]; + + const action = isPendingMode + ? { label: 'View Matches', onPress: onViewMatches } + : isPackMode + ? { label: 'Back to Home', onPress: onBackToHome } + : { label: 'Refresh Questions', onPress: onRefresh, variant: 'secondary' as const }; + + return ( + + ); +}; diff --git a/apps/mobile/src/features/swipe/components/SwipeDailyLimitState.tsx b/apps/mobile/src/features/swipe/components/SwipeDailyLimitState.tsx new file mode 100644 index 0000000..f3a8b21 --- /dev/null +++ b/apps/mobile/src/features/swipe/components/SwipeDailyLimitState.tsx @@ -0,0 +1,229 @@ +import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import Animated, { FadeIn, FadeInUp } from "react-native-reanimated"; +import { LinearGradient } from "expo-linear-gradient"; + +import { DecorativeSeparator, GradientBackground } from "../../../components/ui"; +import { Paywall } from "../../../components/paywall"; +import { colors, radius, spacing, typography } from "../../../theme"; +import type { DailyLimitInfo } from "../types"; + +interface SwipeDailyLimitStateProps { + dailyLimitInfo: DailyLimitInfo; + countdown: string; + showPaywall: boolean; + onShowPaywall: () => void; + onClosePaywall: () => void; + onPaywallSuccess: () => void; +} + +export const SwipeDailyLimitState = ({ + dailyLimitInfo, + countdown, + showPaywall, + onShowPaywall, + onClosePaywall, + onPaywallSuccess, +}: SwipeDailyLimitStateProps) => { + const accent = colors.premium.gold; + + return ( + + + + + + + + + + + DAILY LIMIT REACHED + Take a Breather + + + + + REFRESHES IN + + + {countdown} + + + + + You've answered {dailyLimitInfo.limit_value} questions today!{'\n'} + Let the anticipation build while you wait. + + + + + + Chat about your matches + + + + Fresh questions tomorrow + + + + Go unlimited with Premium + + + + Want to keep exploring? + + + + + Unlock Unlimited + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + centerContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + waitingContent: { + width: '100%', + alignItems: 'center', + paddingHorizontal: spacing.md, + }, + waitingFeatures: { + marginBottom: spacing.xl, + }, + waitingFeatureItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginBottom: spacing.sm, + }, + waitingFeatureText: { + ...typography.subhead, + color: colors.text, + marginLeft: spacing.sm, + }, + dailyLimitIconWrapper: { + position: 'relative', + marginBottom: spacing.xl, + }, + dailyLimitIconGlow: { + position: 'absolute', + width: 120, + height: 120, + borderRadius: 60, + backgroundColor: 'rgba(212, 175, 55, 0.15)', + top: -24, + left: -24, + }, + dailyLimitIconContainer: { + width: 72, + height: 72, + borderRadius: 36, + backgroundColor: 'rgba(212, 175, 55, 0.1)', + borderWidth: 1.5, + borderColor: 'rgba(212, 175, 55, 0.3)', + justifyContent: 'center', + alignItems: 'center', + }, + dailyLimitLabel: { + ...typography.caption2, + fontWeight: '600', + letterSpacing: 2, + color: colors.premium.gold, + textAlign: 'center', + marginBottom: spacing.xs, + }, + dailyLimitTitle: { + ...typography.largeTitle, + color: colors.text, + textAlign: 'center', + }, + countdownContainer: { + alignItems: 'center', + marginBottom: spacing.xl, + }, + countdownLabel: { + ...typography.caption2, + letterSpacing: 1.5, + color: colors.textTertiary, + marginBottom: spacing.sm, + }, + countdownBadge: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: 'rgba(212, 175, 55, 0.1)', + paddingHorizontal: spacing.lg, + paddingVertical: spacing.sm, + borderRadius: radius.full, + borderWidth: 1, + borderColor: 'rgba(212, 175, 55, 0.2)', + }, + countdownText: { + ...typography.title2, + color: colors.premium.gold, + marginLeft: spacing.sm, + letterSpacing: 1, + }, + dailyLimitDescription: { + ...typography.body, + color: colors.textSecondary, + textAlign: 'center', + marginBottom: spacing.xl, + paddingHorizontal: spacing.md, + }, + premiumUpsellText: { + ...typography.subhead, + color: colors.textSecondary, + marginBottom: spacing.sm, + }, + premiumButton: { + marginTop: spacing.sm, + borderRadius: radius.full, + overflow: 'hidden', + }, + premiumButtonGradient: { + ...StyleSheet.absoluteFillObject, + }, + premiumButtonContent: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: spacing.xl, + paddingVertical: spacing.md, + }, + premiumButtonText: { + ...typography.subhead, + color: '#000', + marginLeft: spacing.sm, + fontWeight: '600', + }, +}); diff --git a/apps/mobile/src/features/swipe/components/SwipeHeader.tsx b/apps/mobile/src/features/swipe/components/SwipeHeader.tsx new file mode 100644 index 0000000..fdddf5e --- /dev/null +++ b/apps/mobile/src/features/swipe/components/SwipeHeader.tsx @@ -0,0 +1,139 @@ +import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import Animated, { FadeIn } from "react-native-reanimated"; +import { LinearGradient } from "expo-linear-gradient"; + +import { colors, spacing, typography } from "../../../theme"; +import { useProgressShimmer } from "../hooks/useProgressShimmer"; + +interface SwipeHeaderProps { + currentIndex: number; + effectiveTotal: number; + totalQuestions: number; + mode?: string; + packContext: { name: string } | null; + showBackButton: boolean; + onBack: () => void; +} + +export const SwipeHeader = ({ + currentIndex, + effectiveTotal, + totalQuestions, + mode, + packContext, + showBackButton, + onBack, +}: SwipeHeaderProps) => { + const shimmerStyle = useProgressShimmer(); + const total = effectiveTotal || totalQuestions; + const progressPercent = total > 0 ? ((currentIndex + 1) / total) * 100 : 0; + + return ( + + + {showBackButton && ( + + + + )} + + + {mode === 'pending' ? 'YOUR TURN' : packContext ? packContext.name.toUpperCase() : 'EXPLORE'} + + + {currentIndex + 1} of {total} + + + + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + header: { + paddingTop: 60, + paddingHorizontal: spacing.lg, + paddingBottom: spacing.md, + }, + headerRow: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + backButton: { + position: 'absolute', + left: 0, + padding: spacing.sm, + }, + progressContainer: { + alignItems: "center", + }, + progressLabel: { + ...typography.caption2, + fontWeight: '600', + letterSpacing: 3, + color: colors.premium.gold, + marginBottom: spacing.xs, + }, + progressText: { + ...typography.subhead, + color: colors.textSecondary, + marginBottom: spacing.sm, + }, + progressBar: { + width: 160, + height: 6, + backgroundColor: colors.glass.background, + borderRadius: 3, + overflow: "hidden", + }, + progressBarPremium: { + width: 140, + height: 4, + borderRadius: 2, + backgroundColor: 'rgba(212, 175, 55, 0.1)', + borderWidth: 1, + borderColor: 'rgba(212, 175, 55, 0.15)', + }, + progressFill: { + height: "100%", + borderRadius: 4, + overflow: "hidden", + }, + progressFillPremium: { + backgroundColor: 'rgba(212, 175, 55, 0.5)', + borderRadius: 2, + }, + progressGradient: { + flex: 1, + borderRadius: 4, + }, + progressShimmer: { + position: 'absolute', + width: 60, + height: '100%', + top: 0, + }, +}); diff --git a/apps/mobile/src/features/swipe/components/SwipeInfoStateLayout.tsx b/apps/mobile/src/features/swipe/components/SwipeInfoStateLayout.tsx new file mode 100644 index 0000000..ee10e41 --- /dev/null +++ b/apps/mobile/src/features/swipe/components/SwipeInfoStateLayout.tsx @@ -0,0 +1,173 @@ +import { StyleSheet, Text, TextStyle, View } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import Animated, { FadeIn, FadeInUp } from "react-native-reanimated"; + +import { DecorativeSeparator, GlassButton, GradientBackground } from "../../../components/ui"; +import { colors, radius, spacing, typography } from "../../../theme"; + +export interface FeatureItem { + icon: keyof typeof Ionicons.glyphMap; + text: string; + color?: string; +} + +interface SwipeInfoStateLayoutProps { + accentColor: string; + icon: keyof typeof Ionicons.glyphMap; + label: string; + title: string; + badgeText?: string; + description: string; + features: FeatureItem[]; + teaser: string; + separatorVariant?: "rose" | "gold"; + labelStyle?: TextStyle; + action?: { + label: string; + onPress: () => void; + variant?: "primary" | "secondary"; + }; +} + +export const SwipeInfoStateLayout = ({ + accentColor, + icon, + label, + title, + badgeText, + description, + features, + teaser, + separatorVariant = "rose", + labelStyle, + action, +}: SwipeInfoStateLayoutProps) => ( + + + + + + + + {label} + {title} + + + + {badgeText && ( + + {badgeText} + + )} + + {description} + + + {features.map((feature, index) => ( + + + {feature.text} + + ))} + + + {teaser} + + {action && ( + + {action.label} + + )} + + + +); + +const styles = StyleSheet.create({ + centerContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + waitingContent: { + width: '100%', + alignItems: 'center', + paddingHorizontal: spacing.md, + }, + waitingIconContainer: { + width: 72, + height: 72, + borderRadius: 36, + backgroundColor: 'rgba(232, 164, 174, 0.1)', + borderWidth: 1, + borderColor: 'rgba(232, 164, 174, 0.2)', + justifyContent: 'center', + alignItems: 'center', + marginBottom: spacing.xl, + }, + waitingLabel: { + ...typography.caption1, + fontWeight: '600', + letterSpacing: 3, + color: colors.premium.rose, + textAlign: 'center', + marginBottom: spacing.xs, + }, + waitingTitle: { + ...typography.largeTitle, + color: colors.text, + textAlign: 'center', + }, + waitingBadge: { + backgroundColor: 'rgba(232, 164, 174, 0.1)', + paddingHorizontal: spacing.lg, + paddingVertical: spacing.sm, + borderRadius: radius.full, + borderWidth: 1, + borderColor: 'rgba(232, 164, 174, 0.2)', + marginBottom: spacing.xl, + }, + waitingBadgeText: { + ...typography.caption2, + fontWeight: '600', + letterSpacing: 2, + color: colors.premium.rose, + }, + waitingDescription: { + ...typography.body, + color: colors.textSecondary, + textAlign: 'center', + marginBottom: spacing.xl, + paddingHorizontal: spacing.md, + }, + waitingFeatures: { + marginBottom: spacing.xl, + }, + waitingFeatureItem: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + marginBottom: spacing.sm, + }, + waitingFeatureText: { + ...typography.subhead, + color: colors.text, + marginLeft: spacing.sm, + }, + waitingTeaser: { + ...typography.footnote, + fontStyle: 'italic', + color: colors.textTertiary, + textAlign: 'center', + }, +}); diff --git a/apps/mobile/src/features/swipe/components/SwipeLoadingState.tsx b/apps/mobile/src/features/swipe/components/SwipeLoadingState.tsx new file mode 100644 index 0000000..8d21a10 --- /dev/null +++ b/apps/mobile/src/features/swipe/components/SwipeLoadingState.tsx @@ -0,0 +1,20 @@ +import { ActivityIndicator, StyleSheet, View } from "react-native"; + +import { GradientBackground } from "../../../components/ui"; +import { colors } from "../../../theme"; + +export const SwipeLoadingState = () => ( + + + + + +); + +const styles = StyleSheet.create({ + centerContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, +}); diff --git a/apps/mobile/src/features/swipe/components/SwipeNoPacksState.tsx b/apps/mobile/src/features/swipe/components/SwipeNoPacksState.tsx new file mode 100644 index 0000000..7645fbe --- /dev/null +++ b/apps/mobile/src/features/swipe/components/SwipeNoPacksState.tsx @@ -0,0 +1,31 @@ +import { colors } from "../../../theme"; +import { SwipeInfoStateLayout } from "./SwipeInfoStateLayout"; + +interface SwipeNoPacksStateProps { + onBrowsePacks: () => void; +} + +export const SwipeNoPacksState = ({ onBrowsePacks }: SwipeNoPacksStateProps) => { + const accent = colors.premium.rose; + + return ( + + ); +}; diff --git a/apps/mobile/src/features/swipe/components/SwipeNoPartnerState.tsx b/apps/mobile/src/features/swipe/components/SwipeNoPartnerState.tsx new file mode 100644 index 0000000..ea912bc --- /dev/null +++ b/apps/mobile/src/features/swipe/components/SwipeNoPartnerState.tsx @@ -0,0 +1,35 @@ +import { colors } from "../../../theme"; +import { SwipeInfoStateLayout } from "./SwipeInfoStateLayout"; + +interface SwipeNoPartnerStateProps { + hasCouple: boolean; + onPairPress: () => void; +} + +export const SwipeNoPartnerState = ({ hasCouple, onPairPress }: SwipeNoPartnerStateProps) => { + const accent = colors.premium.rose; + + return ( +