From 75c8873a661381d11c225400a41ad594c3af90c9 Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:20:17 +0200 Subject: [PATCH 01/33] feat: add country selection for AT/DE support Add a `set-country ` command (at/de) that switches the API endpoint between marktguru.at and marktguru.de. The login command also uses the configured country so the extracted API key matches the correct domain. Defaults to "at" for backward compatibility. --- src/api.ts | 10 +++++++--- src/auth.ts | 28 +++++++++++++++------------- src/cli.ts | 29 ++++++++++++++++++++++++++--- src/commands/login.ts | 5 +++-- src/commands/search.ts | 2 ++ src/config.ts | 7 +++++-- 6 files changed, 58 insertions(+), 23 deletions(-) diff --git a/src/api.ts b/src/api.ts index 2836205..5ab0a37 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,6 +1,8 @@ -import { getConfig, DEFAULT_ZIP_CODE } from "./config.js"; +import { getConfig, DEFAULT_ZIP_CODE, DEFAULT_COUNTRY } from "./config.js"; -const API_BASE = "https://api.marktguru.at/api/v1"; +export function getApiBase(country: string): string { + return `https://api.marktguru.${country}/api/v1`; +} export interface Offer { id: number; @@ -43,6 +45,7 @@ export interface SearchResult { export interface SearchOptions { query: string; zipCode?: string; + country?: string; limit?: number; offset?: number; retailerId?: number; @@ -57,6 +60,7 @@ 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", @@ -70,7 +74,7 @@ export async function search(options: SearchOptions): Promise { 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: { diff --git a/src/auth.ts b/src/auth.ts index 7dc4de7..38787ad 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,9 +1,8 @@ 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 MAX_SCRIPTS = 20; @@ -55,14 +54,14 @@ async function fetchFirstOk(urls: string[], headers: Record) { 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("/")) src = `${baseUrl}${src}`; if (src.startsWith("http")) urls.add(src); } return [...urls]; @@ -93,8 +92,8 @@ 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 { @@ -113,14 +112,17 @@ async function validateKey(apiKey: string): Promise { export async function extractApiKey(options: ExtractOptions = {}): Promise { const log = options.log; + const country = options.country ?? "at"; + const baseUrl = `https://www.marktguru.${country}`; + const apiBase = `https://api.marktguru.${country}/api/v1`; const headers = await maybeGetHeaders(); 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..."); @@ -129,7 +131,7 @@ export async function extractApiKey(options: ExtractOptions = {}): Promise { program .command("login") - .description("Extract API key from marktguru.at via HTTP") + .description("Extract API key from marktguru.at/de via HTTP") .option("-j, --json", "Output JSON") .action(async (options) => { await login({ ...options, json: getJsonFlag(options) }); @@ -107,6 +107,27 @@ program } }); +const VALID_COUNTRIES = ["at", "de"]; + +program + .command("set-country ") + .description("Set default country for searches (at, de)") + .option("-j, --json", "Output JSON") + .action(async (code: string, options) => { + const normalized = code.toLowerCase(); + if (!VALID_COUNTRIES.includes(normalized)) { + console.error(`Error: Invalid country "${code}". Valid options: ${VALID_COUNTRIES.join(", ")}`); + process.exit(1); + } + await saveConfig({ country: normalized }); + const json = getJsonFlag(options); + if (json) { + console.log(JSON.stringify({ success: true, country: normalized })); + } else { + console.log(`✓ Default country set to: ${normalized}`); + } + }); + program .command("config") .description("Show current configuration") @@ -119,12 +140,14 @@ program 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(" Country:", config.country || `(default: ${DEFAULT_COUNTRY})`); console.log(" Config file:", config.configPath); } }); diff --git a/src/commands/login.ts b/src/commands/login.ts index a4a6fb9..33218da 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -1,4 +1,4 @@ -import { saveConfig } from "../config.js"; +import { saveConfig, getConfig } from "../config.js"; import { extractApiKey } from "../auth.js"; interface LoginOptions { @@ -29,7 +29,8 @@ export async function login(options: LoginOptions): Promise { 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..d2c3429 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -57,6 +57,7 @@ interface SimpleSearchResult { export interface SearchCommandOptions { zip?: string; + country?: string; limit?: number; retailer?: string; json?: boolean; @@ -174,6 +175,7 @@ async function runSearch(query: string, options: SearchCommandOptions): Promise< const result = await apiSearch({ query, zipCode: options.zip, + country: options.country, limit: fetchLimit, apiKey, }); diff --git a/src/config.ts b/src/config.ts index cf04f5e..04b768d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -5,10 +5,12 @@ 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_COUNTRY = "at"; const CONFIG_DIR = join(homedir(), ".marktguru"); const CONFIG_FILE = join(CONFIG_DIR, "config.json"); @@ -16,9 +18,10 @@ 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 parsed = JSON.parse(data); + return { country: DEFAULT_COUNTRY, ...parsed, configPath: CONFIG_FILE }; } catch { - return { configPath: CONFIG_FILE }; + return { country: DEFAULT_COUNTRY, configPath: CONFIG_FILE }; } } From c9dd26c0ecc6d1dae53f151b1bf35f4461694871 Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:21:57 +0200 Subject: [PATCH 02/33] docs: update README and SKILL.md for AT/DE country support --- README.md | 9 ++++++++- SKILL.md | 31 ++++++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0a68fe7..3e00bd7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![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. +CLI for Marktguru supermarket deals in Austria and Germany. ## AI Agent Skill See [SKILL.md](SKILL.md) for a comprehensive reference designed for AI coding agents. @@ -46,6 +46,11 @@ Set a default ZIP code: marktguru set-zip 1010 ``` +Set a default country (`at` or `de`, default: `at`): +```bash +marktguru set-country de +``` + Show config: ```bash marktguru config @@ -73,6 +78,8 @@ 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) diff --git a/SKILL.md b/SKILL.md index 36af9a6..c22ff4b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,11 +1,11 @@ --- 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.). +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, Kaufland, etc.). --- # Marktguru Grocery Deals -Query Austrian grocery deals from Marktguru. Supports raw queries, structured search building, retailer filtering, and ZIP-code location targeting. +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 @@ -15,8 +15,9 @@ Query Austrian grocery deals from Marktguru. Supports raw queries, structured se | `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 | +| `login` | Extract API key from marktguru.at/de | --- @@ -32,8 +33,16 @@ Scans site HTML and boot scripts for embedded API keys. No browser automation re ```bash npx marktguru-cli set-zip 1010 npx marktguru-cli set-zip 8010 # Graz +npx marktguru-cli set-zip 10115 # Berlin (DE) ``` +### Set Default Country +```bash +npx marktguru-cli set-country at # Austria (default) +npx marktguru-cli set-country de # Germany +``` +After switching country, re-run `login` — API keys are country-specific. + ### Check Config ```bash npx marktguru-cli config @@ -118,6 +127,8 @@ npx marktguru-cli search raw '"Coca Cola"' ## Known Retailers +**Austria (at):** + | Retailer | Notes | |----------|-------| | SPAR | | @@ -131,6 +142,18 @@ npx marktguru-cli search raw '"Coca Cola"' | dm drogerie markt | Drugstore (some food items) | | BIPA | Drugstore | +**Germany (de):** + +| Retailer | Notes | +|----------|-------| +| Lidl | | +| PENNY | | +| Kaufland | | +| REWE | | +| Netto Marken-Discount | | +| ALDI | | +| dm drogerie markt | Drugstore (some food items) | + --- ## JSON Output @@ -209,6 +232,7 @@ npx marktguru-cli config --json "apiKey": "pCcm1AVCYa...", "apiKeySet": true, "zipCode": "1010", + "country": "at", "configPath": "/Users/.../.marktguru/config.json" } ``` @@ -223,6 +247,7 @@ npx marktguru-cli config --json | 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. | +| Wrong country results | Run `set-country de` (or `at`), then `login` again — keys are country-specific. | --- From 8954ba4ce40668527cf49414f9ba1ee000b6e2ce Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:26:35 +0200 Subject: [PATCH 03/33] docs: combine retailer table with AT/DE columns in SKILL.md --- SKILL.md | 42 ++++++++++++++++-------------------------- 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/SKILL.md b/SKILL.md index c22ff4b..6dc97e2 100644 --- a/SKILL.md +++ b/SKILL.md @@ -127,32 +127,22 @@ npx marktguru-cli search raw '"Coca Cola"' ## Known Retailers -**Austria (at):** - -| 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 | - -**Germany (de):** - -| Retailer | Notes | -|----------|-------| -| Lidl | | -| PENNY | | -| Kaufland | | -| REWE | | -| Netto Marken-Discount | | -| ALDI | | -| dm drogerie markt | Drugstore (some food items) | +| 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 | | ✓ | | --- From 8dd2d2e922c857762f61a6250d3678be5b2e08ab Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:28:00 +0200 Subject: [PATCH 04/33] docs: align retailer table columns in SKILL.md --- SKILL.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/SKILL.md b/SKILL.md index 6dc97e2..ace3e21 100644 --- a/SKILL.md +++ b/SKILL.md @@ -127,22 +127,22 @@ npx 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 | | ✓ | | +| 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 | | ✓ | | --- From f64c91cc0106f9e4aaebe607d11a19b9143014a9 Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:30:19 +0200 Subject: [PATCH 05/33] feat: add tests for getApiBase --- tests/api.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/api.test.js diff --git a/tests/api.test.js b/tests/api.test.js new file mode 100644 index 0000000..08122b9 --- /dev/null +++ b/tests/api.test.js @@ -0,0 +1,11 @@ +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"); +}); From b45ef9c052e5b69d5bf4a87024c773b7eba8cd57 Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:31:30 +0200 Subject: [PATCH 06/33] chore: update package description for AT/DE support --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4c55f5e..de5382f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "marktguru-cli", "version": "0.1.0", - "description": "CLI for Austrian Marktguru supermarket deals", + "description": "CLI for Marktguru supermarket deals in Austria and Germany", "type": "module", "bin": { "marktguru": "dist/cli.js" From 8d021cfe98007e7593fffce7d16f48f5b9c30d84 Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:37:39 +0200 Subject: [PATCH 07/33] =?UTF-8?q?chore:=20drop=20CH=20=E2=80=94=20api.mark?= =?UTF-8?q?tguru.ch=20does=20not=20exist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SKILL.md | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SKILL.md b/SKILL.md index ace3e21..bfc77dd 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- 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, Kaufland, etc.). +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 diff --git a/package.json b/package.json index de5382f..59efac7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "marktguru-cli", "version": "0.1.0", - "description": "CLI for Marktguru supermarket deals in Austria and Germany", + "description": "CLI for Marktguru supermarket deals in Austria, Germany and Switzerland", "type": "module", "bin": { "marktguru": "dist/cli.js" From bbda8866a7f2584fbecd772f4fa6ca22e1f3bafa Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:41:10 +0200 Subject: [PATCH 08/33] chore: remove Switzerland from package description --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 59efac7..de5382f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "marktguru-cli", "version": "0.1.0", - "description": "CLI for Marktguru supermarket deals in Austria, Germany and Switzerland", + "description": "CLI for Marktguru supermarket deals in Austria and Germany", "type": "module", "bin": { "marktguru": "dist/cli.js" From d52f2cf28059922826958fedd8ea0e23c16db73f Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:42:33 +0200 Subject: [PATCH 09/33] fix: pass country to extractApiKey during auto-login in search --- src/commands/search.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/search.ts b/src/commands/search.ts index d2c3429..21f3005 100644 --- a/src/commands/search.ts +++ b/src/commands/search.ts @@ -154,20 +154,20 @@ 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 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); + 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; From 8277f3f28f0177b2617564b1e3d035496eef3d5d Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:45:39 +0200 Subject: [PATCH 10/33] fix: clear API key when country changes in set-country --- src/cli.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index f57a342..5383db3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -119,12 +119,17 @@ program console.error(`Error: Invalid country "${code}". Valid options: ${VALID_COUNTRIES.join(", ")}`); process.exit(1); } - await saveConfig({ country: normalized }); + 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 })); + console.log(JSON.stringify({ success: true, country: normalized, apiKeyCleared: countryChanged })); } 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."); + } } }); From beb63ac1584e3b28bc05936c1969b7d8c7c6ca59 Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:50:23 +0200 Subject: [PATCH 11/33] fix: only report apiKeyCleared:true in JSON when a key was actually cleared --- src/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli.ts b/src/cli.ts index 5383db3..f6e0b12 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -124,7 +124,7 @@ program await saveConfig({ country: normalized, ...(countryChanged && { apiKey: undefined }) }); const json = getJsonFlag(options); if (json) { - console.log(JSON.stringify({ success: true, country: normalized, apiKeyCleared: countryChanged })); + console.log(JSON.stringify({ success: true, country: normalized, apiKeyCleared: countryChanged && !!existing.apiKey })); } else { console.log(`✓ Default country set to: ${normalized}`); if (countryChanged && existing.apiKey) { From 4ac9f76a7f4fcad0a5f9da829ffdfd5c856a1ca1 Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:56:44 +0200 Subject: [PATCH 12/33] fix: validate country code before interpolating into URLs --- src/api.ts | 5 ++++- src/auth.ts | 5 +++++ src/cli.ts | 6 ++---- src/config.ts | 2 ++ tests/api.test.js | 4 ++++ 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/api.ts b/src/api.ts index 5ab0a37..05da5e0 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,6 +1,9 @@ -import { getConfig, DEFAULT_ZIP_CODE, DEFAULT_COUNTRY } from "./config.js"; +import { getConfig, DEFAULT_ZIP_CODE, DEFAULT_COUNTRY, VALID_COUNTRIES } from "./config.js"; 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`; } diff --git a/src/auth.ts b/src/auth.ts index 38787ad..f3aceff 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,3 +1,5 @@ +import { VALID_COUNTRIES } from "./config.js"; + interface ExtractOptions { log?: (message: string) => void; country?: string; @@ -113,6 +115,9 @@ async function validateKey(apiKey: string, apiBase: string): Promise { export async function extractApiKey(options: ExtractOptions = {}): Promise { const log = options.log; 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 = await maybeGetHeaders(); diff --git a/src/cli.ts b/src/cli.ts index f6e0b12..1413d80 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2,7 +2,7 @@ import { Command, InvalidArgumentError } from "commander"; import { login } from "./commands/login.js"; import { searchBuildCommand, searchRawCommand } from "./commands/search.js"; -import { getConfig, saveConfig, DEFAULT_ZIP_CODE, DEFAULT_COUNTRY } from "./config.js"; +import { getConfig, saveConfig, DEFAULT_ZIP_CODE, DEFAULT_COUNTRY, VALID_COUNTRIES } from "./config.js"; import { QUERY_SYNTAX_HELP } from "./query.js"; const program = new Command(); @@ -107,15 +107,13 @@ program } }); -const VALID_COUNTRIES = ["at", "de"]; - program .command("set-country ") .description("Set default country for searches (at, de)") .option("-j, --json", "Output JSON") .action(async (code: string, options) => { const normalized = code.toLowerCase(); - if (!VALID_COUNTRIES.includes(normalized)) { + if (!(VALID_COUNTRIES as readonly string[]).includes(normalized)) { console.error(`Error: Invalid country "${code}". Valid options: ${VALID_COUNTRIES.join(", ")}`); process.exit(1); } diff --git a/src/config.ts b/src/config.ts index 04b768d..42a1fb7 100644 --- a/src/config.ts +++ b/src/config.ts @@ -11,6 +11,8 @@ export interface Config { 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"); diff --git a/tests/api.test.js b/tests/api.test.js index 08122b9..24df03a 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -9,3 +9,7 @@ test("getApiBase returns correct URL for AT", () => { 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("de.evil.com"), /Unsupported country/); +}); From 37840bd4384406030ffdcff1f1e0fc5996031acf Mon Sep 17 00:00:00 2001 From: udondan Date: Sat, 30 May 2026 10:57:52 +0200 Subject: [PATCH 13/33] chore: use fr as unsupported country test case instead of de.evil.com --- tests/api.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/api.test.js b/tests/api.test.js index 24df03a..6f25342 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -11,5 +11,5 @@ test("getApiBase returns correct URL for DE", () => { }); test("getApiBase throws on unsupported country", () => { - assert.throws(() => getApiBase("de.evil.com"), /Unsupported country/); + assert.throws(() => getApiBase("fr"), /Unsupported country/); }); From 4cf577e30fe57269e9a344ad3483078424d884d3 Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Thu, 17 Sep 2026 20:02:42 +0200 Subject: [PATCH 14/33] chore: set up release-please, npm trusted publishing and publish as @udondan/marktguru-cli (#2) Release-As: 1.0.0 --- .github/workflows/automerge-schedule.yml | 40 ++ .github/workflows/ci.yml | 18 - .github/workflows/pr-conventional-title.yml | 20 + .github/workflows/publish.yml | 36 ++ .github/workflows/release-please.yml | 20 + .github/workflows/test.yml | 38 ++ .release-please-manifest.json | 3 + LICENSE | 1 + README.md | 22 +- SKILL.md | 50 +-- package-lock.json | 388 +++++++++++++++++++- package.json | 32 +- release-please-config.json | 14 + renovate.json | 18 + 14 files changed, 634 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/automerge-schedule.yml delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pr-conventional-title.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .github/workflows/test.yml create mode 100644 .release-please-manifest.json create mode 100644 release-please-config.json create mode 100644 renovate.json diff --git a/.github/workflows/automerge-schedule.yml b/.github/workflows/automerge-schedule.yml new file mode 100644 index 0000000..549f67b --- /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..198c9a7 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,38 @@ +--- +name: Test + +permissions: + contents: read + +on: + pull_request: + branches: + - main + push: + branches: + - main + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - uses: actions/setup-node@v7 + with: + node-version: 24.x + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Linting + run: npx oxlint + + - name: Test + run: npm test + + - name: Validate package + run: npm pack --dry-run diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..466df71 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.1.0" +} 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/README.md b/README.md index 3e00bd7..6dac139 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,21 @@ # 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) +[![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. ## 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 @@ -59,14 +61,14 @@ 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 -pnpm run dev -- --help +npm run dev -- --help ``` ## Search Options diff --git a/SKILL.md b/SKILL.md index bfc77dd..ed8045a 100644 --- a/SKILL.md +++ b/SKILL.md @@ -25,28 +25,28 @@ Query grocery deals from Marktguru in Austria and Germany. Supports raw queries, ### Login (HTTP scan) ```bash -npx marktguru-cli login +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 marktguru-cli set-zip 1010 -npx marktguru-cli set-zip 8010 # Graz -npx marktguru-cli set-zip 10115 # Berlin (DE) +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 marktguru-cli set-country at # Austria (default) -npx marktguru-cli set-country de # Germany +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 marktguru-cli config -npx marktguru-cli config --json +npx @udondan/marktguru-cli config +npx @udondan/marktguru-cli config --json ``` --- @@ -56,11 +56,11 @@ npx marktguru-cli config --json ### 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 +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 @@ -77,10 +77,10 @@ npx marktguru-cli search raw "Cola" --json 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 +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 | @@ -108,19 +108,19 @@ npx marktguru-cli search build --wildcard "jogh*" --retailer SPAR ```bash # Simple term -npx marktguru-cli search raw "Butter" +npx @udondan/marktguru-cli search raw "Butter" # OR logic -npx marktguru-cli search raw "Käse OR Schinken" +npx @udondan/marktguru-cli search raw "Käse OR Schinken" # Wildcard -npx marktguru-cli search raw "Bio*" +npx @udondan/marktguru-cli search raw "Bio*" # Combined with retailer filter -npx marktguru-cli search raw "Bier" --retailer HOFER --limit 10 +npx @udondan/marktguru-cli search raw "Bier" --retailer HOFER --limit 10 # Exact phrase -npx marktguru-cli search raw '"Coca Cola"' +npx @udondan/marktguru-cli search raw '"Coca Cola"' ``` --- @@ -149,7 +149,7 @@ npx marktguru-cli search raw '"Coca Cola"' ## JSON Output ```bash -npx marktguru-cli search raw "Cola" --limit 3 --json +npx @udondan/marktguru-cli search raw "Cola" --limit 3 --json ``` ```json @@ -214,7 +214,7 @@ Premium Bergbauern H-Milch [Salzburg Milch] Credentials and settings stored at `~/.marktguru/config.json`. ```bash -npx marktguru-cli config --json +npx @udondan/marktguru-cli config --json ``` ```json @@ -236,7 +236,7 @@ npx marktguru-cli config --json | 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. | +| 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. | --- diff --git a/package-lock.json b/package-lock.json index 35552a4..6f98227 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,25 @@ { - "name": "marktguru-cli", + "name": "@udondan/marktguru-cli", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "marktguru-cli", + "name": "@udondan/marktguru-cli", "version": "0.1.0", + "license": "MIT", "dependencies": { - "commander": "^12.0.0", - "header-generator": "^2.1.63" + "commander": "12.1.0", + "header-generator": "2.1.80" }, "bin": { "marktguru": "dist/cli.js" }, "devDependencies": { - "@types/node": "^20.0.0", - "tsx": "^4.0.0", - "typescript": "^5.0.0" + "@types/node": "20.19.32", + "oxlint": "1.83.0", + "tsx": "4.21.0", + "typescript": "5.9.3" } }, "node_modules/@esbuild/aix-ppc64": { @@ -462,6 +464,329 @@ "node": ">=18" } }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.83.0.tgz", + "integrity": "sha512-0yGY24EwsLk5YDe6F+VkmZyRHSwJDALa3nIrPpq7FXmp2lV2d0TzvBCGeZk+wgiULRGr5blhyr4QMp5KCXJUqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.83.0.tgz", + "integrity": "sha512-hHfJ0vc17A4iUjH5p9BsTUPYbYRNxGpvD2lbu1aBRk54bzNIx9o5TtYF39QPZcV95DagZd+4DEAw2RH3G2ZsMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.83.0.tgz", + "integrity": "sha512-hsOjYjszLb/3zym/TkzUMPAoQlTJcuzSyEPOAyA+skXJIX9M0o+4JfOtqopX/Vf4hSLrJ98j0nvFo23gzk8auQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.83.0.tgz", + "integrity": "sha512-mjh5oH2EA+wl5yRJYT9K9G61O2zFlpuv+yf2JwZOi0+dq2FnTUtm1h8i+5Ik0fXPWIu/k84I1psZR9aQsLAnyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.83.0.tgz", + "integrity": "sha512-fNHr64/YaO8YssuoDVC8+F4Uk5enR86q5uxfHkQrjAPs1dbAILOrD2uaud+J7MO8Fx774g44ERLD0IGIvZE48w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.83.0.tgz", + "integrity": "sha512-Qpwy3zzAwMj+8/lyYItHmkSMwbkprFNWTK7jPYDOxSyxEhaSLOWYUTCMkjF334J8/WD0nznCCsoBbIH6hpsuIw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.83.0.tgz", + "integrity": "sha512-s+BirYLFq7JL2k9sP0XI3ZXJ9dYvJ8sX3jLCLoag7tt+zrSHpZxP0jqznfL+Gdgwu7ay0dYgGYJXrQvq3iWloA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.83.0.tgz", + "integrity": "sha512-7lihXt3vKr+GIyapNbHrnFHm/biiW30le6Zv/DExbAFPF6YwCQXVFlONPFehxs0CpGO4CBfYPM9rdDT+XMoIlg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.83.0.tgz", + "integrity": "sha512-q63JalLYVkZiZvls1z3PPUnpmQluOMXp0khqQMznCeAPLGydfNY8JhvuA4WlK57JfrvikU8wB5lPVveqpIXvew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.83.0.tgz", + "integrity": "sha512-krQmDF+dRbxvdqVPV88ZuOoPPu8X5BuqDA8Hd+qcS4YMRQCb+nexA57DazgGsc/rGdKBe3QmV0mnv0bdpW/p5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.83.0.tgz", + "integrity": "sha512-MmOl8Y6txEAXZU1RG8Rr264jQ6D7VPmqFsU/45x/FeWsGe32hklTqGrLE6UxHzp5Rjt0wP+20tY8YXKgSFB3mw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.83.0.tgz", + "integrity": "sha512-u1rMymh0W3JZkq370kzQsYPULGWqhE09pZRqnZvUSoYaI9pVO5yVX+iYIslmWuEgwuzH9YAaOsScJiobWCHoOw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.83.0.tgz", + "integrity": "sha512-y0zK3HNwGysu7rqtE+BQG/d0bx5gh/KwlOtghN8oWeK1KcWzeaLqtZrbm8owqdma1lFyrce/hTO5ismuNu+INQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.83.0.tgz", + "integrity": "sha512-rS5gM0NgD7ngmuJmbIehsidtrOwKkLFwCQbKEeb9KuyQrrWNq5Zkn0uV6AYdXOMJ0grrWEiLwBuvMxt8w5vsNw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.83.0.tgz", + "integrity": "sha512-W2IH4EtpcPaWcvNGCA95YoDg4vxqE/ZiPCi3arrxEEpsK7+JQN9WYwrlYFx9pcdP6KPXqRqkv3zdQPHcx7b6YQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.83.0.tgz", + "integrity": "sha512-6LyKkUyoajssTPLlZmDbZIbu4IZ5B4bGuRUnBgCGpEvHP3FQMaYITncHA/unPUo7q+Z+pIu2HhdkQ+8d1SG7iA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.83.0.tgz", + "integrity": "sha512-Uz/fObEtF0jmNJQJ8CGRBKfefYstS0/wjD3s6IGzP8nUwsJykHQJBiN3npHwKiGRGn/vvBEgNr4B3cCzmmatvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.83.0.tgz", + "integrity": "sha512-u7XcvPW6Bk58tY5iWs2ESb0vJjoE/kuSpHxopbwp/p3ZtWVQXZ6wor5w3ssVTHOqd/v8b+QdhSFWQ4grEUNWpA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.83.0.tgz", + "integrity": "sha512-LZRubd7ph13QmAg4fFecTYVZkiYbROR2Htaxh/ufWRkDhPOm2wrwaEYR89e0YpPFD3dqBrPoxS7myBw5hmYA7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -739,6 +1064,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oxlint": { + "version": "1.83.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.83.0.tgz", + "integrity": "sha512-cyDzSzaw3uzP0TeCeq3lLRPPoaUxkbB4ZOXj+kn+5r+BX9V+4bNVGk9lxer+WrgcpebH4JxLlJ3KQjveVztOLQ==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/oxc-project" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.83.0", + "@oxlint/binding-android-arm64": "1.83.0", + "@oxlint/binding-darwin-arm64": "1.83.0", + "@oxlint/binding-darwin-x64": "1.83.0", + "@oxlint/binding-freebsd-x64": "1.83.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.83.0", + "@oxlint/binding-linux-arm-musleabihf": "1.83.0", + "@oxlint/binding-linux-arm64-gnu": "1.83.0", + "@oxlint/binding-linux-arm64-musl": "1.83.0", + "@oxlint/binding-linux-ppc64-gnu": "1.83.0", + "@oxlint/binding-linux-riscv64-gnu": "1.83.0", + "@oxlint/binding-linux-riscv64-musl": "1.83.0", + "@oxlint/binding-linux-s390x-gnu": "1.83.0", + "@oxlint/binding-linux-x64-gnu": "1.83.0", + "@oxlint/binding-linux-x64-musl": "1.83.0", + "@oxlint/binding-openharmony-arm64": "1.83.0", + "@oxlint/binding-win32-arm64-msvc": "1.83.0", + "@oxlint/binding-win32-ia32-msvc": "1.83.0", + "@oxlint/binding-win32-x64-msvc": "1.83.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", diff --git a/package.json b/package.json index de5382f..79768f5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,23 @@ { - "name": "marktguru-cli", + "name": "@udondan/marktguru-cli", "version": "0.1.0", "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" + }, "type": "module", "bin": { "marktguru": "dist/cli.js" @@ -18,13 +34,17 @@ "README.md", "LICENSE" ], + "publishConfig": { + "access": "public" + }, "dependencies": { - "commander": "^12.0.0", - "header-generator": "^2.1.63" + "commander": "12.1.0", + "header-generator": "2.1.80" }, "devDependencies": { - "@types/node": "^20.0.0", - "tsx": "^4.0.0", - "typescript": "^5.0.0" + "@types/node": "20.19.32", + "oxlint": "1.83.0", + "tsx": "4.21.0", + "typescript": "5.9.3" } } diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..c5338ec --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,14 @@ +{ + "$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": { + ".": {} + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..5e88c3e --- /dev/null +++ b/renovate.json @@ -0,0 +1,18 @@ +{ + "$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" + } + ] +} From 2fc2c58107032f291376af28043e22b32620686a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:08:36 +0000 Subject: [PATCH 15/33] chore(deps): update dependency @types/node to v20.19.43 (#4) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f98227..858f956 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "marktguru": "dist/cli.js" }, "devDependencies": { - "@types/node": "20.19.32", + "@types/node": "20.19.43", "oxlint": "1.83.0", "tsx": "4.21.0", "typescript": "5.9.3" @@ -800,9 +800,9 @@ } }, "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==", + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 79768f5..66f4532 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "header-generator": "2.1.80" }, "devDependencies": { - "@types/node": "20.19.32", + "@types/node": "20.19.43", "oxlint": "1.83.0", "tsx": "4.21.0", "typescript": "5.9.3" From f53424aee32d4dc60d5b4b7990b073ca2800ac6a Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Thu, 17 Sep 2026 20:09:09 +0200 Subject: [PATCH 16/33] chore: add npm keywords for package discoverability (#6) --- package.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/package.json b/package.json index 66f4532..bb8aea1 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,15 @@ "bugs": { "url": "https://github.com/udondan/marktguru-cli/issues" }, + "keywords": [ + "cli", + "marktguru", + "grocery", + "deals", + "discounts", + "austria", + "germany" + ], "type": "module", "bin": { "marktguru": "dist/cli.js" From 83f74d99dc818f99e6c2e93a10159cf13079f278 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:12:15 +0000 Subject: [PATCH 17/33] chore(deps): update dependency header-generator to v2.1.88 (#7) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 26 +++++++++++++------------- package.json | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 858f956..b44184a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "dependencies": { "commander": "12.1.0", - "header-generator": "2.1.80" + "header-generator": "2.1.88" }, "bin": { "marktguru": "dist/cli.js" @@ -810,12 +810,12 @@ } }, "node_modules/adm-zip": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", - "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", "license": "MIT", "engines": { - "node": ">=12.0" + "node": ">=14.0" } }, "node_modules/baseline-browser-mapping": { @@ -986,12 +986,12 @@ } }, "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==", + "version": "2.1.88", + "resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.88.tgz", + "integrity": "sha512-kxbW6CCsiEAVdBYPont/6ZVOa47Pyfv5ldYFIvj8wmOx9uQOZ4c8wdR0jEf2pE6DeVuIAfmolm+91bYne6/3uA==", "license": "Apache-2.0", "dependencies": { - "adm-zip": "^0.5.9", + "adm-zip": "^0.6.0", "tslib": "^2.4.0" } }, @@ -1009,13 +1009,13 @@ } }, "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==", + "version": "2.1.88", + "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.88.tgz", + "integrity": "sha512-12GkTL1CDaPTQ6gkd8TPwxNtn7t3wSExnWU9qgWfEDh8IqpD1NAVcvzfHphIzULez8WP2DK9YQsDegajjDdTKQ==", "license": "Apache-2.0", "dependencies": { "browserslist": "^4.21.1", - "generative-bayesian-network": "^2.1.80", + "generative-bayesian-network": "2.1.88", "ow": "^0.28.1", "tslib": "^2.4.0" }, diff --git a/package.json b/package.json index bb8aea1..0ec7171 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ }, "dependencies": { "commander": "12.1.0", - "header-generator": "2.1.80" + "header-generator": "2.1.88" }, "devDependencies": { "@types/node": "20.19.43", From ddc9b4ac7e0eb115373043e8b5fb3697ec9bf9fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:13:20 +0000 Subject: [PATCH 18/33] chore(deps): update dependency tsx to v4.23.13 (#8) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 248 +++++++++++++++++++++------------------------- package.json | 2 +- 2 files changed, 113 insertions(+), 137 deletions(-) diff --git a/package-lock.json b/package-lock.json index b44184a..b3a2363 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,14 +18,14 @@ "devDependencies": { "@types/node": "20.19.43", "oxlint": "1.83.0", - "tsx": "4.21.0", + "tsx": "4.23.13", "typescript": "5.9.3" } }, "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" ], @@ -40,9 +40,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" ], @@ -57,9 +57,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" ], @@ -74,9 +74,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" ], @@ -91,9 +91,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" ], @@ -108,9 +108,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" ], @@ -125,9 +125,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" ], @@ -142,9 +142,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" ], @@ -159,9 +159,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" ], @@ -176,9 +176,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" ], @@ -193,9 +193,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" ], @@ -210,9 +210,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" ], @@ -227,9 +227,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" ], @@ -244,9 +244,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" ], @@ -261,9 +261,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" ], @@ -278,9 +278,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" ], @@ -295,9 +295,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" ], @@ -312,9 +312,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" ], @@ -329,9 +329,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" ], @@ -346,9 +346,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" ], @@ -363,9 +363,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" ], @@ -380,9 +380,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" ], @@ -397,9 +397,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" ], @@ -414,9 +414,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" ], @@ -431,9 +431,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" ], @@ -448,9 +448,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" ], @@ -920,9 +920,9 @@ "license": "ISC" }, "node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "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", @@ -933,32 +933,32 @@ "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" + "@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/escalade": { @@ -995,19 +995,6 @@ "tslib": "^2.4.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==", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, "node_modules/header-generator": { "version": "2.1.88", "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.88.tgz", @@ -1119,16 +1106,6 @@ "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==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1136,14 +1113,13 @@ "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==", + "version": "4.23.13", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", + "integrity": "sha512-BL5MGkRln6aDYhb0xbQlEAGw743BaZYWdbWtdJOBriYJboKgUUYCadFp2/FpBBZquBC/ezNBn7wMMPx7FDZUDw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" diff --git a/package.json b/package.json index 0ec7171..d59f799 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "devDependencies": { "@types/node": "20.19.43", "oxlint": "1.83.0", - "tsx": "4.21.0", + "tsx": "4.23.13", "typescript": "5.9.3" } } From 0d8deab266c6694dd5bb37f990d4cfe08d0640b7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:14:16 +0000 Subject: [PATCH 19/33] chore(deps): update dependency @types/node to v22 (#9) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Schroeder --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3a2363..4e755ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "marktguru": "dist/cli.js" }, "devDependencies": { - "@types/node": "20.19.43", + "@types/node": "22.20.2", "oxlint": "1.83.0", "tsx": "4.23.13", "typescript": "5.9.3" @@ -800,9 +800,9 @@ } }, "node_modules/@types/node": { - "version": "20.19.43", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "version": "22.20.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz", + "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d59f799..64c7f35 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "header-generator": "2.1.88" }, "devDependencies": { - "@types/node": "20.19.43", + "@types/node": "22.20.2", "oxlint": "1.83.0", "tsx": "4.23.13", "typescript": "5.9.3" From cfb06c0664cafc7e37d3a8dfd6667cbf5a4e49a3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 18:14:59 +0000 Subject: [PATCH 20/33] chore(deps): update dependency commander to v15 (#10) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Schroeder --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e755ed..9ce6b5e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "license": "MIT", "dependencies": { - "commander": "12.1.0", + "commander": "15.0.0", "header-generator": "2.1.88" }, "bin": { @@ -890,12 +890,12 @@ "license": "CC-BY-4.0" }, "node_modules/commander": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", - "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "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": ">=18" + "node": ">=22.12.0" } }, "node_modules/dot-prop": { diff --git a/package.json b/package.json index 64c7f35..c2fdebf 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "access": "public" }, "dependencies": { - "commander": "12.1.0", + "commander": "15.0.0", "header-generator": "2.1.88" }, "devDependencies": { From 1217052020d09ae8355741ccf7f3cc6abd1ebc27 Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Thu, 17 Sep 2026 20:28:33 +0200 Subject: [PATCH 21/33] chore: lint with eslint and prettier, drop oxlint (#12) Same setup as cdk-ec2-key-pair: typescript-eslint with type-checked and stylistic rules, prettier enforced through eslint, and a Makefile that CI calls. The config is ported to ESM, since this package is type: module. Rule exceptions: quoted object properties keep their case (HTTP headers), variables may be PascalCase (class from a dynamic import), and prefer-nullish-coalescing ignores strings and numbers, where `||` is meant to catch empty values too. Fixes the type-aware findings: JSON.parse and response.json() are typed instead of any, commander action handlers get typed parameters, the two floating search promises are awaited via parseAsync, and fetchFirstOk rethrows an Error rather than an unknown. --- .github/workflows/automerge-schedule.yml | 2 +- .github/workflows/test.yml | 8 +- .gitignore | 2 + .prettierignore | 5 + .prettierrc | 11 + Makefile | 59 + README.md | 33 + SKILL.md | 109 +- eslint.config.js | 65 + package-lock.json | 1668 +++++++++++++++++----- package.json | 10 +- src/api.ts | 55 +- src/auth.ts | 65 +- src/cli.ts | 215 ++- src/commands/login.ts | 17 +- src/commands/search.ts | 78 +- src/config.ts | 20 +- src/query.ts | 43 +- tests/api.test.js | 18 +- tests/format.test.js | 18 +- tests/query.test.js | 28 +- tsconfig-lint.json | 12 + 22 files changed, 1936 insertions(+), 605 deletions(-) create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 Makefile create mode 100644 eslint.config.js create mode 100644 tsconfig-lint.json diff --git a/.github/workflows/automerge-schedule.yml b/.github/workflows/automerge-schedule.yml index 549f67b..a6dfdc8 100644 --- a/.github/workflows/automerge-schedule.yml +++ b/.github/workflows/automerge-schedule.yml @@ -3,7 +3,7 @@ name: Schedule Release Automerge on: schedule: - - cron: '0 9 * * 1' # Every Monday at 9am UTC + - cron: "0 9 * * 1" # Every Monday at 9am UTC workflow_dispatch: jobs: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 198c9a7..ac2761a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,13 +26,13 @@ jobs: cache: npm - name: Install dependencies - run: npm ci + run: make install - name: Linting - run: npx oxlint + run: make eslint - name: Test - run: npm test + run: make test - name: Validate package - run: npm pack --dry-run + run: make validate-package diff --git a/.gitignore b/.gitignore index 3af7e1f..4db0bcb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ yarn-error.log* .DS_Store AGENTS.md CLAUDE.md +.npm/ +*.tgz 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/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 6dac139..3c2d1af 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # marktguru-cli 🧘‍♂️ + [![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) @@ -8,10 +9,13 @@ 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. ## Quick Start (Recommended) + Use `npx` to run without installing anything: + ```bash npx --yes @udondan/marktguru-cli login npx --yes @udondan/marktguru-cli search raw "milch OR soja" @@ -19,47 +23,58 @@ 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 npm ci npm run build @@ -67,12 +82,24 @@ npm start -- --help ``` Dev mode (TS directly): + ```bash npm run dev -- --help ``` +Lint, format and test (same targets CI runs): + +```bash +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) @@ -83,6 +110,7 @@ If no API key is configured, `search` will automatically run `login` to extract 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) @@ -91,21 +119,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 index ed8045a..7edd2f0 100644 --- a/SKILL.md +++ b/SKILL.md @@ -9,27 +9,30 @@ Query grocery deals from Marktguru in Austria and Germany. Supports raw queries, ## 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 | +| 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 | +| `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 @@ -37,13 +40,16 @@ 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 @@ -65,12 +71,12 @@ 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 | +| 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 @@ -83,20 +89,21 @@ 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 | +| 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"` @@ -127,22 +134,22 @@ 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 | | ✓ | | +| 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 | | ✓ | | --- @@ -183,14 +190,14 @@ npx @udondan/marktguru-cli search raw "Cola" --limit 3 --json } ``` -| Field | Description | -|-------|-------------| -| `title` | Product name and brand | -| `price` | Current offer price (EUR) | -| `retailer` | Store name | -| `expires` | Offer expiration date (YYYY-MM-DD) | +| 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) | +| `externalUrl` | Direct link to retailer (optional) | --- @@ -231,12 +238,12 @@ npx @udondan/marktguru-cli 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. | +| 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. | --- 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 9ce6b5e..51abcc9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,9 +17,37 @@ }, "devDependencies": { "@types/node": "22.20.2", - "oxlint": "1.83.0", + "eslint": "10.10.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "prettier": "3.9.7", "tsx": "4.23.13", - "typescript": "5.9.3" + "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": { @@ -464,327 +492,214 @@ "node": ">=18" } }, - "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.83.0.tgz", - "integrity": "sha512-0yGY24EwsLk5YDe6F+VkmZyRHSwJDALa3nIrPpq7FXmp2lV2d0TzvBCGeZk+wgiULRGr5blhyr4QMp5KCXJUqA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-android-arm64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.83.0.tgz", - "integrity": "sha512-hHfJ0vc17A4iUjH5p9BsTUPYbYRNxGpvD2lbu1aBRk54bzNIx9o5TtYF39QPZcV95DagZd+4DEAw2RH3G2ZsMg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.83.0.tgz", - "integrity": "sha512-hsOjYjszLb/3zym/TkzUMPAoQlTJcuzSyEPOAyA+skXJIX9M0o+4JfOtqopX/Vf4hSLrJ98j0nvFo23gzk8auQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.83.0.tgz", - "integrity": "sha512-mjh5oH2EA+wl5yRJYT9K9G61O2zFlpuv+yf2JwZOi0+dq2FnTUtm1h8i+5Ik0fXPWIu/k84I1psZR9aQsLAnyA==", - "cpu": [ - "x64" - ], + "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", - "optional": true, - "os": [ - "darwin" - ], + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.83.0.tgz", - "integrity": "sha512-fNHr64/YaO8YssuoDVC8+F4Uk5enR86q5uxfHkQrjAPs1dbAILOrD2uaud+J7MO8Fx774g44ERLD0IGIvZE48w==", - "cpu": [ - "x64" - ], + "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", - "optional": true, - "os": [ - "freebsd" - ], + "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.83.0.tgz", - "integrity": "sha512-Qpwy3zzAwMj+8/lyYItHmkSMwbkprFNWTK7jPYDOxSyxEhaSLOWYUTCMkjF334J8/WD0nznCCsoBbIH6hpsuIw==", - "cpu": [ - "arm" - ], + "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", - "optional": true, - "os": [ - "linux" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.83.0.tgz", - "integrity": "sha512-s+BirYLFq7JL2k9sP0XI3ZXJ9dYvJ8sX3jLCLoag7tt+zrSHpZxP0jqznfL+Gdgwu7ay0dYgGYJXrQvq3iWloA==", - "cpu": [ - "arm" - ], + "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": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.83.0.tgz", - "integrity": "sha512-7lihXt3vKr+GIyapNbHrnFHm/biiW30le6Zv/DExbAFPF6YwCQXVFlONPFehxs0CpGO4CBfYPM9rdDT+XMoIlg==", - "cpu": [ - "arm64" - ], + "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": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.83.0.tgz", - "integrity": "sha512-q63JalLYVkZiZvls1z3PPUnpmQluOMXp0khqQMznCeAPLGydfNY8JhvuA4WlK57JfrvikU8wB5lPVveqpIXvew==", - "cpu": [ - "arm64" - ], + "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": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.83.0.tgz", - "integrity": "sha512-krQmDF+dRbxvdqVPV88ZuOoPPu8X5BuqDA8Hd+qcS4YMRQCb+nexA57DazgGsc/rGdKBe3QmV0mnv0bdpW/p5g==", - "cpu": [ - "ppc64" - ], + "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": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.83.0.tgz", - "integrity": "sha512-MmOl8Y6txEAXZU1RG8Rr264jQ6D7VPmqFsU/45x/FeWsGe32hklTqGrLE6UxHzp5Rjt0wP+20tY8YXKgSFB3mw==", - "cpu": [ - "riscv64" - ], + "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": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.83.0.tgz", - "integrity": "sha512-u1rMymh0W3JZkq370kzQsYPULGWqhE09pZRqnZvUSoYaI9pVO5yVX+iYIslmWuEgwuzH9YAaOsScJiobWCHoOw==", - "cpu": [ - "riscv64" - ], + "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": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.18.0" } }, - "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.83.0.tgz", - "integrity": "sha512-y0zK3HNwGysu7rqtE+BQG/d0bx5gh/KwlOtghN8oWeK1KcWzeaLqtZrbm8owqdma1lFyrce/hTO5ismuNu+INQ==", - "cpu": [ - "s390x" - ], + "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": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.18.0" } }, - "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.83.0.tgz", - "integrity": "sha512-rS5gM0NgD7ngmuJmbIehsidtrOwKkLFwCQbKEeb9KuyQrrWNq5Zkn0uV6AYdXOMJ0grrWEiLwBuvMxt8w5vsNw==", - "cpu": [ - "x64" - ], + "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": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.18.0" } }, - "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.83.0.tgz", - "integrity": "sha512-W2IH4EtpcPaWcvNGCA95YoDg4vxqE/ZiPCi3arrxEEpsK7+JQN9WYwrlYFx9pcdP6KPXqRqkv3zdQPHcx7b6YQ==", - "cpu": [ - "x64" - ], + "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": "MIT", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.83.0.tgz", - "integrity": "sha512-6LyKkUyoajssTPLlZmDbZIbu4IZ5B4bGuRUnBgCGpEvHP3FQMaYITncHA/unPUo7q+Z+pIu2HhdkQ+8d1SG7iA==", - "cpu": [ - "arm64" - ], + "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": "MIT", - "optional": true, - "os": [ - "openharmony" - ], + "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.83.0.tgz", - "integrity": "sha512-Uz/fObEtF0jmNJQJ8CGRBKfefYstS0/wjD3s6IGzP8nUwsJykHQJBiN3npHwKiGRGn/vvBEgNr4B3cCzmmatvg==", - "cpu": [ - "arm64" - ], + "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, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" } }, - "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.83.0.tgz", - "integrity": "sha512-u7XcvPW6Bk58tY5iWs2ESb0vJjoE/kuSpHxopbwp/p3ZtWVQXZ6wor5w3ssVTHOqd/v8b+QdhSFWQ4grEUNWpA==", - "cpu": [ - "ia32" - ], + "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", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "license": "MIT" }, - "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.83.0.tgz", - "integrity": "sha512-LZRubd7ph13QmAg4fFecTYVZkiYbROR2Htaxh/ufWRkDhPOm2wrwaEYR89e0YpPFD3dqBrPoxS7myBw5hmYA7Q==", - "cpu": [ - "x64" - ], + "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, "license": "MIT", - "optional": true, - "os": [ - "win32" - ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" } }, "node_modules/@sindresorhus/is": { @@ -799,6 +714,27 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "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": "22.20.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz", @@ -809,34 +745,327 @@ "undici-types": "~6.21.0" } }, - "node_modules/adm-zip": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", - "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", + "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": { + "@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": ">=14.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "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/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==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" + "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/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" - }, - { + "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": { + "@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": "^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/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": "^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/@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": { + "@typescript-eslint/types": "8.70.0", + "@typescript-eslint/visitor-keys": "8.70.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "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": { + "@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" + }, + "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.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "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": "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": "^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/@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", + "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/@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": { + "@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": { + "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": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adm-zip": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", + "license": "MIT", + "engines": { + "node": ">=14.0" + } + }, + "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", + "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/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==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "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/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" }, @@ -860,6 +1089,20 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "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/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -898,6 +1141,46 @@ "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/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -970,6 +1253,313 @@ "node": ">=6" } }, + "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.10.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", + "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", + "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", @@ -995,6 +1585,32 @@ "tslib": "^2.4.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/header-generator": { "version": "2.1.88", "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.88.tgz", @@ -1010,6 +1626,56 @@ "node": ">=16.0.0" } }, + "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/is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -1019,6 +1685,67 @@ "node": ">=8" } }, + "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/lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -1026,12 +1753,60 @@ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", "license": "MIT" }, + "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/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/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/ow": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/ow/-/ow-0.28.2.tgz", @@ -1051,53 +1826,56 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/oxlint": { - "version": "1.83.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.83.0.tgz", - "integrity": "sha512-cyDzSzaw3uzP0TeCeq3lLRPPoaUxkbB4ZOXj+kn+5r+BX9V+4bNVGk9lxer+WrgcpebH4JxLlJ3KQjveVztOLQ==", + "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", - "bin": { - "oxlint": "bin/oxlint" + "dependencies": { + "yocto-queue": "^0.1.0" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/oxc-project" - }, - "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.83.0", - "@oxlint/binding-android-arm64": "1.83.0", - "@oxlint/binding-darwin-arm64": "1.83.0", - "@oxlint/binding-darwin-x64": "1.83.0", - "@oxlint/binding-freebsd-x64": "1.83.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.83.0", - "@oxlint/binding-linux-arm-musleabihf": "1.83.0", - "@oxlint/binding-linux-arm64-gnu": "1.83.0", - "@oxlint/binding-linux-arm64-musl": "1.83.0", - "@oxlint/binding-linux-ppc64-gnu": "1.83.0", - "@oxlint/binding-linux-riscv64-gnu": "1.83.0", - "@oxlint/binding-linux-riscv64-musl": "1.83.0", - "@oxlint/binding-linux-s390x-gnu": "1.83.0", - "@oxlint/binding-linux-x64-gnu": "1.83.0", - "@oxlint/binding-linux-x64-musl": "1.83.0", - "@oxlint/binding-openharmony-arm64": "1.83.0", - "@oxlint/binding-win32-arm64-msvc": "1.83.0", - "@oxlint/binding-win32-ia32-msvc": "1.83.0", - "@oxlint/binding-win32-x64-msvc": "1.83.0" + "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" }, - "peerDependencies": { - "oxlint-tsgolint": ">=7.0.2001", - "vite-plus": "*" + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "oxlint-tsgolint": { - "optional": true - }, - "vite-plus": { - "optional": true - } + "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/picocolors": { @@ -1106,6 +1884,170 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "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.7", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.7.tgz", + "integrity": "sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==", + "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/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1131,6 +2073,19 @@ "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", @@ -1145,6 +2100,30 @@ "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": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -1182,6 +2161,16 @@ "browserslist": ">= 4.21.0" } }, + "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/vali-date": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", @@ -1190,6 +2179,45 @@ "engines": { "node": ">=0.10.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 c2fdebf..e577899 100644 --- a/package.json +++ b/package.json @@ -36,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": [ @@ -52,8 +54,12 @@ }, "devDependencies": { "@types/node": "22.20.2", - "oxlint": "1.83.0", + "eslint": "10.10.0", + "eslint-config-prettier": "10.1.8", + "eslint-plugin-prettier": "5.5.6", + "prettier": "3.9.7", "tsx": "4.23.13", - "typescript": "5.9.3" + "typescript": "5.9.3", + "typescript-eslint": "8.70.0" } } diff --git a/src/api.ts b/src/api.ts index 05da5e0..682c8fb 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,8 +1,15 @@ -import { getConfig, DEFAULT_ZIP_CODE, DEFAULT_COUNTRY, VALID_COUNTRIES } from "./config.js"; +import { + getConfig, + DEFAULT_ZIP_CODE, + DEFAULT_COUNTRY, + VALID_COUNTRIES, +} from './config.js'; 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(", ")}`); + throw new Error( + `Unsupported country "${country}". Valid options: ${VALID_COUNTRIES.join(', ')}`, + ); } return `https://api.marktguru.${country}/api/v1`; } @@ -20,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; @@ -39,9 +46,9 @@ 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 }[]; }; } @@ -66,7 +73,7 @@ export async function search(options: SearchOptions): Promise { 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), @@ -74,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 = `${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 { @@ -101,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 f3aceff..6179c99 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,34 +1,37 @@ -import { VALID_COUNTRIES } from "./config.js"; +import { VALID_COUNTRIES } from './config.js'; interface ExtractOptions { log?: (message: string) => void; country?: string; } -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 { HeaderGenerator } = await import('header-generator'); const generator = new HeaderGenerator({ - browsers: [{ name: "chrome", minVersion: 110 }], - devices: ["desktop"], - operatingSystems: ["macos"], + browsers: [{ name: 'chrome', minVersion: 110 }], + devices: ['desktop'], + operatingSystems: ['macos'], }); - return generator.getHeaders({ httpVersion: "2" }); + 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", + '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 { +async function fetchText( + url: string, + headers: Record, +): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000); try { @@ -43,17 +46,17 @@ 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, baseUrl: string): string[] { @@ -62,9 +65,9 @@ function extractScriptUrls(html: string, baseUrl: string): string[] { let match: RegExpExecArray | null; while ((match = regex.exec(html))) { let src = match[1]; - if (src.startsWith("//")) src = `https:${src}`; - if (src.startsWith("/")) src = `${baseUrl}${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]; } @@ -86,7 +89,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); } } @@ -101,8 +104,8 @@ async function validateKey(apiKey: string, apiBase: string): Promise { try { const res = await fetch(url, { headers: { - "x-apikey": apiKey, - "accept": "application/json", + 'x-apikey': apiKey, + accept: 'application/json', }, signal: controller.signal, }); @@ -112,11 +115,15 @@ async function validateKey(apiKey: string, apiBase: string): Promise { } } -export async function extractApiKey(options: ExtractOptions = {}): Promise { +export async function extractApiKey( + options: ExtractOptions = {}, +): Promise { const log = options.log; - const country = options.country ?? "at"; + const country = options.country ?? 'at'; if (!(VALID_COUNTRIES as readonly string[]).includes(country)) { - throw new Error(`Unsupported country "${country}". Valid options: ${VALID_COUNTRIES.join(", ")}`); + 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`; @@ -130,7 +137,7 @@ 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/de 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,50 +143,74 @@ program }); program - .command("set-country ") - .description("Set default country for searches (at, de)") - .option("-j, --json", "Output JSON") - .action(async (code: string, 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(", ")}`); + 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 }) }); + 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 })); + 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."); + 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) => { + .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, - country: config.country || DEFAULT_COUNTRY, - 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(" Country:", config.country || `(default: ${DEFAULT_COUNTRY})`); - 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); } }); @@ -160,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 33218da..eddb968 100644 --- a/src/commands/login.ts +++ b/src/commands/login.ts @@ -1,5 +1,5 @@ -import { saveConfig, getConfig } 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,11 +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 config = await getConfig(); - const apiKey = await extractApiKey({ log: json ? undefined : log, country: config.country }); + 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 21f3005..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, @@ -78,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 @@ -91,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}`); @@ -101,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}`); @@ -109,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 { @@ -118,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 @@ -132,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); } @@ -154,19 +154,30 @@ function emitWarnings(warnings: string[]): void { } } -async function ensureApiKey(json?: boolean, country?: string): 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, country: country ?? config.country }); + const apiKey = await extractApiKey({ + log, + country: country ?? config.country, + }); await saveConfig({ apiKey }); return apiKey; } -async function runSearch(query: string, options: SearchCommandOptions): Promise { +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); @@ -187,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; @@ -204,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({ @@ -239,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 42a1fb7..18f61b3 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,6 +1,6 @@ -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; @@ -9,18 +9,18 @@ export interface Config { configPath: string; } -export const DEFAULT_ZIP_CODE = "1010"; // Vienna -export const DEFAULT_COUNTRY = "at"; -export const VALID_COUNTRIES = ["at", "de"] as const; +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"); - const parsed = JSON.parse(data); + 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 { 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 index 6f25342..20aa0f1 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -1,15 +1,15 @@ -import test from "node:test"; -import assert from "node:assert/strict"; -import { getApiBase } from "../dist/api.js"; +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 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 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/); +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/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"] +} From 95d2a7373f5994fd92d6bad5e14eb89fd8eb1066 Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Thu, 17 Sep 2026 20:32:15 +0200 Subject: [PATCH 22/33] fix: keep --version in sync with the released version (#13) The version passed to commander was hardcoded, so the CLI would keep reporting 0.1.0 after any release. Annotate the line with x-release-please-version and list src/cli.ts as an extra-file, so release-please bumps it along with package.json. --- release-please-config.json | 4 +++- src/cli.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/release-please-config.json b/release-please-config.json index c5338ec..b82ce52 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -9,6 +9,8 @@ "prerelease": false, "bootstrap-sha": "9855964f564947c4c8532bdc1523f420780591b3", "packages": { - ".": {} + ".": { + "extra-files": ["src/cli.ts"] + } } } diff --git a/src/cli.ts b/src/cli.ts index c063f33..890746a 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -21,7 +21,7 @@ const program = new Command(); program .name('marktguru') .description('CLI for Marktguru supermarket deals (AT/DE)') - .version('0.1.0') + .version('0.1.0') // x-release-please-version .option('-j, --json', 'Output JSON (for all commands)'); program.addHelpText( From 0be5083e04d3be066094e436d43ef802e14ebdbc Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Thu, 17 Sep 2026 20:45:07 +0200 Subject: [PATCH 23/33] chore: cache npm downloads in CI and stop automerging majors (#14) The test workflow relied on setup-node's cache, which never matched the cache directory make install writes to (--cache .npm), so every run started cold. Cache .npm explicitly, like cdk-ec2-key-pair does. Also adds the concurrency group and bash shell defaults from the same reference setup, and stops Renovate from automerging major updates. --- .github/workflows/test.yml | 25 ++++++++++++++++++++++++- renovate.json | 4 ++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac2761a..5cc4418 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -4,6 +4,10 @@ name: Test permissions: contents: read +concurrency: + group: test + cancel-in-progress: false + on: pull_request: branches: @@ -16,14 +20,33 @@ on: 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 - cache: npm + + - 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 diff --git a/renovate.json b/renovate.json index 5e88c3e..82ff54a 100644 --- a/renovate.json +++ b/renovate.json @@ -13,6 +13,10 @@ { "matchPackageNames": ["/.*/"], "semanticCommitType": "chore" + }, + { + "matchUpdateTypes": ["major"], + "automerge": false } ] } From ef509ce74c6eb387e67e1ca866c8c8bbbd29db47 Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Thu, 17 Sep 2026 20:59:37 +0200 Subject: [PATCH 24/33] chore: add AGENTS.md with contributor guidance for coding agents (#15) --- .gitignore | 14 +++----- AGENTS.md | 101 +++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 1 + 3 files changed, 107 insertions(+), 9 deletions(-) create mode 100644 AGENTS.md create mode 120000 CLAUDE.md diff --git a/.gitignore b/.gitignore index 4db0bcb..4f1ab39 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,7 @@ -scripts/try-http-login.* -node_modules/ -dist/ -npm-debug.log* -pnpm-debug.log* -yarn-error.log* -.DS_Store -AGENTS.md -CLAUDE.md +.claude .npm/ *.tgz +dist/ +node_modules/ +npm-debug.log* +scripts/try-http-login.* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..17fdaed --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,101 @@ +# 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. +- **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 + +`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. 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 From 9992cec1e278edc37b67a3080de9daf3497d8c1a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 21:13:56 +0200 Subject: [PATCH 25/33] chore(deps): update dependency @types/node to v24 (#16) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51abcc9..b9ebcc0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "marktguru": "dist/cli.js" }, "devDependencies": { - "@types/node": "22.20.2", + "@types/node": "24.13.4", "eslint": "10.10.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.6", @@ -736,13 +736,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.20.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.2.tgz", - "integrity": "sha512-xlvWf4Vs9n1PEVYwP1n4vvG07M6y8WgvJ2t0vbrWTmijsIHp1cS+uJ2kMIRdY3nHZK0nCYKrPeD171+SzF4/zw==", + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.18.0" } }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -2125,9 +2125,9 @@ } }, "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==", + "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" }, diff --git a/package.json b/package.json index e577899..2ba1789 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "header-generator": "2.1.88" }, "devDependencies": { - "@types/node": "22.20.2", + "@types/node": "24.13.4", "eslint": "10.10.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.6", From 51a06f4136d8a0973de9eca260e02a3f076a5035 Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Thu, 17 Sep 2026 21:15:42 +0200 Subject: [PATCH 26/33] chore: move SKILL.md into skills/ and document npx skills install (#17) --- AGENTS.md | 8 +++++--- README.md | 8 +++++++- SKILL.md => skills/marktguru-grocery-deals/SKILL.md | 0 3 files changed, 12 insertions(+), 4 deletions(-) rename SKILL.md => skills/marktguru-grocery-deals/SKILL.md (100%) diff --git a/AGENTS.md b/AGENTS.md index 17fdaed..09b6d20 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,6 +96,8 @@ weekly rather than per merge. The resulting `v*` tag triggers ## Keep in sync -`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. +`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/README.md b/README.md index 3c2d1af..84ccd18 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,13 @@ This is a maintained fork of [manmal/marktguru-cli](https://github.com/manmal/ma ## 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) diff --git a/SKILL.md b/skills/marktguru-grocery-deals/SKILL.md similarity index 100% rename from SKILL.md rename to skills/marktguru-grocery-deals/SKILL.md From 79edddb340aa6bdb97dfdddaf2d533f616319b25 Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Thu, 17 Sep 2026 21:16:21 +0200 Subject: [PATCH 27/33] chore(main): release 1.0.0 (#3) --- .release-please-manifest.json | 2 +- CHANGELOG.md | 24 ++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- src/cli.ts | 2 +- 5 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 466df71..37fcefa 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0" + ".": "1.0.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8310ce4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +## [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/package-lock.json b/package-lock.json index b9ebcc0..f59d8c9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@udondan/marktguru-cli", - "version": "0.1.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@udondan/marktguru-cli", - "version": "0.1.0", + "version": "1.0.0", "license": "MIT", "dependencies": { "commander": "15.0.0", diff --git a/package.json b/package.json index 2ba1789..e857000 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@udondan/marktguru-cli", - "version": "0.1.0", + "version": "1.0.0", "description": "CLI for Marktguru supermarket deals in Austria and Germany", "license": "MIT", "author": { diff --git a/src/cli.ts b/src/cli.ts index 890746a..40bbaed 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -21,7 +21,7 @@ const program = new Command(); program .name('marktguru') .description('CLI for Marktguru supermarket deals (AT/DE)') - .version('0.1.0') // x-release-please-version + .version('1.0.0') // x-release-please-version .option('-j, --json', 'Output JSON (for all commands)'); program.addHelpText( From fdb70d072693d2ac99c33ccc54060a61c13d40a1 Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Fri, 18 Sep 2026 08:12:24 +0200 Subject: [PATCH 28/33] chore: drop header-generator dependency (#18) * chore: drop header-generator dependency The marktguru site serves identical responses with static headers, so generated browser fingerprints add nothing. Removing it also drops the deprecated lodash.isequal (via ow@0.28) that warned on install. Release-As: 1.0.1 * test: add live end-to-end tests for login and search Runs the built CLI against marktguru.at and marktguru.de with an isolated HOME, so search has to auto-login by scraping. Catches the scraper breaking when Marktguru ships a new frontend. --- AGENTS.md | 4 +- package-lock.json | 242 +-------------------------------------------- package.json | 3 +- src/auth.ts | 27 ++--- tests/live.test.js | 58 +++++++++++ 5 files changed, 70 insertions(+), 264 deletions(-) create mode 100644 tests/live.test.js diff --git a/AGENTS.md b/AGENTS.md index 09b6d20..d97f5d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,7 +68,9 @@ These are the things that are easy to break without noticing. 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. + 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 diff --git a/package-lock.json b/package-lock.json index f59d8c9..dc6ee28 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,7 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "commander": "15.0.0", - "header-generator": "2.1.88" + "commander": "15.0.0" }, "bin": { "marktguru": "dist/cli.js" @@ -702,18 +701,6 @@ "url": "https://opencollective.com/pkgr" } }, - "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==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -998,15 +985,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/adm-zip": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", - "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", - "license": "MIT", - "engines": { - "node": ">=14.0" - } - }, "node_modules/ajv": { "version": "6.15.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", @@ -1034,15 +1012,6 @@ "node": "18 || 20 || >=22" } }, - "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==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, "node_modules/brace-expansion": { "version": "5.0.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", @@ -1056,39 +1025,6 @@ "node": "20 || >=22" } }, - "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", - "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" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, "node_modules/cacheable": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", @@ -1103,35 +1039,6 @@ "qified": "^0.10.1" } }, - "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", - "engines": { - "node": ">=6" - } - }, - "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/commander": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", @@ -1181,27 +1088,6 @@ "dev": true, "license": "MIT" }, - "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", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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/esbuild": { "version": "0.28.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", @@ -1244,15 +1130,6 @@ "@esbuild/win32-x64": "0.28.2" } }, - "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" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -1575,16 +1452,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/generative-bayesian-network": { - "version": "2.1.88", - "resolved": "https://registry.npmjs.org/generative-bayesian-network/-/generative-bayesian-network-2.1.88.tgz", - "integrity": "sha512-kxbW6CCsiEAVdBYPont/6ZVOa47Pyfv5ldYFIvj8wmOx9uQOZ4c8wdR0jEf2pE6DeVuIAfmolm+91bYne6/3uA==", - "license": "Apache-2.0", - "dependencies": { - "adm-zip": "^0.6.0", - "tslib": "^2.4.0" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1611,21 +1478,6 @@ "node": ">=20" } }, - "node_modules/header-generator": { - "version": "2.1.88", - "resolved": "https://registry.npmjs.org/header-generator/-/header-generator-2.1.88.tgz", - "integrity": "sha512-12GkTL1CDaPTQ6gkd8TPwxNtn7t3wSExnWU9qgWfEDh8IqpD1NAVcvzfHphIzULez8WP2DK9YQsDegajjDdTKQ==", - "license": "Apache-2.0", - "dependencies": { - "browserslist": "^4.21.1", - "generative-bayesian-network": "2.1.88", - "ow": "^0.28.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/hookified": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", @@ -1676,15 +1528,6 @@ "node": ">=0.10.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==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -1746,13 +1589,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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/minimatch": { "version": "10.2.6", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", @@ -1783,12 +1619,6 @@ "dev": true, "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/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -1807,25 +1637,6 @@ "node": ">= 0.8.0" } }, - "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==", - "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" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -1878,12 +1689,6 @@ "node": ">=8" } }, - "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/picomatch": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", @@ -2048,12 +1853,6 @@ "typescript": ">=4.8.4" } }, - "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.23.13", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.13.tgz", @@ -2131,36 +1930,6 @@ "dev": true, "license": "MIT" }, - "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" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -2171,15 +1940,6 @@ "punycode": "^2.1.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==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index e857000..c584986 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,7 @@ "access": "public" }, "dependencies": { - "commander": "15.0.0", - "header-generator": "2.1.88" + "commander": "15.0.0" }, "devDependencies": { "@types/node": "24.13.4", diff --git a/src/auth.ts b/src/auth.ts index 6179c99..6bd4f56 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -8,25 +8,12 @@ interface ExtractOptions { 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', - }; - } -} +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, @@ -127,7 +114,7 @@ export async function extractApiKey( } const baseUrl = `https://www.marktguru.${country}`; const apiBase = `https://api.marktguru.${country}/api/v1`; - const headers = await maybeGetHeaders(); + const headers = BROWSER_HEADERS; const entryUrls = [ `${baseUrl}/`, 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 }); + } + }, + ); +} From b769fcdbc21c753bbd744a305fd959e4f1111438 Mon Sep 17 00:00:00 2001 From: Daniel Schroeder Date: Fri, 18 Sep 2026 08:18:18 +0200 Subject: [PATCH 29/33] chore(main): release 1.0.1 (#19) --- .release-please-manifest.json | 2 +- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- src/cli.ts | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 37fcefa..8d7e5f1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "1.0.0" + ".": "1.0.1" } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8310ce4..3979347 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 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) diff --git a/package-lock.json b/package-lock.json index dc6ee28..fa9b6e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@udondan/marktguru-cli", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@udondan/marktguru-cli", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "commander": "15.0.0" diff --git a/package.json b/package.json index c584986..5681ada 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@udondan/marktguru-cli", - "version": "1.0.0", + "version": "1.0.1", "description": "CLI for Marktguru supermarket deals in Austria and Germany", "license": "MIT", "author": { diff --git a/src/cli.ts b/src/cli.ts index 40bbaed..0882ac0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -21,7 +21,7 @@ const program = new Command(); program .name('marktguru') .description('CLI for Marktguru supermarket deals (AT/DE)') - .version('1.0.0') // x-release-please-version + .version('1.0.1') // x-release-please-version .option('-j, --json', 'Output JSON (for all commands)'); program.addHelpText( From 977c1d3dd3bfac00938308c21540720d2b7f70a0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 00:36:38 +0000 Subject: [PATCH 30/33] chore(deps): update dependency @types/node to v24.13.5 (#20) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index fa9b6e1..dc6e45d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "marktguru": "dist/cli.js" }, "devDependencies": { - "@types/node": "24.13.4", + "@types/node": "24.13.5", "eslint": "10.10.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.6", @@ -723,9 +723,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.13.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", - "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", + "version": "24.13.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.5.tgz", + "integrity": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 5681ada..919c3bc 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "commander": "15.0.0" }, "devDependencies": { - "@types/node": "24.13.4", + "@types/node": "24.13.5", "eslint": "10.10.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.6", From a53c5bebabec702a4dfee0459e6bb69133d1b3b6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2026 22:51:21 +0000 Subject: [PATCH 31/33] chore(deps): update dependency prettier to v3.9.8 (#21) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index dc6e45d..62819a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "eslint": "10.10.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.6", - "prettier": "3.9.7", + "prettier": "3.9.8", "tsx": "4.23.13", "typescript": "5.9.3", "typescript-eslint": "8.70.0" @@ -1713,9 +1713,9 @@ } }, "node_modules/prettier": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.7.tgz", - "integrity": "sha512-T3mF5P7HFzZVLDQuCUNtJj6WK1pcpLMweMNUVwGb01bLNkH9AkKxtZvmInw8K6bnn2O3d6/5RHl6zSHiBAD0Og==", + "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": { diff --git a/package.json b/package.json index 919c3bc..5a02445 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "eslint": "10.10.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.6", - "prettier": "3.9.7", + "prettier": "3.9.8", "tsx": "4.23.13", "typescript": "5.9.3", "typescript-eslint": "8.70.0" From cbf24d64b466bf8442ec7351e939d26343339175 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 00:15:22 +0000 Subject: [PATCH 32/33] chore(deps): update dependency eslint to v10.11.0 (#22) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62819a3..0461443 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ }, "devDependencies": { "@types/node": "24.13.5", - "eslint": "10.10.0", + "eslint": "10.11.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.6", "prettier": "3.9.8", @@ -1144,9 +1144,9 @@ } }, "node_modules/eslint": { - "version": "10.10.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.10.0.tgz", - "integrity": "sha512-NPXn6r5zl4uET1DAVPaOwzX3rut4c0wcmw3dWJAfOsTM5+TogXo0DDjz8pwm/hL8cyVNpHqeK4JpN0NjnyFFNw==", + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", + "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", "dev": true, "license": "MIT", "workspaces": [ diff --git a/package.json b/package.json index 5a02445..3f8e617 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ }, "devDependencies": { "@types/node": "24.13.5", - "eslint": "10.10.0", + "eslint": "10.11.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.6", "prettier": "3.9.8", From 86f63382a2db591cc3a07b1e4028f3f32c6978ba Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 03:01:22 +0000 Subject: [PATCH 33/33] chore(deps): update dependency @types/node to v24.13.6 (#23) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0461443..4cbe488 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "marktguru": "dist/cli.js" }, "devDependencies": { - "@types/node": "24.13.5", + "@types/node": "24.13.6", "eslint": "10.11.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.6", @@ -723,9 +723,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.13.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.5.tgz", - "integrity": "sha512-TXyindR+lBr22aJIdMQzCFHPHR6cR4js838mRDCSz5hOKWZvZwsXSSiXDmjRj4iJmgl+sR9O+1mkoVBSMadNug==", + "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": { diff --git a/package.json b/package.json index 3f8e617..14a07c8 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "commander": "15.0.0" }, "devDependencies": { - "@types/node": "24.13.5", + "@types/node": "24.13.6", "eslint": "10.11.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.6",