diff --git a/.agents/skills/agent-verification/SKILL.md b/.agents/skills/agent-verification/SKILL.md new file mode 100644 index 0000000..d0029e9 --- /dev/null +++ b/.agents/skills/agent-verification/SKILL.md @@ -0,0 +1,56 @@ +--- +name: agent-verification +description: Select and run concise deterministic DodoStream verification checks. Use for implementation validation, CI-equivalent checks, static quality checks, or rerunning one failing verification phase. +--- + +# Agent verification + +Use this skill to select the narrowest deterministic check that observes the changed contract. It complements `slice-development`; it does not replace focused tests or authorized native evidence. + +## Commands + +```bash +# List stable phase keys. +pnpm --silent verify:agent --list + +# Run one or more named phases with concise output. +pnpm --silent verify:agent --only typecheck,lint + +# Emit newline-delimited JSON for an automated caller. +pnpm --silent verify:agent --only format,typecheck --json + +# Stream a child command while diagnosing it. +pnpm --silent verify:agent --only android-prebuild --verbose + +# Run the complete deterministic gate after the last slice. +pnpm --silent verify:agent +# `pnpm verify:ci` is the CI alias. +``` + +`verify:agent` preserves a failing command's output and exit status. It suppresses successful child output so an agent retains the phase result, elapsed time, and failure evidence instead of routine logs. + +## Phase selection + +| Changed area | First check | +| ----------------------------------------------- | ------------------------------------------------------ | +| TypeScript code or imports | `typecheck` | +| Formatting-only or config/documentation changes | `format` | +| JavaScript/TypeScript lint rules | `lint` | +| Expo dependency versions | `expo-deps`, then `expo-doctor` | +| Native config or config plugins | `android-prebuild` | +| Root Jest contract | a focused `pnpm --silent test:agent ` | +| Android E2E runner scripts | `e2e-tools` | +| Deterministic fixture add-on | `e2e-addon-types,e2e-addon` | + +Run the complete gate only after the changed slice's focused check passes. Do not substitute it for a focused RED/GREEN test. + +## Native and diagnostic boundaries + +- `verify:agent` never builds an APK, starts Metro, acquires a device, or proves UI/focus/player behavior. +- For rendered Android/TV behavior, use `android-interactive-verification` after this gate; the device proof is separate. +- Use direct `pnpm lint` when the current warning text matters. The gate intentionally reports a successful lint phase concisely. +- The Jest phase does not force-exit or otherwise conceal open handles. Investigate a handle warning as its own defect; do not weaken the gate to hide it. + +## Exit criteria + +Report the exact phase keys and observed result. A complete UI or native-feature claim also needs the focused test and, when applicable, the authorized device evidence named by `slice-development`. diff --git a/.agents/skills/android-interactive-verification/SKILL.md b/.agents/skills/android-interactive-verification/SKILL.md new file mode 100644 index 0000000..58057ec --- /dev/null +++ b/.agents/skills/android-interactive-verification/SKILL.md @@ -0,0 +1,46 @@ +--- +name: android-interactive-verification +description: Session-scoped Android and Android TV fixture, APK, interaction, evidence, and Maestro procedure through the agent-device MCP server. +--- + +# Android interactive verification + +Use this skill only when the slice's expectation crosses a native Android/Android TV boundary: focus routing, D-pad semantics, native players, lifecycle or system UI, real list recycling, Android-only behavior, rendered geometry, or a stable Maestro regression. Pure parsing, state, hook, transformation, and mockable API work stays on the lower verification ladder. + +## Permission and ownership gate + +Before acquiring or driving a device, confirm that the task explicitly requires device evidence or that the user authorized it. Stop before device acquisition when neither is true. If no agent-device session is active or the selected target is occupied by another agent, ask the user what to do before acquiring, booting, or reusing a target. Never start Metro, use Expo web, call the `agent-device` CLI directly, or use raw `adb`. + +All device interaction goes through the repository's `agent-device` MCP server (`.omp/mcp.json`): keep exactly one MCP session per worktree and bind every operation to it. Keep the session for nearby repairs; release it with `close` after the last related L4/L5 slice, including failure paths. + +## Required inputs + +Record these before starting: + +- The slice's exact expectation and RED interaction, including the expected initial and final state. +- `phone`, `tablet`, or `tv` profile; dev app id `app.dodora.dodostream.dev`; and the required orientation. +- The release APK path recorded by `pnpm e2e:android:build` in `artifacts/e2e//artifact.json`. +- The seeded fixture manifest URL. The fixture's `GET /manifest.json` must identify `com.dodostream.e2e-fixture` before install or launch; do not invent a second health endpoint. +- The affected existing Maestro flow, if a stable regression path already exists. + +## Session sequence + +1. Start the fixture add-on for this worktree (`pnpm e2e:addon`) and probe its `/manifest.json`. +2. Build the release APK artifact: `pnpm e2e:android:build -- --profile `. The build applies `APP_VARIANT=dev`, `EXPO_PUBLIC_E2E=1`, the profile orientation, and `EXPO_TV=1` for TV. Never substitute a Metro bundle. +3. Through the MCP tools: list or boot the profile-matching device and bind the worktree's MCP session to it. +4. Through the MCP tools: disable animations, set the profile orientation, install the artifact, and launch `app.dodora.dodostream.dev`. Clear only the app state when the slice needs a clean start, then capture the initial screenshot, hierarchy snapshot, and app logs before interacting. +5. Execute the recorded sequence through MCP interactions (`tv_remote` for `DPAD_*` and `BACK`, press, type, swipe). Capture the resulting screenshot, hierarchy, and focused-component or app logs at each meaningful state. +6. On failure, return to the same slice's DEBUG loop: repeat the exact sequence, state one causal hypothesis, change one causal area, and retest on the same session. +7. Release the MCP session with `close` when no nearby slice needs it, and stop the fixture add-on process. + +Captures belong beneath the current worktree's ignored `artifacts/` directory. A local emulator reaches the fixture through `http://10.0.2.2:/manifest.json`; record the exact URL the session used. Never assume a host address works from a physical device. + +## Evidence and Maestro promotion + +The final slice record must state `initial state → key/action → observed state` and identify which screenshot, hierarchy, and log artifacts support it. A screenshot alone does not prove focus; name the visual focus indicator plus hierarchy or focused-component evidence. Do not overwrite committed snapshots during exploration. + +Promote the sequence to a targeted Maestro flow only when L4 establishes a stable, user-important regression with reliable selectors and reviewed screenshots. Run the flow through the MCP server (`test` with the flow file and session env such as `ADDON_MANIFEST_URL`, `PROFILE`, `E2E_ORIENTATION`); inspect every screenshot and baseline change before staging. Do not use Maestro as the first debugger for an unclear focus graph, a positional workaround, or a one-off exploratory trace. + +## Exit criteria + +The skill exits only when the slice has concrete interaction evidence, a targeted Maestro result when promotion is warranted, and no live agent-device MCP session or fixture add-on process is left running. diff --git a/.agents/skills/bug-reproduction/SKILL.md b/.agents/skills/bug-reproduction/SKILL.md new file mode 100644 index 0000000..2434db8 --- /dev/null +++ b/.agents/skills/bug-reproduction/SKILL.md @@ -0,0 +1,34 @@ +--- +name: bug-reproduction +description: Reproduce a reported bug on Android/Android TV and capture evidence. Use when a bug report, GitHub issue, or regression must be confirmed before fixing; release-APK route via agent-device, never Metro or Expo web. +--- + +# Bug reproduction + +## Intake + +Extract from the issue: repro steps, affected variant (`app.dodora.dodostream.dev` vs prod), platform (phone portrait / tablet / tv landscape), and expected vs actual behavior. + +## Permission gate + +Per `AGENTS.md`, only acquire a device through the `agent-device` MCP server, one named session per worktree, and only when the user authorized device use for this task. + +## Build route + +Default is the release-APK route: `pnpm e2e:android:build -- --profile ` with the deterministic Stremio fixture (`packages/e2e-addon/`, readiness probe `/manifest.json`). Do not start Metro or use Expo web. If the issue only reproduces in a dev build, ask the user before starting Metro. + +## Reproduce loop + +Install the APK via agent-device, reset state, drive the exact repro steps with D-pad/tap tools, and capture snapshot/screenshot/logs each attempt. Keep going until the bug reproduces or 3 attempts make no progress — report findings instead of thrashing. + +## Regression windows + +If the issue is a regression and the breaking window is unclear, never start a `git bisect` loop unprompted — each step costs a full release-APK build. Present the bisect plan (candidate commits, build count, expected duration) and ask the user first; prefer reasoning from the issue's reported version and `git log` over blind bisection. + +## Fix handoff + +Once reproduced, switch to the `slice-development` skill state loop with the reproduction as the RED expectation; re-verify on device post-fix. + +## Close the loop + +Prepare the issue comment (repro summary + fix commit) as a dry run for `github-write`, and hand the fix off through `pr-handoff`. Post either only after explicit user approval. diff --git a/.agents/skills/feature-delivery/SKILL.md b/.agents/skills/feature-delivery/SKILL.md new file mode 100644 index 0000000..1efdc72 --- /dev/null +++ b/.agents/skills/feature-delivery/SKILL.md @@ -0,0 +1,25 @@ +--- +name: feature-delivery +description: Deliver a feature end to end from user-visible goal to PR-ready branch. Use when implementing a new feature: composes slice-development, android-interactive-verification, What's New generation, the verify:ci gate, and pr-handoff. +--- + +# Feature delivery + +## Compose, don't duplicate + +This skill routes; it does not restate content. `slice-development` owns slicing, RED, checkpoints, and the final evidence structure. `android-interactive-verification` owns the device boundary. Link to them by skill name. + +## Pipeline + +1. Write the goal as user-visible behavior. +2. Slice with `slice-development`. +3. Implement with the invariants in `AGENTS.md` (Restyle tokens, i18n strings, `Focusable`, LegendList, React Query). +4. Prove each slice at the lowest observing ladder level. +5. Any UI change requires a live `agent-device` check of the real surface — Jest alone never closes a UI slice. +6. Add a What's New entry if noteworthy per CONTRIBUTING.md's "Noteworthy feature release notes" criteria (`pnpm new-whats-new` + `pnpm generate-whats-new`). +7. Run the final `pnpm verify:ci`. +8. Hand off via the `pr-handoff` skill. + +## Completion rule + +The feature is done only when every slice is checkpointed, the UI is verified on device with recorded evidence, `verify:ci` is green, and the PR (or approved local-commit delivery, if the user pushes themselves) is out for review. diff --git a/.agents/skills/issue-triage/SKILL.md b/.agents/skills/issue-triage/SKILL.md new file mode 100644 index 0000000..6c22b50 --- /dev/null +++ b/.agents/skills/issue-triage/SKILL.md @@ -0,0 +1,37 @@ +--- +name: issue-triage +description: Rank open GitHub issues and propose the next work. Use when prioritizing the backlog, deciding what to implement next, or reviewing new issues; reads via the readonly `github` server, writes only after user approval. +--- + +# Issue triage + +## Read phase + +Use the read-only `github` MCP server. List open issues with their state, labels, and comments. Never use `github-write` in this phase. + +## Classify + +Classify each issue as bug / feature / chore. Note the affected platform (phone / tablet / tv) from labels or the body. Note repro clarity: are concrete steps present? Is the issue fixture-reproducible? + +## Taxonomy proposals + +For each issue, prepare a dry-run label payload: + +- `type: bug|feature|chore` +- `platform: phone|tablet|tv` +- `repro: clear|unclear|fixture` +- `prio: P1|P2|P3` + +Follow the existing repo label style in `.github/ISSUE_TEMPLATE/`. Execute only after explicit user approval via `github-write`. Once the labels exist on enough issues, ranking becomes mechanical label filtering instead of per-issue reasoning. + +## Rank + +Score each issue: user impact (weight ×3) + frequency signals (+1 per duplicate or mention in comments) + effort estimate (S/M/L; subtract weight for larger effort) + risk. Present a ranked table with per-issue rationale. Map each issue to repo areas via the layout in `AGENTS.md` (`src/components/` domains, `packages/`, `scripts/`). + +## Propose + +Recommend the next 1–2 items to implement with the reasoning table. For each recommendation, state the skill to use (`bug-reproduction` or `feature-delivery`) and the affected build profiles. + +## Write gate + +Prepare exact payloads for any proposed write (label to apply, comment text, priority field) as dry runs in the response. Execute only via `github-write` tools after explicit user approval in the same conversation, and only the exact approved payload. diff --git a/.agents/skills/pr-handoff/SKILL.md b/.agents/skills/pr-handoff/SKILL.md new file mode 100644 index 0000000..e145f06 --- /dev/null +++ b/.agents/skills/pr-handoff/SKILL.md @@ -0,0 +1,20 @@ +--- +name: pr-handoff +description: Prepare the branch, title, and draft PR body for substantive finished work. Use when a change touching runtime code, tests, build config, or behavior-impacting docs is verified and ready for review; output is a PR-ready block, created only after user approval. +--- + +# PR handoff + +## Collect + +- Branch name suggestion: `fix|feat/-`. +- PR title in Conventional Commit style referencing the owning issue (`fix: #195 ...`). +- Draft body assembled from the slice evidence structure (Implemented / Checkpoints / Verified / Not run / Remaining concerns), matching `.github/PULL_REQUEST_TEMPLATE.md`. + +## Emit + +Emit the complete block (branch, title, body) in one fenced markdown chunk so the user can review the exact payload. + +## Write gate + +Create the branch and PR via `github-write` only after explicit user approval of that exact block in the same conversation. Never push directly, never merge, never approve a PR — review and merge are human phases. diff --git a/.agents/skills/slice-development/SKILL.md b/.agents/skills/slice-development/SKILL.md new file mode 100644 index 0000000..52d311a --- /dev/null +++ b/.agents/skills/slice-development/SKILL.md @@ -0,0 +1,97 @@ +--- +name: slice-development +description: Behavior-first planning, verification, debugging, review, and checkpointing for non-trivial DodoStream changes. +--- + +# Slice development + +Use this skill for a non-trivial behavior change. It is mandatory when autonomous work contains two or more behavioral slices. A slice is one user-visible or system-observable outcome plus the smallest coherent state, UI, API, and test work required to deliver it. Do not split a behavior into technical layers such as types, service, component, and tests. + +## Inputs + +Before editing, identify: + +- The requested outcome and affected platforms. +- The existing tests, flows, fixtures, utilities, and components that own the behavior. +- Every known caller of an exported symbol that will change; use LSP references when available. +- Whether device verification is authorized and whether an agent-device MCP session is available. +- Already-created checkpoints and any contracts they establish. + +Write no more than one concise record per slice: + +```md +Goal: + +Slice — +Expectation: +RED: +GREEN scope: +Verify: +Device: no | conditional | yes +Checkpoint: +``` + +## State loop + +1. **INTAKE** — confirm the outcome, affected platforms, number of behavioral slices, assigned worktree, and permission boundary. +2. **SLICE_PLAN** — record the selected slice and its existing callers, owner, RED expectation, proof path, and checkpoint subject. +3. **DEFINE_EXPECTATION** — state the exact observable behavior before changing code. +4. **ESTABLISH_RED** — use a focused failing Jest test for deterministic utilities, parsers, stores, reducers, hooks, transforms, and API behavior. For native focus, visual, lifecycle, player, or layout behavior, use a repeatable interaction and expected hierarchy, screenshot, or log evidence; never invent a low-value unit test. +5. **IMPLEMENT_MINIMUM** — change only the causal code and the tests/flow needed for this slice. Reuse existing repository primitives and leave the app coherent. +6. **TARGETED_VERIFY** — run the lowest ladder level that can observe the expectation. Escalate only when a changed boundary is not observable at that level. +7. **DEVICE_REQUIRED?** — if the expectation involves native Android/TV behavior, use `android-interactive-verification` only with explicit authorization and an agent-device MCP session. +8. **SLICE_REVIEW** — inspect the slice for unrelated files, unnecessary abstractions, duplicate or stale paths, weak assertions, debug noise, accidental snapshots, and behavior outside the stated expectation. +9. **CHECKPOINT_COMMIT** — commit the reviewed slice before selecting the next one. A conversation checkpoint is not a Git checkpoint. +10. **INTEGRATION_VERIFY** — after the last slice, run the union of affected checks and the final gate, then compare the complete change against the original request. + +If a check fails, enter **DEBUG**: rerun the same reproduction, narrow the failure using the relevant diff/log/hierarchy, state one causal hypothesis, change one causal area, and rerun the targeted check. After three consecutive attempts without new evidence, return to `SLICE_PLAN` and split or replace the remaining slice, add diagnostic evidence, or escalate only if the current level cannot observe it. Completed checkpoints remain valid unless their contract is disproven. Revise only unstarted slice records when understanding changes. + +## Verification ladder + +Select the first level that observes the stated behavior; higher levels are for newly exposed boundaries, not ceremony. + +| Level | Execute | Use for | +| ---------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| L0 — static | `pnpm typecheck` and `pnpm exec eslint ...` | Every TypeScript slice before checkpoint; imports, types, hooks, and token names. | +| L1 — focused unit | `pnpm test -- ` or `pnpm test -- -t ""` | Pure parsers, reducers/stores, utilities, transforms, and API error mapping. | +| L2 — focused integration/component | The owning hook, RNTL component, database, or API test file | Rendered interactions and React Query/Zustand, SQLite, or mocked-network boundaries. | +| L3 — affected family/build | The narrow affected directory/package and required non-device build | Cross-component/package boundaries, fixture changes, remote UI, or Expo/native plugin generation. | +| L4 — interactive Android/TV | The authorized agent-device MCP session | Native focus, D-pad, player/lifecycle/system behavior, real recycling, Android defects, and rendered geometry. | +| L5 — targeted Maestro | The named flow with stable selectors and reviewed baselines | Important deterministic regression paths after interactive behavior is understood. | +| L6 — final affected gate | `pnpm verify:ci` plus affected package/build checks and selected L5 flows | The final union after all slices, not every edit. | + +Skip L4/L5 for pure logic and mockable behavior. Enter L4 before claiming behavior involving `Platform.isTV`, `TVFocusGuideView`, `hasTVPreferredFocus`, `nextFocus*`, `useTVEventHandler`, real LegendList recycling, native players, immersive/system UI, lifecycle, or Android/TV geometry. Run L5 only when L4 establishes a stable, important, selector- and baseline-backed regression path. Never start Metro or use Expo web for the release-APK Android E2E route. + +## Checkpoint policy + +For autonomous multi-slice work, each checkpoint contains exactly one observable behavior and everything required for it: production code, focused tests or a reviewed flow/baseline, translations, and tracked generated content when applicable. It passes L0 and its selected targeted proof and uses `(): `. + +Amend the preceding checkpoint only when all of these hold: the next slice has not started, the correction is solely required by that same slice's expectation or test, and the commit has not been reported, handed off, or used as a later baseline. Otherwise create a new `fix(...)` checkpoint, including for integration defects found after valid checkpoints. Do not create commits for discovery, failed experiments, formatting-only churn, or ignored generated artifacts. + +## Final evidence + +Report only observed evidence using this literal structure: + +```md +Implemented: + +- + +Checkpoints: + +- + +Verified: + +- : — + +Not run: + +- — + +Remaining concerns: + +- +``` + +Do not claim a skipped device, CI, credentialed API, screenshot, or Maestro result. Do not duplicate the repository's durable styling, focus, API, or command rules; those remain in `AGENTS.md`. diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 2a452ab..ee9c8de 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -16,7 +16,7 @@ type: 'Bug' Device model and OS version: -DodoStream version (Settings > About): +DodoStream version (Settings > About): ## Steps to reproduce diff --git a/.github/agents/adr-generator.agent.md b/.github/agents/adr-generator.agent.md deleted file mode 100644 index c67998f..0000000 --- a/.github/agents/adr-generator.agent.md +++ /dev/null @@ -1,224 +0,0 @@ ---- -name: ADR Generator -description: Expert agent for creating comprehensive Architectural Decision Records (ADRs) with structured formatting optimized for AI consumption and human readability. ---- - -# ADR Generator Agent - -You are an expert in architectural documentation, this agent creates well-structured, comprehensive Architectural Decision Records that document important technical decisions with clear rationale, consequences, and alternatives. - ---- - -## Core Workflow - -### 1. Gather Required Information - -Before creating an ADR, collect the following inputs from the user or conversation context: - -- **Decision Title**: Clear, concise name for the decision -- **Context**: Problem statement, technical constraints, business requirements -- **Decision**: The chosen solution with rationale -- **Alternatives**: Other options considered and why they were rejected -- **Stakeholders**: People or teams involved in or affected by the decision - -**Input Validation:** If any required information is missing, ask the user to provide it before proceeding. - -### 2. Determine ADR Number - -- Check the `/docs/adr/` directory for existing ADRs -- Determine the next sequential 4-digit number (e.g., 0001, 0002, etc.) -- If the directory doesn't exist, start with 0001 - -### 3. Generate ADR Document in Markdown - -Create an ADR as a markdown file following the standardized format below with these requirements: - -- Generate the complete document in markdown format -- Use precise, unambiguous language -- Include both positive and negative consequences -- Document all alternatives with clear rejection rationale -- Use coded bullet points (3-letter codes + 3-digit numbers) for multi-item sections -- Structure content for both machine parsing and human reference -- Save the file to `/docs/adr/` with proper naming convention - ---- - -## Required ADR Structure (template) - -### Front Matter - -```yaml ---- -title: "ADR-NNNN: [Decision Title]" -status: "Proposed" -date: "YYYY-MM-DD" -authors: "[Stakeholder Names/Roles]" -tags: ["architecture", "decision"] -supersedes: "" -superseded_by: "" ---- -``` - -### Document Sections - -#### Status - -**Proposed** | Accepted | Rejected | Superseded | Deprecated - -Use "Proposed" for new ADRs unless otherwise specified. - -#### Context - -[Problem statement, technical constraints, business requirements, and environmental factors requiring this decision.] - -**Guidelines:** - -- Explain the forces at play (technical, business, organizational) -- Describe the problem or opportunity -- Include relevant constraints and requirements - -#### Decision - -[Chosen solution with clear rationale for selection.] - -**Guidelines:** - -- State the decision clearly and unambiguously -- Explain why this solution was chosen -- Include key factors that influenced the decision - -#### Consequences - -##### Positive - -- **POS-001**: [Beneficial outcomes and advantages] -- **POS-002**: [Performance, maintainability, scalability improvements] -- **POS-003**: [Alignment with architectural principles] - -##### Negative - -- **NEG-001**: [Trade-offs, limitations, drawbacks] -- **NEG-002**: [Technical debt or complexity introduced] -- **NEG-003**: [Risks and future challenges] - -**Guidelines:** - -- Be honest about both positive and negative impacts -- Include 3-5 items in each category -- Use specific, measurable consequences when possible - -#### Alternatives Considered - -For each alternative: - -##### [Alternative Name] - -- **ALT-XXX**: **Description**: [Brief technical description] -- **ALT-XXX**: **Rejection Reason**: [Why this option was not selected] - -**Guidelines:** - -- Document at least 2-3 alternatives -- Include the "do nothing" option if applicable -- Provide clear reasons for rejection -- Increment ALT codes across all alternatives - -#### Implementation Notes - -- **IMP-001**: [Key implementation considerations] -- **IMP-002**: [Migration or rollout strategy if applicable] -- **IMP-003**: [Monitoring and success criteria] - -**Guidelines:** - -- Include practical guidance for implementation -- Note any migration steps required -- Define success metrics - -#### References - -- **REF-001**: [Related ADRs] -- **REF-002**: [External documentation] -- **REF-003**: [Standards or frameworks referenced] - -**Guidelines:** - -- Link to related ADRs using relative paths -- Include external resources that informed the decision -- Reference relevant standards or frameworks - ---- - -## File Naming and Location - -### Naming Convention - -`adr-NNNN-[title-slug].md` - -**Examples:** - -- `adr-0001-database-selection.md` -- `adr-0015-microservices-architecture.md` -- `adr-0042-authentication-strategy.md` - -### Location - -All ADRs must be saved in: `/docs/adr/` - -### Title Slug Guidelines - -- Convert title to lowercase -- Replace spaces with hyphens -- Remove special characters -- Keep it concise (3-5 words maximum) - ---- - -## Quality Checklist - -Before finalizing the ADR, verify: - -- [ ] ADR number is sequential and correct -- [ ] File name follows naming convention -- [ ] Front matter is complete with all required fields -- [ ] Status is set appropriately (default: "Proposed") -- [ ] Date is in YYYY-MM-DD format -- [ ] Context clearly explains the problem/opportunity -- [ ] Decision is stated clearly and unambiguously -- [ ] At least 1 positive consequence documented -- [ ] At least 1 negative consequence documented -- [ ] At least 1 alternative documented with rejection reasons -- [ ] Implementation notes provide actionable guidance -- [ ] References include related ADRs and resources -- [ ] All coded items use proper format (e.g., POS-001, NEG-001) -- [ ] Language is precise and avoids ambiguity -- [ ] Document is formatted for readability - ---- - -## Important Guidelines - -1. **Be Objective**: Present facts and reasoning, not opinions -2. **Be Honest**: Document both benefits and drawbacks -3. **Be Clear**: Use unambiguous language -4. **Be Specific**: Provide concrete examples and impacts -5. **Be Complete**: Don't skip sections or use placeholders -6. **Be Consistent**: Follow the structure and coding system -7. **Be Timely**: Use the current date unless specified otherwise -8. **Be Connected**: Reference related ADRs when applicable -9. **Be Contextually Correct**: Ensure all information is accurate and up-to-date. Use the current - repository state as the source of truth. - ---- - -## Agent Success Criteria - -Your work is complete when: - -1. ADR file is created in `/docs/adr/` with correct naming -2. All required sections are filled with meaningful content -3. Consequences realistically reflect the decision's impact -4. Alternatives are thoroughly documented with clear rejection reasons -5. Implementation notes provide actionable guidance -6. Document follows all formatting standards -7. Quality checklist items are satisfied diff --git a/.github/agents/gpt-5-beast-mode.agent.md b/.github/agents/gpt-5-beast-mode.agent.md deleted file mode 100644 index 9275ebf..0000000 --- a/.github/agents/gpt-5-beast-mode.agent.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -description: 'Beast Mode 2.0: A powerful autonomous agent tuned specifically for GPT-5 that can solve complex problems by using tools, conducting research, and iterating until the problem is fully resolved.' -model: GPT-5 (copilot) -tools: ['vscode', 'execute/runNotebookCell', 'execute/testFailure', 'execute/getTerminalOutput', 'execute/runTask', 'execute/getTaskOutput', 'execute/createAndRunTask', 'execute/runInTerminal', 'read', 'edit/editFiles', 'search', 'web', 'ddg-search/*', 'github/*', 'todo'] -name: 'GPT 5 Beast Mode' ---- - -# Operating principles -- **Beast Mode = Ambitious & agentic.** Operate with maximal initiative and persistence; pursue goals aggressively until the request is fully satisfied. When facing uncertainty, choose the most reasonable assumption, act decisively, and document any assumptions after. Never yield early or defer action when further progress is possible. -- **High signal.** Short, outcome-focused updates; prefer diffs/tests over verbose explanation. -- **Safe autonomy.** Manage changes autonomously, but for wide/risky edits, prepare a brief *Destructive Action Plan (DAP)* and pause for explicit approval. -- **Conflict rule.** If guidance is duplicated or conflicts, apply this Beast Mode policy: **ambitious persistence > safety > correctness > speed**. - -## Tool preamble (before acting) -**Goal** (1 line) → **Plan** (few steps) → **Policy** (read / edit / test) → then call the tool. - -### Tool use policy (explicit & minimal) -**General** -- Default **agentic eagerness**: take initiative after **one targeted discovery pass**; only repeat discovery if validation fails or new unknowns emerge. -- Use tools **only if local context isn’t enough**. Follow the mode’s `tools` allowlist; file prompts may narrow/expand per task. - -**Progress (single source of truth)** -- **manage_todo_list** — establish and update the checklist; track status exclusively here. Do **not** mirror checklists elsewhere. - -**Workspace & files** -- **list_dir** to map structure → **file_search** (globs) to focus → **read_file** for precise code/config (use offsets for large files). -- **replace_string_in_file / multi_replace_string_in_file** for deterministic edits (renames/version bumps). Use semantic tools for refactoring and code changes. - -**Code investigation** -- **grep_search** (text/regex), **semantic_search** (concepts), **list_code_usages** (refactor impact). -- **get_errors** after all edits or when app behavior deviates unexpectedly. - -**Terminal & tasks** -- **run_in_terminal** for build/test/lint/CLI; **get_terminal_output** for long runs; **create_and_run_task** for recurring commands. - -**Git & diffs** -- **get_changed_files** before proposing commit/PR guidance. Ensure only intended files change. - -**Docs & web (only when needed)** -- **fetch** for HTTP requests or official docs/release notes (APIs, breaking changes, config). Prefer vendor docs; cite with title and URL. - -**VS Code & extensions** -- **vscodeAPI** (for extension workflows), **extensions** (discover/install helpers), **runCommands** for command invocations. - -**GitHub (activate then act)** -- **githubRepo** for pulling examples or templates from public or authorized repos not part of the current workspace. - -## Configuration - -Goal: gain actionable context rapidly; stop as soon as you can take effective action. -Approach: single, focused pass. Remove redundancy; avoid repetitive queries. -Early exit: once you can name the exact files/symbols/config to change, or ~70% of top hits focus on one project area. -Escalate just once: if conflicted, run one more refined pass, then proceed. -Depth: trace only symbols you’ll modify or whose interfaces govern your changes. - - - -Continue working until the user request is completely resolved. Don’t stall on uncertainties—make a best judgment, act, and record your rationale after. - - - -Reasoning effort: **high** by default for multi-file/refactor/ambiguous work. Lower only for trivial/latency-sensitive changes. -Verbosity: **low** for chat, **high** for code/tool outputs (diffs, patch-sets, test logs). - - - -Before every tool call, emit Goal/Plan/Policy. Tie progress updates directly to the plan; avoid narrative excess. - - - -If rules clash, apply: **safety > correctness > speed**. DAP supersedes autonomy. - - - -Leverage Markdown for clarity (lists, code blocks). Use backticks for file/dir/function/class names. Maintain brevity in chat. - - - -If output drifts (too verbose/too shallow/over-searching), self-correct the preamble with a one-line directive (e.g., "single targeted pass only") and continue—update the user only if DAP is needed. - - - -If the host supports Responses API, chain prior reasoning (`previous_response_id`) across tool calls for continuity and conciseness. - - -## Anti-patterns -- Multiple context tools when one targeted pass is enough. -- Forums/blogs when official docs are available. -- String-replace used for refactors that require semantics. -- Scaffolding frameworks already present in the repo. - -## Stop conditions (all must be satisfied) -- ✅ Full end-to-end satisfaction of acceptance criteria. -- ✅ `get_errors` yields no new diagnostics. -- ✅ All relevant tests pass (or you add/execute new minimal tests). -- ✅ Concise summary: what changed, why, test evidence, and citations. - -## Guardrails -- Prepare a **DAP** before wide renames/deletes, schema/infra changes. Include scope, rollback plan, risk, and validation plan. -- Only use the **Network** when local context is insufficient. Prefer official docs; never leak credentials or secrets. - -## Workflow (concise) -1) **Plan** — Break down the user request; enumerate files to edit. If unknown, perform a single targeted search (`search`/`usages`). Initialize **todos**. -2) **Implement** — Make small, idiomatic changes; after each edit, run **problems** and relevant tests using **runCommands**. -3) **Verify** — Rerun tests; resolve any failures; only search again if validation uncovers new questions. -4) **Research (if needed)** — Use **fetch** for docs; always cite sources. - -## Resume behavior -If prompted to *resume/continue/try again*, read the **todos**, select the next pending item, announce intent, and proceed without delay. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md deleted file mode 100644 index 43cf9a3..0000000 --- a/.github/copilot-instructions.md +++ /dev/null @@ -1,79 +0,0 @@ -# Copilot / AI Agent Instructions for DodoStream - -## General - -- Use fetch/web search tools if available to gather up-to-date information on libraries, tools or best practices. - -## Project Overview - -This is an Expo React Native app targeting TV platforms (Apple TV, Android TV) and mobile devices. The stack includes: - -- **Expo SDK** with file-based routing via `expo-router` -- **React Native TVOS** fork for TV platform support -- **@shopify/restyle** for styling -- **Zustand** for state management -- **Moti** (on top of Reanimated) for animations -- **TypeScript** with strict mode enabled - -## Directory Structure - -- `src/app/*` — File-system based routes (each file = route) -- `src/components/` — Reusable UI components organized by domain (`media/`, `basic/`, `video/`, `profile/`) -- `src/store/` — Zustand stores for global state -- `src/constants/` — Centralized constants (`playback.ts`, `ui.ts`, `media.ts`, etc.) -- `src/hooks/` — Custom React hooks -- `src/utils/` — Utilities and helpers -- `src/theme/` — Restyle theme configuration -- `src/api/` — API client and React Query hooks - ---- - -## Detailed Instructions - -Detailed instructions have been split into path-specific files in `.github/instructions/`: - -- **Theme & Styling**: `.github/instructions/theme.instructions.md` -- **TV Platform & Focus**: `.github/instructions/tv-focus.instructions.md` -- **Components**: `.github/instructions/components.instructions.md` -- **React Best Practices**: `.github/instructions/react-best-practices.instructions.md` -- **Routing**: `.github/instructions/routing.instructions.md` -- **State Management**: `.github/instructions/state-management.instructions.md` -- **Testing**: `.github/instructions/testing.instructions.md` - ---- - -## Development Commands - -```bash -pnpm start # Start dev client -pnpm android # Run on Android -pnpm ios # Run on iOS -pnpm lint # ESLint -pnpm format # Prettier -npx expo install # Install packages (always use this, not npm/pnpm add) -``` - ---- - -## Critical Rules - -### Always Do - -- Use theme for all colors, spacing, dimensions, and focus values -- Use `` wrapper for TV-interactive components -- Use constants from `playback.ts` and `ui.ts` etc. -- Define TypeScript interfaces for props -- Use React Query for data fetching -- Add debug logs for business logic decisions -- Show toasts for user feedback -- Prefer **Moti** for UI animations (fade/slide/scale/skeleton); use **Reanimated** directly if Moti can’t express the behavior/performance needs -- Keep animation timings in `src/constants/ui.ts` (no magic timing numbers) - -### Never Do - -- Hardcode colors, dimensions, or timing values -- Create inline styles with magic numbers -- Use `useMemo` to memoize components (use `memo()`) -- Use `render*` methods in functional components -- Fetch data in raw `useEffect` (use React Query) -- Use the legacy `Animated` API when Moti/Reanimated is available diff --git a/.github/instructions/components.instructions.md b/.github/instructions/components.instructions.md deleted file mode 100644 index d971f57..0000000 --- a/.github/instructions/components.instructions.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -applyTo: 'src/components/**/*.tsx, src/app/**/*.tsx' ---- - -## Component Design - -### Rules - -- **Use `memo()`** for list items and frequently re-rendered components -- **Use `useCallback`** for event handlers passed to children -- **Use `useMemo`** only for genuinely expensive computations -- **Extract sub-components** - Keep components focused and under ~200 lines -- **Define TypeScript interfaces** for all component props - -### Naming Conventions - -- Use "Media" instead of "Movie" (supports various content types) -- Use `isFocused` for focus state (not `focused`) -- Use `handle*` for internal handlers, `on*` for callback props - -### Reusable Components - -Before creating a new component, check if one exists in: - -- `src/components/basic/` — Button, Input, Tag, Badge, Focusable, ProgressBar, etc. -- `src/components/media/` — MediaCard, MediaList, EpisodeList, StreamList, etc. -- ... other domain-specific folders under `src/components/` - ---- - -## LegendList - -Use `@legendapp/list` (LegendList) for all scrollable lists: - -```tsx -import { LegendList } from '@legendapp/list/react-native'; - - } - keyExtractor={(item) => item.id} - horizontal -/>; -``` - -### Key Points - -- Use `keyExtractor` for proper recycling and layout caching -- Use `getFixedItemSize` when item sizes are known (skips measuring) -- Use `recycleItems` for lists where items have no local state -- Wrap `renderItem` in `useCallback` with correct deps -- For non-scrollable lists, use regular mapping instead diff --git a/.github/instructions/react-best-practices.instructions.md b/.github/instructions/react-best-practices.instructions.md deleted file mode 100644 index c7acbb2..0000000 --- a/.github/instructions/react-best-practices.instructions.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -applyTo: 'src/**/*.ts, src/**/*.tsx' ---- - -## useEffect Best Practices - -Effects are for synchronizing with **external systems** only. - -### Don't Use Effects For - -- Deriving state from props (compute during render or use `useMemo`) -- Handling user actions (use event handlers) -- Chains of state updates (restructure logic) - -### Do Use Effects For - -- Subscriptions and cleanup -- Syncing with native modules -- Timer-based side effects - -### Data Fetching - -Use **React Query** (`@tanstack/react-query`) instead of raw `useEffect`: - -```tsx -const { data, isLoading, isError } = useMeta(type, id); -``` - ---- - -## Debug Logging - -Use the shared debug helpers: - -```tsx -// In React components -const debug = useDebugLogger('ComponentName'); -debug('eventName', { key: value }); - -// In non-React modules -const debug = createDebugLogger('ModuleName'); -debug('eventName', { key: value }); -``` - -Log important decision points: autoplay, stream selection, navigation branches, error recovery. - ---- - -## Optional Chaining for Callbacks - -- Prefer optional chaining over `typeof fn === 'function'` checks. -- Pattern: `const id = getId?.(item) ?? item.fallback;` -- Example: - -```ts -// Good -const groupId = getItemGroupId?.(item) ?? item.groupId ?? null; -const label = getGroupLabel?.(groupId) ?? groupId; - -// Avoid -const groupIdBad = typeof getItemGroupId === 'function' ? getItemGroupId(item) : item.groupId; -``` diff --git a/.github/instructions/routing.instructions.md b/.github/instructions/routing.instructions.md deleted file mode 100644 index 4b921fa..0000000 --- a/.github/instructions/routing.instructions.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -applyTo: 'src/**/*.ts, src/**/*.tsx' ---- - -## Routing (expo-router) - -```tsx -// Navigation -router.push({ pathname: '/details/[id]', params: { id, type } }); - -// Access params -const { id, type } = useLocalSearchParams<{ id: string; type: ContentType }>(); -``` - -- Files in `src/app/` automatically become routes -- Use `_layout.tsx` for shared layouts -- Dynamic routes: `[id].tsx` diff --git a/.github/instructions/state-management.instructions.md b/.github/instructions/state-management.instructions.md deleted file mode 100644 index ee5f310..0000000 --- a/.github/instructions/state-management.instructions.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -applyTo: 'src/**/*.ts, src/**/*.tsx' ---- - -## Zustand Stores - -### Store Patterns - -- Use per-profile data structure: `byProfile: Record` -- Access active profile via `useProfileStore.getState().activeProfileId` -- Use selectors to avoid unnecessary re-renders - -### Available Stores - -- `profile.store.ts` — Profile management -- `profile-settings.store.ts` — Per-profile settings (player, languages) -- `watch-history.store.ts` — Watch progress -- `my-list.store.ts` — Saved items -- `addon.store.ts` — Installed Stremio addons diff --git a/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md deleted file mode 100644 index a4ed6c2..0000000 --- a/.github/instructions/testing.instructions.md +++ /dev/null @@ -1,15 +0,0 @@ ---- -applyTo: '**/*.test.ts, **/*.test.tsx, **/jest.config.js, **/jest.setup.js' ---- - -## Testing (React Native) - -Use the React Native testing approach that matches the confidence you need (fast feedback first; device realism last): - -- **Static analysis**: Prefer catching issues early with **TypeScript** (type checking) and **ESLint** (linting). -- **Write testable code**: Keep modules small; separate **UI (components)** from **business logic/state** so logic can be tested without rendering. -- **Jest (JS tests)**: Use Jest for **unit** and **integration** tests. Keep each test focused and independent; structure as **Arrange / Act / Assert**; use `describe`, `beforeEach`, etc. -- **Mocking**: Prefer real dependencies when practical, but mock **external systems** (network, file/db I/O) and **native modules** when Node-based tests can’t run them. -- **Component tests**: Prefer **React Native Testing Library** and test from the **user perspective** (rendered text / accessibility queries + interactions). Avoid testing implementation details (props/state) and avoid relying on `testID` unless necessary. -- **Snapshots**: Use sparingly and keep them small; prefer explicit expectations when possible to reduce noisy diffs and accidental “update snapshot” behavior. -- **E2E tests**: Use device/simulator tests (commonly **Detox**; alternatives include Appium/Maestro) for critical flows. They provide highest confidence but are slower and can be flaky. diff --git a/.github/instructions/theme.instructions.md b/.github/instructions/theme.instructions.md deleted file mode 100644 index 968aaef..0000000 --- a/.github/instructions/theme.instructions.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -applyTo: '**/*.tsx, src/theme/**/*.ts' ---- - -## Theme & Styling - -### Use Theme for ALL Visual Values - -The theme (`src/theme/theme.ts`) is the **single source of truth** for: - -- **Colors**: Use semantic names (`mainBackground`, `primaryBackground`, `cardBackground`) -- **Spacing**: Use `xs`, `s`, `m`, `l`, `xl`, `xxl` -- **Border radii**: Use `s`, `m`, `l`, `xl`, `full` -- **Card sizes**: Use `theme.cardSizes.media`, `theme.cardSizes.continueWatching`, etc. -- **Sizes**: Use `theme.sizes.inputHeight`, `theme.sizes.modalMinWidth`, etc. -- **Focus styling**: Use `theme.focus.borderWidth`, `theme.focus.scaleMedium`, etc. - -### Styling Rules - -```tsx -// CORRECT - Use Box/Text with theme props - - Title - - -// CORRECT - Use theme values for dimensions -const theme = useTheme(); - - -// WRONG - Hardcoded values - -``` - -### Never Hardcode - -- Colors (use theme colors) -- Spacing/padding/margin (use theme spacing) -- Dimensions for cards, inputs, modals (use `theme.cardSizes` or `theme.sizes`) -- Focus border width or scale (use `theme.focus`) -- Toast durations (use constants from `ui.ts`) -- Playback timing values (use constants from `playback.ts`) diff --git a/.github/instructions/tv-focus.instructions.md b/.github/instructions/tv-focus.instructions.md deleted file mode 100644 index f1bb456..0000000 --- a/.github/instructions/tv-focus.instructions.md +++ /dev/null @@ -1,137 +0,0 @@ ---- -applyTo: '**/*.tsx' ---- - -## TV Platform & Focus Management - -### Use `` Wrapper - -For TV focus handling, use the `` component from `src/components/basic/Focusable.tsx`. - -### Performance Optimization - -The `Focusable` component is optimized for performance: - -- **When children is NOT a function**: No re-renders on focus change. Focus styling (outline, background) is applied directly to the pressable. -- **When children IS a function**: Re-renders only when `isFocused` changes, providing the focus state for custom styling. - -### Focus Styling Rules - -**CRITICAL**: Only MediaCard and ContinueWatchingCard should have outline focus. All other components (buttons, tags, list items, settings, etc.) should use **background and/or text color changes** for focus indication. - -**Pattern 1: Simple focus (NO re-renders - best performance)** - -Use `variant` prop for automatic focus styling. Children don't need to know about focus state. - -```tsx -// BEST - No re-renders, focus handled automatically via variant - - - Card Content - - - -// Outline focus for media cards (no re-renders) - - - - - -``` - -**Pattern 2: Custom focus handling (with render function)** - -Use render function only when you need custom focus behavior (e.g., changing text color, icons). - -```tsx -// Only use when children need to react to focus state - - {({ isFocused }) => ( - - Card Content - - )} - -``` - -**WRONG - Using render function when not needed:** - -```tsx -// WRONG - Unnecessary re-renders, use variant="background" instead - - {({ isFocused }) => ( - - Static content - - )} - -``` - -### Focus State Naming - -Always use `isFocused` (not `focused`) for consistency across the codebase. - -### Focus vs Active State - -Focus and active states must be visually distinct: - -- **Focus**: Use `focusBackground` / `focusForeground` colors -- **Active/Selected**: Use `primaryBackground` / `primaryForeground` colors - -### Focus Visual Feedback - -- Only MediaCard/ContinueWatchingCard use outline focus (`variant="outline"` prop) -- All other components use `focusBackground` and `focusForeground` theme colors -- Use `theme.focus.scaleMedium` (1.05) for scale transform on TV when needed -- Always test with D-pad/remote controls - ---- - -## Advanced Focus Management - -When standard focus behavior is insufficient, use `react-native-tvos` specific features. - -### 1. TVFocusGuideView - -Use `TVFocusGuideView` to manage focus groups, redirect focus, or trap focus. - -```tsx -import { TVFocusGuideView } from 'react-native'; - -// Redirect focus to specific destinations - - {/* content */} - - -// Trap focus within a container (e.g., for modals or sidebars) - - {/* content */} - - -// Auto-focus the first focusable child or remember last focused child - - {/* content */} - -``` - -### 2. Preferred Focus - -Use `hasTVPreferredFocus` to force focus to a specific element when the screen mounts or updates. - -```tsx -// Force focus to this element -{/* content */} -``` - -### 3. Explicit Focus Navigation - -Use `nextFocus*` props to override default directional navigation. - -```tsx - - {/* content */} - -``` diff --git a/.github/workflows/e2e-android.yaml b/.github/workflows/e2e-android.yaml index 22a410e..db0565b 100644 --- a/.github/workflows/e2e-android.yaml +++ b/.github/workflows/e2e-android.yaml @@ -39,18 +39,20 @@ jobs: flow: tv-navigation.yaml steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - - uses: actions/setup-java@v4 + - uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4 with: distribution: temurin java-version: '17' - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c # v4 with: run_install: false - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '24' cache: pnpm @@ -63,7 +65,7 @@ jobs: echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" - name: Run E2E - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2 with: api-level: 35 target: ${{ matrix.target }} @@ -75,7 +77,7 @@ jobs: - name: Upload E2E artifacts if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: e2e-${{ matrix.name }} path: artifacts/maestro/${{ matrix.profile }}/ diff --git a/.github/workflows/github-releases-to-discord.yml b/.github/workflows/github-releases-to-discord.yml index f73d4ca..df8cf8d 100644 --- a/.github/workflows/github-releases-to-discord.yml +++ b/.github/workflows/github-releases-to-discord.yml @@ -5,16 +5,15 @@ on: jobs: github-releases-to-discord: runs-on: ubuntu-latest + permissions: {} steps: - - name: Checkout - uses: actions/checkout@v3 - name: GitHub Releases to Discord - uses: SethCohen/github-releases-to-discord@v1 + uses: SethCohen/github-releases-to-discord@24d166886aee4646d448c8a389ff9e1ebcab3682 # v1 with: webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }} - color: "2105893" - username: "Release Changelog" - avatar_url: "https://cdn.discordapp.com/icons/1454918816596496496/1839bd6097f97f0a0353e82f1ca41d56.webp" - content: "||@everyone||" - footer_title: "Changelog" - reduce_headings: true \ No newline at end of file + color: '2105893' + username: 'Release Changelog' + avatar_url: 'https://cdn.discordapp.com/icons/1454918816596496496/1839bd6097f97f0a0353e82f1ca41d56.webp' + content: '||@everyone||' + footer_title: 'Changelog' + reduce_headings: true diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index e77322d..c45be7e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,7 +1,7 @@ name: React Native CI/CD permissions: - contents: write + contents: read on: push: @@ -53,7 +53,9 @@ jobs: runs-on: ubuntu-latest steps: - name: 🏗 Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: 🏗 Install pnpm uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c @@ -62,37 +64,19 @@ jobs: run_install: false - name: 🏗 Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '24' - name: 📦 Install dependencies - run: pnpm install - - - name: 🩺 Expo dependency compatibility (expo install --check) - run: pnpm exec expo install --check - env: - CI: true - EXPO_NO_TELEMETRY: 1 + run: pnpm install --frozen-lockfile - - name: 🩺 Expo Doctor - run: pnpx expo-doctor + - name: ✅ Run CI-equivalent verification + run: pnpm verify:ci env: CI: true EXPO_NO_TELEMETRY: 1 - - name: 🧪 Run expo prebuild - run: pnpm exec expo prebuild - - - name: 🧪 Run TypeScript check - run: pnpm tsc - - - name: 🧹 Run ESLint & Prettier - run: pnpm run lint - - - name: ✅ Run Tests - run: pnpm run test - build: needs: test if: | @@ -169,7 +153,9 @@ jobs: steps: - name: 🏗 Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: 🏗 Install pnpm uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c @@ -177,13 +163,13 @@ jobs: cache: true - name: 🏗 Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '24' - name: 📦 Install dependencies run: | - pnpm install + pnpm install --frozen-lockfile pnpm i -g eas-cli@latest - name: 🔖 Read app version @@ -195,7 +181,7 @@ jobs: echo "App version: $VERSION" - name: 📁 Setup expo-build-disk-cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: node_modules/.expo-build-disk-cache key: ${{ runner.os }}-expo-build-disk-cache-${{ hashFiles('**/package.json') }} @@ -239,9 +225,6 @@ jobs: shell: bash run: | mkdir -p dist - VERSION="${{ steps.app_version.outputs.version }}" - CHANNEL="${{ matrix.channel }}" - NODE_ENV="${{ matrix.node_env }}" # Extract the output archive echo "Extracting output.tar.gz..." @@ -257,7 +240,7 @@ jobs: echo "Looking for APKs in outputs/apk/${BUILD_TYPE}/" # Find all APKs in the extracted directory - find outputs/apk/${BUILD_TYPE} -name "*.apk" -type f 2>/dev/null | while read apk; do + find outputs/apk/${BUILD_TYPE} -name "*.apk" -type f 2>/dev/null | while read -r apk; do filename=$(basename "$apk") echo "Found APK: $filename" @@ -287,10 +270,14 @@ jobs: echo "No artifacts found!" exit 1 fi + env: + VERSION: ${{ steps.app_version.outputs.version }} + CHANNEL: ${{ matrix.channel }} + NODE_ENV: ${{ matrix.node_env }} - name: 📦 Upload build artifacts if: steps.prepare_artifacts.outputs.artifacts_exist == 'true' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: DodoStream-${{ steps.app_version.outputs.version }}-${{ matrix.channel }} path: dist/*.apk @@ -307,7 +294,9 @@ jobs: steps: - name: 🏗 Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: 🏗 Install pnpm uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c @@ -315,13 +304,13 @@ jobs: cache: true - name: 🏗 Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '24' - name: 📦 Install dependencies run: | - pnpm install + pnpm install --frozen-lockfile pnpm i -g eas-cli@latest - name: 🚀 Publish to Expo @@ -334,10 +323,14 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest name: Publish + permissions: + contents: write steps: - name: 🏗 Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false - name: 🏗 Install pnpm uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c @@ -345,13 +338,13 @@ jobs: cache: true - name: 🏗 Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '24' - name: 📦 Install dependencies run: | - pnpm install + pnpm install --frozen-lockfile pnpm i -g eas-cli@latest - name: 🔖 Read app version @@ -363,7 +356,7 @@ jobs: echo "App version: $VERSION" - name: 📥 Download production APK artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: pattern: DodoStream-*-production* path: dist @@ -375,24 +368,27 @@ jobs: run: | echo "Final list of APK artifacts:" find dist/ -name "*.apk" -type f -exec ls -lh {} \; - + apk_count=$(find dist/ -name "*.apk" -type f 2>/dev/null | wc -l) echo "Total APKs ready for release: $apk_count" - + if [ "$apk_count" -eq 0 ]; then echo "ERROR: No APK files found!" exit 1 fi - name: 🏷️ Create GitHub Release if not exists - uses: ncipollo/release-action@v1 - with: - tag: v${{ steps.app_version.outputs.version }} - name: v${{ steps.app_version.outputs.version }} + env: # Custom PAT token required so that the other on: release workflow is triggered - token: ${{ secrets.RELEASE_PAT }} - artifacts: dist/*.apk - allowUpdates: false - skipIfReleaseExists: true - generateReleaseNotes: true - prerelease: true + GH_TOKEN: ${{ secrets.RELEASE_PAT }} + VERSION: ${{ steps.app_version.outputs.version }} + run: | + TAG="v${VERSION}" + if gh release view "$TAG" > /dev/null 2>&1; then + echo "Release ${TAG} already exists; skipping." + exit 0 + fi + gh release create "$TAG" dist/*.apk \ + --title "$TAG" \ + --generate-notes \ + --prerelease diff --git a/.github/workflows/security.yaml b/.github/workflows/security.yaml new file mode 100644 index 0000000..5848958 --- /dev/null +++ b/.github/workflows/security.yaml @@ -0,0 +1,35 @@ +name: Workflow Security + +on: + pull_request: + paths: + - '.github/workflows/**' + push: + branches: [main] + paths: + - '.github/workflows/**' + workflow_dispatch: + +permissions: + contents: read + +jobs: + audit: + name: actionlint + zizmor + runs-on: ubuntu-latest + steps: + - name: 🏗 Checkout repository + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: 🔍 actionlint + run: >- + docker run --rm -v "$PWD:/repo" -w /repo + rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667 + + - name: 🌈 zizmor + run: >- + docker run --rm -v "$PWD:/repo" -w /repo + ghcr.io/zizmorcore/zizmor@sha256:a2eb396d886c053073405c7a980f2139ba2248ec172243cfa3841e57196e8101 + --no-progress --color never .github/workflows diff --git a/.maestro/flows/details.yaml b/.maestro/flows/details.yaml index a6699ed..0ac194e 100644 --- a/.maestro/flows/details.yaml +++ b/.maestro/flows/details.yaml @@ -119,7 +119,6 @@ appId: app.dodora.dodostream.dev id: settings-picker-season timeout: 15000 - # Episode long-press action menu. - longPressOn: id: episode-e2e:series:harbor-lights:1:1 diff --git a/.maestro/flows/search.yaml b/.maestro/flows/search.yaml index b06e8bd..7a54489 100644 --- a/.maestro/flows/search.yaml +++ b/.maestro/flows/search.yaml @@ -9,16 +9,16 @@ appId: app.dodora.dodostream.dev id: nav-search - tapOn: id: search-input -- inputText: "meridian" +- inputText: 'meridian' - hideKeyboard - tapOn: id: search-submit # Grouped results from the fixture movie catalog. - extendedWaitUntil: - visible: "The Meridian Archive" + visible: 'The Meridian Archive' timeout: 15000 -- assertVisible: "DodoStream Movies" +- assertVisible: 'DodoStream Movies' - takeScreenshot: search/01-results - runFlow: when: @@ -30,7 +30,7 @@ appId: app.dodora.dodostream.dev thresholdPercentage: 98 # Open a result. -- tapOn: "The Meridian Archive" +- tapOn: 'The Meridian Archive' - extendedWaitUntil: visible: id: media-play diff --git a/.omp/AGENTS.md b/.omp/AGENTS.md new file mode 100644 index 0000000..dba71e9 --- /dev/null +++ b/.omp/AGENTS.md @@ -0,0 +1 @@ +@../AGENTS.md diff --git a/.omp/RULES.md b/.omp/RULES.md new file mode 100644 index 0000000..065b1dd --- /dev/null +++ b/.omp/RULES.md @@ -0,0 +1,5 @@ +- Work only in the assigned Git worktree. +- For autonomous work with multiple behavioral slices: state the expected behavior, verify each slice at the lowest meaningful level, review it, and create its checkpoint commit before the next slice. +- Do not claim verification that was not run. Record skipped expensive checks and the reason. +- Do not start Metro, use raw `adb`, or acquire/drive a device unless the task explicitly requires device verification or the user authorizes it; then use an `agent-device` MCP session and release it. +- Never treat an OMP conversation checkpoint as a Git checkpoint. diff --git a/.omp/mcp.json b/.omp/mcp.json new file mode 100644 index 0000000..abf4cbf --- /dev/null +++ b/.omp/mcp.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://raw.githubusercontent.com/can1357/oh-my-pi/main/packages/coding-agent/src/config/mcp-schema.json", + "mcpServers": { + "agent-device": { + "type": "stdio", + "command": "agent-device", + "args": ["mcp"], + "timeout": 120000 + }, + "github": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/x/all/readonly", + "headers": { + "Authorization": "Bearer ${GITHUB_MCP_PAT}" + } + }, + "github-write": { + "type": "http", + "url": "https://api.githubcopilot.com/mcp/", + "headers": { + "Authorization": "Bearer ${GITHUB_MCP_PAT}" + } + } + } +} diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..4edcdd5 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +# pnpm owns this generated lockfile. +pnpm-lock.yaml diff --git a/.superset/config.json b/.superset/config.json index 91ba860..53821e2 100644 --- a/.superset/config.json +++ b/.superset/config.json @@ -1,9 +1,8 @@ { "setup": [ - "pnpm install", - "cp \"$SUPERSET_ROOT_PATH/.env.local\" .env.local", - "pnpm exec expo prebuild" + "pnpm install --frozen-lockfile", + "if [ -f \"$SUPERSET_ROOT_PATH/.env.local\" ] && [ ! -f .env.local ]; then cp \"$SUPERSET_ROOT_PATH/.env.local\" .env.local; fi" ], "teardown": [], - "run": ["pnpm start"] + "run": ["EXPO_NO_TELEMETRY=1 pnpm start"] } diff --git a/AGENTS.md b/AGENTS.md index c621cf6..cefc669 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,323 +1,104 @@ # DodoStream Agent Guidelines -Essential context and rules for AI agents working on DodoStream. +This is the canonical durable guidance for coding agents. Contributor-facing guidance lives in [CONTRIBUTING.md](CONTRIBUTING.md). OMP imports this file through `.omp/AGENTS.md`. -## 1. Project Overview +## Worktree and permission boundary -- **Type:** Expo React Native app for TV (Apple TV, Android TV) and Mobile. -- **Stack:** Expo SDK 54, React Native TVOS, TypeScript (strict), Zustand, React Query, Shopify Restyle, Moti, LegendList (`@legendapp/list`). -- **Routing:** File-based via `expo-router` in `src/app/`. -- **Package Manager:** pnpm (v10). Use `pnpx expo install ` for adding deps, NOT `pnpm add`. +- Work only in the assigned Git worktree. Do not stage, modify, or use another worktree's files or processes. +- Every UX, UI, or user-facing functionality change requires live verification through the configured `agent-device` MCP server; tests supplement but do not replace that evidence. +- Interact with `agent-device` only through its OMP MCP server. Do not invoke its CLI directly, use raw `adb`, boot or drive a simulator/emulator through another tool, or run `pnpm android`/`pnpm ios`. +- If no agent-device session is active or the selected target is occupied by another agent, ask the user what to do before acquiring, booting, or reusing a target. +- Do not start Metro unless the task explicitly requires a development-server workflow. The release-APK verification route does not require Metro. +- Do not use Expo web as verification for this TV/mobile application. +- Never claim a command, device observation, CI result, or screenshot that was not actually run or observed. -## 2. Build & Test Commands +## Project facts -```bash -pnpm install # Install dependencies -pnpm start # Start Expo dev client -pnpm android # Run on Android -pnpm ios # Run on iOS -pnpm lint # ESLint (expo + react-compiler + import-sort + custom rules) -pnpm format # ESLint --fix + Prettier --write -pnpm test # Run all Jest tests -pnpm test -- path/to/file.test.ts # Run a single test file -pnpm test -- -t "test name" # Run tests matching a name pattern -pnpm tsc # Type-check (no emit) -pnpm db:generate # Generate Drizzle migrations (after schema changes) -pnpm db:studio # Open Drizzle Studio (SQLite browser) -``` - -### App Variant Env Vars - -```bash -APP_VARIANT=dev pnpm start # Dev build (different bundle ID / app name) -APP_VARIANT=prod pnpm start # Production build -EXPO_TV=1 pnpm start # Enable TV mode -``` - -### Simulators & Emulators - -- **NEVER boot, launch, build for, or drive iOS simulators / Android emulators on your own** (this includes `pnpm ios`, `pnpm android`, `xcrun simctl`, `adb`, etc.). -- **NEVER start the Metro bundler or another development server on your own** (this includes `pnpm start`); ask the user for explicit instruction first. -- **NEVER invoke `adb` or otherwise interact with Android devices/emulators without the user's explicit instruction**. -- If a simulator/emulator run is needed, stop and ask the user first — they will run it or explicitly approve. -- **NEVER use Expo web preview (`expo start --web`) for verification**; this TV/mobile app does not support the web platform. - -> CI order: `expo install --check` → `pnpm tsc` → `pnpm lint` → `pnpm test` - -## 3. Directory Structure - -``` -src/ - app/ # File-system routes (expo-router). Each file = route. - components/ # UI components by domain: basic/, media/, video/, profile/ - api/ # API clients: github/, introdb/, stremio/ + errors.ts - store/ # Zustand stores (profile, settings, watch-history, my-list, addon) - theme/ # Shopify Restyle theme (theme.ts is the single source of truth) - constants/ # Centralized constants: playback.ts, ui.ts, media.ts (NO magic numbers) - db/ # SQLite schema and Drizzle ORM setup - hooks/ # Custom React hooks - utils/ # Utilities and helpers - types/ # Shared TypeScript types -``` - -## 4. Code Style & Formatting - -### Prettier - -- `printWidth: 100`, `tabWidth: 2`, `singleQuote: true`, `bracketSameLine: true`, `trailingComma: 'es5'`. - -### ESLint - -- Flat config (`eslint.config.js`): expo base + `eslint-plugin-react-compiler` (recommended). -- `react/display-name` is disabled. -- **Auto-enforced rules** (run `pnpm lint` to check, `pnpm format` to auto-fix): - - `simple-import-sort/imports` — import order is auto-sorted on commit. - - `no-restricted-imports` — legacy `Animated` from `react-native` is banned. - - `no-console` — only `console.error` is allowed; use debug helpers for logging. - - `prefer-const`, `no-var` — enforced. - - `max-lines-per-function` — warns at 220 lines for component files. - -### Pre-commit Hooks - -Lefthook runs `eslint --fix` and `prettier --write` on staged files at commit time. - -### Commit Messages - -- Use Conventional Commits: `(): `. -- Use a lower-case type such as `feat`, `fix`, `refactor`, `test`, `docs`, or `chore`; scope the change when it improves clarity. -- Keep the summary concise, imperative, and under 72 characters. Do not use generic messages such as `update` or `fix`. -- Keep each commit to one logical change. Add a body only when it explains a non-obvious decision, risk, or migration. - -### Import Order - -Auto-enforced by `simple-import-sort` (React first, then external, then `@/` aliases, then relative). -Path alias: `@/*` maps to `src/*` (configured in `tsconfig.json`). - -### Naming Conventions - -- **Components/Files:** PascalCase (`MediaCard.tsx`). -- **Hooks:** camelCase with `use` prefix (`useMediaDetails.ts`). -- **Variables/Functions:** camelCase. -- **Booleans:** Prefix with `is`, `has`, `should`. -- **Handlers:** `handle*` for internal handlers, `on*` for callback props. -- **Domain:** Use `Media` (not `Movie`). Use `isFocused` (not `focused`) for focus state. - -### TypeScript - -- Strict mode enabled. No `any`. -- Define `interface ComponentProps` for all component props. -- Rely on type inference for return types unless ambiguous. -- Prefer optional chaining over `typeof fn === 'function'` checks. - ```ts - // Good - const id = getId?.(item) ?? item.fallback; - const label = getGroupLabel?.(groupId) ?? groupId; - - // Avoid - const groupIdBad = typeof getItemGroupId === 'function' ? getItemGroupId(item) : item.groupId; - ``` -- Prefer file-local helper functions with early returns over nested ternary operators. - -### Comments +- DodoStream is an Expo SDK 57 React Native application for Android, Android TV, iOS, and tvOS builds. +- The app uses the `react-native-tvos` 0.86 fork, Expo Router, TypeScript strict mode, Shopify Restyle, Zustand, TanStack React Query, LegendList, Moti/Reanimated, and pnpm 10. +- `APP_VARIANT=dev` produces the separately installable development app (`app.dodora.dodostream.dev`); the default/prod variant uses `app.dodora.dodostream`. +- `EXPO_TV=1` enables TV-specific native configuration. Android E2E builds also use `EXPO_PUBLIC_E2E=1` and `E2E_ORIENTATION=portrait|landscape`. +- `app.config.ts` and the local config plugins are the source of native configuration. Native projects are generated artifacts; review prebuild output and do not hand-edit generated changes when config/plugin changes are the correct source. -- Code must be self-explanatory: descriptive names and small, focused units. If code needs a comment explaining _what_ it does, restructure the code instead of writing the comment. -- Comment only non-obvious _why_: invariants, cross-file coupling, race conditions, or decisions a future reader would otherwise "fix" (e.g. a spacing token that must stay large because a floating label renders into the gap). -- Never narrate the obvious or restate the code; delete such comments. Keep comments to one line where possible. -- JSDoc on exported APIs states the contract, not the implementation. +## Repository layout -## 5. Styling (Shopify Restyle) +- `src/app/` — Expo Router routes and layouts. +- `src/components/` — reusable UI grouped by basic, media, profile, and video domains. +- `src/api/`, `src/hooks/`, `src/store/`, `src/db/` — API clients/hooks, Zustand state, and SQLite/Drizzle persistence. +- `src/theme/`, `src/constants/`, `src/i18n/`, `src/types/`, `src/utils/` — visual tokens, domain constants, translations, shared types, and utilities. +- `packages/e2e-addon/` — deterministic Stremio fixture used by Android E2E; `packages/remote-ui/` — remote UI package. +- `scripts/` — repository tooling, including the Android E2E runner and fixture helpers. +- `.maestro/` — Android E2E flows, configuration, and reviewed visual baselines. -- **Source of truth:** `src/theme/theme.ts`. -- Use `Box`, `Text` from Restyle for all layout and text. -- Use semantic color names (`mainBackground`, `cardBackground`, `textPrimary`, `focusBackground`). -- Use theme spacing (`xs`, `s`, `m`, `l`, `xl`, `xxl`), border radii, card sizes, and focus values. -- **Never** hardcode hex colors, pixel values, or magic numbers. Use theme tokens or constants. +## MCP services -### Correct vs Wrong Styling +- `github` is the read-only GitHub MCP server; `github-write` is the write-capable one. Use `github` for all reading. Never call `github-write` tools (labels, comments, issues, PRs) without explicit user approval in the conversation for that specific action; prepare the exact payload and ask first. +- Both servers authenticate via the `GITHUB_MCP_PAT` environment variable; if it is unset, tell the user rather than retrying. +- Use the `agent-device` MCP tools for opening, installing, resetting, snapshots, screenshots, logs, D-pad actions, and flow execution. Keep one named session per worktree and release it after verification. -```tsx -// CORRECT - Use Box/Text with theme props - - Title - +## Commands and gates -// CORRECT - Use theme values for dimensions -const theme = useTheme(); - +Use pnpm for repository commands. Install Expo dependencies with `pnpm exec expo install `; do not use ad-hoc package executors or package-manager add commands. -// WRONG - Hardcoded values - -``` - -### Never Hardcode -- Colors (use theme colors) -- Spacing/padding/margin (use theme spacing) -- Dimensions for cards, inputs, modals (use `theme.cardSizes` or `theme.sizes`) -- Focus border width or scale (use `theme.focus`) -- Toast durations (use constants from `src/constants/ui.ts`) -- Playback timing values (use constants from `src/constants/playback.ts`) -- **User-facing strings** (use i18n) - -## 6. Internationalization (i18n) - -- **Source of truth:** `src/i18n/translations/en/`. -- **Namespaces:** Translations are split into files (namespaces). -- **Usage:** Use `useTranslation` hook from `react-i18next`. - ```tsx - const { t } = useTranslation('profiles'); - return {t('who_is_watching')}; - ``` -- **Multiple Namespaces:** `const { t } = useTranslation(['setup', 'common']);` -> `t('common:next')`. -- **Dynamic Languages:** Available languages are automatically detected from the `translations/` directory using `require.context`. Use `AVAILABLE_LANGUAGES` from `@/i18n`. -- **Hardcoded Strings:** NEVER use hardcoded strings in components. Always add them to `en/*.json` first. - -## 7. Error Handling & Logging - -- Handle API errors in React Query `onError` or try/catch in services. -- Show Toasts for user-facing failures. -- Use debug helpers for important decision points: autoplay, stream selection, navigation branches, error recovery. - ```ts - const debug = useDebugLogger('ComponentName'); // In components - const debug = createDebugLogger('ModuleName'); // In non-React modules - debug('eventName', { key: value }); - ``` - -## 8. Component Design & Hooks - -- Keep components under ~200 lines. Extract sub-components. (Lint warns at 220 lines.) -- Use `memo()` for list items and frequently re-rendered components. -- Use `useCallback` for event handlers passed to children. -- Use `useMemo` only for genuinely expensive computations (never for components). -- Define TypeScript interfaces for all component props. -- Check `src/components/basic/` and `src/components/media/` before creating new components. - -### useEffect Best Practices -Effects are for synchronizing with **external systems** only. -- **Don't use for:** Deriving state from props (compute during render/useMemo), handling user actions (use event handlers), or chains of state updates. -- **Do use for:** Subscriptions/cleanup, syncing with native modules, timer-based side effects. - -## 9. TV Focus & Navigation - -- **Always** use `` from `src/components/basic/Focusable.tsx` for interactive elements. -- Prefer `variant="background"` or `variant="outline"` (no re-renders). -- Only use render function `({ isFocused }) => ...` when children must react to focus state. -- **Outline focus** (`variant="outline"`): Only for MediaCard and ContinueWatchingCard. -- **Background focus** (`variant="background"`): All other components (buttons, tags, list items). -- Focus vs Active: `focusBackground`/`focusForeground` for focus; `primaryBackground`/`primaryForeground` for selected. -- Use `TVFocusGuideView` for focus groups/traps and `hasTVPreferredFocus` for initial focus. -- Use `nextFocus*` props (`nextFocusDown`, `nextFocusRight`, etc.) to override default directional navigation. -- Always test with D-pad/remote controls. - -### Focus Patterns - -**Pattern 1: Simple focus (Best performance)** -```tsx -// Focus handled automatically via variant - - - Card Content - - -``` - -**Pattern 2: Custom focus (Render function)** -```tsx -// Only use when children need to react to focus state (e.g., text color change) - - {({ isFocused }) => ( - - Card Content - - )} - +```bash +pnpm install +pnpm typecheck +pnpm exec eslint ... +pnpm lint +pnpm format +pnpm format:check +pnpm test +pnpm --silent test:agent +pnpm test -- +pnpm test -- -t "test name" +pnpm --filter @dodostream/e2e-addon typecheck +pnpm test:e2e:addon +pnpm test:e2e:tools +pnpm --silent verify:agent [--only ] [--json] [--verbose] +pnpm verify:ci +pnpm verify:workflows ``` -## 10. State Management (Zustand) - -- Per-profile data structure: `byProfile: Record`. -- Access active profile: `useProfileStore.getState().activeProfileId`. -- **Always** use selectors to prevent unnecessary re-renders. - -### Available Stores -- `profile.store.ts` — Profile management -- `app-settings.store.ts` — Global app settings -- `profile-settings.store.ts` — Per-profile settings (player, languages) -- `watch-history.store.ts` — Watch progress -- `my-list.store.ts` — Saved items -- `addon.store.ts` — Installed Stremio addons -- `integrations.store.ts` — External service integrations (Simkl, etc.) - -## 11. Data Fetching & Lists - -- Use React Query (`@tanstack/react-query`) for all data fetching. Never fetch in raw `useEffect`. -- Use `@legendapp/list` (LegendList) for all scrollable lists. -- Use `useRecyclingState` for local state in list items. -- For non-scrollable lists, use regular `.map()` instead. - -### LegendList Key Points -- Use `keyExtractor` for proper recycling and layout caching. -- Use `getFixedItemSize` when item sizes are known (skips measuring). -- Use `recycleItems` for lists where items have no local state. -- Wrap `renderItem` in `useCallback` with correct dependencies. - -## 12. Animation +`pnpm test:e2e:sync*` is an opt-in real-API suite and requires credentials and authorization. The root Jest suite intentionally excludes `scripts/sync-e2e/` and `packages/e2e-addon/`; run their package-specific commands explicitly. -- Prefer **Moti** for UI animations (fade, slide, scale, skeleton). -- Use **Reanimated** directly only if Moti can't express the behavior. -- **Never** use the legacy `Animated` API. (Lint-enforced: `no-restricted-imports`.) -- Keep animation timings in `src/constants/ui.ts`. +The release APK is the source for Android E2E. Build it with `pnpm e2e:android:build -- --profile ` (see [E2E.md](E2E.md)), then use the `agent-device` MCP server for install, launch, reset, interaction, evidence capture, and Maestro-compatible flow execution. Do not use Metro, Expo web, direct `agent-device` CLI calls, or raw `adb` for this route. -## 13. Routing (expo-router) +Profiles are fixed: `phone` is portrait; `tablet` and `tv` are landscape. The fixture's `/manifest.json` is its readiness and app-contract probe; do not invent a second health endpoint. -- Files in `src/app/` become routes automatically. Dynamic routes: `[id].tsx`. -- Navigation: `router.push({ pathname: '/details/[id]', params: { id, type } })`. -- Params: `const { id, type } = useLocalSearchParams<{ id: string; type: ContentType }>()`. -- Use `_layout.tsx` for shared layouts. +`pnpm verify:ci` is the local PR-equivalent gate: Expo dependency compatibility, Expo Doctor, Android prebuild, root typecheck, full lint, Jest, no-device E2E tool contracts, fixture-package typecheck, and fixture protocol tests. It excludes credentialed sync E2E and live agent-device verification. Run `pnpm verify:workflows` whenever `.github/workflows/**` changes. -## 14. Testing +## UI, platform, and data invariants -- **Static analysis:** Catch issues early with **TypeScript** (type checking) and **ESLint** (linting). -- **Write testable code:** Keep modules small; separate **UI (components)** from **business logic/state** so logic can be tested without rendering. -- **Unit/Integration:** Jest with `jest-expo` preset. Structure as **Arrange / Act / Assert**. -- **Component tests:** React Native Testing Library. Test from **user perspective** (text, accessibility queries). Avoid testing implementation details. -- **Mocking:** Prefer real dependencies when practical, but mock **external systems** (network, native modules). See `jest.setup.js` for existing mocks. -- **Snapshots:** Use sparingly and keep them small. Prefer explicit expectations. -- **E2E tests:** Use device/simulator tests (Detox/Maestro) for critical flows. +- Use Restyle `Box`/`Text` and tokens from `src/theme/theme.ts` for colors, spacing, dimensions, radii, and focus styling. Never add hardcoded visual values or magic animation/playback timing. +- Put user-facing strings in `src/i18n/translations/en/` and access them through `react-i18next`; do not hardcode UI copy. +- Use `useDebugLogger`/`createDebugLogger` for meaningful business decisions and Toasts for user-facing failures. Handle API failures in React Query or service error paths. +- Use React Query for data fetching. Effects synchronize with external systems only; do not fetch or derive state through raw `useEffect` chains. +- Use `memo()` for frequently rendered/list components and `useCallback` for handlers passed to children. Do not use the legacy React Native `Animated` API; prefer Moti and use Reanimated directly only where Moti cannot express the behavior. +- Use `Focusable` from `src/components/basic/Focusable.tsx` for interactive TV/mobile controls. Use `variant="outline"` only for `MediaCard` and `ContinueWatchingCard`; other controls use background/text focus tokens. Keep focus and selected state visually distinct. +- Use `TVFocusGuideView`, `hasTVPreferredFocus`, `nextFocus*`, and imperative/native focus APIs only when the focus graph requires them. Native focus, D-pad transitions, real list recycling, player engines, and Android geometry require a real Android/TV check; Jest mocks cannot prove them. +- Use LegendList for scrollable lists with stable `keyExtractor`, fixed item sizing when known, recycling only when local state permits it, and stable `renderItem` callbacks. Use `.map()` for non-scrollable lists. +- Use expo-router file routes and layouts. Keep per-profile Zustand state keyed by profile and select only the state needed by a component. -- **Android visual E2E:** See [E2E.md](E2E.md) for commands and snapshot updates. -- Run `pnpm lint` and `pnpm test` before finishing any task. +## Code review rules -## 15. Forbidden Patterns +- Generated `android/`/`ios/` output must trace to `app.config.ts` or a config plugin. Reject hand-edits to generated files; fix the owning config instead. +- The fixture `/manifest.json` is the only health probe. Reject a second health endpoint. +- Focus and selected states must stay visually distinct. Reject token changes that merge them — TV users navigate by focus alone. +- Never weaken the deterministic gates (`verify:agent`, `verify:workflows`) to hide a failure; fix the failure or record the debt explicitly. -- `useMemo` for components (use `memo()` instead). -- Inline styles with magic numbers (`style={{ width: 100 }}`). -- Raw `useEffect` for data fetching (use React Query). -- `renderItem` defined inline (use `useCallback` or stable reference). -- Hardcoded colors, dimensions, or timing values (use theme/constants). -- `render*` methods in functional components. +## Generated files, formatting, and commits -> **Lint-enforced:** legacy `Animated` API, `console.log`/`console.warn`, import order, `var`, non-`const` declarations. -> See `pnpm lint` for the full rule list. -## 16. Development Workflow - -1. **Analyze:** Read related files and existing patterns. -2. **Plan:** Check for existing components/utilities before creating new ones. -3. **Implement:** Follow strict typing, theme usage, and conventions above. -4. **Changelog:** If the change is a noteworthy user-facing feature, create a What's New entry (see §17). -5. **Verify:** Run `pnpm lint` and `pnpm test` before finishing. - -## 17. What's New Entries - -Create a What's New entry when a change ships a **noteworthy user-facing feature**: a major new **setting**, a new **integration** or **option**, or **heavy improvements** to an existing flow. Skip minor bug fixes, internal refactors, and cosmetic tweaks. - -Scaffold the entry (do not create the file by hand): - -```bash -pnpm new-whats-new "vX.Y.Z" "Feature Name" -``` +- Use the owning generator for generated content. In particular, create What's New entries with `pnpm new-whats-new` and rebuild `src/constants/whats-new/_registry.ts` with `pnpm generate-whats-new`; see [CONTRIBUTING.md](CONTRIBUTING.md). +- Agents must read the “Noteworthy feature release notes” section in [CONTRIBUTING.md](CONTRIBUTING.md) for the complete What's New criteria and procedure; do not duplicate that procedure here. +- Lefthook formats staged JavaScript/TypeScript/JSON with ESLint and Prettier at commit time. Include only formatting caused by the change. -This creates `src/constants/whats-new/NNNN-vX.Y.Z-slug.md` with a template (see `scripts/new-whats-new.mjs`). Then: +## Agent workflows -1. **Write the content:** Replace the template with a short markdown description — what changed, how to use it, any notes or limitations. -2. **Ask the user for a picture:** If the feature is noteworthy, ask the user to provide a nice image (screenshot or illustration) and save it next to the markdown as `NNNN-vX.Y.Z-slug.png` (`.webp` and `.jpg` are also supported; see `scripts/generate-whats-new-registry.mjs`). The modal picks it up automatically. -3. **Rebuild the registry:** Run `pnpm generate-whats-new` so `src/constants/whats-new/_registry.ts` stays in sync. +- If the task is prioritizing issues or choosing the next feature: load the `issue-triage` skill first. +- If a bug report must be reproduced: load the `bug-reproduction` skill, then `slice-development` for the fix. +- If a feature is delivered end to end: load the `feature-delivery` skill; it composes `slice-development` and `android-interactive-verification`. +- If any code, test, or build behavior changed: run the owning check from the `agent-verification` skill before the next checkpoint. +- If substantive work is ready for review: load the `pr-handoff` skill. +- If a PR, comment, or label write is pending: create it only via `pr-handoff` and `github-write` with the exact approved payload. Never push, open a PR, or merge without explicit user approval for that action. +- All non-trivial changes follow `slice-development` for slicing, proof levels, checkpoint commits, and the final evidence structure. +- If the user intervenes because the agent did something wrong: find the durable rule, skill, or deterministic gate that should have prevented it. Apply small corrections (rule wording, skill steps, cheap deterministic gates) immediately with the next checkpoint; propose larger process changes in the final response instead of improvising them. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d801208..f06a04f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,20 +31,42 @@ pnpm start ### Quality checks +Run the narrowest checks that cover the change while iterating, then run the final applicable gate before opening a PR: + ```bash -pnpm lint -pnpm format +pnpm typecheck +pnpm exec eslint ... +pnpm test -- pnpm test +pnpm verify:ci +``` + +Use `pnpm test:e2e:sync*` only when the required real-service credentials and authorization are available. Use the Android E2E commands in [E2E.md](E2E.md) only with an explicitly authorized device session. + +## Noteworthy feature release notes + +Create a What's New entry for a noteworthy user-facing feature: a major new setting, a new integration or option, or a substantial improvement to an existing flow. Skip minor bug fixes, internal refactors, and cosmetic changes. + +Scaffold the entry instead of creating it by hand: + +```bash +pnpm new-whats-new "vX.Y.Z" "Feature Name" ``` +Then: + +1. Replace the template with a short markdown description covering what changed, how to use it, and relevant limitations. +2. For a noteworthy feature, ask the user for a suitable screenshot or illustration and save it beside the entry as `.png`, `.webp`, or `.jpg` using the generated filename. +3. Run `pnpm generate-whats-new` so `src/constants/whats-new/_registry.ts` stays synchronized. + ## Project conventions - Keep changes minimal and focused; avoid drive-by refactors. - Prefer TypeScript strict correctness. - Use the existing theme tokens and shared components. -- For TV-interactive UI, use the existing focus primitives/components (e.g. `Focusable`). +- For TV-interactive UI, use the existing focus primitives/components (for example, `Focusable`). - For data fetching, prefer the existing React Query hooks. -- **Internationalization**: Do not use hardcoded strings in UI components. Use the `useTranslation` hook from `react-i18next` and add strings to the appropriate namespace in `src/i18n/translations/en/`. +- Do not hardcode UI strings in components. Use `useTranslation` and add strings to the appropriate namespace in `src/i18n/translations/en/`. ## Translating the app @@ -54,18 +76,18 @@ We welcome translations for any language! To add a new language: 2. Copy all JSON files from `src/i18n/translations/en/` to your new folder. 3. Translate the values in the new JSON files. Keep the keys exactly the same. -We use a namespaced JSON structure where each file represents a namespace (e.g., `common`, `settings`, `profiles`). +We use a namespaced JSON structure where each file represents a namespace (for example, `common`, `settings`, `profiles`). ## Submitting changes 1. Fork the repo 2. Create a branch -3. Make your change (include tests if the area already has tests) -4. Run `pnpm lint` and `pnpm test` +3. Make a focused change (include tests when the behavior is covered by an existing test layer) +4. Run the applicable focused checks and `pnpm verify:ci` before opening a PR 5. Open a PR with: - what changed - why - - how you tested + - how you tested it ## Reporting bugs diff --git a/E2E.md b/E2E.md index 80932f7..c517db8 100644 --- a/E2E.md +++ b/E2E.md @@ -1,23 +1,60 @@ # Android E2E -Maestro runs deterministic Android UI journeys against the local fixture add-on. -Every E2E run includes visual assertions from committed snapshots. +Two supported routes share one contract: the release APK built from the E2E +profile, the seeded fixture add-on (`/manifest.json`), and Maestro. Metro is +never part of an E2E run. ## Prerequisites - Node.js 24 and pnpm 10 - Java 17 and Android API 35 -- A booted Android emulator or device available through `adb` +- An Android emulator or device; agent verification uses the `agent-device` MCP server, the runner route uses `adb` - [Maestro](https://maestro.mobile.dev/): ```bash curl -Ls https://get.maestro.mobile.dev | bash ``` -The runner starts the fixture add-on, builds and installs a release APK, configures the -selected device profile, then runs Maestro. Metro is not required. +## Build the release APK (both routes) -## Run tests +```bash +pnpm e2e:android:build -- --profile [--abi ] +``` + +The build applies `APP_VARIANT=dev`, `EXPO_PUBLIC_E2E=1`, the profile +orientation, `EXPO_TV=1` for TV, and `ADDON_MANIFEST_URL` (default +`http://10.0.2.2:8765/manifest.json`, the Android-emulator host alias). It +never touches a device: it prints the selected APK and records +`artifacts/e2e//artifact.json`. Pass `--abi` when the target ABI is +known; otherwise the build selects a universal APK when one exists. + +## Agent route (agent-device MCP) + +This route is required for agent verification and for every UX, UI, or +user-facing change. Interact with `agent-device` only through its MCP tools — +never through the CLI, raw `adb`, or Metro. + +1. Start the fixture add-on for the worktree (`pnpm e2e:addon`) and probe its + `/manifest.json`. +2. Build the release APK for the profile (above). +3. Through the MCP tools, list or boot the profile-matching device and bind + the session to this worktree. If no target is free or it is occupied by + another agent, ask the user what to do. +4. Through the MCP tools: animations off, profile orientation, install the + APK, launch `app.dodora.dodostream.dev`, and clear only the app state when a + clean start is needed. Capture the initial screenshot, hierarchy snapshot, + and app logs before interacting. +5. Interact through the MCP tools (`tv_remote` for `DPAD_*` and `BACK`, press, + type, swipe) and capture evidence at each meaningful state. +6. Run a committed flow through the MCP tools only when it is the reviewed + regression path: `test` with the flow file plus `ADDON_MANIFEST_URL`, + `PROFILE`, and `E2E_ORIENTATION` env. +7. Release the MCP session when finished. + +Profiles are fixed: `phone` is portrait; `tablet` and `tv` are landscape. Flows +are in `.maestro/flows/`. + +## Human and CI route (runner) ```bash # Phone full journey @@ -31,15 +68,15 @@ pnpm e2e:android -- --profile tv pnpm e2e:android -- --profile tv --flow tv-navigation.yaml # Phone, tablet, TV, and TV navigation -pnpm e2e:android:all +pnpm e2e:android:all -- --device ``` -Use `--device ` to select a specific connected device. Physical devices also -need `ADDON_MANIFEST_URL=http://:8765/manifest.json`, because `10.0.2.2` is an -Android-emulator host alias. - -Profiles are fixed: `phone` is portrait; `tablet` and `tv` are landscape. Flows are in -`.maestro/flows/`. +The runner starts the fixture add-on, builds the release APK, installs it +through `adb`, applies `scripts/e2e/configure-device.sh`, and runs Maestro. +Use `--device ` to select a specific connected device; forward it +to `pnpm e2e:android:all` so every profile targets the same device. Physical +devices also need `ADDON_MANIFEST_URL=http://:8765/manifest.json`, +because `10.0.2.2` is an Android-emulator host alias. ## Visual snapshots diff --git a/IDEAS.md b/IDEAS.md index 0ff761e..9cec2a2 100644 --- a/IDEAS.md +++ b/IDEAS.md @@ -45,4 +45,4 @@ These are general ideas for DodoStream that are undescribed and unplanned. ## Technical -- [ ] Auto-updater for Android APKs \ No newline at end of file +- [ ] Auto-updater for Android APKs diff --git a/README.md b/README.md index c8944dd..a5be2cd 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ An open-source, privacy-focused media hub built with Expo + React Native, curren [Request a Feature](https://github.com/DodoraApp/DodoStream/issues/new?labels=enhancement&template=feature_request.md) [Join the Discord Server (for general ideas, suggestions, decisions)][discord-url] - --- - +--- + 📋 If you have 5 minutes of time and want to help us shape the future development please fill out the [DodoStream User Survey](https://forms.gle/8errZef3y24t9rgd6) --- @@ -52,7 +52,7 @@ Supports **Stremio Addons** (see details below). ## Screenshots -These *are* outdated but kept for an initial impression. +These _are_ outdated but kept for an initial impression. ![](docs/screenshots/tv/home-continue-watching.png) ![](docs/screenshots/tv/details.png) diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..afa2724 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,4 @@ +/** @type {import('@commitlint/types').UserConfig} */ +module.exports = { + extends: ['@commitlint/config-conventional'], +}; diff --git a/eslint.config.js b/eslint.config.js index e47da54..6737e80 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,6 +2,7 @@ const expoConfig = require('eslint-config-expo/flat'); const reactCompiler = require('eslint-plugin-react-compiler'); const simpleImportSort = require('eslint-plugin-simple-import-sort'); +const i18next = require('eslint-plugin-i18next'); module.exports = [ ...expoConfig, @@ -64,6 +65,43 @@ module.exports = [ 'max-lines-per-function': ['warn', { max: 220, skipComments: true, skipBlankLines: true }], }, }, + // i18n and theme tokens (AGENTS.md invariants: no hardcoded UI copy, Restyle tokens only). + // Brand names are not translatable copy. + { + files: ['src/app/**/*.{ts,tsx}', 'src/components/**/*.{ts,tsx}'], + plugins: { + i18next, + }, + rules: { + 'i18next/no-literal-string': [ + 'error', + { + mode: 'jsx-text-only', + words: { + exclude: [ + // Plugin defaults (numbers/symbols, ALL_CAPS, emoji)… + '[0-9!-/:-@[-`{-~]+', + '[A-Z_-]+', + /^\p{Emoji}+$/u, + // Decorative separators and dashes rendered as JSX text. + '•', + '—', + // …plus brand names, which are not translatable copy. + 'DodoStream', + 'IMDb', + ], + }, + }, + ], + 'no-restricted-syntax': [ + 'error', + { + selector: 'Literal[value=/^(#[0-9a-fA-F]{3,8}|rgba?\\()/]', + message: 'Use Restyle theme tokens from src/theme/theme.ts instead of hardcoded colors.', + }, + ], + }, + }, // Allow console in build scripts, debug logger, and E2E tests { files: ['plugins/**', 'src/utils/debug.ts', 'scripts/sync-e2e/**'], diff --git a/jest.config.js b/jest.config.js index 76a1675..c631696 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,7 +5,6 @@ module.exports = { setupFiles: ['./jest.setup.js'], setupFilesAfterEnv: ['expo-sqlite-mock/src/setup.ts'], testTimeout: 10000, - // The sync E2E suite hits real Trakt/Simkl APIs and requires interactive auth. // Run it explicitly via `pnpm test:e2e:sync*` (scripts/sync-e2e/jest-e2e.config.js). testPathIgnorePatterns: ['/scripts/sync-e2e/', '/packages/e2e-addon/'], // @ronradtke/react-native-markdown-display ships raw JSX in dist/, so it must diff --git a/jest.setup.js b/jest.setup.js index 3194aeb..34198dd 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -11,6 +11,34 @@ jest.mock('@react-native-async-storage/async-storage', () => require('@react-native-async-storage/async-storage/jest/async-storage-mock') ); +// Timers scheduled during tests (React Query GC timers with per-query gcTime, +// component auto-hide timers) outlive their suites and keep the Jest event loop +// alive — in workers that surfaces as "worker process has failed to exit +// gracefully", in-band as "Jest did not exit". Unref them so the loop can drain +// when the run completes; timers still fire normally while tests run. +const originalSetTimeout = global.setTimeout; +const originalSetInterval = global.setInterval; +global.setTimeout = (...args) => { + const timer = originalSetTimeout(...args); + if (timer && typeof timer.unref === 'function') timer.unref(); + return timer; +}; +global.setInterval = (...args) => { + const timer = originalSetInterval(...args); + if (timer && typeof timer.unref === 'function') timer.unref(); + return timer; +}; + +// expo-sqlite registers every opened database with its devtools client in __DEV__ +// (which jest-expo sets). That client opens a WebSocket that jest workers never +// close — the source of the "worker process has failed to exit gracefully" +// warning. The devtools client itself cannot be resolved as a subpath here, so +// mock the 'expo/devtools' entry it depends on; SQLiteDevToolsClient tolerates a +// null client via optional chaining. +jest.mock('expo/devtools', () => ({ + getDevToolsPluginClientAsync: jest.fn(async () => null), +})); + jest.mock('expo-image', () => { const React = require('react'); const { View } = require('react-native'); diff --git a/lefthook.yml b/lefthook.yml index 5c6b6ef..6cf852e 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -2,10 +2,15 @@ pre-commit: parallel: true commands: eslint: - glob: "*.{js,jsx,ts,tsx}" + glob: '*.{js,jsx,ts,tsx}' run: npx eslint --fix {staged_files} stage_fixed: true prettier: - glob: "*.{js,jsx,ts,tsx,json}" + glob: '*.{js,jsx,ts,tsx,json}' run: npx prettier --write {staged_files} stage_fixed: true + +commit-msg: + commands: + commitlint: + run: pnpm exec commitlint --edit {1} diff --git a/package.json b/package.json index 46c54f5..f9d7c93 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,12 @@ "new-whats-new": "node scripts/new-whats-new.mjs", "generate-whats-new": "node scripts/generate-whats-new-registry.mjs", "prebuild": "expo prebuild", + "typecheck": "tsc --noEmit", "lint": "eslint \"**/*.{js,jsx,ts,tsx}\"", - "format": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix && prettier \"**/*.{js,jsx,ts,tsx,json}\" --write", + "format": "eslint \"**/*.{js,jsx,ts,tsx}\" --fix && prettier --write .", + "format:check": "prettier --check .", "test": "jest", - "web": "expo start --web", + "test:agent": "CI=true NO_COLOR=1 jest --no-watchman --silent", "db:generate": "drizzle-kit generate", "db:studio": "drizzle-kit studio", "postinstall": "lefthook install", @@ -36,7 +38,13 @@ "test:e2e:addon": "pnpm --filter @dodostream/e2e-addon test", "e2e:addon": "pnpm --filter @dodostream/e2e-addon start", "e2e:android": "node scripts/e2e/run-android.mjs", - "e2e:android:all": "node scripts/e2e/run-all.mjs" + "e2e:android:all": "node scripts/e2e/run-all.mjs", + "e2e:android:build": "node scripts/e2e/build-release.mjs", + "test:e2e:tools": "node --test scripts/e2e/test/*.test.mjs", + "test:e2e:tools:agent": "node --test --test-reporter=dot scripts/e2e/test/*.test.mjs", + "verify:workflows": "docker run --rm -v \"$PWD:/repo\" -w /repo rhysd/actionlint@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667 && docker run --rm -v \"$PWD:/repo\" -w /repo ghcr.io/zizmorcore/zizmor@sha256:a2eb396d886c053073405c7a980f2139ba2248ec172243cfa3841e57196e8101 --no-progress --color never .github/workflows", + "verify:agent": "node scripts/agent-verify.mjs", + "verify:ci": "pnpm verify:agent" }, "dependencies": { "@anatine/zod-openapi": "2.2.8", @@ -53,11 +61,11 @@ "@shopify/restyle": "^2.4.5", "@tanstack/react-query": "^5.102.8", "drizzle-orm": "^0.45.2", - "expo": "^57.0.22", - "expo-asset": "~57.0.17", + "expo": "^57.0.24", + "expo-asset": "~57.0.18", "expo-build-disk-cache": "^0.7.2", - "expo-build-properties": "^57.0.17", - "expo-constants": "~57.0.18", + "expo-build-properties": "^57.0.21", + "expo-constants": "~57.0.19", "expo-dev-client": "~57.0.19", "expo-device": "~57.0.2", "expo-drizzle-studio-plugin": "^0.2.1", @@ -70,12 +78,12 @@ "expo-linking": "~57.0.10", "expo-localization": "~57.0.2", "expo-network": "~57.0.2", - "expo-router": "~57.0.21", + "expo-router": "~57.0.22", "expo-splash-screen": "~57.0.9", "expo-sqlite": "~57.0.3", "expo-status-bar": "~57.0.1", "expo-system-ui": "~57.0.4", - "expo-updates": "~57.0.22", + "expo-updates": "~57.0.23", "i18next": "^26.4.1", "moti": "^0.30.0", "react": "19.2.3", @@ -96,9 +104,11 @@ "zustand": "^5.0.15" }, "devDependencies": { + "@commitlint/cli": "^21.2.2", + "@commitlint/config-conventional": "^21.2.2", "@eslint/js": "^9.39.1", "@expo/dom-webview": "~57.0.1", - "@expo/metro-runtime": "^57.0.15", + "@expo/metro-runtime": "^57.0.16", "@react-native-tvos/config-tv": "^0.1.6", "@testing-library/react-native": "^13.3.3", "@ts-rest/core": "3.52.1", @@ -113,8 +123,10 @@ "eslint": "^9.25.1", "eslint-config-expo": "~57.0.2", "eslint-config-prettier": "^10.1.2", + "eslint-plugin-i18next": "^6.1.5", "eslint-plugin-react-compiler": "19.1.0-rc.2", "eslint-plugin-simple-import-sort": "^14.0.0", + "expo-doctor": "^1.20.4", "expo-sqlite-mock": "^4.0.0", "jest": "^29.7.0", "jest-expo": "^57.0.5", diff --git a/packages/e2e-addon/package.json b/packages/e2e-addon/package.json index 9631fb9..e3d2972 100644 --- a/packages/e2e-addon/package.json +++ b/packages/e2e-addon/package.json @@ -9,6 +9,7 @@ "start": "tsx src/server.ts", "build": "tsx scripts/generate-assets.ts", "test": "tsx --test test/*.test.ts", + "test:agent": "tsx --test --test-reporter=dot test/*.test.ts", "typecheck": "tsc --noEmit" }, "dependencies": { diff --git a/packages/remote-ui/src/index.css b/packages/remote-ui/src/index.css index 92a298d..51cc426 100644 --- a/packages/remote-ui/src/index.css +++ b/packages/remote-ui/src/index.css @@ -1,9 +1,13 @@ -@import "tailwindcss"; +@import 'tailwindcss'; @theme { - --font-sans: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --font-sans: + 'Inter', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, + 'Helvetica Neue', Arial, sans-serif; } -html, body, #root { +html, +body, +#root { height: 100%; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 393bc69..c073d9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,76 +58,76 @@ importers: version: 5.102.8(react@19.2.3) drizzle-orm: specifier: ^0.45.2 - version: 0.45.2(better-sqlite3@12.6.2)(expo-sqlite@57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3)) + version: 0.45.2(better-sqlite3@12.6.2)(expo-sqlite@57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3)) expo: - specifier: ^57.0.22 - version: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + specifier: ^57.0.24 + version: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-asset: - specifier: ~57.0.17 - version: 57.0.17(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3) + specifier: ~57.0.18 + version: 57.0.18(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3) expo-build-disk-cache: specifier: ^0.7.2 - version: 0.7.2(@expo/cli@57.0.24(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3))(@expo/config@57.0.9(typescript@6.0.3))(typescript@6.0.3) + version: 0.7.2(@expo/cli@57.0.26(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3))(@expo/config@57.0.9(typescript@6.0.3))(typescript@6.0.3) expo-build-properties: - specifier: ^57.0.17 - version: 57.0.17(expo@57.0.22) + specifier: ^57.0.21 + version: 57.0.21(expo@57.0.24) expo-constants: - specifier: ~57.0.18 - version: 57.0.18(expo@57.0.22)(react-native-tvos@0.86.2-0) + specifier: ~57.0.19 + version: 57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0) expo-dev-client: specifier: ~57.0.19 - version: 57.0.19(expo@57.0.22)(react-native-tvos@0.86.2-0) + version: 57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0) expo-device: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.22) + version: 57.0.2(expo@57.0.24) expo-drizzle-studio-plugin: specifier: ^0.2.1 - version: 0.2.1(expo-sqlite@57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3))(expo@57.0.22) + version: 0.2.1(expo-sqlite@57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3))(expo@57.0.24) expo-font: specifier: ^57.0.4 - version: 57.0.4(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + version: 57.0.4(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) expo-haptics: specifier: ~57.0.3 - version: 57.0.3(expo@57.0.22) + version: 57.0.3(expo@57.0.24) expo-image: specifier: ~57.0.5 - version: 57.0.5(expo@57.0.22)(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + version: 57.0.5(expo@57.0.24)(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) expo-keep-awake: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.22)(react@19.2.3) + version: 57.0.2(expo@57.0.24)(react@19.2.3) expo-libvlc-player: specifier: 57.0.28 - version: 57.0.28(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + version: 57.0.28(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) expo-linear-gradient: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + version: 57.0.2(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) expo-linking: specifier: ~57.0.10 - version: 57.0.10(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + version: 57.0.10(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) expo-localization: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.22)(react@19.2.3) + version: 57.0.2(expo@57.0.24)(react@19.2.3) expo-network: specifier: ~57.0.2 - version: 57.0.2(expo@57.0.22)(react@19.2.3) + version: 57.0.2(expo@57.0.24)(react@19.2.3) expo-router: - specifier: ~57.0.21 - version: 57.0.21(719806e0ddf700953ff39fc2ded2cadd) + specifier: ~57.0.22 + version: 57.0.22(c4b2198b165dcee23bdfdd48580aa1ee) expo-splash-screen: specifier: ~57.0.9 - version: 57.0.9(expo@57.0.22)(typescript@6.0.3) + version: 57.0.9(expo@57.0.24)(typescript@6.0.3) expo-sqlite: specifier: ~57.0.3 - version: 57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + version: 57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) expo-status-bar: specifier: ~57.0.1 - version: 57.0.1(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + version: 57.0.1(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) expo-system-ui: specifier: ~57.0.4 - version: 57.0.4(expo@57.0.22)(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) + version: 57.0.4(expo@57.0.24)(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)) expo-updates: - specifier: ~57.0.22 - version: 57.0.22(expo-dev-client@57.0.19(expo@57.0.22)(react-native-tvos@0.86.2-0))(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + specifier: ~57.0.23 + version: 57.0.23(expo-dev-client@57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0))(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) i18next: specifier: ^26.4.1 version: 26.4.1(typescript@6.0.3) @@ -183,18 +183,24 @@ importers: specifier: ^5.0.15 version: 5.0.15(@types/react@19.2.18)(react@19.2.3)(use-sync-external-store@1.6.0(react@19.2.3)) devDependencies: + '@commitlint/cli': + specifier: ^21.2.2 + version: 21.2.2(@types/node@25.0.9)(conventional-commits-parser@7.1.2)(typescript@6.0.3) + '@commitlint/config-conventional': + specifier: ^21.2.2 + version: 21.2.2 '@eslint/js': specifier: ^9.39.1 version: 9.39.1 '@expo/dom-webview': specifier: ^57.0.1 - version: 57.0.1(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + version: 57.0.1(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) '@expo/metro-runtime': - specifier: ^57.0.15 - version: 57.0.15(@expo/log-box@57.0.4)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3) + specifier: ^57.0.16 + version: 57.0.16(@expo/log-box@57.0.4)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3) '@react-native-tvos/config-tv': specifier: ^0.1.6 - version: 0.1.6(expo@57.0.22) + version: 0.1.6(expo@57.0.24) '@testing-library/react-native': specifier: ^13.3.3 version: 13.3.3(jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3)))(react-native-tvos@0.86.2-0)(react-test-renderer@19.2.3(react@19.2.3))(react@19.2.3) @@ -218,7 +224,7 @@ importers: version: 6.9.4 babel-preset-expo: specifier: ~57.0.9 - version: 57.0.9(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.22)(react-refresh@0.17.0) + version: 57.0.9(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.24)(react-refresh@0.17.0) dotenv: specifier: ^17.4.2 version: 17.4.2 @@ -234,21 +240,27 @@ importers: eslint-config-prettier: specifier: ^10.1.2 version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-i18next: + specifier: ^6.1.5 + version: 6.1.5 eslint-plugin-react-compiler: specifier: 19.1.0-rc.2 version: 19.1.0-rc.2(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-simple-import-sort: specifier: ^14.0.0 version: 14.0.0(eslint@9.39.1(jiti@2.6.1)) + expo-doctor: + specifier: ^1.20.4 + version: 1.20.4 expo-sqlite-mock: specifier: ^4.0.0 - version: 4.0.0(better-sqlite3@12.6.2)(expo-sqlite@57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3))(expo@57.0.22)(jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3))) + version: 4.0.0(better-sqlite3@12.6.2)(expo-sqlite@57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3))(expo@57.0.24)(jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3))) jest: specifier: ^29.7.0 version: 29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3)) jest-expo: specifier: ^57.0.5 - version: 57.0.5(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(expo@57.0.22)(jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3)))(react-native-tvos@0.86.2-0)(react@19.2.3) + version: 57.0.5(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(expo@57.0.24)(jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3)))(react-native-tvos@0.86.2-0)(react@19.2.3) lefthook: specifier: ^2.1.12 version: 2.1.12 @@ -260,7 +272,7 @@ importers: version: 19.2.3(react@19.2.3) trakt-api: specifier: github:trakt/trakt-api - version: https://codeload.github.com/trakt/trakt-api/tar.gz/1768a87dedfd3e18ea36ca95b6ce42bd32c1fea6 + version: https://codeload.github.com/trakt/trakt-api/tar.gz/4ec0b28e97520f5239ecb924b7430d0f3aa366b0 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@25.0.9)(typescript@6.0.3) @@ -980,6 +992,91 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@commitlint/cli@21.2.2': + resolution: {integrity: sha512-a+6hQxIxnpdvSvS2apvttPNbEliYsVC3PqFYDiiB2kjbwIsQsj1urvQ4Tkf70pKYozPalKAuRQmm/GHwndduqA==} + engines: {node: '>=22.12.0'} + hasBin: true + + '@commitlint/config-conventional@21.2.2': + resolution: {integrity: sha512-NxA37SZviusFUEYOQZ5hNnZ1h7O/KiemPkxjOlpzKJNnWxThiwc6/SaZhaPa8fyLvfRBAywhQhJJk8XESHWlpQ==} + engines: {node: '>=22.12.0'} + + '@commitlint/config-validator@21.2.0': + resolution: {integrity: sha512-t7AzNHAKeIdo/3NRGwzpufKHsKkPHmFs/56N2Fnsh0/r0rGtnQzTxk6vnFgjaGr4hdSQKNB50/KAhR9Yk4LJKA==} + engines: {node: '>=22.12.0'} + + '@commitlint/ensure@21.2.0': + resolution: {integrity: sha512-76IF9vDNS13lAzEEik9eKwzt8f9hYhWiwVXZ2AnyLCz5/f511FsEQ3pw1X3/zSQpdRLQU7i5qDMVKyXi1GWjSg==} + engines: {node: '>=22.12.0'} + + '@commitlint/execute-rule@21.0.1': + resolution: {integrity: sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA==} + engines: {node: '>=22.12.0'} + + '@commitlint/format@21.2.2': + resolution: {integrity: sha512-v6fvxZSc/AvVMROlr3H34+1766bZSYApRUSCAMjWamStPjKMvZ8GdvVA5YW/VQNgbFTmcMz6OYmSTJEvIjPrfA==} + engines: {node: '>=22.12.0'} + + '@commitlint/is-ignored@21.2.2': + resolution: {integrity: sha512-9UoKNgfFE3LU7FrzierCvk3CdDfMDeVGC86qZiT/n0TIjfq/dmZ9MHuXd45OTNRa26ZanmJRxEtmiXk/lEJihg==} + engines: {node: '>=22.12.0'} + + '@commitlint/lint@21.2.2': + resolution: {integrity: sha512-Fy8JxEBzdmsYWFude/61GxXu5O+wEymwiRK2z9GL9R8mCsXphCoGxAFc5iHn5mjlfcSrhiiONE+ksf4KOjnaPg==} + engines: {node: '>=22.12.0'} + + '@commitlint/load@21.2.2': + resolution: {integrity: sha512-0Tt6wDPX167cjKC5D4zhm0+20wJJG+TN/TKovMOspfSe78rOnKX+MNzlVNiu6HyQPZChPJ8QBH31MVt6Bb8fCg==} + engines: {node: '>=22.12.0'} + + '@commitlint/message@21.2.0': + resolution: {integrity: sha512-YxGoiXD/HXNXLJPrQwE5poXa+XH0CBEm+mdvbHQP0g6MV/dmJyUFCzPNzZbxL93GvZ70TmtTK0Z0/IBpAqHv8g==} + engines: {node: '>=22.12.0'} + + '@commitlint/parse@21.2.2': + resolution: {integrity: sha512-MEkobPfvRp+z06Wro8HMG1BDGHzZmj82A1LH1nWeG3ipHpg/x4m6v3wEDvMBIKjRFUnfR3nBeFs3MVCr7UdAmg==} + engines: {node: '>=22.12.0'} + + '@commitlint/read@21.2.1': + resolution: {integrity: sha512-hUW7EJQnNTL0vPOmVMNK4CrnrNBN0nN+JJHReFkdHO5y4iyHeEmTBwuC15OCqUTjxWo7idnH1LftfpWVIaPWIA==} + engines: {node: '>=22.12.0'} + + '@commitlint/resolve-extends@21.2.2': + resolution: {integrity: sha512-RPkJ/IFi7sMUUVbZLqwWFtWw/zRDcfFsmrPSiTMrt5wb7AdxOr86EGQFvmGzef5QKV5IPBWWCujqVTw1RWX44A==} + engines: {node: '>=22.12.0'} + + '@commitlint/rules@21.2.2': + resolution: {integrity: sha512-eplQzyYkBjYB1HyyRj8hkcK11Y9DU9nuBz7uOKEd6NpE9NGDytLFCAnlRE+OoiK/5sHEJsaz2RGhuWBvYzIbNA==} + engines: {node: '>=22.12.0'} + + '@commitlint/to-lines@21.0.1': + resolution: {integrity: sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw==} + engines: {node: '>=22.12.0'} + + '@commitlint/top-level@21.2.0': + resolution: {integrity: sha512-Y5gmQ+KxzqCrBFJfLvFEPvvwD3LDiNZoTT2yeFBm96M8qhmqSzQc5DvX3rheAaAMjyIvMXOCLS/mWfdpONsjyQ==} + engines: {node: '>=22.12.0'} + + '@commitlint/types@21.2.0': + resolution: {integrity: sha512-7zVFCDB2reMvJH5dmbKnOQPjZEvjdJTH8jc0U/PIPU1r3/+vf5pD1HlfitV2MWsWXrvu7u39iY1lyLUPOaN0Gw==} + engines: {node: '>=22.12.0'} + + '@conventional-changelog/git-client@3.1.2': + resolution: {integrity: sha512-jZqwnJwf7nboIlAcw/mkOjVa6DexCcUOgT2oOQgkoi3z9vR8tGFkcMy2BFcYwjhL9sYcDDXkRQDayiDieCoW7A==} + engines: {node: '>=22'} + peerDependencies: + conventional-commits-filter: ^6.0.1 + conventional-commits-parser: ^7.1.2 + peerDependenciesMeta: + conventional-commits-filter: + optional: true + conventional-commits-parser: + optional: true + + '@conventional-changelog/template@1.4.0': + resolution: {integrity: sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==} + engines: {node: '>=22'} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -1687,8 +1784,8 @@ packages: '@expo-google-fonts/poppins@0.4.1': resolution: {integrity: sha512-LiX5oEryJqDt3UmWwr4oqf11hGMz/JEffkkNcZ3UtGwyoMFzXmBmTwcrAWrEttxypxmLNmPykDg+CY2VjKve9A==} - '@expo/cli@57.0.24': - resolution: {integrity: sha512-CCBkvhPgAcrzJQ3daYbyOD6YuqKKkvOIyUfAbsUIvlpLomIlocYxFHYdmaC/KHqfCsbGFYSV/QKTSYshhSFNEA==} + '@expo/cli@57.0.26': + resolution: {integrity: sha512-vm769xuEyHMToi3uY66JLzRA2GGK+3WzdH71HNn67FuVhry82dKTvMUoeHYtHiindyeieiWfEvHT8Kr1VyEVPQ==} hasBin: true peerDependencies: expo: '*' @@ -1775,8 +1872,8 @@ packages: '@expo/metro-file-map@57.0.3': resolution: {integrity: sha512-1OXy+uPYY5uc7Tm4VBsd2NRn+3wHhqeqNuEO/Xo4kmYgv8FjYgUAc+bUXON9FpC2ikcLn4EVlGM9ce2exx9Mlg==} - '@expo/metro-runtime@57.0.15': - resolution: {integrity: sha512-9RaFkqIzRud6ol9fc5ZKHgNDYeDjQVBdAQC+rjSQ6RiKV0pY+SjDqGmKY5a4M9TaV0qLMCNNrp5R88BqC2xbuQ==} + '@expo/metro-runtime@57.0.16': + resolution: {integrity: sha512-x43WgZoICpp37BhDV7vixe0bNxbVPPGaBLHYc98ZJ8MU+UC2dk39Y2XvkfYMRRbAUSIkCIJiwdERnptlPg0Ckw==} peerDependencies: '@expo/log-box': ^57.0.4 expo: '*' @@ -1811,13 +1908,13 @@ packages: typescript: optional: true - '@expo/router-server@57.0.9': - resolution: {integrity: sha512-/PxRQozFesIyCJZOAtrQE8XcmcojNiL5ctPMQnbE4ojC2EJPu0zc7c0Y4PuXsoRxrZxm8Usw76/lCVrXcfTZ2w==} + '@expo/router-server@57.0.10': + resolution: {integrity: sha512-8rGh6N5di7jzu7UC1SuIA1TWmVd+vBwOHwC7/1ww+FA6CcxcWyQlcPl39h2+Jpnul78CVzWpIjOcdKsZsp2zXw==} peerDependencies: '@expo/metro-runtime': ^57.0.15 expo: '*' - expo-constants: ^57.0.17 - expo-font: ^57.0.3 + expo-constants: ^57.0.18 + expo-font: ^57.0.4 expo-router: '*' expo-server: ^57.0.3 react: '*' @@ -1846,8 +1943,8 @@ packages: '@expo/sudo-prompt@9.3.2': resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==} - '@expo/ui@57.0.18': - resolution: {integrity: sha512-9RkA26v0FSyHlkQCyavZrT8EzsALfkNHIFO5uC+IsAxzWzItdOWdy6asjpg1yaCvBEGMYkP17FUZxluhVbeleA==} + '@expo/ui@57.0.19': + resolution: {integrity: sha512-WB95zQPge5nb04zgY3O2NmMtxg6G+qFH3yOAHcBkTUrJXoBReQoyY3cfp/8N5Q/azcfHJcBE3Jy0Rwq9OoRrfA==} peerDependencies: '@babel/core': '*' expo: '*' @@ -2801,6 +2898,14 @@ packages: react: '*' react-native: '*' + '@simple-libs/child-process-utils@2.0.0': + resolution: {integrity: sha512-dvNoRKLijXnD0XoJAz94pbNuB5GQgDr55UhpSPhffDkTT0Cmcqh9jSCOtwfT2d4H6MI9E7c4SgtMuJXZ6F3c6A==} + engines: {node: '>=22'} + + '@simple-libs/stream-utils@2.0.0': + resolution: {integrity: sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==} + engines: {node: '>=22'} + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -3274,6 +3379,9 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + anser@1.4.10: resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==} @@ -3317,6 +3425,10 @@ packages: resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} engines: {node: '>=10'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + anymatch@3.1.3: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} @@ -3333,6 +3445,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + argue-cli@3.2.0: + resolution: {integrity: sha512-VipTB0gXgGIFO2Rg9yEVN5wLt2AurJcZqDbgmYSwPwsykhLrQhs240/bfceev4w68lI8JshoOJIk9uW7tFzROw==} + engines: {node: '>=22'} + aria-hidden@1.2.6: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} @@ -3448,12 +3564,12 @@ packages: peerDependencies: '@babel/core': ^7.0.0 || ^8.0.0-0 - babel-preset-expo@57.0.11: - resolution: {integrity: sha512-R0NouDI3nzQUsjBh5TUJSDXLmuVzE2bp2YE0ywJkKxzrdWVjShpAlZMBuDeOQy6iKGB6ZCCvMqbQvpqgNMJ7iw==} + babel-preset-expo@57.0.12: + resolution: {integrity: sha512-rrZqJPYbVevy7rqibJZQFd3AsleGslJ63vmB3Qs8b5poOO5AHaehUGbZHjb0HoOZO59d86BEifYbq0/ncoM2UQ==} peerDependencies: '@babel/runtime': ^7.20.0 expo: '*' - expo-widgets: ^57.0.18 + expo-widgets: ^57.0.19 react-refresh: '>=0.14.0 <1.0.0' peerDependenciesMeta: '@babel/runtime': @@ -3654,6 +3770,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -3731,6 +3851,19 @@ packages: resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} engines: {node: '>=18'} + conventional-changelog-angular@9.4.0: + resolution: {integrity: sha512-HdxRxuS8bBXVIuo4V82gvSwAXT0vYQUizrjs/izmPg5JdDstr8v8I5hduGL3iQbG+o310dUDxC4+LetuS5hu9w==} + engines: {node: '>=22'} + + conventional-changelog-conventionalcommits@10.4.0: + resolution: {integrity: sha512-Rriac6ZrAlVm6cy9Bz4NSp+WMHpwNXoPIYex+HjCgduAVUSbnew29DQjQw0C4g9u3HtSYzGiGY+pdBXAZo+4aA==} + engines: {node: '>=22'} + + conventional-commits-parser@7.1.2: + resolution: {integrity: sha512-O+x4N2yH+ijvqWlIyTHsXTAP+algNWgGbjY2duCe8w2vUMvUB95cLRslCPfTMQyLAKlet3bhZTdu6ozn4M+QJQ==} + engines: {node: '>=22'} + hasBin: true + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -3748,6 +3881,14 @@ packages: resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} engines: {node: '>= 0.10'} + cosmiconfig-typescript-loader@6.3.0: + resolution: {integrity: sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA==} + engines: {node: '>=v18'} + peerDependencies: + '@types/node': '*' + cosmiconfig: '>=9' + typescript: '>=5' + cosmiconfig@7.1.0: resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} engines: {node: '>=10'} @@ -3761,6 +3902,15 @@ packages: typescript: optional: true + cosmiconfig@9.0.2: + resolution: {integrity: sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + create-jest@29.7.0: resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -4068,6 +4218,9 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4136,6 +4289,9 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + es-toolkit@1.52.0: + resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==} + esbuild@0.18.20: resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} engines: {node: '>=12'} @@ -4234,6 +4390,10 @@ packages: peerDependencies: eslint: '>=8.10' + eslint-plugin-i18next@6.1.5: + resolution: {integrity: sha512-xCTfstbK9ZpQ6UFT5S1s6zbZMhKn2o2jRSQYiU2UkV7wt8y1m3WjkUYGkGlipgRdFInYI9+LAqE+JQU/L73HmA==} + engines: {node: '>=18.10.0'} + eslint-plugin-import@2.32.0: resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} engines: {node: '>=4'} @@ -4350,8 +4510,8 @@ packages: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - expo-asset@57.0.17: - resolution: {integrity: sha512-qSdWZBSfEg06o3GJMwcj36tw0vXkB6eYjqkQistWmyJkHxhUOKnsKJnev6OraXxLdoI+KOd1+qgp/ENx8gcqYA==} + expo-asset@57.0.18: + resolution: {integrity: sha512-zDIQkUr0NM2S7YRj98Zohf/OBuM1q0o4ctxLz1C1oe4/33lHNLjp+2v3qUtBWVwn0zKka8UGADmS8M+83Wi6pw==} peerDependencies: expo: '*' react: '*' @@ -4364,13 +4524,13 @@ packages: '@expo/cli': '*' '@expo/config': '*' - expo-build-properties@57.0.17: - resolution: {integrity: sha512-+qA5we2h1Gpnd24RGCR1amfSx7Mtf8glZ0jA5bbe1Nfdw9NdAIl+HPUqU7xyOUDKD0JWsHh9ap6yJG/jTs0tvA==} + expo-build-properties@57.0.21: + resolution: {integrity: sha512-IF5/FPU4SezMYMupB2gfH6eAu197/oKI6Bn0oOT9UQiAg70BNiiQJ499eV21kIWIOGGPdLHg6XqIV5Zq4Gnn7Q==} peerDependencies: expo: '*' - expo-constants@57.0.18: - resolution: {integrity: sha512-cLDwDOniPfuCZPHNY++cAY2ZkK97u5aobbBvilPK2LLmaWxR4ZpD31LWUIzUDLrwMPxLjmh1uURnbeYEf0Mivw==} + expo-constants@57.0.19: + resolution: {integrity: sha512-3eK1kZRwCdMizQLlM/PPlPwjaPnB5qKvdRDMBMnB1ZxmvWJBf9TB4uEO4DMMfIPaFUxYFs29dsR5fPr1gh3FTQ==} peerDependencies: expo: '*' react-native: '*' @@ -4402,6 +4562,11 @@ packages: peerDependencies: expo: '*' + expo-doctor@1.20.4: + resolution: {integrity: sha512-Jm1lIqVdO8br9wiqTg5QhJ1u4zLWAhoue4bXsEuzW543vC3XY+U0cpEOmiLkvJhTTgZgIK+24l4i8VzyZ2VLMg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true + expo-drizzle-studio-plugin@0.2.1: resolution: {integrity: sha512-AjMC7SOutMAv/MkeJSp/26gnHLAA17SG8EtsjIKW6tDcBkq4plDVhjMLusoGe9f0vzYK8KrOvWjEWZ/R+KvIGg==} peerDependencies: @@ -4512,14 +4677,14 @@ packages: expo: '*' react: '*' - expo-router@57.0.21: - resolution: {integrity: sha512-r7UDLZU0gtVm8FW4nv2pxSl9TDhKlf2shcBbfp1T9STXJ3xYl9llLH701qQkICuXqqt3vGpOZ57GXC1ioVWg4g==} + expo-router@57.0.22: + resolution: {integrity: sha512-kOuS3RjMC73YLyBFiV9BzO0Z5OK1nLhPwmptK2II+tePtWXH7oA4zWaPwdWsdzTJRnmGMs/8Sr8YdTPHm1LUCg==} peerDependencies: '@expo/log-box': ^57.0.4 - '@expo/metro-runtime': ^57.0.15 + '@expo/metro-runtime': ^57.0.16 '@testing-library/react-native': '>= 13.2.0' expo: '*' - expo-constants: ^57.0.18 + expo-constants: ^57.0.19 expo-linking: ^57.0.10 react: '*' react-dom: '*' @@ -4601,8 +4766,8 @@ packages: peerDependencies: expo: '*' - expo-updates@57.0.22: - resolution: {integrity: sha512-W/wR3PZiIiu+DtbNFju0WXJF0eqR6m0CNorjmxoQ9/RGiplpIZTrFOBp74dwsU1DOk3qpsivfLHsotR+mGTwkQ==} + expo-updates@57.0.23: + resolution: {integrity: sha512-try1ndXkEH1qpMqs2Nf1fXxN/0GShZad9dWp8pbQpXAngyRk8k/MxJ5gJuVabkPJ2vEW3IgNvTkq54aOELkpZA==} hasBin: true peerDependencies: expo: '*' @@ -4613,8 +4778,8 @@ packages: expo-dev-client: optional: true - expo@57.0.22: - resolution: {integrity: sha512-YWUGqWtchsRQzcbU+Iqqt/1kFbZ9Pp3/eg+UenINahNt2RIaDElFJFFWLD1Yfex6WnBm6ESSE8h0/ja28hRqZg==} + expo@57.0.24: + resolution: {integrity: sha512-LxSnKMv7en/w4XDNxW3VOx7SbtQ2Bs4MyGhtoZCToDsEsBPO7pJXlv5GnQUCd7Wi8118QV/VDwsDLBKTB3Z3iQ==} hasBin: true peerDependencies: '@expo/dom-webview': ^57.0.1 @@ -4656,6 +4821,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} + fb-dotslash@0.5.8: resolution: {integrity: sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA==} engines: {node: '>=20'} @@ -4796,6 +4964,10 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} + engines: {node: '>=18'} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -4850,6 +5022,10 @@ packages: glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + global-directory@5.0.0: + resolution: {integrity: sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==} + engines: {node: '>=20'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -5036,6 +5212,10 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + inline-style-prefixer@7.0.1: resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} @@ -5152,6 +5332,10 @@ packages: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -5454,6 +5638,9 @@ packages: json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -6595,9 +6782,17 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + requireindex@1.1.0: + resolution: {integrity: sha512-LBnkqsDE7BZKvqylbmn7lTIVdpx4K/QCduRATpO5R+wtPmky/a8pN1bO2D6wXppn1497AJF9mNjqAXr6bdl9jg==} + engines: {node: '>=0.10.5'} + requires-port@1.0.0: resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} @@ -6930,6 +7125,14 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} + string.prototype.matchall@4.0.12: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} @@ -7063,6 +7266,10 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -7100,8 +7307,8 @@ packages: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} - trakt-api@https://codeload.github.com/trakt/trakt-api/tar.gz/1768a87dedfd3e18ea36ca95b6ce42bd32c1fea6: - resolution: {tarball: https://codeload.github.com/trakt/trakt-api/tar.gz/1768a87dedfd3e18ea36ca95b6ce42bd32c1fea6} + trakt-api@https://codeload.github.com/trakt/trakt-api/tar.gz/4ec0b28e97520f5239ecb924b7430d0f3aa366b0: + resolution: {tarball: https://codeload.github.com/trakt/trakt-api/tar.gz/4ec0b28e97520f5239ecb924b7430d0f3aa366b0} version: 0.0.0 ts-api-utils@2.5.0: @@ -7449,6 +7656,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -7542,6 +7753,10 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} @@ -7554,6 +7769,10 @@ packages: resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} engines: {node: '>=12'} + yargs@18.1.0: + resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'} @@ -8360,6 +8579,125 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@commitlint/cli@21.2.2(@types/node@25.0.9)(conventional-commits-parser@7.1.2)(typescript@6.0.3)': + dependencies: + '@commitlint/config-conventional': 21.2.2 + '@commitlint/format': 21.2.2 + '@commitlint/lint': 21.2.2 + '@commitlint/load': 21.2.2(@types/node@25.0.9)(typescript@6.0.3) + '@commitlint/read': 21.2.1(conventional-commits-parser@7.1.2) + '@commitlint/types': 21.2.0 + tinyexec: 1.3.1 + yargs: 18.1.0 + transitivePeerDependencies: + - '@types/node' + - conventional-commits-filter + - conventional-commits-parser + - typescript + + '@commitlint/config-conventional@21.2.2': + dependencies: + '@commitlint/types': 21.2.0 + conventional-changelog-conventionalcommits: 10.4.0 + + '@commitlint/config-validator@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + ajv: 8.20.0 + + '@commitlint/ensure@21.2.0': + dependencies: + '@commitlint/types': 21.2.0 + es-toolkit: 1.52.0 + + '@commitlint/execute-rule@21.0.1': {} + + '@commitlint/format@21.2.2': + dependencies: + '@commitlint/types': 21.2.0 + picocolors: 1.1.1 + + '@commitlint/is-ignored@21.2.2': + dependencies: + '@commitlint/types': 21.2.0 + semver: 7.8.5 + + '@commitlint/lint@21.2.2': + dependencies: + '@commitlint/is-ignored': 21.2.2 + '@commitlint/parse': 21.2.2 + '@commitlint/rules': 21.2.2 + '@commitlint/types': 21.2.0 + + '@commitlint/load@21.2.2(@types/node@25.0.9)(typescript@6.0.3)': + dependencies: + '@commitlint/config-validator': 21.2.0 + '@commitlint/execute-rule': 21.0.1 + '@commitlint/resolve-extends': 21.2.2 + '@commitlint/types': 21.2.0 + cosmiconfig: 9.0.2(typescript@6.0.3) + cosmiconfig-typescript-loader: 6.3.0(@types/node@25.0.9)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3) + es-toolkit: 1.52.0 + is-plain-obj: 4.1.0 + picocolors: 1.1.1 + transitivePeerDependencies: + - '@types/node' + - typescript + + '@commitlint/message@21.2.0': {} + + '@commitlint/parse@21.2.2': + dependencies: + '@commitlint/types': 21.2.0 + conventional-changelog-angular: 9.4.0 + conventional-commits-parser: 7.1.2 + + '@commitlint/read@21.2.1(conventional-commits-parser@7.1.2)': + dependencies: + '@commitlint/top-level': 21.2.0 + '@commitlint/types': 21.2.0 + '@conventional-changelog/git-client': 3.1.2(conventional-commits-parser@7.1.2) + tinyexec: 1.3.1 + transitivePeerDependencies: + - conventional-commits-filter + - conventional-commits-parser + + '@commitlint/resolve-extends@21.2.2': + dependencies: + '@commitlint/config-validator': 21.2.0 + '@commitlint/types': 21.2.0 + es-toolkit: 1.52.0 + global-directory: 5.0.0 + resolve-from: 5.0.0 + + '@commitlint/rules@21.2.2': + dependencies: + '@commitlint/ensure': 21.2.0 + '@commitlint/message': 21.2.0 + '@commitlint/to-lines': 21.0.1 + '@commitlint/types': 21.2.0 + + '@commitlint/to-lines@21.0.1': {} + + '@commitlint/top-level@21.2.0': + dependencies: + escalade: 3.2.0 + + '@commitlint/types@21.2.0': + dependencies: + conventional-commits-parser: 7.1.2 + picocolors: 1.1.1 + + '@conventional-changelog/git-client@3.1.2(conventional-commits-parser@7.1.2)': + dependencies: + '@simple-libs/child-process-utils': 2.0.0 + '@simple-libs/stream-utils': 2.0.0 + semver: 7.8.5 + optionalDependencies: + conventional-commits-parser: 7.1.2 + + '@conventional-changelog/template@1.4.0': {} + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -8786,7 +9124,7 @@ snapshots: '@expo-google-fonts/poppins@0.4.1': {} - '@expo/cli@57.0.24(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3)': + '@expo/cli@57.0.26(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3)': dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/config': 57.0.9(typescript@6.0.3) @@ -8796,16 +9134,16 @@ snapshots: '@expo/image-utils': 0.11.5(typescript@6.0.3) '@expo/inline-modules': 0.1.7(typescript@6.0.3) '@expo/json-file': 11.0.1 - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) '@expo/metro': 56.0.2 - '@expo/metro-config': 57.0.12(expo@57.0.22)(typescript@6.0.3) + '@expo/metro-config': 57.0.12(expo@57.0.24)(typescript@6.0.3) '@expo/metro-file-map': 57.0.3 '@expo/osascript': 2.7.1 '@expo/package-manager': 1.13.1 '@expo/plist': 0.8.1 '@expo/prebuild-config': 57.0.16(typescript@6.0.3) '@expo/require-utils': 57.0.5(typescript@6.0.3) - '@expo/router-server': 57.0.9(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo-server@57.0.3)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@expo/router-server': 57.0.10(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo-server@57.0.3)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@expo/schema-utils': 57.0.2 '@expo/spawn-async': 1.8.0 '@expo/ws-tunnel': 2.0.0(ws@8.18.3) @@ -8822,7 +9160,7 @@ snapshots: connect: 3.7.0 debug: 4.4.3 dnssd-advertise: 1.1.6 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-server: 57.0.3 fetch-nodeshim: 0.4.10 getenv: 2.0.0 @@ -8849,7 +9187,7 @@ snapshots: ws: 8.18.3 zod: 3.25.76 optionalDependencies: - expo-router: 57.0.21(719806e0ddf700953ff39fc2ded2cadd) + expo-router: 57.0.22(c4b2198b165dcee23bdfdd48580aa1ee) react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) transitivePeerDependencies: - '@expo/dom-webview' @@ -8919,9 +9257,9 @@ snapshots: react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - '@expo/dom-webview@57.0.1(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3)': + '@expo/dom-webview@57.0.1(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3)': dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) @@ -8984,16 +9322,16 @@ snapshots: - supports-color - typescript - '@expo/log-box@57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3)': + '@expo/log-box@57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3)': dependencies: - '@expo/dom-webview': 57.0.1(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + '@expo/dom-webview': 57.0.1(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) anser: 1.4.10 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) stacktrace-parser: 0.1.11 - '@expo/metro-config@57.0.12(expo@57.0.22)(typescript@6.0.3)': + '@expo/metro-config@57.0.12(expo@57.0.24)(typescript@6.0.3)': dependencies: '@babel/code-frame': 7.29.7 '@babel/core': 7.29.7 @@ -9019,7 +9357,7 @@ snapshots: postcss: 8.5.26 resolve-from: 5.0.0 optionalDependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -9037,11 +9375,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/metro-runtime@57.0.15(@expo/log-box@57.0.4)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)': + '@expo/metro-runtime@57.0.16(@expo/log-box@57.0.4)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)': dependencies: - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) anser: 1.4.10 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) pretty-format: 29.7.0 react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) @@ -9116,17 +9454,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/router-server@57.0.9(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo-server@57.0.3)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@expo/router-server@57.0.10(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo-server@57.0.3)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: debug: 4.4.3 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.18(expo@57.0.22)(react-native-tvos@0.86.2-0) - expo-font: 57.0.4(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0) + expo-font: 57.0.4(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) expo-server: 57.0.3 react: 19.2.3 optionalDependencies: - '@expo/metro-runtime': 57.0.15(@expo/log-box@57.0.4)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-router: 57.0.21(719806e0ddf700953ff39fc2ded2cadd) + '@expo/metro-runtime': 57.0.16(@expo/log-box@57.0.4)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3) + expo-router: 57.0.22(c4b2198b165dcee23bdfdd48580aa1ee) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -9141,9 +9479,9 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/ui@57.0.18(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)': + '@expo/ui@57.0.19(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)': dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) sf-symbols-typescript: 2.2.0 @@ -9820,9 +10158,9 @@ snapshots: react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - '@react-native-tvos/config-tv@0.1.6(expo@57.0.22)': + '@react-native-tvos/config-tv@0.1.6(expo@57.0.24)': dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) getenv: 1.0.0 glob: 11.1.0 @@ -9844,7 +10182,7 @@ snapshots: react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) optionalDependencies: '@react-native/assets-registry': 0.86.2 - expo-font: 57.0.4(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + expo-font: 57.0.4(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) '@react-native-vector-icons/ionicons@13.1.3(@expo/config-plugins@57.0.9(typescript@6.0.3))(@react-native/assets-registry@0.86.2)(expo-font@57.0.4)(react-native-tvos@0.86.2-0)(react@19.2.3)': dependencies: @@ -9946,7 +10284,7 @@ snapshots: invariant: 2.2.4 nullthrows: 1.1.1 tinyglobby: 0.2.15 - yargs: 17.7.2 + yargs: 17.7.3 '@react-native/codegen@0.86.3(@babel/core@7.29.7)': dependencies: @@ -10193,6 +10531,12 @@ snapshots: react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) + '@simple-libs/child-process-utils@2.0.0': + dependencies: + '@simple-libs/stream-utils': 2.0.0 + + '@simple-libs/stream-utils@2.0.0': {} + '@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.34.41': {} @@ -10643,6 +10987,13 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.8 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + anser@1.4.10: {} ansi-escapes@3.2.0: {} @@ -10671,6 +11022,8 @@ snapshots: ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} + anymatch@3.1.3: dependencies: normalize-path: 3.0.0 @@ -10686,6 +11039,8 @@ snapshots: argparse@2.0.1: {} + argue-cli@3.2.0: {} + aria-hidden@1.2.6: dependencies: tslib: 2.8.1 @@ -10875,7 +11230,7 @@ snapshots: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) - babel-preset-expo@57.0.11(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.22)(react-refresh@0.14.2): + babel-preset-expo@57.0.12(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.24)(react-refresh@0.14.2): dependencies: '@babel/generator': 7.29.8 '@babel/helper-module-imports': 7.29.7 @@ -10922,12 +11277,12 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) transitivePeerDependencies: - '@babel/core' - supports-color - babel-preset-expo@57.0.9(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.22)(react-refresh@0.17.0): + babel-preset-expo@57.0.9(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.24)(react-refresh@0.17.0): dependencies: '@babel/generator': 7.29.8 '@babel/helper-module-imports': 7.29.7 @@ -10974,7 +11329,7 @@ snapshots: react-refresh: 0.17.0 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -11173,6 +11528,12 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrap-ansi: 9.0.2 + clone@1.0.4: {} clsx@2.1.1: {} @@ -11248,6 +11609,19 @@ snapshots: content-type@2.1.0: {} + conventional-changelog-angular@9.4.0: + dependencies: + '@conventional-changelog/template': 1.4.0 + + conventional-changelog-conventionalcommits@10.4.0: + dependencies: + '@conventional-changelog/template': 1.4.0 + + conventional-commits-parser@7.1.2: + dependencies: + '@simple-libs/stream-utils': 2.0.0 + argue-cli: 3.2.0 + convert-source-map@2.0.0: {} cookie-signature@1.0.7: {} @@ -11263,6 +11637,13 @@ snapshots: object-assign: 4.1.1 vary: 1.1.2 + cosmiconfig-typescript-loader@6.3.0(@types/node@25.0.9)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): + dependencies: + '@types/node': 25.0.9 + cosmiconfig: 9.0.2(typescript@6.0.3) + jiti: 2.6.1 + typescript: 6.0.3 + cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 @@ -11281,6 +11662,15 @@ snapshots: optionalDependencies: typescript: 6.0.3 + cosmiconfig@9.0.2(typescript@6.0.3): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + parse-json: 5.2.0 + optionalDependencies: + typescript: 6.0.3 + create-jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3)): dependencies: '@jest/types': 29.6.3 @@ -11474,10 +11864,10 @@ snapshots: esbuild: 0.25.12 tsx: 4.23.13 - drizzle-orm@0.45.2(better-sqlite3@12.6.2)(expo-sqlite@57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3)): + drizzle-orm@0.45.2(better-sqlite3@12.6.2)(expo-sqlite@57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3)): optionalDependencies: better-sqlite3: 12.6.2 - expo-sqlite: 57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + expo-sqlite: 57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) dunder-proto@1.0.1: dependencies: @@ -11491,6 +11881,8 @@ snapshots: emittery@0.13.1: {} + emoji-regex@10.6.0: {} + emoji-regex@8.0.0: {} encodeurl@1.0.2: {} @@ -11621,6 +12013,8 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + es-toolkit@1.52.0: {} + esbuild@0.18.20: optionalDependencies: '@esbuild/android-arm': 0.18.20 @@ -11815,6 +12209,10 @@ snapshots: - supports-color - typescript + eslint-plugin-i18next@6.1.5: + dependencies: + requireindex: 1.1.0 + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.69.0(eslint@9.39.1(jiti@2.6.1))(typescript@6.0.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 @@ -11999,20 +12397,20 @@ snapshots: jest-message-util: 29.7.0 jest-util: 29.7.0 - expo-asset@57.0.17(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3): + expo-asset@57.0.18(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.11.5(typescript@6.0.3) - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.18(expo@57.0.22)(react-native-tvos@0.86.2-0) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) transitivePeerDependencies: - supports-color - typescript - expo-build-disk-cache@0.7.2(@expo/cli@57.0.24(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3))(@expo/config@57.0.9(typescript@6.0.3))(typescript@6.0.3): + expo-build-disk-cache@0.7.2(@expo/cli@57.0.26(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3))(@expo/config@57.0.9(typescript@6.0.3))(typescript@6.0.3): dependencies: - '@expo/cli': 57.0.24(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3) + '@expo/cli': 57.0.26(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3) '@expo/config': 57.0.9(typescript@6.0.3) cosmiconfig: 9.0.0(typescript@6.0.3) env-paths: 2.2.1 @@ -12020,87 +12418,89 @@ snapshots: transitivePeerDependencies: - typescript - expo-build-properties@57.0.17(expo@57.0.22): + expo-build-properties@57.0.21(expo@57.0.24): dependencies: '@expo/schema-utils': 57.0.2 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) resolve-from: 5.0.0 semver: 7.8.5 - expo-constants@57.0.18(expo@57.0.22)(react-native-tvos@0.86.2-0): + expo-constants@57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0): dependencies: '@expo/env': 2.4.3 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) transitivePeerDependencies: - supports-color - expo-dev-client@57.0.19(expo@57.0.22)(react-native-tvos@0.86.2-0): + expo-dev-client@57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-dev-launcher: 57.0.20(expo@57.0.22)(react-native-tvos@0.86.2-0) - expo-dev-menu: 57.0.18(expo@57.0.22)(react-native-tvos@0.86.2-0) - expo-dev-menu-interface: 57.0.0(expo@57.0.22) - expo-manifests: 57.0.2(expo@57.0.22) - expo-updates-interface: 57.0.2(expo@57.0.22) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-dev-launcher: 57.0.20(expo@57.0.24)(react-native-tvos@0.86.2-0) + expo-dev-menu: 57.0.18(expo@57.0.24)(react-native-tvos@0.86.2-0) + expo-dev-menu-interface: 57.0.0(expo@57.0.24) + expo-manifests: 57.0.2(expo@57.0.24) + expo-updates-interface: 57.0.2(expo@57.0.24) transitivePeerDependencies: - react-native - expo-dev-launcher@57.0.20(expo@57.0.22)(react-native-tvos@0.86.2-0): + expo-dev-launcher@57.0.20(expo@57.0.24)(react-native-tvos@0.86.2-0): dependencies: '@expo/schema-utils': 57.0.2 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-dev-menu: 57.0.18(expo@57.0.22)(react-native-tvos@0.86.2-0) - expo-manifests: 57.0.2(expo@57.0.22) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-dev-menu: 57.0.18(expo@57.0.24)(react-native-tvos@0.86.2-0) + expo-manifests: 57.0.2(expo@57.0.24) react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-dev-menu-interface@57.0.0(expo@57.0.22): + expo-dev-menu-interface@57.0.0(expo@57.0.24): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-dev-menu@57.0.18(expo@57.0.22)(react-native-tvos@0.86.2-0): + expo-dev-menu@57.0.18(expo@57.0.24)(react-native-tvos@0.86.2-0): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-dev-menu-interface: 57.0.0(expo@57.0.22) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-dev-menu-interface: 57.0.0(expo@57.0.24) react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-device@57.0.2(expo@57.0.22): + expo-device@57.0.2(expo@57.0.24): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) ua-parser-js: 0.7.41 - expo-drizzle-studio-plugin@0.2.1(expo-sqlite@57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3))(expo@57.0.22): + expo-doctor@1.20.4: {} + + expo-drizzle-studio-plugin@0.2.1(expo-sqlite@57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3))(expo@57.0.24): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-sqlite: 57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-sqlite: 57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) expo-eas-client@57.0.4: {} - expo-file-system@57.0.7(expo@57.0.22)(react-native-tvos@0.86.2-0): + expo-file-system@57.0.7(expo@57.0.24)(react-native-tvos@0.86.2-0): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-font@57.0.4(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3): + expo-font@57.0.4(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) fontfaceobserver: 2.3.0 react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-glass-effect@57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3): + expo-glass-effect@57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-haptics@57.0.3(expo@57.0.22): + expo-haptics@57.0.3(expo@57.0.24): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-image@57.0.5(expo@57.0.22)(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): + expo-image@57.0.5(expo@57.0.24)(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) sf-symbols-typescript: 2.2.0 @@ -12109,26 +12509,26 @@ snapshots: expo-json-utils@57.0.2: {} - expo-keep-awake@57.0.2(expo@57.0.22)(react@19.2.3): + expo-keep-awake@57.0.2(expo@57.0.24)(react@19.2.3): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 - expo-libvlc-player@57.0.28(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3): + expo-libvlc-player@57.0.28(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-linear-gradient@57.0.2(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3): + expo-linear-gradient@57.0.2(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-linking@57.0.10(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3): + expo-linking@57.0.10(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3): dependencies: - expo-constants: 57.0.18(expo@57.0.22)(react-native-tvos@0.86.2-0) + expo-constants: 57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0) invariant: 2.2.4 react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) @@ -12136,15 +12536,15 @@ snapshots: - expo - supports-color - expo-localization@57.0.2(expo@57.0.22)(react@19.2.3): + expo-localization@57.0.2(expo@57.0.24)(react@19.2.3): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 rtl-detect: 1.1.2 - expo-manifests@57.0.2(expo@57.0.22): + expo-manifests@57.0.2(expo@57.0.24): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-json-utils: 57.0.2 expo-modules-autolinking@57.0.13(typescript@6.0.3): @@ -12171,17 +12571,17 @@ snapshots: dependencies: react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-network@57.0.2(expo@57.0.22)(react@19.2.3): + expo-network@57.0.2(expo@57.0.24)(react@19.2.3): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 - expo-router@57.0.21(719806e0ddf700953ff39fc2ded2cadd): + expo-router@57.0.22(c4b2198b165dcee23bdfdd48580aa1ee): dependencies: - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) - '@expo/metro-runtime': 57.0.15(@expo/log-box@57.0.4)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) + '@expo/metro-runtime': 57.0.16(@expo/log-box@57.0.4)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3) '@expo/schema-utils': 57.0.2 - '@expo/ui': 57.0.18(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3) + '@expo/ui': 57.0.19(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3) '@radix-ui/react-slot': 1.2.3(@types/react@19.2.18)(react@19.2.3) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-native-masked-view/masked-view': 0.3.2(react-native-tvos@0.86.2-0)(react@19.2.3) @@ -12189,12 +12589,12 @@ snapshots: color: 4.2.3 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.18(expo@57.0.22)(react-native-tvos@0.86.2-0) - expo-glass-effect: 57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-linking: 57.0.10(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-constants: 57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0) + expo-glass-effect: 57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) + expo-linking: 57.0.10(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) expo-server: 57.0.3 - expo-symbols: 57.0.3(expo-font@57.0.4)(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + expo-symbols: 57.0.3(expo-font@57.0.4)(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) fast-deep-equal: 3.1.3 invariant: 2.2.4 nanoid: 3.3.18 @@ -12227,63 +12627,63 @@ snapshots: expo-server@57.0.3: {} - expo-splash-screen@57.0.9(expo@57.0.22)(typescript@6.0.3): + expo-splash-screen@57.0.9(expo@57.0.24)(typescript@6.0.3): dependencies: '@expo/config-plugins': 57.0.9(typescript@6.0.3) '@expo/image-utils': 0.11.5(typescript@6.0.3) - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) xml2js: 0.6.0 transitivePeerDependencies: - supports-color - typescript - expo-sqlite-mock@4.0.0(better-sqlite3@12.6.2)(expo-sqlite@57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3))(expo@57.0.22)(jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3))): + expo-sqlite-mock@4.0.0(better-sqlite3@12.6.2)(expo-sqlite@57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3))(expo@57.0.24)(jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3))): dependencies: better-sqlite3: 12.6.2 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-sqlite: 57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-sqlite: 57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) jest: 29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3)) - expo-sqlite@57.0.3(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3): + expo-sqlite@57.0.3(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3): dependencies: await-lock: 2.2.2 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-status-bar@57.0.1(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3): + expo-status-bar@57.0.1(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) expo-structured-headers@57.0.1: {} - expo-symbols@57.0.3(expo-font@57.0.4)(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3): + expo-symbols@57.0.3(expo-font@57.0.4)(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3): dependencies: '@expo-google-fonts/material-symbols': 0.4.45 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-font: 57.0.4(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo-font: 57.0.4(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) react: 19.2.3 react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) sf-symbols-typescript: 2.2.0 - expo-system-ui@57.0.4(expo@57.0.22)(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): + expo-system-ui@57.0.4(expo@57.0.24)(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)): dependencies: '@react-native/normalize-colors': 0.86.3 debug: 4.4.3 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) optionalDependencies: react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) transitivePeerDependencies: - supports-color - expo-updates-interface@57.0.2(expo@57.0.22): + expo-updates-interface@57.0.2(expo@57.0.24): dependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-updates@57.0.22(expo-dev-client@57.0.19(expo@57.0.22)(react-native-tvos@0.86.2-0))(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3): + expo-updates@57.0.23(expo-dev-client@57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0))(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3): dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/plist': 0.8.1 @@ -12291,11 +12691,11 @@ snapshots: arg: 4.1.0 chalk: 4.1.2 debug: 4.4.3 - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-eas-client: 57.0.4 - expo-manifests: 57.0.2(expo@57.0.22) + expo-manifests: 57.0.2(expo@57.0.24) expo-structured-headers: 57.0.1 - expo-updates-interface: 57.0.2(expo@57.0.22) + expo-updates-interface: 57.0.2(expo@57.0.24) getenv: 2.0.0 glob: 13.0.0 ignore: 5.3.2 @@ -12304,29 +12704,29 @@ snapshots: react-native: react-native-tvos@0.86.2-0(patch_hash=e91fec82df6b5870c3625e79ba3c16e25c5cecceb339f6f21cd184eca5d363df)(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(@types/react@19.2.18)(react-native-tvos@0.86.2-0)(react@19.2.3) resolve-from: 5.0.0 optionalDependencies: - expo-dev-client: 57.0.19(expo@57.0.22)(react-native-tvos@0.86.2-0) + expo-dev-client: 57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0) transitivePeerDependencies: - supports-color - expo@57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): + expo@57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.7 - '@expo/cli': 57.0.24(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-constants@57.0.18)(expo-font@57.0.4)(expo-router@57.0.21)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3) + '@expo/cli': 57.0.26(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-constants@57.0.19)(expo-font@57.0.4)(expo-router@57.0.22)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3) '@expo/config': 57.0.9(typescript@6.0.3) '@expo/config-plugins': 57.0.9(typescript@6.0.3) '@expo/devtools': 57.0.1(react-native-tvos@0.86.2-0)(react@19.2.3) '@expo/fingerprint': 0.20.13 '@expo/local-build-cache-provider': 57.0.8(typescript@6.0.3) - '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) '@expo/metro': 56.0.2 - '@expo/metro-config': 57.0.12(expo@57.0.22)(typescript@6.0.3) + '@expo/metro-config': 57.0.12(expo@57.0.24)(typescript@6.0.3) '@ungap/structured-clone': 1.3.0 - babel-preset-expo: 57.0.11(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.22)(react-refresh@0.14.2) - expo-asset: 57.0.17(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.18(expo@57.0.22)(react-native-tvos@0.86.2-0) - expo-file-system: 57.0.7(expo@57.0.22)(react-native-tvos@0.86.2-0) - expo-font: 57.0.4(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) - expo-keep-awake: 57.0.2(expo@57.0.22)(react@19.2.3) + babel-preset-expo: 57.0.12(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.24)(react-refresh@0.14.2) + expo-asset: 57.0.18(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3)(typescript@6.0.3) + expo-constants: 57.0.19(expo@57.0.24)(react-native-tvos@0.86.2-0) + expo-file-system: 57.0.7(expo@57.0.24)(react-native-tvos@0.86.2-0) + expo-font: 57.0.4(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) + expo-keep-awake: 57.0.2(expo@57.0.24)(react@19.2.3) expo-modules-autolinking: 57.0.13(typescript@6.0.3) expo-modules-core: 57.0.18(react-native-tvos@0.86.2-0)(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3) pretty-format: 29.7.0 @@ -12335,8 +12735,8 @@ snapshots: react-refresh: 0.14.2 whatwg-url-minimum: 0.1.2 optionalDependencies: - '@expo/dom-webview': 57.0.1(expo@57.0.22)(react-native-tvos@0.86.2-0)(react@19.2.3) - '@expo/metro-runtime': 57.0.15(@expo/log-box@57.0.4)(expo@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3) + '@expo/dom-webview': 57.0.1(expo@57.0.24)(react-native-tvos@0.86.2-0)(react@19.2.3) + '@expo/metro-runtime': 57.0.16(@expo/log-box@57.0.4)(expo@57.0.24)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react@19.2.3) react-dom: 19.2.3(react@19.2.3) react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) transitivePeerDependencies: @@ -12400,6 +12800,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-uri@3.1.8: {} + fb-dotslash@0.5.8: {} fb-watchman@2.0.2: @@ -12560,6 +12962,8 @@ snapshots: get-caller-file@2.0.5: {} + get-east-asian-width@1.7.0: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -12628,6 +13032,10 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-directory@5.0.0: + dependencies: + ini: 6.0.0 + globals@14.0.0: {} globals@16.5.0: {} @@ -12800,6 +13208,8 @@ snapshots: ini@1.3.8: {} + ini@6.0.0: {} + inline-style-prefixer@7.0.1: dependencies: css-in-js-utils: 3.1.0 @@ -12923,6 +13333,8 @@ snapshots: is-plain-obj@2.1.0: {} + is-plain-obj@4.1.0: {} + is-potential-custom-element-name@1.0.1: {} is-regex@1.2.1: @@ -13162,7 +13574,7 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - jest-expo@57.0.5(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(expo@57.0.22)(jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3)))(react-native-tvos@0.86.2-0)(react@19.2.3): + jest-expo@57.0.5(@babel/core@7.29.7)(@react-native/jest-preset@0.86.3(@babel/core@7.29.7)(react@19.2.3))(expo@57.0.24)(jest@29.7.0(@types/node@25.0.9)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@types/node@25.0.9)(typescript@6.0.3)))(react-native-tvos@0.86.2-0)(react@19.2.3): dependencies: '@jest/create-cache-key-function': 29.7.0 '@jest/globals': 29.7.0 @@ -13179,7 +13591,7 @@ snapshots: server-only: 0.0.1 stacktrace-js: 2.0.2 optionalDependencies: - expo: 57.0.22(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-router@57.0.21)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.24(@babel/core@7.29.7)(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.16)(expo-router@57.0.22)(react-dom@19.2.3(react@19.2.3))(react-native-tvos@0.86.2-0)(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.87.1(@babel/core@7.29.7))(react-native-tvos@0.86.2-0)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) transitivePeerDependencies: - '@babel/core' - bufferutil @@ -13470,6 +13882,8 @@ snapshots: json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json5@1.0.2: @@ -13988,7 +14402,7 @@ snapshots: source-map: 0.5.7 throat: 5.0.0 ws: 7.5.10 - yargs: 17.7.2 + yargs: 17.7.3 transitivePeerDependencies: - bufferutil - supports-color @@ -14844,8 +15258,12 @@ snapshots: require-directory@2.1.1: {} + require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} + requireindex@1.1.0: {} + requires-port@1.0.0: {} resolve-cwd@3.0.0: @@ -15264,6 +15682,17 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.7.0 + strip-ansi: 7.1.2 + + string-width@8.2.2: + dependencies: + get-east-asian-width: 1.7.0 + strip-ansi: 7.1.2 + string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 @@ -15417,6 +15846,8 @@ snapshots: through@2.3.8: {} + tinyexec@1.3.1: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -15454,7 +15885,7 @@ snapshots: dependencies: punycode: 2.3.1 - trakt-api@https://codeload.github.com/trakt/trakt-api/tar.gz/1768a87dedfd3e18ea36ca95b6ce42bd32c1fea6: {} + trakt-api@https://codeload.github.com/trakt/trakt-api/tar.gz/4ec0b28e97520f5239ecb924b7430d0f3aa366b0: {} ts-api-utils@2.5.0(typescript@6.0.3): dependencies: @@ -15810,6 +16241,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.1.2 + wrappy@1.0.2: {} write-file-atomic@4.0.2: @@ -15859,6 +16296,8 @@ snapshots: yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} + yargs@15.4.1: dependencies: cliui: 6.0.0 @@ -15893,6 +16332,15 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yargs@18.1.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.2.2 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yn@3.1.1: {} yocto-queue@0.1.0: {} diff --git a/scripts/agent-verify.mjs b/scripts/agent-verify.mjs new file mode 100755 index 0000000..317cef1 --- /dev/null +++ b/scripts/agent-verify.mjs @@ -0,0 +1,299 @@ +#!/usr/bin/env node + +import { Buffer } from 'node:buffer'; +import { spawn } from 'node:child_process'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const MAX_CAPTURED_OUTPUT_BYTES = 1_000_000; + +const checks = [ + { + key: 'expo-deps', + label: 'Expo dependency compatibility', + command: 'pnpm', + args: ['exec', 'expo', 'install', '--check'], + }, + { + key: 'expo-doctor', + label: 'Expo Doctor', + command: 'pnpm', + args: ['exec', 'expo-doctor'], + }, + { + key: 'android-prebuild', + label: 'Android prebuild', + command: 'pnpm', + args: ['exec', 'expo', 'prebuild', '--platform', 'android', '--no-install'], + }, + { + key: 'format', + label: 'Prettier', + command: 'pnpm', + args: ['format:check'], + }, + { + key: 'typecheck', + label: 'TypeScript', + command: 'pnpm', + args: ['typecheck'], + }, + { + key: 'lint', + label: 'ESLint', + command: 'pnpm', + args: ['lint'], + }, + { + key: 'i18n-parity', + label: 'i18n key parity', + command: 'node', + args: ['scripts/verify-i18n-parity.mjs'], + }, + { + key: 'whats-new-registry', + label: "What's New registry drift", + command: 'node', + args: ['scripts/verify-whats-new-registry.mjs'], + }, + { + key: 'maestro-flows', + label: 'Maestro flow integrity', + command: 'node', + args: ['scripts/verify-maestro-flows.mjs'], + }, + { + key: 'test', + label: 'Jest', + command: 'pnpm', + args: ['test:agent'], + }, + { + key: 'e2e-tools', + label: 'Android E2E tooling', + command: 'pnpm', + args: ['test:e2e:tools:agent'], + }, + { + key: 'e2e-addon-types', + label: 'E2E add-on TypeScript', + command: 'pnpm', + args: ['--filter', '@dodostream/e2e-addon', 'typecheck'], + }, + { + key: 'e2e-addon', + label: 'E2E add-on tests', + command: 'pnpm', + args: ['--filter', '@dodostream/e2e-addon', 'test:agent'], + }, +]; + +const usage = `Usage: pnpm verify:agent [--only ] [--json] [--verbose] + +Runs the same checks as pnpm verify:ci with terse, phase-scoped output. + +Options: + --only Comma-separated check keys to run + --json Emit newline-delimited JSON status events + --verbose Stream child-command output instead of retaining it for failures + --list Print check keys + --help Print this help + +Check keys: ${checks.map(({ key }) => key).join(', ')}`; + +const failUsage = (message) => { + process.stderr.write(`${message}\n\n${usage}\n`); + process.exitCode = 2; +}; + +const parseOptions = (args) => { + const options = { json: false, only: null, verbose: false }; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + + if (arg === '--help') return { help: true }; + if (arg === '--list') return { list: true }; + if (arg === '--json') { + options.json = true; + continue; + } + if (arg === '--verbose') { + options.verbose = true; + continue; + } + if (arg === '--only') { + const value = args[index + 1]; + if (!value || value.startsWith('--')) + return { error: '--only requires at least one check key' }; + options.only = value.split(',').filter(Boolean); + index += 1; + continue; + } + if (arg.startsWith('--only=')) { + options.only = arg.slice('--only='.length).split(',').filter(Boolean); + continue; + } + + return { error: `Unknown option: ${arg}` }; + } + + if (options.json && options.verbose) { + return { error: '--json and --verbose cannot be used together' }; + } + + return options; +}; + +const commandText = ({ command, args }) => [command, ...args].join(' '); + +const runCheck = (check, { verbose }) => + new Promise((resolve) => { + const output = []; + let capturedOutputBytes = 0; + let outputTruncated = false; + const startedAt = Date.now(); + const child = spawn(check.command, check.args, { + cwd: ROOT, + env: { + ...process.env, + CI: 'true', + EXPO_NO_TELEMETRY: '1', + FORCE_COLOR: '0', + NO_COLOR: '1', + }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + const collect = (stream, target) => { + stream.on('data', (chunk) => { + if (verbose) { + target.write(chunk); + return; + } + + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + const remainingBytes = MAX_CAPTURED_OUTPUT_BYTES - capturedOutputBytes; + if (remainingBytes <= 0) { + outputTruncated = true; + return; + } + + const capturedChunk = buffer.subarray(0, remainingBytes); + output.push(capturedChunk); + capturedOutputBytes += capturedChunk.length; + outputTruncated ||= capturedChunk.length < buffer.length; + }); + }; + + const capturedOutput = () => { + if (verbose) return ''; + + const suffix = outputTruncated + ? `\n[agent verifier truncated output after ${MAX_CAPTURED_OUTPUT_BYTES} bytes]\n` + : ''; + return `${Buffer.concat(output).toString()}${suffix}`; + }; + + collect(child.stdout, process.stdout); + collect(child.stderr, process.stderr); + + child.once('error', (error) => { + resolve({ + command: commandText(check), + durationMs: Date.now() - startedAt, + error: error.message, + key: check.key, + label: check.label, + ok: false, + output: capturedOutput(), + }); + }); + child.once('close', (code, signal) => { + resolve({ + code, + command: commandText(check), + durationMs: Date.now() - startedAt, + key: check.key, + label: check.label, + ok: code === 0, + output: capturedOutput(), + signal, + }); + }); + }); + +const printEvent = (event, json) => { + if (json) { + const { output: _output, ...jsonEvent } = event; + process.stdout.write(`${JSON.stringify(jsonEvent)}\n`); + return; + } + + if (event.event === 'start') { + process.stdout.write(`… ${event.label}\n`); + return; + } + + if (event.event === 'check') { + const symbol = event.ok ? '✓' : '✗'; + process.stdout.write(`${symbol} ${event.label} (${(event.durationMs / 1000).toFixed(1)}s)\n`); + return; + } + + process.stdout.write( + `${event.ok ? '✓' : '✗'} Agent verification ${event.ok ? 'passed' : 'failed'} (${( + event.durationMs / 1000 + ).toFixed(1)}s)\n` + ); +}; + +const options = parseOptions(process.argv.slice(2)); +if (options.error) { + failUsage(options.error); +} else if (options.help) { + process.stdout.write(`${usage}\n`); +} else if (options.list) { + process.stdout.write(`${checks.map(({ key }) => key).join('\n')}\n`); +} else { + const selectedChecks = options.only + ? checks.filter(({ key }) => options.only.includes(key)) + : checks; + const unknownKeys = + options.only?.filter((key) => !checks.some((check) => check.key === key)) ?? []; + + if (unknownKeys.length > 0) { + failUsage(`Unknown check key(s): ${unknownKeys.join(', ')}`); + } else if (selectedChecks.length === 0) { + failUsage('No checks selected'); + } else { + const startedAt = Date.now(); + let failed = false; + + for (const check of selectedChecks) { + printEvent({ event: 'start', key: check.key, label: check.label }, options.json); + const result = await runCheck(check, options); + printEvent({ event: 'check', ...result }, options.json); + + if (!result.ok) { + failed = true; + const failureOutput = result.output || result.error; + if (failureOutput) { + process.stderr.write(`\n${result.command}\n${failureOutput}`); + } + break; + } + } + + printEvent( + { + event: 'summary', + durationMs: Date.now() - startedAt, + ok: !failed, + }, + options.json + ); + process.exitCode = failed ? 1 : 0; + } +} diff --git a/scripts/e2e/android-build.mjs b/scripts/e2e/android-build.mjs new file mode 100644 index 0000000..2c4b438 --- /dev/null +++ b/scripts/e2e/android-build.mjs @@ -0,0 +1,27 @@ +const DEFAULT_ADDON_PORT = 8765; +const APP_ID = 'app.dodora.dodostream.dev'; +const PROFILES = new Set(['phone', 'tablet', 'tv']); + +export const createBuildConfig = ({ + profile, + addonManifestUrl = `http://10.0.2.2:${DEFAULT_ADDON_PORT}/manifest.json`, +}) => { + if (!PROFILES.has(profile)) { + throw new Error(`unknown profile: ${profile} (expected phone|tablet|tv)`); + } + + const orientation = profile === 'phone' ? 'portrait' : 'landscape'; + return { + appId: APP_ID, + profile, + orientation, + addonManifestUrl, + environment: { + APP_VARIANT: 'dev', + EXPO_PUBLIC_E2E: '1', + E2E_ORIENTATION: orientation, + EXPO_TV: profile === 'tv' ? '1' : '0', + ADDON_MANIFEST_URL: addonManifestUrl, + }, + }; +}; diff --git a/scripts/e2e/build-release.mjs b/scripts/e2e/build-release.mjs new file mode 100755 index 0000000..e10df99 --- /dev/null +++ b/scripts/e2e/build-release.mjs @@ -0,0 +1,104 @@ +#!/usr/bin/env node +/** + * Build the Android E2E release APK as a device-free artifact. + * + * Usage: + * node scripts/e2e/build-release.mjs --profile [--abi ] + * + * Applies the profile's build environment, runs the Expo prebuild and the + * release Gradle build, then prints the selected APK and records + * artifacts/e2e//artifact.json for the agent-device MCP install step. + * No Metro bundle and no device interaction. + */ +import { spawnSync } from 'node:child_process'; +import { mkdirSync, readdirSync, writeFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { createBuildConfig } from './android-build.mjs'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); + +export const parseBuildArgs = (argv, env = process.env) => { + const option = (name) => { + const index = argv.indexOf(name); + if (index === -1) return undefined; + const value = argv[index + 1]; + if (!value || value.startsWith('--')) throw new Error(`missing value for ${name}`); + return value; + }; + + const abi = option('--abi'); + const profile = option('--profile') ?? 'phone'; + return { + profile, + abi, + skipLint: (env.E2E_LINT ?? '0') !== '1', + addonManifestUrl: env.ADDON_MANIFEST_URL, + }; +}; + +export const selectReleaseArtifact = (apkFiles, abi) => { + if (!abi && apkFiles.includes('app-universal-release.apk')) { + return 'app-universal-release.apk'; + } + const wanted = abi ? `app-${abi}-release.apk` : 'app-release.apk'; + if (apkFiles.includes(wanted)) return wanted; + throw new Error( + `no matching release APK (wanted ${wanted}); available: ${apkFiles.sort().join(', ') || 'none'}` + ); +}; + +const resolveArtifactAbi = (apkFile) => { + if (apkFile === 'app-universal-release.apk') return 'universal'; + if (apkFile === 'app-release.apk') return 'device'; + return apkFile.replace(/^app-/, '').replace(/-release\.apk$/, ''); +}; + +const runChecked = (command, args, options) => { + const result = spawnSync(command, args, { stdio: 'inherit', ...options }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error(`${command} exited ${result.status ?? 'with an unknown status'}`); + } +}; + +const main = () => { + const { profile, abi, skipLint, addonManifestUrl } = parseBuildArgs(process.argv.slice(2)); + const config = createBuildConfig({ profile, addonManifestUrl }); + const env = { ...process.env, ...config.environment }; + + runChecked('pnpm', ['exec', 'expo', 'prebuild', '--platform', 'android', '--no-install'], { + cwd: ROOT, + env, + }); + + const gradleArgs = ['assembleRelease']; + if (skipLint) gradleArgs.push('-x', 'lintVitalRelease'); + runChecked('./gradlew', gradleArgs, { cwd: path.join(ROOT, 'android'), env }); + + const apkDirectory = path.join(ROOT, 'android', 'app', 'build', 'outputs', 'apk', 'release'); + const apkFile = selectReleaseArtifact(readdirSync(apkDirectory), abi); + const apk = path.join(apkDirectory, apkFile); + + const outputDirectory = path.join(ROOT, 'artifacts', 'e2e', profile); + mkdirSync(outputDirectory, { recursive: true }); + const artifact = { + profile, + abi: resolveArtifactAbi(apkFile), + apk, + addonManifestUrl: config.addonManifestUrl, + orientation: config.orientation, + builtAt: new Date().toISOString(), + }; + writeFileSync( + path.join(outputDirectory, 'artifact.json'), + `${JSON.stringify(artifact, null, 2)}\n` + ); + + process.stdout.write(`[e2e:build] ${profile} release APK: ${apk} (abi: ${artifact.abi})\n`); +}; + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + main(); +} diff --git a/scripts/e2e/run-android.mjs b/scripts/e2e/run-android.mjs index 82045f0..b4a5198 100755 --- a/scripts/e2e/run-android.mjs +++ b/scripts/e2e/run-android.mjs @@ -227,7 +227,9 @@ const runVisualAssertions = async () => { continue; } - const details = failures.map((failure) => ` step ${failure.step}: ${failure.message}`).join('\n'); + const details = failures + .map((failure) => ` step ${failure.step}: ${failure.message}`) + .join('\n'); throw new Error(`screenshot regression(s) for ${profile}:\n${details}`); } }; diff --git a/scripts/e2e/test/android-build.test.mjs b/scripts/e2e/test/android-build.test.mjs new file mode 100644 index 0000000..df0f26d --- /dev/null +++ b/scripts/e2e/test/android-build.test.mjs @@ -0,0 +1,80 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; + +import { createBuildConfig } from '../android-build.mjs'; +import { parseBuildArgs, selectReleaseArtifact } from '../build-release.mjs'; + +test('selects TV build environment and metadata', () => { + const addonManifestUrl = 'http://10.0.2.2:8765/manifest.json'; + const config = createBuildConfig({ profile: 'tv', addonManifestUrl }); + + assert.deepEqual(config, { + appId: 'app.dodora.dodostream.dev', + profile: 'tv', + orientation: 'landscape', + addonManifestUrl, + environment: { + APP_VARIANT: 'dev', + EXPO_PUBLIC_E2E: '1', + E2E_ORIENTATION: 'landscape', + EXPO_TV: '1', + ADDON_MANIFEST_URL: addonManifestUrl, + }, + }); +}); + +test('selects phone and tablet orientation without TV mode', () => { + const phone = createBuildConfig({ profile: 'phone' }); + const tablet = createBuildConfig({ profile: 'tablet' }); + + assert.equal(phone.orientation, 'portrait'); + assert.equal(phone.environment.EXPO_TV, '0'); + assert.equal(tablet.orientation, 'landscape'); + assert.equal(tablet.environment.EXPO_TV, '0'); +}); + +test('rejects an unsupported build profile', () => { + assert.throws(() => createBuildConfig({ profile: 'desktop' }), /phone\|tablet\|tv/); +}); + +test('parses build arguments with deterministic defaults', () => { + const args = parseBuildArgs([], {}); + + assert.equal(args.profile, 'phone'); + assert.equal(args.abi, undefined); + assert.equal(args.skipLint, true); + assert.equal(args.addonManifestUrl, undefined); +}); + +test('parses explicit profile, ABI, lint, and manifest overrides', () => { + const args = parseBuildArgs(['--profile', 'tablet', '--abi', 'arm64-v8a'], { + E2E_LINT: '1', + ADDON_MANIFEST_URL: 'http://10.0.2.2:9999/manifest.json', + }); + + assert.equal(args.profile, 'tablet'); + assert.equal(args.abi, 'arm64-v8a'); + assert.equal(args.skipLint, false); + assert.equal(args.addonManifestUrl, 'http://10.0.2.2:9999/manifest.json'); +}); + +test('rejects missing option values', () => { + assert.throws(() => parseBuildArgs(['--profile'], {}), /missing value/); + assert.throws(() => parseBuildArgs(['--abi'], {}), /missing value/); +}); + +test('prefers the universal artifact when no ABI is requested', () => { + const files = ['app-arm64-v8a-release.apk', 'app-universal-release.apk']; + + assert.equal(selectReleaseArtifact(files, undefined), 'app-universal-release.apk'); + assert.equal(selectReleaseArtifact(files, 'arm64-v8a'), 'app-arm64-v8a-release.apk'); + assert.equal(selectReleaseArtifact(['app-release.apk'], undefined), 'app-release.apk'); +}); + +test('fails with the available artifact list', () => { + assert.throws( + () => selectReleaseArtifact(['app-x86_64-release.apk'], 'arm64-v8a'), + /app-x86_64-release\.apk/ + ); + assert.throws(() => selectReleaseArtifact([], undefined), /available: none/); +}); diff --git a/scripts/verify-i18n-parity.mjs b/scripts/verify-i18n-parity.mjs new file mode 100755 index 0000000..b7bb623 --- /dev/null +++ b/scripts/verify-i18n-parity.mjs @@ -0,0 +1,80 @@ +#!/usr/bin/env node +/** + * Deterministic gate: every locale must key-match `src/i18n/en`, the source of truth. + * Fails on missing files, missing/extra keys, and placeholder mismatches. + */ +import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; +import { join, resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const i18nDir = resolve(root, 'src/i18n'); +const sourceLocale = 'en'; + +const flattenLeaves = (object, prefix = '') => + Object.entries(object).flatMap(([key, value]) => + value !== null && typeof value === 'object' + ? flattenLeaves(value, `${prefix}${key}.`) + : [[`${prefix}${key}`, value]] + ); + +const placeholders = (value) => + typeof value === 'string' + ? [...new Set([...value.matchAll(/\{\{(\w+)\}\}/g)].map((match) => match[1]))].sort().join(',') + : ''; + +const jsonFiles = (dir) => + readdirSync(dir) + .filter((file) => file.endsWith('.json')) + .sort(); +const locales = readdirSync(i18nDir) + .filter((name) => statSync(join(i18nDir, name)).isDirectory() && name !== sourceLocale) + .sort(); + +const sourceFiles = jsonFiles(join(i18nDir, sourceLocale)); +const problems = []; + +for (const locale of locales) { + for (const file of sourceFiles) { + const localePath = join(i18nDir, locale, file); + if (!existsSync(localePath)) { + problems.push(`${locale}/${file}: file missing`); + continue; + } + + const source = new Map( + flattenLeaves(JSON.parse(readFileSync(join(i18nDir, sourceLocale, file), 'utf8'))) + ); + const target = new Map(flattenLeaves(JSON.parse(readFileSync(localePath, 'utf8')))); + + for (const key of source.keys()) { + if (!target.has(key)) { + problems.push(`${locale}/${file}: missing key ${key}`); + continue; + } + if (placeholders(source.get(key)) !== placeholders(target.get(key))) { + problems.push( + `${locale}/${file}: placeholder mismatch on ${key} (en: ${placeholders(source.get(key))}, ${locale}: ${placeholders(target.get(key))})` + ); + } + } + for (const key of target.keys()) { + if (!source.has(key)) + problems.push(`${locale}/${file}: extra key ${key} (stale? remove or re-add to en)`); + } + } + for (const file of jsonFiles(join(i18nDir, locale))) { + if (!sourceFiles.includes(file)) + problems.push(`${locale}/${file}: no en counterpart (stale file?)`); + } +} + +if (problems.length > 0) { + for (const problem of problems) console.error(problem); + console.error(`✗ i18n parity: ${problems.length} problem(s) against en`); + process.exit(1); +} + +console.log( + `✓ i18n parity: ${locales.length} locale(s) × ${sourceFiles.length} namespace(s) in sync with en` +); diff --git a/scripts/verify-maestro-flows.mjs b/scripts/verify-maestro-flows.mjs new file mode 100755 index 0000000..ea6a6f9 --- /dev/null +++ b/scripts/verify-maestro-flows.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +/** + * Deterministic gate: `.maestro` flow references must resolve. + * Validates the `flows:` globs in config.yaml, every `runFlow:` target (inline and + * nested `path:` form), and that no flow file on disk is orphaned from the globs. + */ +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { dirname, join, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const maestroDir = resolve(root, '.maestro'); +const flowsDir = join(maestroDir, 'flows'); + +const problems = []; + +const config = readFileSync(join(maestroDir, 'config.yaml'), 'utf8'); +if (!/^flows:\s*$/m.test(config)) { + console.error('✗ .maestro/config.yaml has no flows: section'); + process.exit(1); +} + +const afterFlows = config.slice(config.search(/^flows:\s*$/m)); +const globs = []; +for (const line of afterFlows.split('\n').slice(1)) { + const item = line.match(/^\s*-\s*(\S+)\s*$/); + if (item) { + globs.push(item[1]); + continue; + } + if (line.trim() && !line.trim().startsWith('#')) break; +} + +const globToRegExp = (pattern) => + new RegExp( + `^${pattern + .replace(/[.+^${}()|[\\\]]/g, '\\$&') + .replace(/\*/g, '[^/]*') + .replace(/\?/g, '.')}$` + ); + +const flowFiles = readdirSync(flowsDir).filter((file) => /\.ya?ml$/.test(file)); +const declared = new Set(); +for (const glob of globs) { + const matcher = globToRegExp(glob); + for (const file of flowFiles) { + const rel = relative(maestroDir, join(flowsDir, file)); + if (matcher.test(rel)) declared.add(rel); + } +} +for (const file of flowFiles) { + const rel = relative(maestroDir, join(flowsDir, file)); + if (!declared.has(rel)) + problems.push(`${rel}: on disk but not matched by any config.yaml flows glob`); +} +if (declared.size === 0) problems.push('config.yaml flows globs match no flow files'); + +const resolveTarget = (fromFile, target) => { + const cleaned = target.replace(/^['"]|['"]$/g, ''); + if (cleaned.startsWith('~')) return null; + return cleaned.startsWith('/') + ? join(maestroDir, cleaned) + : resolve(dirname(join(maestroDir, fromFile)), cleaned); +}; + +for (const file of [...declared].sort()) { + const lines = readFileSync(join(maestroDir, file), 'utf8').split('\n'); + for (let index = 0; index < lines.length; index += 1) { + const inline = lines[index].match(/^\s*-\s*runFlow:\s*(\S+)\s*$/); + if (inline) { + const target = resolveTarget(file, inline[1]); + if (target && !existsSync(target)) + problems.push(`${file}: runFlow target not found: ${inline[1]}`); + continue; + } + if (!/^\s*-\s*runFlow:\s*$/.test(lines[index])) continue; + const indent = lines[index].indexOf('-'); + let childIndent = -1; + for (let next = index + 1; next < lines.length; next += 1) { + const column = lines[next].search(/\S/); + if (column === -1) continue; + if (column <= indent) break; + if (childIndent === -1) childIndent = column; + if (column !== childIndent) continue; + const path = lines[next].match(/^\s*path:\s*(\S+)\s*$/); + if (path) { + const target = resolveTarget(file, path[1]); + if (target && !existsSync(target)) + problems.push(`${file}: runFlow path not found: ${path[1]}`); + break; + } + } + } +} + +if (problems.length > 0) { + for (const problem of problems) console.error(problem); + console.error(`✗ Maestro flow integrity: ${problems.length} problem(s)`); + process.exit(1); +} + +console.log(`✓ Maestro flow integrity: ${declared.size} flow(s), all runFlow references resolve`); diff --git a/scripts/verify-whats-new-registry.mjs b/scripts/verify-whats-new-registry.mjs new file mode 100755 index 0000000..d9cfc95 --- /dev/null +++ b/scripts/verify-whats-new-registry.mjs @@ -0,0 +1,35 @@ +#!/usr/bin/env node +/** + * Deterministic gate: `src/constants/whats-new/_registry.ts` must match the output of + * `scripts/generate-whats-new-registry.mjs`. Restores the original file on drift so the + * gate never leaves the working tree dirty. + */ +import { spawnSync } from 'node:child_process'; +import { readFileSync, writeFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const registryPath = resolve(root, 'src/constants/whats-new/_registry.ts'); +const generatorPath = resolve(root, 'scripts/generate-whats-new-registry.mjs'); + +const original = readFileSync(registryPath); + +const generator = spawnSync(process.execPath, [generatorPath], { cwd: root, stdio: 'ignore' }); +if (generator.error) { + console.error(`✗ could not run generator: ${generator.error.message}`); + process.exit(1); +} +if (generator.status !== 0) { + console.error(`✗ registry generator exited with ${generator.status}`); + process.exit(generator.status ?? 1); +} + +if (original.equals(readFileSync(registryPath))) { + console.log("✓ What's New registry matches generated output"); + process.exit(0); +} + +writeFileSync(registryPath, original); +console.error("✗ What's New registry drift — run pnpm generate-whats-new and commit the result"); +process.exit(1); diff --git a/src/api/trakt/sync-service.ts b/src/api/trakt/sync-service.ts index 10d062c..4d26074 100644 --- a/src/api/trakt/sync-service.ts +++ b/src/api/trakt/sync-service.ts @@ -59,8 +59,7 @@ type TraktSeason = { number: number; episodes: { number: number; watched_at?: st /** Trakt sync payload ids: requires exactly one identifying field (imdb or tmdb). */ type TraktRequestIds = - | { imdb: string; tmdb?: number | null } - | { tmdb: number; imdb?: string | null }; + { imdb: string; tmdb?: number | null } | { tmdb: number; imdb?: string | null }; /** * Narrow a partial `TraktIds` to a request-valid ids object. diff --git a/src/components/basic/ColorPicker.tsx b/src/components/basic/ColorPicker.tsx index 9762cd7..98a72b1 100644 --- a/src/components/basic/ColorPicker.tsx +++ b/src/components/basic/ColorPicker.tsx @@ -5,24 +5,9 @@ import { useTheme } from '@shopify/restyle'; import { Focusable } from '@/components/basic/Focusable'; import { Modal } from '@/components/basic/Modal'; +import { PICKER_COLORS } from '@/constants/colors'; import { Box, Text, Theme } from '@/theme/theme'; -/** Default color palette for general use */ -const DEFAULT_COLORS = [ - '#FFFFFF', // White - '#FFFF00', // Yellow - '#00FFFF', // Cyan - '#00FF00', // Green - '#FF00FF', // Magenta - '#FF0000', // Red - '#FFA500', // Orange - '#FFD700', // Gold - '#FFFACD', // Lemon chiffon - '#000000', // Black - '#333333', // Dark gray - '#666666', // Gray -]; - interface ColorPickerProps { /** Currently selected color in hex format */ value: string; @@ -67,7 +52,7 @@ ColorSwatch.displayName = 'ColorSwatch'; * Designed for TV remote navigation with focus states. */ export const ColorPicker: FC = memo( - ({ value, onValueChange, label, colors = DEFAULT_COLORS, disabled = false }) => { + ({ value, onValueChange, label, colors = PICKER_COLORS, disabled = false }) => { const theme = useTheme(); const { t } = useTranslation('common'); const [isModalVisible, setIsModalVisible] = useState(false); diff --git a/src/components/basic/Toast.tsx b/src/components/basic/Toast.tsx index 4438bf6..a65d313 100644 --- a/src/components/basic/Toast.tsx +++ b/src/components/basic/Toast.tsx @@ -79,7 +79,7 @@ const ToastItem: FC = memo(({ toast, index, onDismiss }) => { }; case 'warning': return { - background: '#F59E0B', // Amber/yellow for warning + background: theme.colors.warning, icon: theme.colors.mainBackground, }; default: diff --git a/src/components/media/CastTab.tsx b/src/components/media/CastTab.tsx index 79ce087..79a87e1 100644 --- a/src/components/media/CastTab.tsx +++ b/src/components/media/CastTab.tsx @@ -109,7 +109,7 @@ export const CastTab: FC = ({ media, isActive }) => { return ( - No cast information available + {t('media:no_cast_information')} ); diff --git a/src/components/media/HistoryCard.tsx b/src/components/media/HistoryCard.tsx index 6b4d27c..d654e24 100644 --- a/src/components/media/HistoryCard.tsx +++ b/src/components/media/HistoryCard.tsx @@ -1,4 +1,5 @@ import { memo, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import { useTheme } from '@shopify/restyle'; @@ -25,6 +26,7 @@ interface HistoryCardProps { export const HistoryCard = memo( ({ entry, onPress, onLongPress, hasTVPreferredFocus = false, testID }: HistoryCardProps) => { const theme = useTheme(); + const { t } = useTranslation(); const isMissingMeta = !entry.metaName; const { data: resolvedMeta } = useMeta(entry.type, entry.id, isMissingMeta); @@ -56,7 +58,7 @@ export const HistoryCard = memo( backgroundColor="cardBackground" borderRadius="l"> - Unavailable + {t('common:unavailable')} ); diff --git a/src/components/media/MediaInfo.tsx b/src/components/media/MediaInfo.tsx index 33fc8d9..74c88c9 100644 --- a/src/components/media/MediaInfo.tsx +++ b/src/components/media/MediaInfo.tsx @@ -5,6 +5,7 @@ import { CompletedBadge } from '@/components/basic/CompletedBadge'; import { ExpandableSection } from '@/components/basic/ExpandableSection'; import FadeIn from '@/components/basic/FadeIn'; import { Tag } from '@/components/basic/Tag'; +import { IMDB_BLACK, IMDB_YELLOW } from '@/constants/colors'; import { useMediaWatchStatus } from '@/hooks/useMediaWatchStatus'; import { Box, Text } from '@/theme/theme'; import type { MetaDetail, MetaLink, MetaVideo } from '@/types/stremio'; @@ -45,9 +46,6 @@ const parseImdbRating = (imdbRating: string | undefined) => { return Number.isFinite(n) ? n : undefined; }; -const IMDB_YELLOW = '#F5C518'; -const IMDB_BLACK = '#000000'; - const extractGenres = (media: MetaDetail) => { const fromLinks = getLinksByCategory(media.links, 'genre').map((l) => l.name); return uniqStrings(fromLinks); diff --git a/src/components/media/SectionPlaceholders.tsx b/src/components/media/SectionPlaceholders.tsx index e03b263..eb9343c 100644 --- a/src/components/media/SectionPlaceholders.tsx +++ b/src/components/media/SectionPlaceholders.tsx @@ -1,4 +1,5 @@ import { memo, PropsWithChildren, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; import Ionicons from '@react-native-vector-icons/ionicons/static'; import { useTheme } from '@shopify/restyle'; @@ -106,10 +107,11 @@ type SectionEmptyPlaceholderProps = SectionPlaceholderBaseProps; * Non-focusable since there's nothing to interact with. */ export const SectionEmptyPlaceholder = memo((props: SectionEmptyPlaceholderProps) => { + const { t } = useTranslation(); return ( - No content available + {t('common:no_content_available')} ); diff --git a/src/components/settings/SubtitleStyleSettings.tsx b/src/components/settings/SubtitleStyleSettings.tsx index 5aa25c8..d53a710 100644 --- a/src/components/settings/SubtitleStyleSettings.tsx +++ b/src/components/settings/SubtitleStyleSettings.tsx @@ -19,6 +19,7 @@ import { SUBTITLE_POSITION_MAX, SUBTITLE_POSITION_MIN, SUBTITLE_POSITION_STEP, + SUBTITLE_PREVIEW_BAND_COLORS, } from '@/constants/subtitles'; import { useResponsiveLayout } from '@/hooks/useBreakpoint'; import { usePlaybackStore } from '@/store/playback.store'; @@ -64,7 +65,7 @@ const SubtitlePreview = memo(({ style, containerWidth }) = left={0} width="33%" height="100%" - style={{ backgroundColor: '#0a0a14' }} + style={{ backgroundColor: SUBTITLE_PREVIEW_BAND_COLORS[0] }} /> (({ style, containerWidth }) = left="33%" width="34%" height="100%" - style={{ backgroundColor: '#3d4566' }} + style={{ backgroundColor: SUBTITLE_PREVIEW_BAND_COLORS[1] }} /> (({ style, containerWidth }) = right={0} width="33%" height="100%" - style={{ backgroundColor: '#8899bb' }} + style={{ backgroundColor: SUBTITLE_PREVIEW_BAND_COLORS[2] }} /> diff --git a/src/components/setup/WizardProgress.tsx b/src/components/setup/WizardProgress.tsx index 016e84f..63dbaeb 100644 --- a/src/components/setup/WizardProgress.tsx +++ b/src/components/setup/WizardProgress.tsx @@ -1,4 +1,5 @@ import { FC, memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { SetupWizardStep, WIZARD_STEPS } from '@/store/setup-wizard.store'; import { Box, Text } from '@/theme/theme'; @@ -11,6 +12,7 @@ interface WizardProgressProps { * Progress indicator showing current step in the wizard */ export const WizardProgress: FC = memo(({ currentStep }) => { + const { t } = useTranslation('setup'); const currentIndex = WIZARD_STEPS.indexOf(currentStep); const totalSteps = WIZARD_STEPS.length; @@ -29,7 +31,7 @@ export const WizardProgress: FC = memo(({ currentStep }) => ))} - Step {displayIndex} of {totalSteps} + {t('progress.step', { current: displayIndex, total: totalSteps })} ); diff --git a/src/components/video/PlayerStatisticsOverlay.tsx b/src/components/video/PlayerStatisticsOverlay.tsx index 659fad1..7170f43 100644 --- a/src/components/video/PlayerStatisticsOverlay.tsx +++ b/src/components/video/PlayerStatisticsOverlay.tsx @@ -1,4 +1,5 @@ import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useTheme } from '@shopify/restyle'; @@ -134,6 +135,7 @@ const StatisticRow = memo(({ label, value }: StatisticRowProps) => ( const BufferChart = memo(({ history }: { history: number[] }) => { const theme = useTheme(); + const { t } = useTranslation('player'); const samples = history.slice(-PLAYER_STATISTICS_HISTORY_SIZE); const maxSeconds = Math.max(...samples, 1); const chartHeight = theme.sizes.iconMedium; @@ -147,11 +149,11 @@ const BufferChart = memo(({ history }: { history: number[] }) => { gap="s" style={{ minWidth: 0, width: '100%' }}> - buffer ahead over time + {t('buffer_chart_title')} {samples.length > 0 && ( - max {formatBufferDuration(maxSeconds)} + {t('buffer_chart_max', { duration: formatBufferDuration(maxSeconds) })} )} @@ -182,7 +184,7 @@ const BufferChart = memo(({ history }: { history: number[] }) => { ) : ( - no samples yet + {t('buffer_chart_no_samples')} )} @@ -204,6 +206,7 @@ export const PlayerStatisticsOverlay = memo( source, }) => { const theme = useTheme(); + const { t } = useTranslation('player'); const nativeEntries = NATIVE_STATISTIC_ORDER.flatMap((key) => { const value = statistics[key]; return value === undefined ? [] : [[key, formatStatisticValue(value)] as const]; @@ -252,7 +255,7 @@ export const PlayerStatisticsOverlay = memo( gap="xs" overflow="hidden"> - PLAYBACK DIAGNOSTICS + {t('statistics_title')} ): string : (JSON.stringify(error) ?? 'Unknown player error'); }; +const usePlaybackSettings = (activeProfileId: string | undefined) => + usePlaybackStore( + useShallow((state) => { + const settings = activeProfileId + ? state.byProfile[activeProfileId] + : DEFAULT_PROFILE_PLAYBACK_SETTINGS; + return { + tunneled: settings?.tunneled ?? DEFAULT_PROFILE_PLAYBACK_SETTINGS.tunneled, + audioPassthrough: + settings?.audioPassthrough ?? DEFAULT_PROFILE_PLAYBACK_SETTINGS.audioPassthrough, + enableWorkarounds: + settings?.enableWorkarounds ?? DEFAULT_PROFILE_PLAYBACK_SETTINGS.enableWorkarounds, + showVideoStatistics: + settings?.showVideoStatistics ?? DEFAULT_PROFILE_PLAYBACK_SETTINGS.showVideoStatistics, + matchFrameRate: + settings?.matchFrameRate ?? DEFAULT_PROFILE_PLAYBACK_SETTINGS.matchFrameRate, + enableVideoSoftwareDecoding: + settings?.enableVideoSoftwareDecoding ?? + DEFAULT_PROFILE_PLAYBACK_SETTINGS.enableVideoSoftwareDecoding, + }; + }) + ); + +/** Translates native video events into app callbacks. Kept outside the component body. */ +const usePlayerEventHandlers = (props: PlayerProps) => { + const { + paused, + onProgress, + onBuffer, + onPlaying, + onLoad, + onError, + onAudioTracks, + onTextTracks, + onChapters, + onStatistics, + onBandwidthUpdate, + } = props; + + const handleProgress = useCallback( + (data: OnProgressData) => { + if (!paused) onPlaying?.(); + onProgress?.({ + currentTime: data.currentTime, + duration: data.seekableDuration || 0, + bufferedDuration: data.playableDuration || 0, + seekableDuration: data.seekableDuration || 0, + }); + }, + [onPlaying, onProgress, paused] + ); + const handleBuffer = useCallback( + (data: OnBufferData) => { + debug('buffering', { buffering: data.isBuffering }); + onBuffer?.(data.isBuffering); + }, + [onBuffer] + ); + + const handleLoad = useCallback( + (data: OnLoadData) => { + debug('load', { duration: data.duration, naturalSize: data.naturalSize }); + onLoad?.({ duration: data.duration }); + }, + [onLoad] + ); + + const handleError = useCallback( + (data: OnVideoErrorData) => { + const error = data?.error; + debug('error', { error }); + onError?.(composeErrorString(error)); + }, + [onError] + ); + + const handleAudioTracks = useCallback( + (data: OnAudioTracksData) => { + const audioTracks = Array.isArray(data?.audioTracks) ? data.audioTracks : []; + debug('audioTracks', { count: audioTracks.length }); + const tracks: AudioTrack[] = audioTracks.map((track, index) => ({ + index, + title: track.title, + language: track.language, + type: track.type, + })); + onAudioTracks?.(tracks); + }, + [onAudioTracks] + ); + + const handleTextTracks = useCallback( + (data: OnTextTracksData) => { + const textTracks = Array.isArray(data?.textTracks) ? data.textTracks : []; + debug('textTracks', { count: textTracks.length }); + const tracks: TextTrack[] = textTracks.map((track, idx) => ({ + source: 'video' as const, + index: idx, + title: track.title, + language: track.language, + playerIndex: track.index, // Use the player's track index for selection + })); + onTextTracks?.(tracks); + }, + [onTextTracks] + ); + + const handleVideoStatistics = useCallback( + (data: OnVideoStatisticsData) => { + onStatistics?.(data as PlayerStatistics); + }, + [onStatistics] + ); + + const handleChapters = useCallback( + (data: OnChaptersData) => { + const chapters: VideoChapter[] = Array.isArray(data?.chapters) + ? data.chapters.map(({ title, startTime, endTime, type }) => ({ + title, + startTime, + endTime, + type, + })) + : []; + debug('chapters', { count: chapters.length, chapters }); + onChapters?.(chapters); + }, + [onChapters] + ); + + const handleBandwidthUpdate = useCallback( + (data: OnBandwidthUpdateData) => { + debug('bandwidth', data); + onBandwidthUpdate?.({ + bitrate: data.bitrate, + width: data.width, + height: data.height, + trackId: data.trackId, + }); + }, + [onBandwidthUpdate] + ); + + return { + handleProgress, + handleBuffer, + handleLoad, + handleError, + handleAudioTracks, + handleTextTracks, + handleVideoStatistics, + handleChapters, + handleBandwidthUpdate, + }; +}; + export const RNVideoPlayer = memo( forwardRef((props, ref) => { const { source, paused, - onProgress, - onBuffer, - onPlaying, - onLoad, onEnd, - onError, - onAudioTracks, - onTextTracks, - onChapters, - onStatistics, - onBandwidthUpdate, selectedAudioTrack, selectedTextTrack, subtitleStyle, @@ -66,27 +219,7 @@ export const RNVideoPlayer = memo( showVideoStatistics, matchFrameRate, enableVideoSoftwareDecoding, - } = usePlaybackStore( - useShallow((state) => { - const settings = activeProfileId - ? state.byProfile[activeProfileId] - : DEFAULT_PROFILE_PLAYBACK_SETTINGS; - return { - tunneled: settings?.tunneled ?? DEFAULT_PROFILE_PLAYBACK_SETTINGS.tunneled, - audioPassthrough: - settings?.audioPassthrough ?? DEFAULT_PROFILE_PLAYBACK_SETTINGS.audioPassthrough, - enableWorkarounds: - settings?.enableWorkarounds ?? DEFAULT_PROFILE_PLAYBACK_SETTINGS.enableWorkarounds, - showVideoStatistics: - settings?.showVideoStatistics ?? DEFAULT_PROFILE_PLAYBACK_SETTINGS.showVideoStatistics, - matchFrameRate: - settings?.matchFrameRate ?? DEFAULT_PROFILE_PLAYBACK_SETTINGS.matchFrameRate, - enableVideoSoftwareDecoding: - settings?.enableVideoSoftwareDecoding ?? - DEFAULT_PROFILE_PLAYBACK_SETTINGS.enableVideoSoftwareDecoding, - }; - }) - ); + } = usePlaybackSettings(activeProfileId); useImperativeHandle(ref, () => ({ seekTo: (time: number) => { @@ -94,109 +227,17 @@ export const RNVideoPlayer = memo( }, })); - const handleProgress = useCallback( - (data: OnProgressData) => { - if (!paused) onPlaying?.(); - onProgress?.({ - currentTime: data.currentTime, - duration: data.seekableDuration || 0, - bufferedDuration: data.playableDuration || 0, - seekableDuration: data.seekableDuration || 0, - }); - }, - [onPlaying, onProgress, paused] - ); - const handleBuffer = useCallback( - (data: OnBufferData) => { - debug('buffering', { buffering: data.isBuffering }); - onBuffer?.(data.isBuffering); - }, - [onBuffer] - ); - - const handleLoad = useCallback( - (data: OnLoadData) => { - debug('load', { duration: data.duration, naturalSize: data.naturalSize }); - onLoad?.({ duration: data.duration }); - }, - [onLoad] - ); - - const handleError = useCallback( - (data: OnVideoErrorData) => { - const error = data?.error; - debug('error', { error }); - onError?.(composeErrorString(error)); - }, - [onError] - ); - - const handleAudioTracks = useCallback( - (data: OnAudioTracksData) => { - const audioTracks = Array.isArray(data?.audioTracks) ? data.audioTracks : []; - debug('audioTracks', { count: audioTracks.length }); - const tracks: AudioTrack[] = audioTracks.map((track, index) => ({ - index, - title: track.title, - language: track.language, - type: track.type, - })); - onAudioTracks?.(tracks); - }, - [onAudioTracks] - ); - - const handleTextTracks = useCallback( - (data: OnTextTracksData) => { - const textTracks = Array.isArray(data?.textTracks) ? data.textTracks : []; - debug('textTracks', { count: textTracks.length }); - const tracks: TextTrack[] = textTracks.map((track, idx) => ({ - source: 'video' as const, - index: idx, - title: track.title, - language: track.language, - playerIndex: track.index, // Use the player's track index for selection - })); - onTextTracks?.(tracks); - }, - [onTextTracks] - ); - - const handleVideoStatistics = useCallback( - (data: OnVideoStatisticsData) => { - onStatistics?.(data as PlayerStatistics); - }, - [onStatistics] - ); - - const handleChapters = useCallback( - (data: OnChaptersData) => { - const chapters: VideoChapter[] = Array.isArray(data?.chapters) - ? data.chapters.map(({ title, startTime, endTime, type }) => ({ - title, - startTime, - endTime, - type, - })) - : []; - debug('chapters', { count: chapters.length, chapters }); - onChapters?.(chapters); - }, - [onChapters] - ); - - const handleBandwidthUpdate = useCallback( - (data: OnBandwidthUpdateData) => { - debug('bandwidth', data); - onBandwidthUpdate?.({ - bitrate: data.bitrate, - width: data.width, - height: data.height, - trackId: data.trackId, - }); - }, - [onBandwidthUpdate] - ); + const { + handleProgress, + handleBuffer, + handleLoad, + handleError, + handleAudioTracks, + handleTextTracks, + handleVideoStatistics, + handleChapters, + handleBandwidthUpdate, + } = usePlayerEventHandlers(props); const audioTrackSelection: SelectedTrack | undefined = selectedAudioTrack ? { diff --git a/src/components/video/SubtitleCuePreview.tsx b/src/components/video/SubtitleCuePreview.tsx index 9132cac..50b6137 100644 --- a/src/components/video/SubtitleCuePreview.tsx +++ b/src/components/video/SubtitleCuePreview.tsx @@ -1,4 +1,5 @@ import React, { FC, memo, useEffect, useMemo, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; import { SUBTITLE_CUE_PREVIEW_COUNT, @@ -96,6 +97,7 @@ function findCurrentCueIndexFrom( * Shows surrounding cues with fading opacity to give context. */ export const SubtitleCuePreview: FC = memo(({ cues, adjustedTime }) => { + const { t } = useTranslation('player'); const halfCount = Math.floor(SUBTITLE_CUE_PREVIEW_COUNT / 2); const lastIndexRef = useRef(0); @@ -137,7 +139,7 @@ export const SubtitleCuePreview: FC = memo(({ cues, adj return ( - No subtitles loaded + {t('no_subtitles_loaded')} ); diff --git a/src/components/video/__tests__/PlayerControls.test.tsx b/src/components/video/__tests__/PlayerControls.test.tsx index ecaeddc..2e8f288 100644 --- a/src/components/video/__tests__/PlayerControls.test.tsx +++ b/src/components/video/__tests__/PlayerControls.test.tsx @@ -67,34 +67,38 @@ jest.mock('@/components/video/PlayerMenuOverlay', () => ({ : null, })); +const renderControls = (overrides: Partial> = {}) => + renderWithProviders( + {}} + onSeek={() => {}} + onSkipBackward={() => {}} + onSkipForward={() => {}} + onSelectAudioTrack={() => {}} + onSelectTextTrack={() => {}} + subtitleDelay={0} + onSubtitleDelayChange={() => {}} + fitMode="contain" + onToggleFitMode={() => {}} + onStreamSelect={() => {}} + onEpisodeSelect={() => {}} + {...overrides} + /> + ); + describe('PlayerControls basic interactions', () => { it('renders title and toggles visibility on press', () => { // Arrange - const { getByText, queryByText, getByTestId } = renderWithProviders( - {}} - onSeek={() => {}} - onSkipBackward={() => {}} - onSkipForward={() => {}} - onSelectAudioTrack={() => {}} - onSelectTextTrack={() => {}} - subtitleDelay={0} - onSubtitleDelayChange={() => {}} - fitMode="contain" - onToggleFitMode={() => {}} - onStreamSelect={() => {}} - onEpisodeSelect={() => {}} - /> - ); + const { getByText, queryByText, getByTestId } = renderControls(); // Assert (initial - controls are hidden) expect(queryByText('My Title')).toBeNull(); @@ -151,32 +155,12 @@ describe('PlayerControls basic interactions', () => { it('shows the loading indicator without disabling controls during buffering', () => { // Arrange - const { getByText, getByTestId } = renderWithProviders( - {}} - onSeek={() => {}} - onSkipBackward={() => {}} - onSkipForward={() => {}} - onSelectAudioTrack={() => {}} - onSelectTextTrack={() => {}} - subtitleDelay={0} - onSubtitleDelayChange={() => {}} - fitMode="contain" - onToggleFitMode={() => {}} - mediaType="movie" - metaId="movie-1" - onStreamSelect={() => {}} - onEpisodeSelect={() => {}} - /> - ); + const { getByText, getByTestId } = renderControls({ + paused: false, + showLoadingIndicator: true, + disableControls: false, + metaId: 'movie-1', + }); // Act fireEvent.press(getByTestId('player-controls-invisible-area')); @@ -226,31 +210,7 @@ describe('PlayerControls basic interactions', () => { { source: 'video', index: 3, title: 'Video ES', language: 'es' }, ]; - const { getByText, getByTestId } = renderWithProviders( - {}} - onSeek={() => {}} - onSkipBackward={() => {}} - onSkipForward={() => {}} - onSelectAudioTrack={() => {}} - onSelectTextTrack={() => {}} - subtitleDelay={0} - onSubtitleDelayChange={() => {}} - fitMode="contain" - onToggleFitMode={() => {}} - onStreamSelect={() => {}} - onEpisodeSelect={() => {}} - /> - ); + const { getByText, getByTestId } = renderControls({ textTracks: tracks as any }); // First show controls fireEvent.press(getByTestId('player-controls-invisible-area')); @@ -269,32 +229,13 @@ describe('PlayerControls basic interactions', () => { }); it('floats the skip button bottom-right while controls are hidden', () => { const onSkipChapter = jest.fn(); - const { getByText, getByTestId, queryByTestId } = renderWithProviders( - {}} - onSeek={() => {}} - onSkipBackward={() => {}} - onSkipForward={() => {}} - onSelectAudioTrack={() => {}} - onSelectTextTrack={() => {}} - subtitleDelay={0} - onSubtitleDelayChange={() => {}} - fitMode="contain" - onToggleFitMode={() => {}} - onStreamSelect={() => {}} - onEpisodeSelect={() => {}} - /> - ); + const { getByText, getByTestId, queryByTestId } = renderControls({ + paused: false, + currentTime: 90, + title: undefined, + skipTargets: [{ type: 'CREDITS', startTime: 80, endTime: 100 }], + onSkipChapter, + }); // Assert: skip button floats while controls are hidden expect(getByText('skip_credits')).toBeTruthy(); @@ -304,32 +245,12 @@ describe('PlayerControls basic interactions', () => { it('places the skip button inside the controls bar when controls are visible', () => { const onSkipChapter = jest.fn(); - const { getByText, getByTestId } = renderWithProviders( - {}} - onSeek={() => {}} - onSkipBackward={() => {}} - onSkipForward={() => {}} - onSelectAudioTrack={() => {}} - onSelectTextTrack={() => {}} - subtitleDelay={0} - onSubtitleDelayChange={() => {}} - fitMode="contain" - onToggleFitMode={() => {}} - onStreamSelect={() => {}} - onEpisodeSelect={() => {}} - /> - ); + const { getByText, getByTestId } = renderControls({ + currentTime: 90, + title: undefined, + skipTargets: [{ type: 'CREDITS', startTime: 80, endTime: 100 }], + onSkipChapter, + }); // Act: reveal the controls fireEvent.press(getByTestId('player-controls-invisible-area')); diff --git a/src/components/video/__tests__/PlayerStatisticsOverlay.test.tsx b/src/components/video/__tests__/PlayerStatisticsOverlay.test.tsx index 7ec7431..c951b4b 100644 --- a/src/components/video/__tests__/PlayerStatisticsOverlay.test.tsx +++ b/src/components/video/__tests__/PlayerStatisticsOverlay.test.tsx @@ -42,7 +42,7 @@ describe('PlayerStatisticsOverlay', () => { /> ); - expect(getByText('PLAYBACK DIAGNOSTICS')).toBeTruthy(); + expect(getByText('statistics_title')).toBeTruthy(); expect(getByText('source:')).toBeTruthy(); expect(getByText('cdn.example')).toBeTruthy(); expect(getByText('state:')).toBeTruthy(); @@ -55,7 +55,7 @@ describe('PlayerStatisticsOverlay', () => { expect(getByText('stream:')).toBeTruthy(); expect(getByText('video codec:')).toBeTruthy(); expect(getByText('HEVC')).toBeTruthy(); - expect(getByText('max 10.0s')).toBeTruthy(); + expect(getByText('buffer_chart_max')).toBeTruthy(); expect(getByTestId('player-statistics-buffer-chart')).toBeTruthy(); expect(getByTestId('player-statistics-overlay').props.pointerEvents).toBe('box-none'); expect(getByTestId('player-statistics-overlay-surface').props.pointerEvents).toBe('auto'); diff --git a/src/components/video/__tests__/RNVideoPlayer.events.test.tsx b/src/components/video/__tests__/RNVideoPlayer.events.test.tsx index f3dc1d8..3c7a4a8 100644 --- a/src/components/video/__tests__/RNVideoPlayer.events.test.tsx +++ b/src/components/video/__tests__/RNVideoPlayer.events.test.tsx @@ -56,6 +56,13 @@ const getNativeCallback = any>(name: string): T = return callback as T; }; +// Element factory so render() and rerender() share one source of truth for the base props. +const renderPlayer = ( + overrides: Partial> & { + ref?: React.Ref; + } = {} +) => ; + describe('RNVideoPlayer native adapter', () => { beforeEach(() => { mockNativeVideoProps = undefined; @@ -82,18 +89,7 @@ describe('RNVideoPlayer native adapter', () => { const onBandwidthUpdate = jest.fn(); const onEnd = jest.fn(); const onStatistics = jest.fn(); - render( - - ); + render(renderPlayer({ onLoad, onProgress, onBuffer, onBandwidthUpdate, onEnd, onStatistics })); act(() => { getNativeCallback('onLoad')({ duration: 120 }); @@ -150,7 +146,7 @@ describe('RNVideoPlayer native adapter', () => { mockPlaybackState = { byProfile: { 'profile-1': {} } }; try { - render(); + render(renderPlayer()); expect(mockNativeVideoProps?.audioPassthrough).toBe(false); } finally { Object.defineProperty(Platform, 'isTV', { configurable: true, value: originalIsTV }); @@ -163,13 +159,7 @@ describe('RNVideoPlayer native adapter', () => { { title: 'Main', startTime: 62.5, endTime: 120 }, ]; const onChapters = jest.fn(); - render( - - ); + render(renderPlayer({ onChapters })); act(() => { getNativeCallback('onChapters')({ chapters }); @@ -182,14 +172,7 @@ describe('RNVideoPlayer native adapter', () => { it('maps native tracks to stable app tracks and preserves the native subtitle index', () => { const onAudioTracks = jest.fn(); const onTextTracks = jest.fn(); - render( - - ); + render(renderPlayer({ onAudioTracks, onTextTracks })); act(() => { getNativeCallback('onAudioTracks')({ @@ -219,15 +202,7 @@ describe('RNVideoPlayer native adapter', () => { const onAudioTracks = jest.fn(); const onTextTracks = jest.fn(); const onError = jest.fn(); - render( - - ); + render(renderPlayer({ onAudioTracks, onTextTracks, onError })); act(() => { getNativeCallback('onAudioTracks')({}); @@ -242,9 +217,7 @@ describe('RNVideoPlayer native adapter', () => { it('produces useful error text from partial and empty native error payloads', () => { const onError = jest.fn(); - render( - - ); + render(renderPlayer({ onError })); act(() => { getNativeCallback('onError')({ @@ -263,9 +236,7 @@ describe('RNVideoPlayer native adapter', () => { }); it('selects requested audio and embedded subtitle tracks while explicitly disabling subtitles by default', () => { - const { rerender } = render( - - ); + const { rerender } = render(renderPlayer()); expect(mockNativeVideoProps).toEqual( expect.objectContaining({ @@ -275,12 +246,10 @@ describe('RNVideoPlayer native adapter', () => { ); rerender( - + renderPlayer({ + selectedAudioTrack: { index: 3, language: 'de' }, + selectedTextTrack: { source: 'video', index: 2, playerIndex: 9, language: 'en' }, + }) ); expect(mockNativeVideoProps).toEqual( @@ -293,7 +262,7 @@ describe('RNVideoPlayer native adapter', () => { it('forwards imperative seek requests and applies all active playback settings', () => { const ref = React.createRef(); - render(); + render(renderPlayer({ ref, paused: true })); act(() => { ref.current?.seekTo(42.25, 120); diff --git a/src/components/video/__tests__/VideoPlayerSession.test.tsx b/src/components/video/__tests__/VideoPlayerSession.test.tsx index 62dff71..701122f 100644 --- a/src/components/video/__tests__/VideoPlayerSession.test.tsx +++ b/src/components/video/__tests__/VideoPlayerSession.test.tsx @@ -154,60 +154,65 @@ const triggerBuffering = (playerProps: any) => { }); }; -describe('VideoPlayerSession', () => { - let dateNowSpy: jest.SpyInstance; - - beforeEach(() => { - jest.useFakeTimers(); - mockLastExoProps = undefined; - mockLastVlcProps = undefined; - mockPlayerControlsProps = undefined; - mockUpNextResolved = undefined; - mockUpNextProps = undefined; - mockSeekTo.mockReset(); - mockUpsertItem.mockReset(); - mockSetLastStreamTarget.mockReset().mockResolvedValue(undefined); - mockResumeHistoryItem = undefined; - mockIntroData = undefined; - mockPreferredAudioLanguages = undefined; - mockShowVideoStatistics = false; - mockShowToast.mockReset(); - mockReplaceToStreams.mockReset(); - dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(10_000); - }); +let dateNowSpy: jest.SpyInstance; + +const setupSessionMocks = () => { + jest.useFakeTimers(); + mockLastExoProps = undefined; + mockLastVlcProps = undefined; + mockPlayerControlsProps = undefined; + mockUpNextResolved = undefined; + mockUpNextProps = undefined; + mockSeekTo.mockReset(); + mockUpsertItem.mockReset(); + mockSetLastStreamTarget.mockReset().mockResolvedValue(undefined); + mockResumeHistoryItem = undefined; + mockIntroData = undefined; + mockPreferredAudioLanguages = undefined; + mockShowVideoStatistics = false; + mockShowToast.mockReset(); + mockReplaceToStreams.mockReset(); + dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(10_000); +}; - afterEach(() => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); - dateNowSpy.mockRestore(); - }); +const teardownSessionTimers = () => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + dateNowSpy.mockRestore(); +}; - const renderSession = ( - overrides: Partial> = {} - ) => { - const props: React.ComponentProps = { - source: 'https://example.com/stream.m3u8', - title: 'Title', - mediaType: 'movie' as any, - metaId: 'm1', - videoId: undefined, - bingeGroup: undefined, - onStop: jest.fn(), - onError: jest.fn(), - usedPlayerType: 'exoplayer', - setUsedPlayerType: jest.fn(), - playerType: 'exoplayer', - automaticFallback: true, - ...overrides, - }; - - return { - ...renderWithProviders(, { - queryClient: createTestQueryClient(), - }), - props, - }; +const renderSession = ( + overrides: Partial> = {} +) => { + const props: React.ComponentProps = { + source: 'https://example.com/stream.m3u8', + title: 'Title', + mediaType: 'movie' as any, + metaId: 'm1', + videoId: undefined, + bingeGroup: undefined, + onStop: jest.fn(), + onError: jest.fn(), + usedPlayerType: 'exoplayer', + setUsedPlayerType: jest.fn(), + playerType: 'exoplayer', + automaticFallback: true, + ...overrides, + }; + + return { + ...renderWithProviders(, { + queryClient: createTestQueryClient(), + }), + props, }; +}; + +// The suite is split across same-titled describes purely to stay under the +// max-lines-per-function lint budget; the shared hooks keep behavior identical. +describe('VideoPlayerSession', () => { + beforeEach(setupSessionMocks); + afterEach(teardownSessionTimers); it('renders the correct player component based on usedPlayerType', () => { renderSession({ usedPlayerType: 'exoplayer' }); @@ -430,6 +435,11 @@ describe('VideoPlayerSession', () => { }) ); }); +}); + +describe('VideoPlayerSession', () => { + beforeEach(setupSessionMocks); + afterEach(teardownSessionTimers); it('attempts automatic fallback on error when enabled and user-selected player fails', () => { // Arrange @@ -491,6 +501,11 @@ describe('VideoPlayerSession', () => { ); } ); +}); + +describe('VideoPlayerSession', () => { + beforeEach(setupSessionMocks); + afterEach(teardownSessionTimers); it('autoplays next episode on end when Up Next is resolved and not cancelled', () => { // Arrange diff --git a/src/constants/colors.ts b/src/constants/colors.ts new file mode 100644 index 0000000..6e62ab6 --- /dev/null +++ b/src/constants/colors.ts @@ -0,0 +1,25 @@ +/** + * Non-theme color data: user-selectable palettes and third-party brand colors. + * Component styling must use theme tokens; these are product data values + * (the set of colors a user can pick, or another company's brand identity). + */ + +/** Default color palette for general use (profile avatars, subtitle color pickers) */ +export const PICKER_COLORS = [ + '#FFFFFF', // White + '#FFFF00', // Yellow + '#00FFFF', // Cyan + '#00FF00', // Green + '#FF00FF', // Magenta + '#FF0000', // Red + '#FFA500', // Orange + '#FFD700', // Gold + '#FFFACD', // Lemon chiffon + '#000000', // Black + '#333333', // Dark gray + '#666666', // Gray +]; + +/** IMDb brand colors for the rating badge */ +export const IMDB_YELLOW = '#F5C518'; +export const IMDB_BLACK = '#000000'; diff --git a/src/constants/subtitles.ts b/src/constants/subtitles.ts index efb1c1f..4756383 100644 --- a/src/constants/subtitles.ts +++ b/src/constants/subtitles.ts @@ -26,6 +26,9 @@ export const SUBTITLE_PADDING_RATIO = { // Reference resolution for scaling (1080p) export const SUBTITLE_REFERENCE_HEIGHT = 1080; +/** Static band colors for the subtitle style preview backdrop (dark to light mock gradient) */ +export const SUBTITLE_PREVIEW_BAND_COLORS = ['#0a0a14', '#3d4566', '#8899bb']; + /** * Available font family options for the picker */ diff --git a/src/i18n/de/common.json b/src/i18n/de/common.json index 8515932..ad29197 100644 --- a/src/i18n/de/common.json +++ b/src/i18n/de/common.json @@ -23,5 +23,7 @@ "select_icon": "Symbol auswählen", "select_color": "Farbe auswählen", "show_more": "Mehr anzeigen", - "show_less": "Weniger anzeigen" + "show_less": "Weniger anzeigen", + "unavailable": "Nicht verfügbar", + "no_content_available": "Keine Inhalte verfügbar" } diff --git a/src/i18n/de/media.json b/src/i18n/de/media.json index 0e07267..83502c9 100644 --- a/src/i18n/de/media.json +++ b/src/i18n/de/media.json @@ -23,6 +23,7 @@ "specials": "Specials", "unknown_episode": "Unbekannte Episode", "cast": "Besetzung", + "no_cast_information": "Keine Besetzungsinformationen verfügbar", "directors": "Regie", "writers": "Drehbuch", "search_placeholder": "Suche nach Filmen und Serien...", diff --git a/src/i18n/de/player.json b/src/i18n/de/player.json index 25f3129..692e0c9 100644 --- a/src/i18n/de/player.json +++ b/src/i18n/de/player.json @@ -32,5 +32,10 @@ "error_codec": "Videoformat wird von diesem Player nicht unterstützt", "error_network": "Netzwerkfehler - überprüfe deine Verbindung", "error_source": "Stream nicht verfügbar oder abgelaufen", - "error_unknown": "Ein Wiedergabefehler ist aufgetreten" + "error_unknown": "Ein Wiedergabefehler ist aufgetreten", + "statistics_title": "WIEDERGABE-DIAGNOSTIK", + "buffer_chart_title": "Puffer voraus im Zeitverlauf", + "buffer_chart_max": "max. {{duration}}", + "buffer_chart_no_samples": "noch keine Messwerte", + "no_subtitles_loaded": "Keine Untertitel geladen" } diff --git a/src/i18n/de/profiles.json b/src/i18n/de/profiles.json index c070714..cdc5474 100644 --- a/src/i18n/de/profiles.json +++ b/src/i18n/de/profiles.json @@ -26,6 +26,8 @@ "profile_switched": "Profil gewechselt", "cannot_delete_active": "Das aktive Profil kann nicht gelöscht werden", "profile_deleted": "Profil gelöscht", + "delete_confirm_title": "Profil löschen", + "delete_confirm_msg": "Möchtest du \"{{name}}\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", "current_profile": "Aktuelles Profil", "switch_profile": "Profil wechseln", "manage_profiles_desc": "Profile wechseln, bearbeiten oder löschen. Wiedergabeeinstellungen und Meine Liste gelten pro Profil.", diff --git a/src/i18n/de/setup.json b/src/i18n/de/setup.json index ceb8567..c9259df 100644 --- a/src/i18n/de/setup.json +++ b/src/i18n/de/setup.json @@ -56,5 +56,8 @@ "complete": { "title": "Alles bereit!", "start_exploring": "Jetzt entdecken" + }, + "progress": { + "step": "Schritt {{current}} von {{total}}" } } diff --git a/src/i18n/en/common.json b/src/i18n/en/common.json index 91440cb..801e50a 100644 --- a/src/i18n/en/common.json +++ b/src/i18n/en/common.json @@ -23,5 +23,7 @@ "select_icon": "Select Icon", "select_color": "Select Color", "show_more": "Show more", - "show_less": "Show less" + "show_less": "Show less", + "unavailable": "Unavailable", + "no_content_available": "No content available" } diff --git a/src/i18n/en/media.json b/src/i18n/en/media.json index b0050ae..34411a7 100644 --- a/src/i18n/en/media.json +++ b/src/i18n/en/media.json @@ -25,6 +25,7 @@ "specials": "Specials", "unknown_episode": "Unknown Episode", "cast": "Cast", + "no_cast_information": "No cast information available", "directors": "Directors", "writers": "Writers", "search_placeholder": "Search movies and shows...", diff --git a/src/i18n/en/player.json b/src/i18n/en/player.json index a858440..314f05c 100644 --- a/src/i18n/en/player.json +++ b/src/i18n/en/player.json @@ -32,5 +32,10 @@ "error_codec": "Video format not supported by this player", "error_network": "Network error - check your connection", "error_source": "Stream unavailable or expired", - "error_unknown": "Playback error occurred" + "error_unknown": "Playback error occurred", + "statistics_title": "PLAYBACK DIAGNOSTICS", + "buffer_chart_title": "buffer ahead over time", + "buffer_chart_max": "max {{duration}}", + "buffer_chart_no_samples": "no samples yet", + "no_subtitles_loaded": "No subtitles loaded" } diff --git a/src/i18n/en/setup.json b/src/i18n/en/setup.json index 86e12b6..1ee6e40 100644 --- a/src/i18n/en/setup.json +++ b/src/i18n/en/setup.json @@ -56,5 +56,8 @@ "complete": { "title": "You're All Set!", "start_exploring": "Start Exploring" + }, + "progress": { + "step": "Step {{current}} of {{total}}" } } diff --git a/src/i18n/pt/common.json b/src/i18n/pt/common.json index 34da486..00e30ab 100644 --- a/src/i18n/pt/common.json +++ b/src/i18n/pt/common.json @@ -23,5 +23,7 @@ "select_icon": "Selecionar Ícone", "select_color": "Selecionar Cor", "show_more": "Mostrar mais", - "show_less": "Mostrar menos" + "show_less": "Mostrar menos", + "unavailable": "Indisponível", + "no_content_available": "Sem conteúdo disponível" } diff --git a/src/i18n/pt/media.json b/src/i18n/pt/media.json index 46b355a..d8d0fc4 100644 --- a/src/i18n/pt/media.json +++ b/src/i18n/pt/media.json @@ -25,6 +25,7 @@ "specials": "Especiais", "unknown_episode": "Episódio Desconhecido", "cast": "Elenco", + "no_cast_information": "Sem informações de elenco disponíveis", "directors": "Realizadores", "writers": "Argumentistas", "search_placeholder": "Pesquisar filmes e séries...", diff --git a/src/i18n/pt/player.json b/src/i18n/pt/player.json index 193bcff..c63f3e4 100644 --- a/src/i18n/pt/player.json +++ b/src/i18n/pt/player.json @@ -32,5 +32,10 @@ "error_codec": "Formato de vídeo não suportado por este leitor", "error_network": "Erro de rede - verifica a tua ligação", "error_source": "Stream indisponível ou expirada", - "error_unknown": "Ocorreu um erro de reprodução" + "error_unknown": "Ocorreu um erro de reprodução", + "statistics_title": "DIAGNÓSTICOS DE REPRODUÇÃO", + "buffer_chart_title": "buffer disponível ao longo do tempo", + "buffer_chart_max": "máx. {{duration}}", + "buffer_chart_no_samples": "sem amostras ainda", + "no_subtitles_loaded": "Nenhuma legenda carregada" } diff --git a/src/i18n/pt/setup.json b/src/i18n/pt/setup.json index 03e1522..e48adbd 100644 --- a/src/i18n/pt/setup.json +++ b/src/i18n/pt/setup.json @@ -56,5 +56,8 @@ "complete": { "title": "Estás Pronto!", "start_exploring": "Começar a Explorar" + }, + "progress": { + "step": "Passo {{current}} de {{total}}" } } diff --git a/src/theme/theme.ts b/src/theme/theme.ts index 049464c..088c25a 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -27,6 +27,7 @@ const palette = { // Functional red: '#FF3B30', + amber: '#F59E0B', overlayBlack: 'rgba(0, 0, 0, 0.6)', semiTransparentBlack: 'rgba(0, 0, 0, 0.4)', transparent: 'transparent', @@ -76,6 +77,7 @@ const createAppTheme = (scalingFactor: number) => textLink: palette.greenPrimary, danger: palette.red, + warning: palette.amber, transparent: 'transparent', inputBackground: palette.dark2, diff --git a/src/types/continue-watching.ts b/src/types/continue-watching.ts index 93d9e89..3b8f095 100644 --- a/src/types/continue-watching.ts +++ b/src/types/continue-watching.ts @@ -1,7 +1,2 @@ export type ContinueWatchingAction = - | 'details' - | 'play' - | 'resume' - | 'play-from-start' - | 'hide' - | 'remove-from-history'; + 'details' | 'play' | 'resume' | 'play-from-start' | 'hide' | 'remove-from-history';