diff --git a/.taskmaster/config.json b/.taskmaster/config.json
index 712b04b..3df642f 100644
--- a/.taskmaster/config.json
+++ b/.taskmaster/config.json
@@ -22,23 +22,8 @@
   "global": {
     "logLevel": "info",
     "debug": false,
-    "defaultNumTasks": 10,
     "defaultSubtasks": 5,
     "defaultPriority": "medium",
-    "projectName": "TaskMaster",
-    "ollamaBaseURL": "http://localhost:11434/api",
-    "bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
-    "responseLanguage": "English",
-    "enableCodebaseAnalysis": true,
-    "enableProxy": false,
-    "anonymousTelemetry": true,
-    "userId": "1234567890"
-  },
-  "claudeCode": {},
-  "codexCli": {},
-  "grokCli": {
-    "timeout": 120000,
-    "workingDirectory": null,
-    "defaultModel": "grok-4-latest"
+    "projectName": "TaskMaster"
   }
 }
\ No newline at end of file
diff --git a/app_spec.txt b/app_spec.txt
deleted file mode 100644
index 4a47408..0000000
--- a/app_spec.txt
+++ /dev/null
@@ -1,381 +0,0 @@
-# Zman TCG Fulfillment - Unified Product Requirements Document (PRD)
-
-This document serves as the comprehensive, developer-ready blueprint and product requirements document for the Zman TCG Fulfillment system. It consolidates all architectural designs, implementation plans, integration research, workflows, and database schemas into a single source of truth.
-
----
-
-## 1. Product Overview
-
-The Zman TCG Fulfillment project is a centralized inventory, pricing, and order management system for a Trading Card Game (TCG) business. It unifies fragmented data across multiple external platforms and APIs (ManaPool, TCGPlayer, TCGCSV, MTGJSON) into a single source of truth. 
-
-The system empowers effortless platform-agnostic order fulfillment, integrates physical hardware (printers and scanners) to streamline warehouse workflows, implements a highly granular "Chaos Sort" inventory mechanism, and lays the groundwork for a proprietary internal storefront.
-
-### 1.1 Core Objectives
-- **Centralized Source of Truth:** Establish a unified catalog and inventory system that avoids alphabetized sorting delays.
-- **Multi-Channel Orchestration:** Treat TCGPlayer, ManaPool, and custom storefronts as unified streams of orders and inventory.
-- **Hardware Integration:** Connect cloud-based serverless systems securely to local physical warehouse hardware.
-- **Automation at Scale:** Reduce manual data entry and repetitive fulfillment tasks through scraping, APIs, and intelligent shipping rules.
-
----
-
-## 2. Technical Architecture & Hosting Strategy
-
-The system relies on a bifurcated architecture to leverage the best parts of Serverless computing while bypassing its limitations for specific heavy tasks.
-
-### 2.1 The Core Application (Serverless)
-**Stack:** Next.js (React), Tailwind CSS, Radix UI, Node.js API Routes.
-**Database:** PostgreSQL (using a Serverless-native provider like Neon/Supabase, or a connection proxy like PgBouncer to prevent connection limits from bursting webhooks). ORM via Drizzle or Prisma.
-
-*Included in the Core:*
-- The UI Dashboard.
-- Internal APIs for storefronts, scanner lookups, and order resolutions.
-- Webhook endpoints to ingest real-time data from ManaPool.
-- Outbound API calls to fast services (EasyPost, API-driven syncs).
-
-*Why Serverless?*
-Zero-maintenance auto-scaling. Webhooks from ManaPool or rapid bursts of scanner gun inputs instantiate instantly without managing server loads. The predictable compute costs and edge network delivery make the UI incredibly fast.
-
-### 2.2 The Quarantined Services (Persistent / Local)
-By moving pain points out of the core app, system bottlenecks are eliminated:
-
-**A. The "Warehouse" Hardware Daemon:**
-- A tiny script (Node or Python) or a lightweight desktop app (Electron) running on a warehouse PC.
-- It securely polls the serverless API for pending print jobs (utilizing secure exponential backoff to minimize edge invocations).
-- Downloads PDFs or labels and routes them directly to local printers (Brother QL-1100 or network laser printers via `SumatraPDF` or `lp`). 
-- **Advantage:** Serverless never has to worry about local IP configurations or port forwarding. You pay $0.00 for idle hardware management.
-
-**B. The ETL & Scraper Worker:**
-- A persistent, low-cost container (e.g., Railway, DigitalOcean, Render) running BullMQ + Redis.
-- Manages strict 15-second+ tasks: Puppeteer headless browsers to hijack TCGPlayer cookies, downloading massive bulk CSVs from TCGCSV, and asynchronous web scraping.
-
-## Part 1: Database Schema Design (PostgreSQL)
-
-Before any code is written, the central database must be structured to handle the "Chaos Sort" coordinates, universal product mapping, and multi-channel order orchestration.
-
-```sql
--- 1. Catalog & Products (Sourced from TCGCSV & MTGJSON)
-CREATE TABLE categories (
-    id INT PRIMARY KEY, -- e.g. 1 (MTG), 3 (Pokemon)
-    name VARCHAR(255) NOT NULL
-);
-
-CREATE TABLE groups (
-    id INT PRIMARY KEY, -- e.g. 3170 (Silver Tempest)
-    category_id INT REFERENCES categories(id),
-    name VARCHAR(255) NOT NULL,
-    published_on DATE
-);
-
-CREATE TABLE products (
-    id INT PRIMARY KEY, -- TCGPlayer Product ID
-    group_id INT REFERENCES groups(id),
-    name VARCHAR(255) NOT NULL,
-    clean_name VARCHAR(255),
-    image_url TEXT,
-    -- Native TCGTracking & Affiliate IDs
-    scryfall_id UUID,
-    mtgjson_uuid UUID,
-    tcgplayer_url TEXT,
-    manapool_url TEXT,
-    card_number VARCHAR(50),
-    rarity VARCHAR(50)
-);
-
-CREATE TABLE skus (
-    id INT PRIMARY KEY, -- TCGPlayer SKU ID
-    product_id INT REFERENCES products(id),
-    condition VARCHAR(10) NOT NULL, -- NM, LP, MP, HP, DMG
-    variant VARCHAR(10) NOT NULL, -- N (Normal), F (Foil), etc.
-    language VARCHAR(10) NOT NULL, -- EN, JP, DE, etc.
-    market_price_cents INT,
-    lowest_listing_cents INT
-);
-
--- 2. Chaos Sort Locations
-CREATE TABLE physical_locations (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    coord_string VARCHAR(50) UNIQUE NOT NULL, -- Format: 01:01:01:01:01:01
-    section VARCHAR(10), aisle VARCHAR(10), rack VARCHAR(10), 
-    shelf VARCHAR(10), box VARCHAR(10), slot VARCHAR(10)
-);
-
--- 3. Inventory Master
-CREATE TABLE inventory (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    sku_id INT REFERENCES skus(id), -- Maps exactly to the physical SKU combination
-    location_id UUID REFERENCES physical_locations(id),
-    quantity INT DEFAULT 0,
-    listed_price_cents INT NOT NULL,
-    -- Sync tracking
-    last_synced_tcgplayer TIMESTAMP,
-    last_synced_manapool TIMESTAMP
-);
-
--- 4. Unified Orders
-CREATE TABLE orders (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    marketplace VARCHAR(50) NOT NULL, -- 'TCGPLAYER', 'MANAPOOL', 'INTERNAL'
-    marketplace_order_id VARCHAR(255) UNIQUE NOT NULL,
-    status VARCHAR(50) DEFAULT 'PENDING', -- PENDING, PRINTED, SHIPPED, CANCELLED
-    buyer_name VARCHAR(255),
-    shipping_address JSONB NOT NULL,
-    total_cents INT NOT NULL,
-    shipping_cents INT NOT NULL,
-    tracking_number VARCHAR(255),
-    easypost_label_url TEXT,
-    created_at TIMESTAMP DEFAULT NOW()
-);
-
-CREATE TABLE order_lines (
-    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    order_id UUID REFERENCES orders(id),
-    inventory_id UUID REFERENCES inventory(id), -- Maps exactly to the physical card leaving
-    quantity INT NOT NULL,
-    price_cents INT NOT NULL
-);
-```
-
----
-
-## Part 2: Configurable Hybrid Catalog ETL Worker (Phase 1)
-**Stack:** Node.js (TypeScript), BullMQ + Redis, Node `pg` Pool.
-
-**Core Philosophy:** The structural shape of the data (endpoints, schemas) can and should be hardcoded. However, execution parameters (when to run, what to fetch) must be driven by configuration.
-
-**Developer Steps:**
-1. **Configuration Driven Execution:** The cron schedules for both the TCGCSV and TCGTracking pulls must be derived from `process.env` (e.g., `CRON_TCGCSV_SYNC`, `CRON_TCGTRACKING_SYNC`). 
-2. **Dynamic Game Targeting:** The worker must read an array of `SUPPORTED_CATEGORIES` (e.g., `[1, 3, 27]`) from the configuration. The worker iterates over this array, hitting the hardcoded `https://tcgcsv.com/tcgplayer` endpoints for those specific IDs.
-3. **SKU & Pricing Enrichment (TCGTracking):** Following the base catalog sync, the worker iterates over those identical dynamic `SUPPORTED_CATEGORIES` against the Open TCG API (`/skus` endpoints) to build out the `skus` table and inject `mtgjson_uuid` where available.
-4. **Resilient Upsert Logic:** 
-    - Use `ON CONFLICT (id) DO UPDATE` to safely build the `products` table from TCGCSV.
-    - Process TCGTracking payloads concurrently in queues to build out child `skus` without locking the database or exceeding memory limits.
-
----
-
-## Part 3: Undocumented TCGPlayer API Reverse Engineering (Phase 3)
-*This is the most critical and fragile component. It runs exclusively in the Persistent Worker to avoid serverless timeouts.*
-
-**The Methodology:**
-1. **Puppeteer Stealth:** Install `puppeteer-extra` and `puppeteer-extra-plugin-stealth` to bypass TCGPlayer's Cloudflare/DataDome challenges.
-2. **Session Interception:**
-    *   Launch browser, navigate to `store.tcgplayer.com/admin/login`.
-    *   Inject credentials securely. Wait for navigation.
-    *   On the network tab, listen for XHR requests to `https://order-management-api.tcgplayer.com/*`.
-    *   Extract the `Authorization: Bearer <TOKEN>` or the specific `Cookie` string holding the session ID.
-3. **API Exploitation:** Once the Bearer token/Cookie is captured, close the heavy browser. Store the token in Redis with a TTL (Time-To-Live). Proceed to make lightweight `axios`/`fetch` calls to the `order-management-api` endpoints until a `401 Unauthorized` is hit, which triggers the Puppeteer flow to restart.
-3. **API Exploitation:** Once the Bearer token/Cookie is captured, close the heavy browser. Store the token in Redis with a TTL (Time-To-Live). Proceed to make lightweight `axios`/`fetch` calls to the `order-management-api` endpoints until a `401 Unauthorized` is hit, which triggers the Puppeteer flow to restart.
-4. **Operations to map:**
-
-    **A. POST `/orders/search`** (To find new orders)
-    *   *Payload:* Requires pagination and filter logic (e.g., `status=New`).
-    *   *Response Structure:* Provides high-level `orderNumber` and totals for iteration.
-
-    **B. GET `/orders/{order_id}`** (To pull the line items)
-    *   *Response Structure:* Maps perfectly to our unified database!
-    ```json
-    {
-      "orderNumber": "E576ED4C...",
-      "status": "Ready to Ship",
-      "shippingAddress": { "recipientName": "John Doe", "addressOne": "123 St"... },
-      "products": [
-        {
-          "quantity": 1,
-          "productId": "652840",
-          "skuId": "8925977" // Explicit target for our skus table!
-        }
-      ]
-    }
-    ```
-    
-    **C. Inventory Operations & Document Exports:**
-    *   **Updating Price/Quantity for a Condition:** `POST https://store.tcgplayer.com/admin/product/updateinventory`
-        *Payload (URL Encoded Form Data):* Requires building a specific nested array string (e.g., `productQuantityPrices[0][ConditionQuantityPrices][0][Quantity]=2&...[Price]=0.26`).
-    *   **Inventory Mass Export CSV:** `POST https://store.tcgplayer.com/admin/pricing/downloadexportcsv` (Returns a CSV containing your active Inventory mapped to their SKU IDs).
-    *   **Orders Mass Export CSV:** `POST https://order-management-api.tcgplayer.com/orders/export` (Returns a CSV of multi-order data).
-    *   **Generate Native Pull Sheets:** `POST https://order-management-api.tcgplayer.com/orders/pull-sheets/export`
-        *Payload:* `{"orderNumbers": ["E576ED4..."], "timezoneOffset": -5}` (Returns a PDF buffer).
-    *   **Generate Native Packing Slips:** `POST https://order-management-api.tcgplayer.com/orders/packing-slips/export`
-        *Payload:* `{"sortingType": "ByRelease", "format": "Default", "orderNumbers": ["E576ED4..."]}` (Returns a PDF buffer).
-
-    **D. Order Fulfillment (Placeholder)** 
-    Because marking an order as shipped could not be tested in the current network capture, this API call must be intercepted live during development.
-    *   *Action:* Find the API triggered when clicking "Add Tracking" or "Mark as Shipped" on the TCGPlayer UI.
-    *   *Expected Form:* Likely a `POST /orders/{id}/tracking` or `POST /orders/shipments` payload taking the `carrier` and `trackingNumber`.
-
----
-
-## Part 4: ManaPool OpenAPI Sync & Unification (Phase 4)
-**Stack:** Next.js API Routes (Serverless)
-
-Per the `https://manapool.com/api/docs/v1` specification, ManaPool operates on a standard REST API with predictable payloads. We must build robust webhook receivers to ingest this native shape and normalize it into our universal `orders` table.
-
-### 4.1 Ingestion (Webhook Listeners)
-**Endpoint:** `POST /api/webhooks/manapool/order-created`
-
-**Expected Native Payload Structure:**
-```json
-{
-  "event": "order.created",
-  "data": {
-    "id": "ord_12345abc",
-    "status": "unfulfilled",
-    "buyer": { "name": "John Doe" },
-    "shipping": {
-      "address1": "123 Main St",
-      "city": "Seattle",
-      "state": "WA",
-      "zip": "98101",
-      "country": "US"
-    },
-    "totals": {
-      "grand_total": 4550, // cents
-      "shipping": 500
-    },
-    "items": [
-      {
-        "id": "item_890xyz",
-        "product_id": "557921", // TCGPlayer ID Match
-        "condition": "NM",
-        "foil": true,
-        "quantity": 1,
-        "price": 2050
-      }
-    ]
-  }
-}
-```
-
-**Developer Steps:**
-1.  **Validation:** Validate the webhook signature using the configured `MANAPOOL_WEBHOOK_SECRET` header.
-2.  **Normalization:** Map the incoming Manapool native `items` array directly to our overarching `skus` table (using `product_id`, `condition`, and `foil` status dynamically) to create `order_lines`.
-3.  **Insert:** Write to the unify `orders` table.
-
-### 4.2 Fulfillment Upstream Sync
-When an order transitions to `SHIPPED` inside our UI, we must push the tracking state back to the external marketplace.
-
-**Endpoint:** `POST https://manapool.com/api/v1/orders/{order_id}/fulfill`
-**Method:** Create a background job triggered by the DB `status` change.
-
-**Payload Assembly:**
-```json
-{
-  "tracking_number": "9400123456789",
-  "carrier": "USPS"
-}
-```
-*Note: TCGPlayer fulfillment sync is fundamentally different and will require enqueuing a Selenium/Puppeteer automation job via the Phase 3 Session Manager.*
-
----
-
-## Part 5: EasyPost & Configurable Shipping Rules (Phase 5)
-**Stack:** Core Serverless App (Next.js) + `@easypost/api`
-
-The shipment generation flow must be incredibly fast (1-click) on the dashboard. To achieve this, the underlying rules engine cannot rely on hardcoded thresholds (e.g., "12 cards = Letter"). It must look up user-defined rules from a configuration file or database setting.
-
-### 5.1 The Configurable Rules Engine
-Before interacting with EasyPost, the system evaluates the unified `orders` record against settings defined in a `shipping_rules` table (or environment variables).
-
-**Example Configuration Structure:**
-```json
-{
-  "usps_letter_thresholds": {
-    "max_card_count": 14,
-    "max_order_value_cents": 1999,
-    "default_weight_oz": 2.0
-  },
-  "usps_ground_advantage_thresholds": {
-    "default_weight_oz": 4.0 // Baseline bubble mailer
-  }
-}
-```
-
-### 5.2 EasyPost Implementation Flow
-
-**Developer Steps:**
-1.  **Address Verification:** When the user clicks "Buy Postage", first call `client.Address.create({ verify: true, ... })`. If `verification.delivery.success` is false, halt and prompt the user to override.
-2.  **Evaluate Thresholds:**
-    *   *Calculate Order Stats:* Sum `order_lines.quantity` and read `orders.total_cents`.
-    *   *Decision:* If `sum(qty) <= max_card_count` AND `total_cents <= max_order_value_cents`, proceed with **USPS Letter**. Otherwise, proceed with **USPS Ground Advantage**.
-3.  **Create the Parcel & Shipment:**
-    ```javascript
-    // Example: USPS Letter Generation
-    const shipment = await client.Shipment.create({
-      to_address: verifiedBuyerAddress,
-      from_address: warehouseAddress,
-      parcel: {
-        length: 9.5, // Standard #10 envelope or PWE dimension
-        width: 4.1,
-        height: 0.1, // Crucial for PWE classification
-        weight: rules.usps_letter_thresholds.default_weight_oz 
-      }
-    });
-    ```
-4.  **Buy the Rate:** Iterate through `shipment.rates` to find the exact matching service (e.g., `service: 'First'` for USPS Letter, or `service: 'GroundAdvantage'`).
-    ```javascript
-    const purchasedShipment = await client.Shipment.buy(
-      shipment.id,
-      shipment.rates.find(r => r.service === targetService).id
-    );
-    ```
-5.  **Store & Queue:**
-    *   Save `purchasedShipment.postage_label.label_url` to the DB `orders` table.
-    *   Save `purchasedShipment.tracking_code`.
-    *   Insert a job into the `print_queue` for the Hardware Daemon to fetch and print locally.
-
----
-
-## Part 6: Hardware Interactor Daemon (Phase 6)
-**Stack:** Node.js locally running on warehouse PC, or Electron.
-
-**The Serverless Advantage:** Because the warehouse printing hardware is inactive 90% of the time, the *Serverless Core* handles the print queue perfectly. When no orders are being fulfilled, the cloud infrastructure scales to zero, meaning you pay exactly $0.00 for idle hardware management.
-
-**The Interaction Model:** 
-Since the Serverless Core (in the cloud) cannot securely "push" a PDF through your local router to your physical printer, we use a lightweight, outbound-only "pull" mechanism.
-
-**Developer Steps:**
-1. **The Print Queue (Cloud):** The serverless database maintains a simple `print_jobs` table (containing the PDF URL or ZPL string).
-2. **The Local Daemon (Warehouse):** A tiny Node.js script runs constantly in the background on your warehouse PC (a machine that is already turned on anyway).
-3. **Smart Polling Loop:** The daemon executes an HTTP `GET /api/internal/print-queue` against your serverless endpoint.
-    *   *Efficiency:* To minimize edge-function invocations when idle, the daemon should implement exponential backoff. Start polling every 5 seconds. If empty, increase to 10s, then 30s, up to a maximum of 60 seconds. As soon as a job is found (or a user clicks a "wake" button on the UI), it resets to 5 seconds.
-4. **Execution Routing:**
-    *   **ZPL/Label Queue:** If the job is a thermal label, pipe the EasyPost URL payload directly to the Brother QL-1100 native network IP (`raw socket port 9100`) or USB interface via `node-thermal-printer`.
-    *   **PDF Queue (Pull Sheets):** Download the generated PDF locally, then execute native OS print commands (e.g., `SumatraPDF -print-to <PrinterName>` on Windows, or `lp -d <PrinterName>` on Mac).
-    *   **Acknowledge:** Send a `POST /api/internal/print-queue/{id}/complete` to the serverless core to dequeue the job.
-
----
-
-## Verification Plan
-
-### Database & Types
-Ensure TypeScript types are auto-generated from the provided PostgreSQL schema using an ORM like Drizzle or Prisma, guaranteeing strict type-safety across the monorepo.
-
-### TCGPlayer Session Automation
-**Test Script:** A standalone manual script (`scripts/test-tcg-auth.ts`) must be written first. It will launch Puppeteer visibly (`headless: false`) so the developer can watch the bot navigate DataDome. The script will `console.log` the bearer token. If this token successfully curls the `/orders` endpoint, Phase 3 is validated and can be moved to headless production.
-
-### Hardware Validation
-A simulated endpoint will push a test label into the `/internal/print-queue`. The local developer machine will run the daemon, expecting a physical print from a configured local printer within 5 seconds.
-
-# Part 1.1: The Hybrid Catalog Architecture
-
-The foundation of the Zman TCG Fulfillment system relies on marrying the broad scope of **TCGCSV** with the deep, explicit data modeling of **TCGTracking**.
-
-### The Problem
-*   **TCGCSV** provides the absolute best, exhaustive coverage of TCGPlayer's catalog (Categories, Groups, Products) across *all* games (Pokemon, Lorcana, etc.). However, it does not provide SKU-level data (Condition/Finish) or prices for those specific conditions.
-*   **TCGTracking** completely maps the `Product -> SKU` relationship and native MTGJSON mapping, but its coverage outside of Magic: The Gathering is not guaranteed to be fully exhaustive.
-
-### The Hybrid Solution
-
-We will utilize both APIs in an ordered sequence to build our database.
-
-1.  **The Base Layer (TCGCSV):** 
-    We run our ETL pipeline against TCGCSV first. This ensures our `categories`, `groups`, and `products` tables are populated exhaustively for every single game on TCGPlayer. 
-    *   *Result:* We have a record for Product #`557921` (Llanowar Elves).
-
-2.  **The Enrichment Layer (TCGTracking):**
-    We immediately follow up by pinging the TCGTracking API for supported games. We use their `/skus` payload to generate the child records in our `skus` table and map the conditions (NM/LP) to the base `products` we just synced. 
-    *   *Result:* Product #`557921` is enriched with its native `mtgjson_uuid`. We generate SKU #`4639` (NM, Foil) and link it.
-
-3.  **The Fallback (Custom Webscrapers - Phase 3):**
-    If a user scans a Pokemon card, and we have the Product info (from TCGCSV) but no SKUs exist in our DB (because TCGTracking didn't cover it), we fall back to our custom persistent webscrapers. The scraper hits the TCGPlayer frontend, extracts the SKU IDs and prices for that specific Pokemon card, and writes them to the `skus` table permanently.
-
-By isolating the `products` table from the `skus` table, **our internal inventory ("Chaos Sort") only ever interacts with the explicit SKU ID.** It does not care where the SKU data originated from (TCGTracking vs. Custom Scraper), allowing for a highly extensible architecture.
diff --git a/app_spec.txt b/app_spec.txt
new file mode 120000
index 0000000..27217f0
--- /dev/null
+++ b/app_spec.txt
@@ -0,0 +1 @@
+/Users/zman/workspace/projects/zman-tcg-fulfillment/code/app_spec.txt
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index d8f239d..62863b4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1655,6 +1655,122 @@
       "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==",
       "license": "MIT"
     },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.2.2",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+      "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+      "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@isaacs/cliui/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+      "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/fs-minipass": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+      "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^7.0.4"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
     "node_modules/@jridgewell/gen-mapping": {
       "version": "0.3.13",
       "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -2049,6 +2165,17 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/@radix-ui/colors": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz",
@@ -3643,6 +3770,17 @@
         "@types/react": "^19.2.0"
       }
     },
+    "node_modules/@types/yauzl": {
+      "version": "2.10.3",
+      "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
+      "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
     "node_modules/@zman-tcg/daemon": {
       "resolved": "packages/daemon",
       "link": true
@@ -3682,6 +3820,16 @@
         "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
       }
     },
+    "node_modules/agent-base": {
+      "version": "7.1.4",
+      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+      "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/ajv": {
       "version": "6.14.0",
       "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
@@ -3921,6 +4069,16 @@
         "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
       }
     },
+    "node_modules/buffer-crc32": {
+      "version": "0.2.13",
+      "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+      "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
     "node_modules/buffer-from": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -3994,6 +4152,19 @@
         "node": ">=6"
       }
     },
+    "node_modules/camelcase": {
+      "version": "6.3.0",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+      "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/camelcase-css": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
@@ -4079,6 +4250,16 @@
         "node": ">= 6"
       }
     },
+    "node_modules/chownr": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+      "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/classnames": {
       "version": "2.5.1",
       "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
@@ -4154,6 +4335,13 @@
         "node": ">= 6"
       }
     },
+    "node_modules/commondir": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+      "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/component-emitter": {
       "version": "1.3.1",
       "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
@@ -4490,6 +4678,13 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/electron-to-chromium": {
       "version": "1.5.302",
       "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
@@ -4503,6 +4698,16 @@
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
       "license": "MIT"
     },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
     "node_modules/env-paths": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz",
@@ -4789,6 +4994,27 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/extract-zip": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz",
+      "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "debug": "^4.1.1",
+        "get-stream": "^5.1.0",
+        "yauzl": "^2.10.0"
+      },
+      "bin": {
+        "extract-zip": "cli.js"
+      },
+      "engines": {
+        "node": ">= 10.17.0"
+      },
+      "optionalDependencies": {
+        "@types/yauzl": "^2.9.1"
+      }
+    },
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4856,6 +5082,16 @@
         "reusify": "^1.0.4"
       }
     },
+    "node_modules/fd-slicer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
+      "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pend": "~1.2.0"
+      }
+    },
     "node_modules/file-entry-cache": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -4882,6 +5118,31 @@
         "node": ">=8"
       }
     },
+    "node_modules/find-cache-dir": {
+      "version": "3.3.2",
+      "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
+      "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "commondir": "^1.0.1",
+        "make-dir": "^3.0.2",
+        "pkg-dir": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+      }
+    },
+    "node_modules/find-package-json": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/find-package-json/-/find-package-json-1.2.0.tgz",
+      "integrity": "sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/find-up": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -4940,6 +5201,36 @@
         }
       }
     },
+    "node_modules/foreground-child": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
+      },
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/foreground-child/node_modules/signal-exit": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/form-data": {
       "version": "4.0.5",
       "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
@@ -5098,6 +5389,19 @@
         "node": ">=6"
       }
     },
+    "node_modules/get-port": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz",
+      "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/get-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
@@ -5111,6 +5415,22 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/get-stream": {
+      "version": "5.2.0",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz",
+      "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "pump": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/get-tsconfig": {
       "version": "4.13.6",
       "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz",
@@ -5124,6 +5444,28 @@
         "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
       }
     },
+    "node_modules/glob": {
+      "version": "10.5.0",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
+      "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
+      "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/glob-parent": {
       "version": "6.0.2",
       "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -5137,6 +5479,45 @@
         "node": ">=10.13.0"
       }
     },
+    "node_modules/glob/node_modules/balanced-match": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+      "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/glob/node_modules/brace-expansion": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz",
+      "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^4.0.2"
+      },
+      "engines": {
+        "node": "18 || 20 || >=22"
+      }
+    },
+    "node_modules/glob/node_modules/minimatch": {
+      "version": "9.0.6",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz",
+      "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^5.0.2"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/globals": {
       "version": "14.0.0",
       "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
@@ -5211,6 +5592,20 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/https-proxy-agent": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+      "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "agent-base": "^7.1.2",
+        "debug": "4"
+      },
+      "engines": {
+        "node": ">= 14"
+      }
+    },
     "node_modules/ignore": {
       "version": "5.3.2",
       "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5350,6 +5745,22 @@
       "dev": true,
       "license": "ISC"
     },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
     "node_modules/jiti": {
       "version": "1.21.7",
       "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
@@ -5454,6 +5865,16 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/lockfile": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz",
+      "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "signal-exit": "^3.0.2"
+      }
+    },
     "node_modules/lodash.defaults": {
       "version": "4.2.0",
       "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
@@ -5473,6 +5894,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/luxon": {
       "version": "3.7.2",
       "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
@@ -5482,6 +5910,32 @@
         "node": ">=12"
       }
     },
+    "node_modules/make-dir": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+      "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "semver": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/make-dir/node_modules/semver": {
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      }
+    },
     "node_modules/math-intrinsics": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5570,6 +6024,29 @@
         "node": "*"
       }
     },
+    "node_modules/minipass": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
+      "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
+    "node_modules/minizlib": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
+      "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "minipass": "^7.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
     "node_modules/ms": {
       "version": "2.1.3",
       "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5866,6 +6343,23 @@
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
+    },
     "node_modules/parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -5906,6 +6400,30 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/pend": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+      "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/pg": {
       "version": "8.18.0",
       "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
@@ -6043,6 +6561,75 @@
         "node": ">= 6"
       }
     },
+    "node_modules/pkg-dir": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+      "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "find-up": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pkg-dir/node_modules/find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pkg-dir/node_modules/locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^4.1.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pkg-dir/node_modules/p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-try": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/pkg-dir/node_modules/p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^2.2.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/postcss": {
       "version": "8.5.6",
       "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -6283,6 +6870,17 @@
       "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
       "license": "MIT"
     },
+    "node_modules/pump": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+      "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
     "node_modules/punycode": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -6528,6 +7126,46 @@
         "node": ">=4"
       }
     },
+    "node_modules/redis-memory-server": {
+      "version": "0.16.0",
+      "resolved": "https://registry.npmjs.org/redis-memory-server/-/redis-memory-server-0.16.0.tgz",
+      "integrity": "sha512-NumE5HHR5ku8k3hD54xUjBhv2XoKQd/UC8gtlAh3jFKqhH3kSn3C8pB3MBS5K3hj7rhgiJbXu+2Xhm+Mqi+SWQ==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "camelcase": "^6.3.0",
+        "cross-spawn": "^7.0.3",
+        "debug": "^4.3.4",
+        "extract-zip": "^2.0.1",
+        "find-cache-dir": "^3.3.2",
+        "find-package-json": "^1.2.0",
+        "get-port": "^5.1.1",
+        "https-proxy-agent": "^7.0.0",
+        "lockfile": "^1.0.4",
+        "rimraf": "^5.0.1",
+        "semver": "^7.5.3",
+        "tar": "^7.5.4",
+        "tmp": "^0.2.1",
+        "uuid": "^8.3.2"
+      },
+      "bin": {
+        "redis-memory-server": "bin/index.js"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/redis-memory-server/node_modules/uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
     "node_modules/redis-parser": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz",
@@ -6607,6 +7245,22 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/rimraf": {
+      "version": "5.0.10",
+      "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz",
+      "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "glob": "^10.3.7"
+      },
+      "bin": {
+        "rimraf": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
     "node_modules/run-parallel": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -6802,6 +7456,13 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/signal-exit": {
+      "version": "3.0.7",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/source-map": {
       "version": "0.6.1",
       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -6859,6 +7520,22 @@
         "node": ">=8"
       }
     },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-ansi": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@@ -6871,6 +7548,20 @@
         "node": ">=8"
       }
     },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-json-comments": {
       "version": "3.1.1",
       "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -7016,6 +7707,23 @@
         "node": ">=14.0.0"
       }
     },
+    "node_modules/tar": {
+      "version": "7.5.9",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz",
+      "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/fs-minipass": "^4.0.0",
+        "chownr": "^3.0.0",
+        "minipass": "^7.1.2",
+        "minizlib": "^3.1.0",
+        "yallist": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/thenify": {
       "version": "3.3.1",
       "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -7087,6 +7795,16 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/tmp": {
+      "version": "0.2.5",
+      "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz",
+      "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.14"
+      }
+    },
     "node_modules/to-regex-range": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -7755,6 +8473,25 @@
         "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
       }
     },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/wrappy": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -7779,6 +8516,16 @@
         "node": ">=10"
       }
     },
+    "node_modules/yallist": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+      "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "engines": {
+        "node": ">=18"
+      }
+    },
     "node_modules/yargs": {
       "version": "17.7.2",
       "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
@@ -7806,6 +8553,17 @@
         "node": ">=12"
       }
     },
+    "node_modules/yauzl": {
+      "version": "2.10.0",
+      "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
+      "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "buffer-crc32": "~0.2.3",
+        "fd-slicer": "~1.1.0"
+      }
+    },
     "node_modules/yocto-queue": {
       "version": "0.1.0",
       "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -7895,6 +8653,7 @@
       },
       "devDependencies": {
         "@types/node": "^22.0.0",
+        "redis-memory-server": "^0.16.0",
         "tsx": "^4.19.0",
         "typescript": "^5.7.0"
       }
diff --git a/packages/worker/package.json b/packages/worker/package.json
index 3c574f8..6c104f7 100644
--- a/packages/worker/package.json
+++ b/packages/worker/package.json
@@ -11,15 +11,16 @@
   },
   "dependencies": {
     "@zman-tcg/shared": "*",
+    "axios": "^1.7.0",
     "bullmq": "^5.30.0",
-    "ioredis": "^5.4.0",
-    "pg": "^8.13.0",
+    "csv-parse": "^5.6.0",
     "drizzle-orm": "^0.38.0",
-    "axios": "^1.7.0",
-    "csv-parse": "^5.6.0"
+    "ioredis": "^5.4.0",
+    "pg": "^8.13.0"
   },
   "devDependencies": {
     "@types/node": "^22.0.0",
+    "redis-memory-server": "^0.16.0",
     "tsx": "^4.19.0",
     "typescript": "^5.7.0"
   }
diff --git a/packages/worker/src/scripts/test-queues.ts b/packages/worker/src/scripts/test-queues.ts
index 6b33ee4..73a1c4c 100644
--- a/packages/worker/src/scripts/test-queues.ts
+++ b/packages/worker/src/scripts/test-queues.ts
@@ -1,27 +1,26 @@
 /**
  * Test Script: Redis + BullMQ Queue Infrastructure
  *
- * Two modes of operation:
+ * Two-phase test approach:
  *
- * 1. WITH Redis running — runs full integration tests:
- *    - Connect to Redis successfully
- *    - Add test job to queue
- *    - Verify job appears in queue
- *    - Process test job with simple worker
- *    - Test job retry on failure
+ * Phase 1 — STRUCTURAL TESTS (always run, no Redis needed):
+ *   - Verify all queue names are defined
+ *   - Verify queue configuration (retry, backoff, retention)
+ *   - Verify job type interfaces are usable
+ *   - Verify worker module exports
+ *   - Verify dashboard module exports
  *
- * 2. WITHOUT Redis — runs structural validation tests:
- *    - Verify all queue names are defined
- *    - Verify queue configuration (retry, backoff, retention)
- *    - Verify job type interfaces are usable
- *    - Verify worker module exports
- *    - Verify dashboard module exports
+ * Phase 2 — INTEGRATION TESTS (always run, embedded Redis):
+ *   Uses `redis-memory-server` to spin up an embedded Redis instance,
+ *   so integration tests run without any external Redis dependency.
+ *   - Connect to Redis successfully
+ *   - Add test job to queue
+ *   - Verify job appears in queue
+ *   - Process test job with simple worker
+ *   - Test job retry on failure
  *
  * Usage:
  *   npm run test:queues --workspace=@zman-tcg/worker
- *
- * To run integration tests, start Redis first:
- *   docker compose up -d   (or: redis-server)
  */
 
 // Suppress noisy Redis connection errors during tests (set before imports)
@@ -73,6 +72,7 @@ process.on('unhandledRejection', (reason) => {
 
 import IORedis from 'ioredis';
 import { Queue, Worker, QueueEvents } from 'bullmq';
+import { RedisMemoryServer } from 'redis-memory-server';
 import {
   QUEUE_NAMES,
   type QueueName,
@@ -88,7 +88,6 @@ import {
 
 let passed = 0;
 let failed = 0;
-let skipped = 0;
 
 // Test output functions use original console to bypass noise filter
 function log(msg: string): void {
@@ -97,61 +96,14 @@ function log(msg: string): void {
 
 function assert(condition: boolean, message: string): void {
   if (condition) {
-    log(`  \u2705 PASS: ${message}`);
+    log(`  ✅ PASS: ${message}`);
     passed++;
   } else {
-    originalConsoleError(`  \u274c FAIL: ${message}`);
+    originalConsoleError(`  ❌ FAIL: ${message}`);
     failed++;
   }
 }
 
-function skip(message: string): void {
-  log(`  \u23ed\ufe0f  SKIP: ${message}`);
-  skipped++;
-}
-
-// ---------------------------------------------------------------------------
-// Redis probe — quick check, no infinite retries
-// ---------------------------------------------------------------------------
-
-const REDIS_URL = process.env.REDIS_URL || 'redis://localhost:6379';
-
-async function probeRedis(): Promise<boolean> {
-  return new Promise((resolve) => {
-    const probe = new IORedis(REDIS_URL, {
-      maxRetriesPerRequest: 1,
-      enableReadyCheck: false,
-      connectTimeout: 3_000,
-      lazyConnect: true,
-      retryStrategy() {
-        return null; // Do not retry — we just want to know if Redis is there
-      },
-    });
-
-    // Suppress error logging during probe
-    probe.on('error', () => {});
-
-    const timeout = setTimeout(() => {
-      probe.disconnect();
-      resolve(false);
-    }, 3_500);
-
-    probe
-      .connect()
-      .then(() => probe.ping())
-      .then((result) => {
-        clearTimeout(timeout);
-        probe.disconnect();
-        resolve(result === 'PONG');
-      })
-      .catch(() => {
-        clearTimeout(timeout);
-        probe.disconnect();
-        resolve(false);
-      });
-  });
-}
-
 // ============================================================================
 // STRUCTURAL TESTS — run without Redis
 // ============================================================================
@@ -208,7 +160,6 @@ function testQueueConfiguration(): void {
   log('\n--- Structural Test 3: Queue Configuration ---');
 
   // Import the queue default options by examining the queue module
-  // We verify that the queues module exports correctly
   const queueExports = require('../queues');
 
   // Verify all queue instances are exported
@@ -287,7 +238,7 @@ function testDashboardModuleExports(): void {
 }
 
 // ============================================================================
-// INTEGRATION TESTS — require Redis
+// INTEGRATION TESTS — use embedded Redis via redis-memory-server
 // ============================================================================
 
 async function testRedisConnection(testRedis: IORedis): Promise<void> {
@@ -296,19 +247,22 @@ async function testRedisConnection(testRedis: IORedis): Promise<void> {
   const pong = await testRedis.ping();
   assert(pong === 'PONG', 'Redis responds to PING with PONG');
 
-  // Also verify the health check utility works with this connection
+  // Verify the connection can execute commands
+  await testRedis.set('test:health', 'ok');
+  const val = await testRedis.get('test:health');
+  assert(val === 'ok', 'Redis SET/GET works correctly');
+  await testRedis.del('test:health');
+
+  // Verify the checkRedisHealth function exists and is callable
   const { checkRedisHealth } = require('../config/redis');
-  const isHealthy = await checkRedisHealth();
-  // This may fail if the shared redisConnection can't connect (it uses its own instance)
-  // But we still validate the function runs without throwing
-  assert(typeof isHealthy === 'boolean', 'checkRedisHealth returns a boolean');
+  assert(typeof checkRedisHealth === 'function', 'checkRedisHealth function exists');
 }
 
-async function testAddJobToQueue(testRedis: IORedis): Promise<void> {
+async function testAddJobToQueue(connOpts: { host: string; port: number; maxRetriesPerRequest: null }): Promise<void> {
   log('\n--- Integration Test 2: Add Test Job to Queue ---');
 
   const testQueueName = 'test-add-' + Date.now();
-  const testQueue = new Queue(testQueueName, { connection: testRedis });
+  const testQueue = new Queue(testQueueName, { connection: connOpts });
 
   try {
     const job = await testQueue.add('test-job', { message: 'hello from test' });
@@ -328,12 +282,13 @@ async function testAddJobToQueue(testRedis: IORedis): Promise<void> {
   }
 }
 
-async function testProcessJobWithWorker(testRedis: IORedis): Promise<void> {
+async function testProcessJobWithWorker(connOpts: { host: string; port: number; maxRetriesPerRequest: null }): Promise<void> {
   log('\n--- Integration Test 3: Process Test Job with Worker ---');
 
   const testQueueName = 'test-process-' + Date.now();
-  const testQueue = new Queue(testQueueName, { connection: testRedis });
-  const testEvents = new QueueEvents(testQueueName, { connection: testRedis });
+  const testQueue = new Queue(testQueueName, { connection: connOpts });
+  const testEvents = new QueueEvents(testQueueName, { connection: connOpts });
+  await testEvents.waitUntilReady();
 
   let processedData: unknown = null;
 
@@ -343,8 +298,9 @@ async function testProcessJobWithWorker(testRedis: IORedis): Promise<void> {
       processedData = job.data;
       return { success: true, processed: job.data.value };
     },
-    { connection: testRedis },
+    { connection: connOpts },
   );
+  await worker.waitUntilReady();
 
   try {
     const job = await testQueue.add('process-test', { value: 42 });
@@ -365,18 +321,19 @@ async function testProcessJobWithWorker(testRedis: IORedis): Promise<void> {
   }
 }
 
-async function testJobRetryOnFailure(testRedis: IORedis): Promise<void> {
+async function testJobRetryOnFailure(connOpts: { host: string; port: number; maxRetriesPerRequest: null }): Promise<void> {
   log('\n--- Integration Test 4: Job Retry on Failure ---');
 
   const testQueueName = 'test-retry-' + Date.now();
   const testQueue = new Queue(testQueueName, {
-    connection: testRedis,
+    connection: connOpts,
     defaultJobOptions: {
       attempts: 3,
       backoff: { type: 'fixed', delay: 200 },
     },
   });
-  const testEvents = new QueueEvents(testQueueName, { connection: testRedis });
+  const testEvents = new QueueEvents(testQueueName, { connection: connOpts });
+  await testEvents.waitUntilReady();
 
   let attemptCount = 0;
 
@@ -389,8 +346,9 @@ async function testJobRetryOnFailure(testRedis: IORedis): Promise<void> {
       }
       return { success: true, totalAttempts: attemptCount };
     },
-    { connection: testRedis },
+    { connection: connOpts },
   );
+  await worker.waitUntilReady();
 
   try {
     const job = await testQueue.add('retry-test', { shouldFail: true });
@@ -410,26 +368,28 @@ async function testJobRetryOnFailure(testRedis: IORedis): Promise<void> {
   }
 }
 
-async function testPermanentJobFailure(testRedis: IORedis): Promise<void> {
+async function testPermanentJobFailure(connOpts: { host: string; port: number; maxRetriesPerRequest: null }): Promise<void> {
   log('\n--- Integration Test 5: Permanent Job Failure ---');
 
   const testQueueName = 'test-fail-' + Date.now();
   const testQueue = new Queue(testQueueName, {
-    connection: testRedis,
+    connection: connOpts,
     defaultJobOptions: {
       attempts: 2,
       backoff: { type: 'fixed', delay: 100 },
     },
   });
-  const testEvents = new QueueEvents(testQueueName, { connection: testRedis });
+  const testEvents = new QueueEvents(testQueueName, { connection: connOpts });
+  await testEvents.waitUntilReady();
 
   const worker = new Worker(
     testQueueName,
     async () => {
       throw new Error('Always fails');
     },
-    { connection: testRedis },
+    { connection: connOpts },
   );
+  await worker.waitUntilReady();
 
   try {
     const job = await testQueue.add('fail-test', { shouldFail: true });
@@ -481,46 +441,43 @@ async function runTests(): Promise<void> {
   }
 
   // -----------------------------------------------------------------------
-  // Phase 2: Integration tests (only if Redis is available)
+  // Phase 2: Integration tests (embedded Redis via redis-memory-server)
   // -----------------------------------------------------------------------
-  log('\n========== INTEGRATION TESTS (require Redis) ==========');
-  log(`\n  Probing Redis at ${REDIS_URL} ...`);
-
-  const redisAvailable = await probeRedis();
-
-  if (!redisAvailable) {
-    log(`  Redis is NOT available at ${REDIS_URL}`);
-    log('  Skipping integration tests.\n');
-    log('  To run integration tests, start Redis first:');
-    log('    docker compose up -d');
-    log('    # or: redis-server\n');
-
-    skip('Integration Test 1: Redis Connection');
-    skip('Integration Test 2: Add Test Job to Queue');
-    skip('Integration Test 3: Process Test Job with Worker');
-    skip('Integration Test 4: Job Retry on Failure');
-    skip('Integration Test 5: Permanent Job Failure');
-  } else {
-    log('  Redis is available! Running integration tests.\n');
+  log('\n========== INTEGRATION TESTS (embedded Redis) ==========');
+  log('\n  Starting embedded Redis via redis-memory-server...');
 
-    // Create a dedicated test connection (not the shared one)
-    const testRedis = new IORedis(REDIS_URL, {
-      maxRetriesPerRequest: null,
-      enableReadyCheck: false,
-    });
+  let redisServer: RedisMemoryServer | null = null;
+  let testRedis: IORedis | null = null;
+
+  try {
+    redisServer = new RedisMemoryServer();
+    const host = await redisServer.getHost();
+    const port = await redisServer.getPort();
+    log(`  Embedded Redis running at ${host}:${port}`);
+
+    // BullMQ requires maxRetriesPerRequest: null on all connections
+    const connOpts = { host, port, maxRetriesPerRequest: null as null };
+
+    testRedis = new IORedis(connOpts);
     testRedis.on('error', () => {}); // Suppress duplicate error logs
 
-    try {
-      await testRedisConnection(testRedis);
-      await testAddJobToQueue(testRedis);
-      await testProcessJobWithWorker(testRedis);
-      await testJobRetryOnFailure(testRedis);
-      await testPermanentJobFailure(testRedis);
-    } catch (err) {
-      originalConsoleError('\n  UNEXPECTED ERROR in integration tests:', err);
-      failed++;
-    } finally {
-      await testRedis.quit();
+    await testRedisConnection(testRedis);
+    await testAddJobToQueue(connOpts);
+    await testProcessJobWithWorker(connOpts);
+    await testJobRetryOnFailure(connOpts);
+    await testPermanentJobFailure(connOpts);
+  } catch (err) {
+    originalConsoleError('\n  UNEXPECTED ERROR in integration tests:', err);
+    failed++;
+  } finally {
+    // Cleanup embedded Redis
+    if (testRedis) {
+      await testRedis.quit().catch(() => {});
+    }
+    if (redisServer) {
+      log('\n  Stopping embedded Redis...');
+      await redisServer.stop();
+      log('  Embedded Redis stopped.');
     }
   }
 
@@ -528,10 +485,7 @@ async function runTests(): Promise<void> {
   // Results
   // -----------------------------------------------------------------------
   log('\n===========================================');
-  log(`  Results: ${passed} passed, ${failed} failed, ${skipped} skipped`);
-  if (skipped > 0) {
-    log(`  (${skipped} integration tests skipped — start Redis to run them)`);
-  }
+  log(`  Results: ${passed} passed, ${failed} failed`);
   log('===========================================');
 
   // Disconnect all shared connections created by importing the queues module
@@ -548,7 +502,6 @@ async function runTests(): Promise<void> {
 
   // Give event loop a tick to flush pending disconnects, then exit
   setTimeout(() => {
-    // Structural tests must all pass. Skipped integration tests are OK.
     process.exit(failed > 0 ? 1 : 0);
   }, 100);
 }
