diff --git a/.github/workflows/automerge-schedule.yml b/.github/workflows/automerge-schedule.yml new file mode 100644 index 0000000..a6dfdc8 --- /dev/null +++ b/.github/workflows/automerge-schedule.yml @@ -0,0 +1,40 @@ +--- +name: Schedule Release Automerge + +on: + schedule: + - cron: "0 9 * * 1" # Every Monday at 9am UTC + workflow_dispatch: + +jobs: + automerge: + if: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }} + runs-on: ubuntu-latest + steps: + - name: Enable auto-merge on release-please PR + env: + GH_TOKEN: ${{ secrets.OVERRIDE_TOKEN }} + run: | + PRS=$(gh pr list \ + --repo "${{ github.repository }}" \ + --label "autorelease: pending" \ + --base "${{ github.event.repository.default_branch }}" \ + --state open \ + --json number) + + PR_COUNT=$(printf '%s' "$PRS" | jq 'length') + + if [ "$PR_COUNT" -gt 1 ]; then + echo "Expected at most one open release-please PR, found $PR_COUNT" + exit 1 + fi + + PR=$(printf '%s' "$PRS" | jq -r '.[0].number // empty') + + if [ -n "$PR" ]; then + gh pr merge "$PR" --auto --squash --delete-branch \ + --repo "${{ github.repository }}" + echo "Auto-merge enabled on PR #$PR" + else + echo "No open release-please PR found" + fi diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 647b59b..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: CI - -on: - push: - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: 20 - cache: npm - - run: npm ci - - run: npx oxlint - - run: npm test diff --git a/.github/workflows/pr-conventional-title.yml b/.github/workflows/pr-conventional-title.yml new file mode 100644 index 0000000..25a13ef --- /dev/null +++ b/.github/workflows/pr-conventional-title.yml @@ -0,0 +1,20 @@ +--- +name: Conventional PR Title + +on: + pull_request_target: + types: + - opened + - reopened + - edited + - synchronize + +jobs: + conventional-pr-title: + runs-on: ubuntu-latest + permissions: + statuses: write + steps: + - uses: aslafy-z/conventional-pr-title-action@v3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..eb71222 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,36 @@ +--- +name: Publish packages + +on: + push: + tags: + - "v*" + workflow_dispatch: + +jobs: + publish: + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 1 + + - uses: actions/setup-node@v7 + with: + node-version: 24.x + registry-url: https://registry.npmjs.org + + - name: Install dependencies + run: npm ci + + - name: Test + run: npm test + + - name: Publish to npm + run: npm publish --provenance --access public diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..75b613c --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,20 @@ +--- +name: release-please + +on: + workflow_dispatch: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v5 + with: + token: ${{ secrets.OVERRIDE_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..5cc4418 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,61 @@ +--- +name: Test + +permissions: + contents: read + +concurrency: + group: test + cancel-in-progress: false + +on: + pull_request: + branches: + - main + push: + branches: + - main + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + defaults: + run: + shell: bash + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 1 + + - uses: actions/setup-node@v7 + with: + node-version: 24.x + + - name: Cache node modules + id: cache-npm + uses: actions/cache@v6 + env: + cache-name: cache-node-modules + with: + path: .npm + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-build-${{ env.cache-name }}- + ${{ runner.os }}-build- + ${{ runner.os }}- + + - name: Install dependencies + run: make install + + - name: Linting + run: make eslint + + - name: Test + run: make test + + - name: Validate package + run: make validate-package diff --git a/.gitignore b/.gitignore index 3af7e1f..4f1ab39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,7 @@ -scripts/try-http-login.* -node_modules/ +.claude +.npm/ +*.tgz dist/ +node_modules/ npm-debug.log* -pnpm-debug.log* -yarn-error.log* -.DS_Store -AGENTS.md -CLAUDE.md +scripts/try-http-login.* diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1b8859c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist +node_modules +package-lock.json +CHANGELOG.md +tests/fixtures diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c8ca84d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,11 @@ +{ + "singleQuote": true, + "overrides": [ + { + "files": ["*.yaml", "*.yml"], + "options": { + "singleQuote": false + } + } + ] +} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..8d7e5f1 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "1.0.1" +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d97f5d8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# AGENTS.md + +Guidance for coding agents working in this repository. + +## Commands + +The Make targets are exactly what CI runs (`.github/workflows/test.yml`), so +prefer them: + +```bash +make install # npm clean-install --prefer-offline --cache .npm +make eslint # lint +make format # prettier --write . +make test # build, then run the test suite +make validate-package # npm pack and assert the tarball contents +``` + +Running the CLI locally — the `--` separator is required to pass flags through +npm: + +```bash +npm run dev -- --help # tsx, straight from TypeScript +npm start -- --help # from dist/, needs a build first +``` + +Single test file / single test case: + +```bash +npm run build && node --test tests/query.test.js +npm run build && node --test --test-name-pattern "" tests/query.test.js +``` + +## Architecture + +ESM throughout (`"type": "module"`), TypeScript compiled to `dist/` with +`NodeNext` resolution — relative imports must carry the `.js` extension. + +`src/cli.ts` is the Commander entry point and the `bin` target. It delegates to +a command module in `src/commands/`, which calls `ensureApiKey` (reads +`src/config.ts`, falls through to `src/auth.ts` when no key is stored), then +`search()` in `src/api.ts`, then renders through `formatResultsText` or +`simplifyOffer` in `src/commands/search.ts`. `src/query.ts` is pure and +I/O-free, which is why it carries most of the test coverage. + +Results go to stdout; `--explain` and every warning go to stderr, so `--json` +output stays pipeable. + +## Constraints + +These are the things that are easy to break without noticing. + +- **Tests run against `dist/`.** `tests/*.test.js` import `../dist/*.js`, so a + stale build silently tests old code. `npm test` runs `npm run build` first for + exactly this reason — never invoke bare `node --test`. +- **release-please owns the version.** `src/cli.ts` carries + `.version('0.1.0') // x-release-please-version`; the trailing comment is + load-bearing and matches `extra-files` in `release-please-config.json`. Never + hand-edit the version in `package.json`, `src/cli.ts`, or + `.release-please-manifest.json`. +- **Country validation is a security guard.** The country code is interpolated + into a hostname, so `getApiBase()` and `extractApiKey()` hard-validate against + `VALID_COUNTRIES` before building a URL. Do not relax this. Only `at` and `de` + exist. +- **API keys are country-scoped.** `set-country` deliberately clears `apiKey` + when the country actually changes, and `search` auto-logs-in using the + configured country. +- **`login` is scraping, not an account login.** It fetches the public site, + regex-scans the HTML and boot scripts for an embedded key, and brute-force + validates candidates against the live search endpoint. It makes real network + requests, cannot be unit-tested offline, and breaks whenever Marktguru ships a + new frontend bundle. `tests/live.test.js` runs the built CLI end to end + against the live sites for both countries (isolated `HOME`), so `make test` + needs network access and fails when the scraper breaks. +- **The key is stored in plaintext** at `~/.marktguru/config.json`. The `config` + and `login` output truncates it — preserve that. +- **`--retailer` filters client-side** on `advertisers[].name`, which is why + `runSearch` over-fetches 100 results and rewrites `totalResults` to the + post-filter count. +- **Two different default limits:** 10 in `src/commands/search.ts`, 20 in the + `search()` fallback in `src/api.ts`. +- **ESLint covers only `src/` TypeScript** via the `tsconfig-lint.json` project. + The JS tests are not linted, and a new `.ts` file outside `src/` makes + type-aware linting fail. +- **Exact dependency versions only** — no `^` or `~`. +- **`make validate-package` asserts a hardcoded file list.** Extend it when a + new shipped entrypoint is added. It currently omits `dist/commands/*.js` even + though those ship. + +## Release flow + +PRs are squash-merged, so the **PR title must be a Conventional Commit** — it +becomes the changelog entry, and `pr-conventional-title.yml` enforces it. +Renovate forces `chore:` for dependency bumps and does not automerge majors. +release-please opens a release PR on every push to `main`; a Monday 09:00 UTC +cron automerges the single PR labeled `autorelease: pending`, so releases go out +weekly rather than per merge. The resulting `v*` tag triggers +`npm publish --provenance` via npm trusted publishing (no `NPM_TOKEN`). + +## Keep in sync + +`skills/marktguru-grocery-deals/SKILL.md` documents _using_ the CLI for agents +that consume it; `README.md` documents the same surface for humans. Changing a +command or flag means updating both. The directory name has to match the +skill's frontmatter `name`, and the `skills//SKILL.md` layout is what the +`skills` CLI discovers and installs. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3979347 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Changelog + +## [1.0.1](https://github.com/udondan/marktguru-cli/compare/v1.0.0...v1.0.1) (2026-09-18) + + +### Miscellaneous Chores + +* drop header-generator dependency ([fdb70d0](https://github.com/udondan/marktguru-cli/commit/fdb70d072693d2ac99c33ccc54060a61c13d40a1)) + +## [1.0.0](https://github.com/udondan/marktguru-cli/compare/v0.1.0...v1.0.0) (2026-09-17) + + +### Features + +* add country selection for AT/DE support ([6f65c21](https://github.com/udondan/marktguru-cli/commit/6f65c216014e935f7cbfe168e2e4deff1326e6ab)) +* add country selection for AT/DE support ([75c8873](https://github.com/udondan/marktguru-cli/commit/75c8873a661381d11c225400a41ad594c3af90c9)) +* add tests for getApiBase ([f64c91c](https://github.com/udondan/marktguru-cli/commit/f64c91cc0106f9e4aaebe607d11a19b9143014a9)) + + +### Bug Fixes + +* clear API key when country changes in set-country ([8277f3f](https://github.com/udondan/marktguru-cli/commit/8277f3f28f0177b2617564b1e3d035496eef3d5d)) +* keep --version in sync with the released version ([#13](https://github.com/udondan/marktguru-cli/issues/13)) ([95d2a73](https://github.com/udondan/marktguru-cli/commit/95d2a7373f5994fd92d6bad5e14eb89fd8eb1066)) +* only report apiKeyCleared:true in JSON when a key was actually cleared ([beb63ac](https://github.com/udondan/marktguru-cli/commit/beb63ac1584e3b28bc05936c1969b7d8c7c6ca59)) +* pass country to extractApiKey during auto-login in search ([d52f2cf](https://github.com/udondan/marktguru-cli/commit/d52f2cf28059922826958fedd8ea0e23c16db73f)) +* validate country code before interpolating into URLs ([4ac9f76](https://github.com/udondan/marktguru-cli/commit/4ac9f76a7f4fcad0a5f9da829ffdfd5c856a1ca1)) + + +### Miscellaneous Chores + +* set up release-please, npm trusted publishing and publish as @udondan/marktguru-cli ([#2](https://github.com/udondan/marktguru-cli/issues/2)) ([4cf577e](https://github.com/udondan/marktguru-cli/commit/4cf577e30fe57269e9a344ad3483078424d884d3)) diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/LICENSE b/LICENSE index 1d49230..c0acadc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2026 Manuel Maly +Copyright (c) 2026 Daniel Schroeder Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f9cfb98 --- /dev/null +++ b/Makefile @@ -0,0 +1,59 @@ +SHELL := /bin/bash -euo pipefail + +NO_COLOR=\x1b[0m +TARGET_COLOR=\x1b[96m + +build: + @echo -e "$(TARGET_COLOR)Running build$(NO_COLOR)" + @npm run build + +clean: + @echo -e "$(TARGET_COLOR)Running clean$(NO_COLOR)" + @rm -rf node_modules package-lock.json dist + +install: + @echo -e "$(TARGET_COLOR)Running install$(NO_COLOR)" + @npm clean-install --prefer-offline --cache .npm + +test: + @echo -e "$(TARGET_COLOR)Running tests$(NO_COLOR)" + @npm test + +eslint: + @echo -e "$(TARGET_COLOR)Running eslint $$(npx eslint --version)$(NO_COLOR)" + @npx eslint .; \ + echo "Passed" + +format: + @echo -e "$(TARGET_COLOR)Running prettier$(NO_COLOR)" + @npx prettier --write . + +validate-package: + @echo -e "$(TARGET_COLOR)Checking package content$(NO_COLOR)" + @\ + if ! TARBALL=$$(npm pack --quiet) || [ -z "$$TARBALL" ]; then \ + echo "❌ npm pack failed"; \ + exit 1; \ + fi; \ + TARBALL=$$(printf '%s\n' "$$TARBALL" | tail -n 1); \ + if [ ! -f "$$TARBALL" ]; then \ + echo "❌ npm pack package file not found: $$TARBALL"; \ + exit 1; \ + fi; \ + trap 'rm -f "$$TARBALL"' EXIT; \ + if ! tar -tf "$$TARBALL" >/dev/null; then \ + echo "❌ Failed to list tarball contents"; \ + exit 1; \ + fi; \ + FILES_TO_CHECK="dist/cli.js dist/api.js dist/auth.js dist/config.js dist/query.js LICENSE README.md"; \ + MISSING_FILES=""; \ + for file in $$FILES_TO_CHECK; do \ + if ! tar -tf "$$TARBALL" "package/$$file" >/dev/null 2>&1; then \ + MISSING_FILES="$$MISSING_FILES $$file"; \ + fi; \ + done; \ + if [ -n "$$MISSING_FILES" ]; then \ + echo "❌ The following files are NOT included in the package:$$MISSING_FILES"; \ + exit 1; \ + fi; \ + echo "✅ Package content looks good" diff --git a/README.md b/README.md index 0a68fe7..84ccd18 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,111 @@ # marktguru-cli 🧘‍♂️ -[![CI](https://github.com/manmal/marktguru-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/manmal/marktguru-cli/actions/workflows/ci.yml) -[![npm](https://img.shields.io/npm/v/marktguru-cli.svg)](https://www.npmjs.com/package/marktguru-cli) -[![license](https://img.shields.io/github/license/manmal/marktguru-cli.svg)](https://github.com/manmal/marktguru-cli/blob/main/LICENSE) -CLI for Austrian Marktguru supermarket deals. +[![Test](https://github.com/udondan/marktguru-cli/actions/workflows/test.yml/badge.svg)](https://github.com/udondan/marktguru-cli/actions/workflows/test.yml) +[![npm](https://img.shields.io/npm/v/@udondan/marktguru-cli.svg)](https://www.npmjs.com/package/@udondan/marktguru-cli) +[![license](https://img.shields.io/github/license/udondan/marktguru-cli.svg)](https://github.com/udondan/marktguru-cli/blob/main/LICENSE) + +CLI for Marktguru supermarket deals in Austria and Germany. + +This is a maintained fork of [manmal/marktguru-cli](https://github.com/manmal/marktguru-cli), published as [`@udondan/marktguru-cli`](https://www.npmjs.com/package/@udondan/marktguru-cli). ## AI Agent Skill -See [SKILL.md](SKILL.md) for a comprehensive reference designed for AI coding agents. + +Install into your coding agent with the [`skills`](https://skills.sh) CLI: + +```bash +npx skills add udondan/marktguru-cli +``` + +Or read [skills/marktguru-grocery-deals/SKILL.md](skills/marktguru-grocery-deals/SKILL.md) directly — a comprehensive reference designed for AI coding agents. ## Quick Start (Recommended) + Use `npx` to run without installing anything: + ```bash -npx --yes marktguru-cli login -npx --yes marktguru-cli search raw "milch OR soja" -npx --yes marktguru-cli search build --term milch --or soja +npx --yes @udondan/marktguru-cli login +npx --yes @udondan/marktguru-cli search raw "milch OR soja" +npx --yes @udondan/marktguru-cli search build --term milch --or soja ``` ## Requirements + - Node.js 18+ (built-in `fetch`) - Works with `npm`, `pnpm`, and `bun` ## Commands + Login (extracts API key via HTTP by scanning the site’s JS): + ```bash marktguru login ``` Search (raw query string syntax): + ```bash marktguru search raw "kellys OR \"erdnuss snips\"" ``` Search (structured builder): + ```bash marktguru search build --term kellys --phrase "erdnuss snips" --or manner --explain ``` Show supported query syntax: + ```bash marktguru search syntax ``` Set a default ZIP code: + ```bash marktguru set-zip 1010 ``` +Set a default country (`at` or `de`, default: `at`): + +```bash +marktguru set-country de +``` + Show config: + ```bash marktguru config ``` ## Also Working (Install Locally) + Install and run from source: + ```bash -pnpm install -pnpm run build -pnpm run start -- --help +npm ci +npm run build +npm start -- --help ``` Dev mode (TS directly): + +```bash +npm run dev -- --help +``` + +Lint, format and test (same targets CI runs): + ```bash -pnpm run dev -- --help +make eslint +make format +make test +make validate-package ``` ## Search Options + Available for both `search raw` and `search build`: + - `-z, --zip `: ZIP code for location-based results - `-n, --limit `: Number of results (default: 10) - `-r, --retailer `: Filter by retailer (client-side) @@ -73,7 +113,10 @@ Available for both `search raw` and `search build`: If no API key is configured, `search` will automatically run `login` to extract one. +Note: API keys are country-specific. After switching country with `set-country`, run `login` again to fetch the matching key. + Builder-only: + - `--term `: Add a term (repeatable) - `--phrase `: Add an exact phrase (repeatable) - `--wildcard `: Add a wildcard term like `kell*` (repeatable) @@ -82,21 +125,26 @@ Builder-only: - `--explain`: Print the built query to stderr ## Query Syntax (Observed) + The API appears to accept a Lucene/Elasticsearch-style query string, not SQL. Supported: + - `OR` for boolean OR - `*` wildcard (e.g., `kell*`) - `"..."` exact phrase - `( ... )` grouping Not supported (observed): + - `AND`, `NOT`, `~`, `^` ## Notes on `login` + - Uses HTTP requests (no browser automation). - Scans entry HTML and boot scripts for embedded API keys and validates them. - May break if the website changes. ## Config Location + - `~/.marktguru/config.json` diff --git a/SKILL.md b/SKILL.md deleted file mode 100644 index 36af9a6..0000000 --- a/SKILL.md +++ /dev/null @@ -1,235 +0,0 @@ ---- -name: marktguru-grocery-deals -description: Look up grocery deals and offers via Marktguru CLI/API. Use when user asks about supermarket discounts, product prices, current promotions, or comparing deals across Austrian retailers (Hofer, Billa, Spar, Lidl, etc.). ---- - -# Marktguru Grocery Deals - -Query Austrian grocery deals from Marktguru. Supports raw queries, structured search building, retailer filtering, and ZIP-code location targeting. - -## Quick Reference - -| Command | Purpose | -|---------|---------| -| `search raw ` | Search with raw query string | -| `search build` | Build query from structured flags | -| `search syntax` | Show supported query syntax | -| `set-zip ` | Set default ZIP code | -| `config` | Show current configuration | -| `login` | Extract API key from marktguru.at | - ---- - -## Setup - -### Login (HTTP scan) -```bash -npx marktguru-cli login -``` -Scans site HTML and boot scripts for embedded API keys. No browser automation required. - -### Set Default ZIP Code -```bash -npx marktguru-cli set-zip 1010 -npx marktguru-cli set-zip 8010 # Graz -``` - -### Check Config -```bash -npx marktguru-cli config -npx marktguru-cli config --json -``` - ---- - -## Search Commands - -### Raw Query Search - -```bash -npx marktguru-cli search raw "Milch" -npx marktguru-cli search raw "Milch" --limit 5 -npx marktguru-cli search raw "Bier" --retailer HOFER -npx marktguru-cli search raw "Brot" --zip 8010 -npx marktguru-cli search raw "Cola" --json -``` - -### Common Options - -| Flag | Description | Default | -|------|-------------|---------| -| `--limit ` / `-n` | Number of results | 10 | -| `--retailer ` / `-r` | Filter by retailer (e.g., SPAR, BILLA, HOFER) | all | -| `--zip ` / `-z` | ZIP code for location-based results | config default | -| `--json` / `-j` | Output JSON | false | - -### Structured Builder - -Build queries from flags instead of raw strings: - -```bash -npx marktguru-cli search build --term butter --explain -npx marktguru-cli search build --or butter --or margarine --explain -npx marktguru-cli search build --phrase "frische milch" --limit 5 -npx marktguru-cli search build --wildcard "jogh*" --retailer SPAR -``` - -| Flag | Description | -|------|-------------| -| `--term ` | Add a search term | -| `--phrase ` | Add exact phrase (quoted) | -| `--wildcard ` | Add wildcard term (e.g., `kell*`) | -| `--or ` | Add term to OR group (repeat for multiple) | -| `--group ` | Add raw parenthesized group | -| `--explain` | Print the built query to stderr | - ---- - -## Query Syntax - -**Supported:** -- `OR` — boolean OR: `Milch OR Sahne` -- `*` — wildcard: `Jogh*` (matches Joghurt, Joghurtdrink, etc.) -- `"..."` — exact phrase: `"frische Milch"` -- `()` — grouping: `(Milch OR Sahne) Bio` - -**NOT supported:** `AND`, `NOT`, `~`, `^` - -### Examples - -```bash -# Simple term -npx marktguru-cli search raw "Butter" - -# OR logic -npx marktguru-cli search raw "Käse OR Schinken" - -# Wildcard -npx marktguru-cli search raw "Bio*" - -# Combined with retailer filter -npx marktguru-cli search raw "Bier" --retailer HOFER --limit 10 - -# Exact phrase -npx marktguru-cli search raw '"Coca Cola"' -``` - ---- - -## Known Retailers - -| Retailer | Notes | -|----------|-------| -| SPAR | | -| INTERSPAR | Larger SPAR format | -| SPAR-Gourmet | Premium SPAR | -| BILLA | | -| BILLA PLUS | Larger BILLA format | -| HOFER | Austrian Aldi | -| Lidl | | -| PENNY | | -| dm drogerie markt | Drugstore (some food items) | -| BIPA | Drugstore | - ---- - -## JSON Output - -```bash -npx marktguru-cli search raw "Cola" --limit 3 --json -``` - -```json -{ - "query": "Cola", - "total": 23, - "offers": [ - { - "title": "Coca-Cola - Cola - oder Fanta 1,5l", - "price": 1.49, - "retailer": "Sizin Foods GmbH", - "expires": "2026-02-11", - "discountPercent": null - }, - { - "title": "Coca-Cola - Cola - Zero / Fanta / Sprite Dose 330ml", - "price": 0.6, - "retailer": "Sizin Foods GmbH", - "expires": "2026-02-11", - "discountPercent": null - }, - { - "title": "Coca-Cola - Cola - div. Sorten 0,33 Liter", - "price": 0.67, - "retailer": "BILLA", - "expires": "2026-02-11", - "discountPercent": 50, - "externalUrl": "https://shop.billa.at/produkte/..." - } - ] -} -``` - -| Field | Description | -|-------|-------------| -| `title` | Product name and brand | -| `price` | Current offer price (EUR) | -| `retailer` | Store name | -| `expires` | Offer expiration date (YYYY-MM-DD) | -| `discountPercent` | Discount percentage (null if not on sale) | -| `externalUrl` | Direct link to retailer (optional) | - ---- - -## Human-Readable Output - -``` -Found 147 offers for "Milch": - -Premium Bergbauern H-Milch [Salzburg Milch] - 💰 €0.99 (was €1.59) -38% · €0.99/l - 📦 3,5% Fett oder 0,5% Fett aus Österreich, 1 Liter - 🏪 SPAR · 20 days left - -📍 Retailers: Lidl (33), SPAR (30), PENNY (17), INTERSPAR (16), BILLA PLUS (11) -``` - ---- - -## Config - -Credentials and settings stored at `~/.marktguru/config.json`. - -```bash -npx marktguru-cli config --json -``` - -```json -{ - "apiKey": "pCcm1AVCYa...", - "apiKeySet": true, - "zipCode": "1010", - "configPath": "/Users/.../.marktguru/config.json" -} -``` - ---- - -## Troubleshooting - -| Issue | Solution | -|-------|----------| -| Login fails | Site structure may have changed. Re-run `login` or check for CLI updates. | -| No results | Try broader terms, wildcards (`*`), or alternative spellings. | -| Wrong location | Set ZIP code with `set-zip` or use `--zip` flag. | -| API key expired | Re-run `npx marktguru-cli login` to refresh. | - ---- - -## Usage Tips - -1. **Compare prices:** Use `--json` output to programmatically compare across retailers -2. **Find best deals:** Look for high `discountPercent` values -3. **Check availability:** Use `--zip` with local ZIP code for accurate results -4. **Wildcards for variants:** Use `Jogh*` to catch Joghurt, Joghurtdrink, etc. -5. **OR for alternatives:** `Butter OR Margarine` to compare substitutes diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..e110192 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,65 @@ +import tseslint from 'typescript-eslint'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; + +export default tseslint.config( + { + ignores: ['**/*.js', '**/*.d.ts', 'dist/**', 'node_modules/**'], + }, + { + files: ['**/*.ts'], + extends: [ + ...tseslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + eslintPluginPrettierRecommended, + ], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: './tsconfig-lint.json', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-deprecated': 'error', + 'prefer-template': 'error', + '@typescript-eslint/naming-convention': [ + 'error', + { selector: 'default', format: ['camelCase'] }, + { + selector: 'variable', + // PascalCase for classes pulled out of a dynamic import + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allow', + }, + { + selector: 'parameter', + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + { selector: 'typeLike', format: ['PascalCase'] }, + { selector: 'typeProperty', format: ['camelCase'] }, + // HTTP header names are not camelCase + { + selector: 'objectLiteralProperty', + format: null, + modifiers: ['requiresQuotes'], + }, + ], + // `||` is intentional where an empty string or 0 should fall back + '@typescript-eslint/prefer-nullish-coalescing': [ + 'error', + { ignorePrimitives: { string: true, number: true } }, + ], + }, + }, +); diff --git a/package-lock.json b/package-lock.json index 35552a4..b77bb0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,29 +1,58 @@ { - "name": "marktguru-cli", - "version": "0.1.0", + "name": "@udondan/marktguru-cli", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "marktguru-cli", - "version": "0.1.0", + "name": "@udondan/marktguru-cli", + "version": "1.0.1", + "license": "MIT", "dependencies": { - "commander": "^12.0.0", - "header-generator": "^2.1.63" + "commander": "15.0.0" }, "bin": { "marktguru": "dist/cli.js" }, "devDependencies": { - "@types/node": "^20.0.0", - "tsx": "^4.0.0", - "typescript": "^5.0.0" + "@types/node": "24.13.6", + "eslint": "10.11.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "prettier": "3.9.8", + "tsx": "4.23.15", + "typescript": "5.9.3", + "typescript-eslint": "8.70.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -38,9 +67,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -55,9 +84,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -72,9 +101,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -89,9 +118,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -106,9 +135,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -123,9 +152,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -140,9 +169,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -157,9 +186,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -174,9 +203,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -191,9 +220,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -208,9 +237,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -225,9 +254,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -242,9 +271,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -259,9 +288,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -276,9 +305,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -293,9 +322,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -310,9 +339,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -327,9 +356,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -344,9 +373,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -361,9 +390,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -378,9 +407,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -395,9 +424,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -412,9 +441,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -429,9 +458,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -446,9 +475,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -462,384 +491,1493 @@ "node": ">=18" } }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": ">=10" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@types/node": { - "version": "20.19.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz", - "integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, "license": "MIT", "engines": { - "node": ">=12.0" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "@eslint/core": "^1.2.1" }, - "bin": { - "browserslist": "cli.js" + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=6" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } }, - "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", - "license": "MIT", + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { - "node": ">=18" + "node": ">=18.18.0" } }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "license": "MIT", + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", "dependencies": { - "is-obj": "^2.0.0" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": ">=10" + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", - "license": "ISC" + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, - "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" }, "engines": { - "node": ">=18" + "node": ">= 18" }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" + "peerDependencies": { + "keyv": "^5.6.0" } }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pkgr/core": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.3.6.tgz", + "integrity": "sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==", "dev": true, - "hasInstallScript": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" } }, - "node_modules/generative-bayesian-network": { - "version": "2.1.80", - "resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.80.tgz", - "integrity": "sha512-LyCc23TIFvZDkUJclZ3ixCZvd+dhktr9Aug1EKz5VrfJ2eA5J2HrprSwWRna3VObU2Wy8quXMUF8j2em0bJSLw==", - "license": "Apache-2.0", + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.6.tgz", + "integrity": "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==", + "dev": true, + "license": "MIT", "dependencies": { - "adm-zip": "^0.5.9", - "tslib": "^2.4.0" + "undici-types": "~7.18.0" } }, - "node_modules/get-tsconfig": { - "version": "4.13.6", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", - "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.70.0.tgz", + "integrity": "sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/type-utils": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.70.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/header-generator": { - "version": "2.1.80", - "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.80.tgz", - "integrity": "sha512-7gvv2Xm6Q0gNN3BzMD/D3sGvSJRcV1+k8XehPmBYTpTkBmKshwnYyi0jJJnpP3S6YP7vdOoEobeBV87aG9YTtQ==", - "license": "Apache-2.0", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.9.tgz", + "integrity": "sha512-brTTsvFRt5C1gGHtPst/281UjPD5t9fBqbgoMPlVWy11ZLTPfu7HxK4ZYqO9H7o/yC9rSTCI85EaQ4OoY12qYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.70.0.tgz", + "integrity": "sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==", + "dev": true, + "license": "MIT", "dependencies": { - "browserslist": "^4.21.1", - "generative-bayesian-network": "^2.1.80", - "ow": "^0.28.1", - "tslib": "^2.4.0" + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3" }, "engines": { - "node": ">=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.70.0.tgz", + "integrity": "sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==", + "dev": true, "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.70.0", + "@typescript-eslint/types": "^8.70.0", + "debug": "^4.4.3" + }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", - "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", - "license": "MIT" - }, - "node_modules/ow": { - "version": "0.28.2", - "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz", - "integrity": "sha512-dD4UpyBh/9m4X2NVjA+73/ZPBRF+uF4zIMFvvQsabMiEK8x41L3rQ8EENOi35kyyoaJwNxEeJcP6Fj1H4U409Q==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.70.0.tgz", + "integrity": "sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==", + "dev": true, "license": "MIT", "dependencies": { - "@sindresorhus/is": "^4.2.0", - "callsites": "^3.1.0", - "dot-prop": "^6.0.1", - "lodash.isequal": "^4.5.0", - "vali-date": "^1.0.0" + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" }, "engines": { - "node": ">=12" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.70.0.tgz", + "integrity": "sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==", "dev": true, "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.70.0.tgz", + "integrity": "sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" }, - "bin": { - "tsx": "dist/cli.mjs" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.70.0.tgz", + "integrity": "sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "optionalDependencies": { - "fsevents": "~2.3.3" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.70.0.tgz", + "integrity": "sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==", "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.70.0", + "@typescript-eslint/tsconfig-utils": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" }, "engines": { - "node": ">=14.17" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "node_modules/@typescript-eslint/utils": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.70.0.tgz", + "integrity": "sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.70.0", + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.70.0.tgz", + "integrity": "sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==", + "dev": true, "license": "MIT", "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" + "@typescript-eslint/types": "8.70.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", "bin": { - "update-browserslist-db": "cli.js" + "acorn": "bin/acorn" }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", "peerDependencies": { - "browserslist": ">= 4.21.0" + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/vali-date": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", - "integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==", + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, "license": "MIT", - "engines": { + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/commander": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", + "license": "MIT", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", + "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.6.tgz", + "integrity": "sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.13" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.8.tgz", + "integrity": "sha512-WRFq3Wn3WId7LLROfMLdH7xaFr2jR62wU8nLO6rQUOLOxNZUviyJQs1M0iIhLexSFy+L+w0ch66wtoO2jRjG0A==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.13", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.13.tgz", + "integrity": "sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.3.6" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.70.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.70.0.tgz", + "integrity": "sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.70.0", + "@typescript-eslint/parser": "8.70.0", + "@typescript-eslint/typescript-estree": "8.70.0", + "@typescript-eslint/utils": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { "node": ">=0.10.0" } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 4c55f5e..5cc1428 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,32 @@ { - "name": "marktguru-cli", - "version": "0.1.0", - "description": "CLI for Austrian Marktguru supermarket deals", + "name": "@udondan/marktguru-cli", + "version": "1.0.1", + "description": "CLI for Marktguru supermarket deals in Austria and Germany", + "license": "MIT", + "author": { + "name": "Daniel Schroeder", + "url": "https://www.udondan.com/" + }, + "contributors": [ + "Manuel Maly" + ], + "homepage": "https://github.com/udondan/marktguru-cli", + "repository": { + "type": "git", + "url": "https://github.com/udondan/marktguru-cli.git" + }, + "bugs": { + "url": "https://github.com/udondan/marktguru-cli/issues" + }, + "keywords": [ + "cli", + "marktguru", + "grocery", + "deals", + "discounts", + "austria", + "germany" + ], "type": "module", "bin": { "marktguru": "dist/cli.js" @@ -11,6 +36,8 @@ "dev": "tsx src/cli.ts", "start": "node dist/cli.js", "test": "npm run build && node --test tests/*.test.js", + "lint": "eslint .", + "format": "prettier --write .", "prepublishOnly": "npm run build" }, "files": [ @@ -18,13 +45,20 @@ "README.md", "LICENSE" ], + "publishConfig": { + "access": "public" + }, "dependencies": { - "commander": "^12.0.0", - "header-generator": "^2.1.63" + "commander": "15.0.0" }, "devDependencies": { - "@types/node": "^20.0.0", - "tsx": "^4.0.0", - "typescript": "^5.0.0" + "@types/node": "24.13.6", + "eslint": "10.11.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "prettier": "3.9.8", + "tsx": "4.23.15", + "typescript": "5.9.3", + "typescript-eslint": "8.70.0" } } diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..b82ce52 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "node", + "changelog-path": "CHANGELOG.md", + "include-component-in-tag": false, + "bump-minor-pre-major": false, + "bump-patch-for-minor-pre-major": false, + "draft": false, + "prerelease": false, + "bootstrap-sha": "9855964f564947c4c8532bdc1523f420780591b3", + "packages": { + ".": { + "extra-files": ["src/cli.ts"] + } + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..82ff54a --- /dev/null +++ b/renovate.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "semanticCommitType": "chore", + "mode": "auto", + "prHourlyLimit": 1, + "rebaseWhen": "behind-base-branch", + "minimumReleaseAge": "3 days", + "automerge": true, + "automergeType": "pr", + "automergeStrategy": "squash", + "packageRules": [ + { + "matchPackageNames": ["/.*/"], + "semanticCommitType": "chore" + }, + { + "matchUpdateTypes": ["major"], + "automerge": false + } + ] +} diff --git a/skills/marktguru-grocery-deals/SKILL.md b/skills/marktguru-grocery-deals/SKILL.md new file mode 100644 index 0000000..7edd2f0 --- /dev/null +++ b/skills/marktguru-grocery-deals/SKILL.md @@ -0,0 +1,257 @@ +--- +name: marktguru-grocery-deals +description: Look up grocery deals and offers via Marktguru CLI/API. Use when user asks about supermarket discounts, product prices, current promotions, or comparing deals across Austrian or German retailers (Hofer, Billa, Spar, Lidl, Penny, REWE, Kaufland, etc.). +--- + +# Marktguru Grocery Deals + +Query grocery deals from Marktguru in Austria and Germany. Supports raw queries, structured search building, retailer filtering, ZIP-code location targeting, and country selection (AT/DE). + +## Quick Reference + +| Command | Purpose | +| -------------------- | ------------------------------------------------- | +| `search raw ` | Search with raw query string | +| `search build` | Build query from structured flags | +| `search syntax` | Show supported query syntax | +| `set-zip ` | Set default ZIP code | +| `set-country ` | Set default country (`at` or `de`, default: `at`) | +| `config` | Show current configuration | +| `login` | Extract API key from marktguru.at/de | + +--- + +## Setup + +### Login (HTTP scan) + +```bash +npx @udondan/marktguru-cli login +``` + +Scans site HTML and boot scripts for embedded API keys. No browser automation required. + +### Set Default ZIP Code + +```bash +npx @udondan/marktguru-cli set-zip 1010 +npx @udondan/marktguru-cli set-zip 8010 # Graz +npx @udondan/marktguru-cli set-zip 10115 # Berlin (DE) +``` + +### Set Default Country + +```bash +npx @udondan/marktguru-cli set-country at # Austria (default) +npx @udondan/marktguru-cli set-country de # Germany +``` + +After switching country, re-run `login` — API keys are country-specific. + +### Check Config + +```bash +npx @udondan/marktguru-cli config +npx @udondan/marktguru-cli config --json +``` + +--- + +## Search Commands + +### Raw Query Search + +```bash +npx @udondan/marktguru-cli search raw "Milch" +npx @udondan/marktguru-cli search raw "Milch" --limit 5 +npx @udondan/marktguru-cli search raw "Bier" --retailer HOFER +npx @udondan/marktguru-cli search raw "Brot" --zip 8010 +npx @udondan/marktguru-cli search raw "Cola" --json +``` + +### Common Options + +| Flag | Description | Default | +| -------------------------- | --------------------------------------------- | -------------- | +| `--limit ` / `-n` | Number of results | 10 | +| `--retailer ` / `-r` | Filter by retailer (e.g., SPAR, BILLA, HOFER) | all | +| `--zip ` / `-z` | ZIP code for location-based results | config default | +| `--json` / `-j` | Output JSON | false | + +### Structured Builder + +Build queries from flags instead of raw strings: + +```bash +npx @udondan/marktguru-cli search build --term butter --explain +npx @udondan/marktguru-cli search build --or butter --or margarine --explain +npx @udondan/marktguru-cli search build --phrase "frische milch" --limit 5 +npx @udondan/marktguru-cli search build --wildcard "jogh*" --retailer SPAR +``` + +| Flag | Description | +| -------------------- | ------------------------------------------ | +| `--term ` | Add a search term | +| `--phrase ` | Add exact phrase (quoted) | +| `--wildcard ` | Add wildcard term (e.g., `kell*`) | +| `--or ` | Add term to OR group (repeat for multiple) | +| `--group ` | Add raw parenthesized group | +| `--explain` | Print the built query to stderr | + +--- + +## Query Syntax + +**Supported:** + +- `OR` — boolean OR: `Milch OR Sahne` +- `*` — wildcard: `Jogh*` (matches Joghurt, Joghurtdrink, etc.) +- `"..."` — exact phrase: `"frische Milch"` +- `()` — grouping: `(Milch OR Sahne) Bio` + +**NOT supported:** `AND`, `NOT`, `~`, `^` + +### Examples + +```bash +# Simple term +npx @udondan/marktguru-cli search raw "Butter" + +# OR logic +npx @udondan/marktguru-cli search raw "Käse OR Schinken" + +# Wildcard +npx @udondan/marktguru-cli search raw "Bio*" + +# Combined with retailer filter +npx @udondan/marktguru-cli search raw "Bier" --retailer HOFER --limit 10 + +# Exact phrase +npx @udondan/marktguru-cli search raw '"Coca Cola"' +``` + +--- + +## Known Retailers + +| Retailer | AT | DE | Notes | +| --------------------- | --- | --- | --------------------------- | +| Lidl | ✓ | ✓ | | +| PENNY | ✓ | ✓ | | +| dm drogerie markt | ✓ | ✓ | Drugstore (some food items) | +| SPAR | ✓ | | | +| INTERSPAR | ✓ | | Larger SPAR format | +| SPAR-Gourmet | ✓ | | Premium SPAR | +| BILLA | ✓ | | | +| BILLA PLUS | ✓ | | Larger BILLA format | +| HOFER | ✓ | | Austrian Aldi | +| BIPA | ✓ | | Drugstore | +| Kaufland | | ✓ | | +| REWE | | ✓ | | +| Netto Marken-Discount | | ✓ | | +| ALDI | | ✓ | | + +--- + +## JSON Output + +```bash +npx @udondan/marktguru-cli search raw "Cola" --limit 3 --json +``` + +```json +{ + "query": "Cola", + "total": 23, + "offers": [ + { + "title": "Coca-Cola - Cola - oder Fanta 1,5l", + "price": 1.49, + "retailer": "Sizin Foods GmbH", + "expires": "2026-02-11", + "discountPercent": null + }, + { + "title": "Coca-Cola - Cola - Zero / Fanta / Sprite Dose 330ml", + "price": 0.6, + "retailer": "Sizin Foods GmbH", + "expires": "2026-02-11", + "discountPercent": null + }, + { + "title": "Coca-Cola - Cola - div. Sorten 0,33 Liter", + "price": 0.67, + "retailer": "BILLA", + "expires": "2026-02-11", + "discountPercent": 50, + "externalUrl": "https://shop.billa.at/produkte/..." + } + ] +} +``` + +| Field | Description | +| ----------------- | ----------------------------------------- | +| `title` | Product name and brand | +| `price` | Current offer price (EUR) | +| `retailer` | Store name | +| `expires` | Offer expiration date (YYYY-MM-DD) | +| `discountPercent` | Discount percentage (null if not on sale) | +| `externalUrl` | Direct link to retailer (optional) | + +--- + +## Human-Readable Output + +``` +Found 147 offers for "Milch": + +Premium Bergbauern H-Milch [Salzburg Milch] + 💰 €0.99 (was €1.59) -38% · €0.99/l + 📦 3,5% Fett oder 0,5% Fett aus Österreich, 1 Liter + 🏪 SPAR · 20 days left + +📍 Retailers: Lidl (33), SPAR (30), PENNY (17), INTERSPAR (16), BILLA PLUS (11) +``` + +--- + +## Config + +Credentials and settings stored at `~/.marktguru/config.json`. + +```bash +npx @udondan/marktguru-cli config --json +``` + +```json +{ + "apiKey": "pCcm1AVCYa...", + "apiKeySet": true, + "zipCode": "1010", + "country": "at", + "configPath": "/Users/.../.marktguru/config.json" +} +``` + +--- + +## Troubleshooting + +| Issue | Solution | +| --------------------- | ------------------------------------------------------------------------------- | +| Login fails | Site structure may have changed. Re-run `login` or check for CLI updates. | +| No results | Try broader terms, wildcards (`*`), or alternative spellings. | +| Wrong location | Set ZIP code with `set-zip` or use `--zip` flag. | +| API key expired | Re-run `npx @udondan/marktguru-cli login` to refresh. | +| Wrong country results | Run `set-country de` (or `at`), then `login` again — keys are country-specific. | + +--- + +## Usage Tips + +1. **Compare prices:** Use `--json` output to programmatically compare across retailers +2. **Find best deals:** Look for high `discountPercent` values +3. **Check availability:** Use `--zip` with local ZIP code for accurate results +4. **Wildcards for variants:** Use `Jogh*` to catch Joghurt, Joghurtdrink, etc. +5. **OR for alternatives:** `Butter OR Margarine` to compare substitutes diff --git a/src/api.ts b/src/api.ts index 2836205..682c8fb 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,6 +1,18 @@ -import { getConfig, DEFAULT_ZIP_CODE } from "./config.js"; +import { + getConfig, + DEFAULT_ZIP_CODE, + DEFAULT_COUNTRY, + VALID_COUNTRIES, +} from './config.js'; -const API_BASE = "https://api.marktguru.at/api/v1"; +export function getApiBase(country: string): string { + if (!(VALID_COUNTRIES as readonly string[]).includes(country)) { + throw new Error( + `Unsupported country "${country}". Valid options: ${VALID_COUNTRIES.join(', ')}`, + ); + } + return `https://api.marktguru.${country}/api/v1`; +} export interface Offer { id: number; @@ -15,13 +27,13 @@ export interface Offer { brand: { name: string; } | null; - advertisers: Array<{ + advertisers: { name: string; - }>; - validityDates: Array<{ + }[]; + validityDates: { from: string; to: string; - }>; + }[]; referencePrice: number; unit: { shortName: string; @@ -34,15 +46,16 @@ export interface SearchResult { totalResults: number; results: Offer[]; filters: { - retailers: Array<{ id: number; name: string; resultsCount: number }>; - brands: Array<{ id: number; name: string; resultsCount: number }>; - categories: Array<{ id: number; name: string; resultsCount: number }>; + retailers: { id: number; name: string; resultsCount: number }[]; + brands: { id: number; name: string; resultsCount: number }[]; + categories: { id: number; name: string; resultsCount: number }[]; }; } export interface SearchOptions { query: string; zipCode?: string; + country?: string; limit?: number; offset?: number; retailerId?: number; @@ -57,9 +70,10 @@ export async function search(options: SearchOptions): Promise { } const zipCode = options.zipCode || config.zipCode || DEFAULT_ZIP_CODE; + const country = options.country || config.country || DEFAULT_COUNTRY; const params = new URLSearchParams({ - as: "web", + as: 'web', q: options.query, limit: String(options.limit || 20), offset: String(options.offset || 0), @@ -67,26 +81,28 @@ export async function search(options: SearchOptions): Promise { }); if (options.retailerId) { - params.set("retailerIds", String(options.retailerId)); + params.set('retailerIds', String(options.retailerId)); } - const url = `${API_BASE}/offers/search?${params}`; + const url = `${getApiBase(country)}/offers/search?${params}`; const response = await fetch(url, { headers: { - "x-apikey": apiKey, - Accept: "application/json", + 'x-apikey': apiKey, + accept: 'application/json', }, }); if (!response.ok) { if (response.status === 401) { - throw new Error("API key invalid or expired. Run 'marktguru login' to refresh."); + throw new Error( + "API key invalid or expired. Run 'marktguru login' to refresh.", + ); } throw new Error(`API error: ${response.status} ${response.statusText}`); } - return response.json(); + return (await response.json()) as SearchResult; } export function formatPrice(price: number): string { @@ -94,18 +110,20 @@ export function formatPrice(price: number): string { } export function formatDiscount(price: number, oldPrice: number | null): string { - if (!oldPrice || oldPrice <= price) return ""; + if (!oldPrice || oldPrice <= price) return ''; const percent = Math.round((1 - price / oldPrice) * 100); return `-${percent}%`; } -export function formatValidity(dates: Offer["validityDates"]): string { - if (!dates.length) return ""; +export function formatValidity(dates: Offer['validityDates']): string { + if (!dates.length) return ''; const to = new Date(dates[0].to); const now = new Date(); - const daysLeft = Math.ceil((to.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)); - if (daysLeft < 0) return "expired"; - if (daysLeft === 0) return "today"; - if (daysLeft === 1) return "1 day left"; + const daysLeft = Math.ceil( + (to.getTime() - now.getTime()) / (1000 * 60 * 60 * 24), + ); + if (daysLeft < 0) return 'expired'; + if (daysLeft === 0) return 'today'; + if (daysLeft === 1) return '1 day left'; return `${daysLeft} days left`; } diff --git a/src/auth.ts b/src/auth.ts index 7dc4de7..6bd4f56 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,33 +1,24 @@ +import { VALID_COUNTRIES } from './config.js'; + interface ExtractOptions { log?: (message: string) => void; + country?: string; } -const BASE_URL = "https://www.marktguru.at"; -const API_BASE = "https://api.marktguru.at/api/v1"; -const DEFAULT_ZIP_CODE = "1010"; +const DEFAULT_ZIP_CODE = '1010'; const MAX_SCRIPTS = 20; -async function maybeGetHeaders(): Promise> { - try { - const { HeaderGenerator } = await import("header-generator"); - const generator = new HeaderGenerator({ - browsers: [{ name: "chrome", minVersion: 110 }], - devices: ["desktop"], - operatingSystems: ["macos"], - }); - return generator.getHeaders({ httpVersion: "2" }); - } catch { - return { - "user-agent": - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", - "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "accept-language": "en-US,en;q=0.9", - "accept-encoding": "gzip, deflate, br", - }; - } -} - -async function fetchText(url: string, headers: Record): Promise { +const BROWSER_HEADERS: Record = { + 'user-agent': + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36', + accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'accept-language': 'en-US,en;q=0.9', +}; + +async function fetchText( + url: string, + headers: Record, +): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); try { @@ -42,28 +33,28 @@ async function fetchText(url: string, headers: Record): Promise< } async function fetchFirstOk(urls: string[], headers: Record) { - let lastError: unknown = null; + let lastError: Error | null = null; for (const url of urls) { try { const text = await fetchText(url, headers); return { url, text }; } catch (error) { - lastError = error; + lastError = error instanceof Error ? error : new Error(String(error)); } } if (lastError) throw lastError; - throw new Error("No URLs to fetch."); + throw new Error('No URLs to fetch.'); } -function extractScriptUrls(html: string): string[] { +function extractScriptUrls(html: string, baseUrl: string): string[] { const urls = new Set(); const regex = /]+src=["']([^"']+)["'][^>]*>/gi; let match: RegExpExecArray | null; while ((match = regex.exec(html))) { let src = match[1]; - if (src.startsWith("//")) src = `https:${src}`; - if (src.startsWith("/")) src = `${BASE_URL}${src}`; - if (src.startsWith("http")) urls.add(src); + if (src.startsWith('//')) src = `https:${src}`; + if (src.startsWith('/')) src = `${baseUrl}${src}`; + if (src.startsWith('http')) urls.add(src); } return [...urls]; } @@ -85,7 +76,7 @@ function findCandidates(text: string): string[] { const base64Regex = /[A-Za-z0-9+/]{40,80}={0,2}/g; while ((match = base64Regex.exec(text))) { const value = match[0]; - if (value.length >= 40 && value.length <= 60 && value.includes("=")) { + if (value.length >= 40 && value.length <= 60 && value.includes('=')) { candidates.add(value); } } @@ -93,15 +84,15 @@ function findCandidates(text: string): string[] { return [...candidates]; } -async function validateKey(apiKey: string): Promise { - const url = `${API_BASE}/offers/search?as=web&q=test&limit=1&zipCode=${DEFAULT_ZIP_CODE}`; +async function validateKey(apiKey: string, apiBase: string): Promise { + const url = `${apiBase}/offers/search?as=web&q=test&limit=1&zipCode=${DEFAULT_ZIP_CODE}`; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); try { const res = await fetch(url, { headers: { - "x-apikey": apiKey, - "accept": "application/json", + 'x-apikey': apiKey, + accept: 'application/json', }, signal: controller.signal, }); @@ -111,27 +102,37 @@ async function validateKey(apiKey: string): Promise { } } -export async function extractApiKey(options: ExtractOptions = {}): Promise { +export async function extractApiKey( + options: ExtractOptions = {}, +): Promise { const log = options.log; - const headers = await maybeGetHeaders(); + const country = options.country ?? 'at'; + if (!(VALID_COUNTRIES as readonly string[]).includes(country)) { + throw new Error( + `Unsupported country "${country}". Valid options: ${VALID_COUNTRIES.join(', ')}`, + ); + } + const baseUrl = `https://www.marktguru.${country}`; + const apiBase = `https://api.marktguru.${country}/api/v1`; + const headers = BROWSER_HEADERS; const entryUrls = [ - `${BASE_URL}/`, - `${BASE_URL}/search`, - `${BASE_URL}/search?q=test`, - `${BASE_URL}/suche`, - `${BASE_URL}/suche?q=test`, + `${baseUrl}/`, + `${baseUrl}/search`, + `${baseUrl}/search?q=test`, + `${baseUrl}/suche`, + `${baseUrl}/suche?q=test`, ]; - log?.("→ Fetching entry HTML..."); + log?.('→ Fetching entry HTML...'); const { url: entryUrl, text: html } = await fetchFirstOk(entryUrls, headers); log?.(`✓ Using entry URL: ${entryUrl}`); const candidates = new Set(findCandidates(html)); - const scripts = extractScriptUrls(html).slice(0, MAX_SCRIPTS); + const scripts = extractScriptUrls(html, baseUrl).slice(0, MAX_SCRIPTS); if (scripts.length === 0) { - throw new Error("No scripts found to scan for API keys."); + throw new Error('No scripts found to scan for API keys.'); } log?.(`→ Scanning ${scripts.length} script(s)...`); @@ -147,10 +148,12 @@ export async function extractApiKey(options: ExtractOptions = {}): Promise @@ -32,7 +43,7 @@ const getJsonFlag = (options?: { json?: boolean }) => const parsePositiveInt = (value: string): number => { const parsed = Number.parseInt(value, 10); if (!Number.isFinite(parsed) || parsed <= 0) { - throw new InvalidArgumentError("Value must be a positive integer."); + throw new InvalidArgumentError('Value must be a positive integer.'); } return parsed; }; @@ -43,48 +54,72 @@ const collectValues = (value: string, previous: string[]): string[] => { }; program - .command("login") - .description("Extract API key from marktguru.at via HTTP") - .option("-j, --json", "Output JSON") - .action(async (options) => { + .command('login') + .description('Extract API key from marktguru.at/de via HTTP') + .option('-j, --json', 'Output JSON') + .action(async (options: { json?: boolean }) => { await login({ ...options, json: getJsonFlag(options) }); }); const search = program - .command("search") - .description("Search for product deals using the Marktguru query syntax"); + .command('search') + .description('Search for product deals using the Marktguru query syntax'); search - .command("raw ") - .description("Search using a raw query string") - .option("-z, --zip ", "ZIP code for location-based results") - .option("-n, --limit ", "Number of results (default: 10)", parsePositiveInt) - .option("-r, --retailer ", "Filter by retailer (e.g., SPAR, BILLA, HOFER)") - .option("-j, --json", "Output JSON") - .action((query, options) => { - searchRawCommand(query, { ...options, json: getJsonFlag(options) }); + .command('raw ') + .description('Search using a raw query string') + .option('-z, --zip ', 'ZIP code for location-based results') + .option( + '-n, --limit ', + 'Number of results (default: 10)', + parsePositiveInt, + ) + .option( + '-r, --retailer ', + 'Filter by retailer (e.g., SPAR, BILLA, HOFER)', + ) + .option('-j, --json', 'Output JSON') + .action(async (query: string, options: SearchCommandOptions) => { + await searchRawCommand(query, { ...options, json: getJsonFlag(options) }); }); search - .command("build") - .description("Build a query from structured flags") - .option("--term ", "Add a term", collectValues, []) - .option("--phrase ", "Add an exact phrase", collectValues, []) - .option("--wildcard ", "Add a wildcard term (e.g., kell*)", collectValues, []) - .option("--or ", "Add a term to the OR group", collectValues, []) - .option("--group ", "Add a raw group (wrapped in parentheses)", collectValues, []) - .option("--explain", "Print the built query to stderr") - .option("-z, --zip ", "ZIP code for location-based results") - .option("-n, --limit ", "Number of results (default: 10)", parsePositiveInt) - .option("-r, --retailer ", "Filter by retailer (e.g., SPAR, BILLA, HOFER)") - .option("-j, --json", "Output JSON") - .action((options) => { - searchBuildCommand({ ...options, json: getJsonFlag(options) }); + .command('build') + .description('Build a query from structured flags') + .option('--term ', 'Add a term', collectValues, []) + .option('--phrase ', 'Add an exact phrase', collectValues, []) + .option( + '--wildcard ', + 'Add a wildcard term (e.g., kell*)', + collectValues, + [], + ) + .option('--or ', 'Add a term to the OR group', collectValues, []) + .option( + '--group ', + 'Add a raw group (wrapped in parentheses)', + collectValues, + [], + ) + .option('--explain', 'Print the built query to stderr') + .option('-z, --zip ', 'ZIP code for location-based results') + .option( + '-n, --limit ', + 'Number of results (default: 10)', + parsePositiveInt, + ) + .option( + '-r, --retailer ', + 'Filter by retailer (e.g., SPAR, BILLA, HOFER)', + ) + .option('-j, --json', 'Output JSON') + .action(async (options: SearchBuildOptions) => { + await searchBuildCommand({ ...options, json: getJsonFlag(options) }); }); search - .command("syntax") - .description("Show supported query syntax") + .command('syntax') + .description('Show supported query syntax') .action(() => { console.log(QUERY_SYNTAX_HELP); }); @@ -94,10 +129,10 @@ search.action(() => { }); program - .command("set-zip ") - .description("Set default ZIP code for searches") - .option("-j, --json", "Output JSON") - .action(async (code: string, options) => { + .command('set-zip ') + .description('Set default ZIP code for searches') + .option('-j, --json', 'Output JSON') + .action(async (code: string, options: { json?: boolean }) => { await saveConfig({ zipCode: code }); const json = getJsonFlag(options); if (json) { @@ -108,24 +143,74 @@ program }); program - .command("config") - .description("Show current configuration") - .option("-j, --json", "Output JSON") - .action(async (options) => { + .command('set-country ') + .description('Set default country for searches (at, de)') + .option('-j, --json', 'Output JSON') + .action(async (code: string, options: { json?: boolean }) => { + const normalized = code.toLowerCase(); + if (!(VALID_COUNTRIES as readonly string[]).includes(normalized)) { + console.error( + `Error: Invalid country "${code}". Valid options: ${VALID_COUNTRIES.join(', ')}`, + ); + process.exit(1); + } + const existing = await getConfig(); + const countryChanged = existing.country !== normalized; + await saveConfig({ + country: normalized, + ...(countryChanged && { apiKey: undefined }), + }); + const json = getJsonFlag(options); + if (json) { + console.log( + JSON.stringify({ + success: true, + country: normalized, + apiKeyCleared: countryChanged && !!existing.apiKey, + }), + ); + } else { + console.log(`✓ Default country set to: ${normalized}`); + if (countryChanged && existing.apiKey) { + console.log( + " API key cleared — run 'marktguru login' to fetch a matching key.", + ); + } + } + }); + +program + .command('config') + .description('Show current configuration') + .option('-j, --json', 'Output JSON') + .action(async (options: { json?: boolean }) => { const config = await getConfig(); const json = getJsonFlag(options); if (json) { - console.log(JSON.stringify({ - apiKey: config.apiKey ? config.apiKey.substring(0, 10) + "..." : null, - apiKeySet: !!config.apiKey, - zipCode: config.zipCode || DEFAULT_ZIP_CODE, - configPath: config.configPath, - })); + console.log( + JSON.stringify({ + apiKey: config.apiKey ? `${config.apiKey.substring(0, 10)}...` : null, + apiKeySet: !!config.apiKey, + zipCode: config.zipCode || DEFAULT_ZIP_CODE, + country: config.country || DEFAULT_COUNTRY, + configPath: config.configPath, + }), + ); } else { - console.log("Configuration:"); - console.log(" API Key:", config.apiKey ? config.apiKey.substring(0, 10) + "..." : "(not set)"); - console.log(" ZIP Code:", config.zipCode || `(default: ${DEFAULT_ZIP_CODE})`); - console.log(" Config file:", config.configPath); + console.log('Configuration:'); + console.log( + ' API Key:', + config.apiKey ? `${config.apiKey.substring(0, 10)}...` : '(not set)', + ); + console.log( + ' ZIP Code:', + config.zipCode || `(default: ${DEFAULT_ZIP_CODE})`, + ); + console.log( + ' Country:', + config.country || `(default: ${DEFAULT_COUNTRY})`, + ); + console.log(' Config file:', config.configPath); } }); @@ -134,4 +219,4 @@ if (process.argv.length <= 2) { process.exit(0); } -program.parse(); +await program.parseAsync(); diff --git a/src/commands/login.ts b/src/commands/login.ts index a4a6fb9..eddb968 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -1,5 +1,5 @@ -import { saveConfig } from "../config.js"; -import { extractApiKey } from "../auth.js"; +import { saveConfig, getConfig } from '../config.js'; +import { extractApiKey } from '../auth.js'; interface LoginOptions { json?: boolean; @@ -15,10 +15,10 @@ function output(result: LoginResult, json: boolean): void { if (json) { console.log(JSON.stringify(result)); } else if (result.success) { - console.log("\n✓ API key extracted and saved!"); - console.log(" Key:", result.apiKey!.substring(0, 15) + "..."); + console.log('\n✓ API key extracted and saved!'); + console.log(' Key:', `${result.apiKey!.substring(0, 15)}...`); } else { - console.error("\n✗", result.error); + console.error('\n✗', result.error); } } @@ -26,10 +26,14 @@ export async function login(options: LoginOptions): Promise { const json = options.json ?? false; const log = (msg: string) => !json && console.log(msg); - log("Extracting Marktguru API key (HTTP-only)...\n"); + log('Extracting Marktguru API key (HTTP-only)...\n'); try { - const apiKey = await extractApiKey({ log: json ? undefined : log }); + const config = await getConfig(); + const apiKey = await extractApiKey({ + log: json ? undefined : log, + country: config.country, + }); await saveConfig({ apiKey }); output({ success: true, apiKey }, json); } catch (e) { diff --git a/src/commands/search.ts b/src/commands/search.ts index f4dc142..c8f6edd 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -5,10 +5,10 @@ import { formatValidity, type Offer, type SearchResult, -} from "../api.js"; -import { getConfig, saveConfig } from "../config.js"; -import { extractApiKey } from "../auth.js"; -import { buildQuery } from "../query.js"; +} from '../api.js'; +import { getConfig, saveConfig } from '../config.js'; +import { extractApiKey } from '../auth.js'; +import { buildQuery } from '../query.js'; export interface SimpleOffer { title: string; @@ -26,7 +26,7 @@ export function simplifyOffer(offer: Offer): SimpleOffer { offer.product.name, offer.description, ].filter(Boolean); - const title = parts.join(" - "); + const title = parts.join(' - '); // Calculate discount let discountPercent: number | null = null; @@ -35,14 +35,14 @@ export function simplifyOffer(offer: Offer): SimpleOffer { } // Get expiry date - const expires = offer.validityDates[0]?.to - ? new Date(offer.validityDates[0].to).toISOString().split("T")[0] - : ""; + const expires = offer.validityDates[0]?.to + ? new Date(offer.validityDates[0].to).toISOString().split('T')[0] + : ''; return { title, price: offer.price, - retailer: offer.advertisers[0]?.name || "Unknown", + retailer: offer.advertisers[0]?.name || 'Unknown', expires, discountPercent, externalUrl: offer.externalUrl ?? undefined, @@ -57,6 +57,7 @@ interface SimpleSearchResult { export interface SearchCommandOptions { zip?: string; + country?: string; limit?: number; retailer?: string; json?: boolean; @@ -77,7 +78,7 @@ export function formatOfferText(offer: Offer): string { const lines: string[] = []; // Product name and brand - const brand = offer.brand?.name ? `[${offer.brand.name}]` : ""; + const brand = offer.brand?.name ? `[${offer.brand.name}]` : ''; lines.push(`${offer.product.name} ${brand}`.trim()); // Price line @@ -90,7 +91,7 @@ export function formatOfferText(offer: Offer): string { const unitInfo = offer.volume && offer.unit ? ` · ${formatPrice(offer.referencePrice)}/${offer.unit.shortName}` - : ""; + : ''; lines.push(` 💰 ${priceInfo}${unitInfo}`); @@ -100,7 +101,7 @@ export function formatOfferText(offer: Offer): string { } // Retailer and validity - const retailer = offer.advertisers[0]?.name || "Unknown"; + const retailer = offer.advertisers[0]?.name || 'Unknown'; const validity = formatValidity(offer.validityDates); lines.push(` 🏪 ${retailer} · ${validity}`); @@ -108,7 +109,7 @@ export function formatOfferText(offer: Offer): string { lines.push(` 🔗 ${offer.externalUrl}`); } - return lines.join("\n"); + return lines.join('\n'); } export function formatResultsText(result: SearchResult, query: string): string { @@ -117,13 +118,13 @@ export function formatResultsText(result: SearchResult, query: string): string { lines.push(`Found ${result.totalResults} offers for "${query}":\n`); if (result.results.length === 0) { - lines.push("No offers found."); - return lines.join("\n"); + lines.push('No offers found.'); + return lines.join('\n'); } for (const offer of result.results) { lines.push(formatOfferText(offer)); - lines.push(""); // Empty line between offers + lines.push(''); // Empty line between offers } // Show available filters summary @@ -131,17 +132,17 @@ export function formatResultsText(result: SearchResult, query: string): string { const topRetailers = result.filters.retailers .slice(0, 5) .map((r) => `${r.name} (${r.resultsCount})`) - .join(", "); + .join(', '); lines.push(`📍 Retailers: ${topRetailers}`); } - return lines.join("\n"); + return lines.join('\n'); } function normalizeLimit(limit?: number): number { if (limit === undefined) return DEFAULT_LIMIT; if (!Number.isFinite(limit) || limit <= 0) { - throw new Error("Limit must be a positive number."); + throw new Error('Limit must be a positive number.'); } return Math.floor(limit); } @@ -153,20 +154,31 @@ function emitWarnings(warnings: string[]): void { } } -async function ensureApiKey(json?: boolean): Promise { +async function ensureApiKey( + json?: boolean, + country?: string, +): Promise { const config = await getConfig(); if (config.apiKey) return config.apiKey; - const log = json ? (msg: string) => console.error(msg) : (msg: string) => console.log(msg); - log("No API key configured. Running login..."); + const log = json + ? (msg: string) => console.error(msg) + : (msg: string) => console.log(msg); + log('No API key configured. Running login...'); - const apiKey = await extractApiKey({ log }); + const apiKey = await extractApiKey({ + log, + country: country ?? config.country, + }); await saveConfig({ apiKey }); return apiKey; } -async function runSearch(query: string, options: SearchCommandOptions): Promise { - const apiKey = await ensureApiKey(options.json); +async function runSearch( + query: string, + options: SearchCommandOptions, +): Promise { + const apiKey = await ensureApiKey(options.json, options.country); // Fetch more results if filtering by retailer (we'll filter client-side) const limit = normalizeLimit(options.limit); const fetchLimit = options.retailer ? Math.max(limit, 100) : limit; @@ -174,6 +186,7 @@ async function runSearch(query: string, options: SearchCommandOptions): Promise< const result = await apiSearch({ query, zipCode: options.zip, + country: options.country, limit: fetchLimit, apiKey, }); @@ -185,8 +198,8 @@ async function runSearch(query: string, options: SearchCommandOptions): Promise< const retailerLower = options.retailer.toLowerCase(); filteredResults = filteredResults.filter((offer) => offer.advertisers.some((a) => - a.name.toLowerCase().includes(retailerLower) - ) + a.name.toLowerCase().includes(retailerLower), + ), ); filteredResults = filteredResults.slice(0, limit); totalResults = filteredResults.length; @@ -202,24 +215,29 @@ async function runSearch(query: string, options: SearchCommandOptions): Promise< }; console.log(JSON.stringify(simple, null, 2)); } else { - console.log(formatResultsText({ ...result, results: filteredResults, totalResults }, query)); + console.log( + formatResultsText( + { ...result, results: filteredResults, totalResults }, + query, + ), + ); } } export async function searchRawCommand( query: string, - options: SearchCommandOptions + options: SearchCommandOptions, ): Promise { try { await runSearch(query, options); } catch (e) { - console.error("Error:", (e as Error).message); + console.error('Error:', (e as Error).message); process.exit(1); } } export async function searchBuildCommand( - options: SearchBuildOptions + options: SearchBuildOptions, ): Promise { try { const { query, warnings } = buildQuery({ @@ -237,7 +255,7 @@ export async function searchBuildCommand( await runSearch(query, options); } catch (e) { - console.error("Error:", (e as Error).message); + console.error('Error:', (e as Error).message); process.exit(1); } } diff --git a/src/config.ts b/src/config.ts index cf04f5e..18f61b3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,24 +1,29 @@ -import { homedir } from "os"; -import { join } from "path"; -import { readFile, writeFile, mkdir } from "fs/promises"; +import { homedir } from 'os'; +import { join } from 'path'; +import { readFile, writeFile, mkdir } from 'fs/promises'; export interface Config { apiKey?: string; zipCode?: string; + country?: string; configPath: string; } -export const DEFAULT_ZIP_CODE = "1010"; // Vienna +export const DEFAULT_ZIP_CODE = '1010'; // Vienna +export const DEFAULT_COUNTRY = 'at'; +export const VALID_COUNTRIES = ['at', 'de'] as const; +export type Country = (typeof VALID_COUNTRIES)[number]; -const CONFIG_DIR = join(homedir(), ".marktguru"); -const CONFIG_FILE = join(CONFIG_DIR, "config.json"); +const CONFIG_DIR = join(homedir(), '.marktguru'); +const CONFIG_FILE = join(CONFIG_DIR, 'config.json'); export async function getConfig(): Promise { try { - const data = await readFile(CONFIG_FILE, "utf-8"); - return { ...JSON.parse(data), configPath: CONFIG_FILE }; + const data = await readFile(CONFIG_FILE, 'utf-8'); + const parsed = JSON.parse(data) as Partial; + return { country: DEFAULT_COUNTRY, ...parsed, configPath: CONFIG_FILE }; } catch { - return { configPath: CONFIG_FILE }; + return { country: DEFAULT_COUNTRY, configPath: CONFIG_FILE }; } } diff --git a/src/query.ts b/src/query.ts index b408cf7..3f2cf29 100644 --- a/src/query.ts +++ b/src/query.ts @@ -12,26 +12,26 @@ export interface QueryBuildResult { } export const QUERY_SYNTAX_HELP = [ - "Query syntax (observed):", - "- OR : boolean OR", - "- * : wildcard, e.g. kell*", - "- \"...\" : exact phrase", - "- ( ... ) : grouping", - "- NOT supported: AND, NOT, ~, ^", - "", - "Build mode flags:", - "- --term : add a term", - "- --phrase : add an exact phrase", - "- --wildcard : add a wildcard term (e.g. kell*)", - "- --or : add a term to an OR group", - "- --group : add a raw group (wrapped in parentheses)", -].join("\n"); + 'Query syntax (observed):', + '- OR : boolean OR', + '- * : wildcard, e.g. kell*', + '- "..." : exact phrase', + '- ( ... ) : grouping', + '- NOT supported: AND, NOT, ~, ^', + '', + 'Build mode flags:', + '- --term : add a term', + '- --phrase : add an exact phrase', + '- --wildcard : add a wildcard term (e.g. kell*)', + '- --or : add a term to an OR group', + '- --group : add a raw group (wrapped in parentheses)', +].join('\n'); const WHITESPACE_REGEX = /\s/; const QUOTE_ESCAPE_REGEX = /["\\]/g; function escapeQuotes(value: string): string { - return value.replace(QUOTE_ESCAPE_REGEX, "\\$&"); + return value.replace(QUOTE_ESCAPE_REGEX, '\\$&'); } function quote(value: string): string { @@ -53,13 +53,16 @@ function normalizePhrase(value: string): string | null { return quote(trimmed); } -function normalizeWildcard(value: string): { token: string | null; warning?: string } { +function normalizeWildcard(value: string): { + token: string | null; + warning?: string; +} { const trimmed = value.trim(); if (!trimmed) return { token: null }; if (WHITESPACE_REGEX.test(trimmed)) { return { token: quote(trimmed), - warning: "Wildcard contained whitespace and was quoted as a phrase.", + warning: 'Wildcard contained whitespace and was quoted as a phrase.', }; } return { token: trimmed }; @@ -97,7 +100,7 @@ export function buildQuery(input: QueryBuildInput): QueryBuildResult { if (token) orTerms.push(token); } if (orTerms.length > 0) { - parts.push(`(${orTerms.join(" OR ")})`); + parts.push(`(${orTerms.join(' OR ')})`); } for (const group of input.groups ?? []) { @@ -105,10 +108,10 @@ export function buildQuery(input: QueryBuildInput): QueryBuildResult { if (token) parts.push(token); } - const query = parts.join(" ").trim(); + const query = parts.join(' ').trim(); if (!query) { throw new Error( - "No query parts provided. Use --term, --phrase, --wildcard, --or, or --group." + 'No query parts provided. Use --term, --phrase, --wildcard, --or, or --group.', ); } diff --git a/tests/api.test.js b/tests/api.test.js new file mode 100644 index 0000000..20aa0f1 --- /dev/null +++ b/tests/api.test.js @@ -0,0 +1,15 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { getApiBase } from '../dist/api.js'; + +test('getApiBase returns correct URL for AT', () => { + assert.equal(getApiBase('at'), 'https://api.marktguru.at/api/v1'); +}); + +test('getApiBase returns correct URL for DE', () => { + assert.equal(getApiBase('de'), 'https://api.marktguru.de/api/v1'); +}); + +test('getApiBase throws on unsupported country', () => { + assert.throws(() => getApiBase('fr'), /Unsupported country/); +}); diff --git a/tests/format.test.js b/tests/format.test.js index 37ac95b..a082664 100644 --- a/tests/format.test.js +++ b/tests/format.test.js @@ -1,20 +1,22 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { readFile } from "node:fs/promises"; -import { formatResultsText, simplifyOffer } from "../dist/commands/search.js"; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readFile } from 'node:fs/promises'; +import { formatResultsText, simplifyOffer } from '../dist/commands/search.js'; async function loadFixture() { - const raw = await readFile(new URL("./fixtures/search-result.json", import.meta.url)); + const raw = await readFile( + new URL('./fixtures/search-result.json', import.meta.url), + ); return JSON.parse(raw.toString()); } -test("formatResultsText includes externalUrl when available", async () => { +test('formatResultsText includes externalUrl when available', async () => { const fixture = await loadFixture(); - const output = formatResultsText(fixture, "chips"); + const output = formatResultsText(fixture, 'chips'); assert.match(output, /https:\/\/shop\.billa\.at/); }); -test("simplifyOffer exposes externalUrl only when present", async () => { +test('simplifyOffer exposes externalUrl only when present', async () => { const fixture = await loadFixture(); const offer = fixture.results[0]; const simplified = simplifyOffer(offer); diff --git a/tests/live.test.js b/tests/live.test.js new file mode 100644 index 0000000..50581e3 --- /dev/null +++ b/tests/live.test.js @@ -0,0 +1,58 @@ +// Live end-to-end tests: these hit marktguru.at/.de and api.marktguru.* for +// real, so they catch the scraper breaking when Marktguru ships a new frontend. +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { execFile } from 'node:child_process'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { promisify } from 'node:util'; + +const run = promisify(execFile); +const CLI = new URL('../dist/cli.js', import.meta.url).pathname; + +const COUNTRIES = [ + { country: 'at', zip: '1010' }, + { country: 'de', zip: '10115' }, +]; + +for (const { country, zip } of COUNTRIES) { + test( + `live: login and search in ${country}`, + { timeout: 120000 }, + async () => { + // Isolated HOME so the test never reads or writes a real ~/.marktguru. + const home = await mkdtemp(join(tmpdir(), 'marktguru-live-')); + const cli = (...args) => + run(process.execPath, [CLI, ...args], { + env: { ...process.env, HOME: home, USERPROFILE: home }, + }); + try { + await cli('set-country', country); + + // No key is stored yet, so search has to auto-login by scraping. + const { stdout } = await cli( + 'search', + 'raw', + 'milch', + '--zip', + zip, + '--limit', + '5', + '--json', + ); + const data = JSON.parse(stdout); + + assert.ok(data.total > 0, `no results for "milch" in ${country}`); + assert.ok(data.offers.length > 0); + assert.ok(data.offers.length <= 5); + for (const offer of data.offers) { + assert.equal(typeof offer.title, 'string'); + assert.equal(typeof offer.price, 'number'); + } + } finally { + await rm(home, { recursive: true, force: true }); + } + }, + ); +} diff --git a/tests/query.test.js b/tests/query.test.js index cdc28ad..edc3d25 100644 --- a/tests/query.test.js +++ b/tests/query.test.js @@ -1,32 +1,32 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { buildQuery } from "../dist/query.js"; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { buildQuery } from '../dist/query.js'; -test("buildQuery builds a structured query", () => { +test('buildQuery builds a structured query', () => { const { query, warnings } = buildQuery({ - terms: ["milch"], - phrases: ["frische milch"], - wildcards: ["bio*"], - ors: ["soja", "hafer"], - groups: ["(milch OR sahne)"], + terms: ['milch'], + phrases: ['frische milch'], + wildcards: ['bio*'], + ors: ['soja', 'hafer'], + groups: ['(milch OR sahne)'], }); assert.equal( query, - "milch \"frische milch\" bio* (soja OR hafer) ((milch OR sahne))" + 'milch "frische milch" bio* (soja OR hafer) ((milch OR sahne))', ); assert.deepEqual(warnings, []); }); -test("buildQuery warns on wildcard with whitespace", () => { +test('buildQuery warns on wildcard with whitespace', () => { const { query, warnings } = buildQuery({ - wildcards: ["bio milch"], + wildcards: ['bio milch'], }); - assert.equal(query, "\"bio milch\""); + assert.equal(query, '"bio milch"'); assert.equal(warnings.length, 1); }); -test("buildQuery throws on empty input", () => { +test('buildQuery throws on empty input', () => { assert.throws(() => buildQuery({}), /No query parts provided/); }); diff --git a/tsconfig-lint.json b/tsconfig-lint.json new file mode 100644 index 0000000..9e5f9dc --- /dev/null +++ b/tsconfig-lint.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +}