commit c8f8bbc5ac81825fa435eeff4e57ffe779ca5ee9 Author: Rawman Grey Date: Sun Oct 25 11:19:54 2026 +0300 feat: pre-generate Directus image previews Bundle extension (upload hook + API endpoint + admin module) that warms Directus image transform cache in-process with an explicit acceptFormat, so the generated file matches the browser's cache key byte-for-byte. - resumable, window-aware backfill with progress + cancel - system + custom presets, effective-format reporting, shadow warnings - storage breakdown (previews vs originals), human-readable API fields - diagnostics: resolve-plan prediction, per-file generate, ops ring buffer - isolated dev/staging compose stack + deploy/rollback scripts Verified on Directus 11.17.4: 40/40 tests, hash-match gate passed, idempotent backfill, custom fixed-format presets, error surfacing. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..21df11a --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +node_modules +dist +dev/extension +dev/.smoke.env diff --git a/.opencode/.gitignore b/.opencode/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.opencode/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/.opencode/feature_list.json b/.opencode/feature_list.json new file mode 100644 index 0000000..9186709 --- /dev/null +++ b/.opencode/feature_list.json @@ -0,0 +1,206 @@ +{ + "$schema": [ + "/home/rawman/.config/opencode/schemas/feature_list.schema.json" + ], + "features": [ + { + "feature_id": "F01", + "title": "Bundle scaffold (module + hook + endpoint), build pipeline, and local load", + "acceptance_steps": [ + "Run `npm install` → resolves the pinned devDependencies including vue.", + "Run `npm run build` → emits `dist/app.js` and `dist/api.js`; `npm run typecheck` reports 0 errors.", + "`directus-extension link` (or the dev compose stack) loads the bundle into a local Directus 11.17.4 with no error.", + "The `image-previews` module appears in the admin sidebar, `GET /image-previews/status` answers, and a test upload emits the hook debug line.", + "`npm run publish` copies `dist/` + `package.json` to the configured deploy target (staging only at this stage)." + ], + "passes": false, + "notes": [ + "Manifest is a bundle: `path:{app,api}`, `entries[{type:module|hook|endpoint,name,source}]`, `host:^10.10.0 || ^11.0.0`.", + "Module sidebar fields (id/name/icon/routes) live in the entrypoint object, not package.json." + ] + }, + { + "feature_id": "F02", + "title": "Staging environment harness + deploy/rollback scripts (safe testing)", + "acceptance_steps": [ + "`dev/compose.dev.yml` brings up Directus 11.17.4 with its own postgres + redis on a dedicated port, with the extension mounted.", + "The stack seeds from a **restored anonymized DB dump** and a **copy of uploads** (`dev/seed/`), so previews can be tested against realistic data.", + "A documented script builds and deploys to the staging instance, and a rollback script restores the previous `dist/` and disables the extension.", + "Confirmed the local/staging stack shares NOTHING with prod (separate DB, separate storage, separate ports).", + "README documents how to start/stop/seed the stack and the exact anonymization rules used for the dump." + ], + "passes": false, + "blocked_by": [ + "F01" + ], + "notes": [ + "This must land before any feature is tested on real-ish data; never test on the prod instance.", + "Decide staging placement (second compose project on land-nest vs a separate dev box) — goal.md open item #5.", + "Dev harness (dev/compose.dev.yml + sync-extension.sh) verified; staging compose/deploy/rollback templates added but not executed remotely." + ] + }, + { + "feature_id": "F03", + "title": "Persisted settings + progress-state store and the config/status API endpoints", + "acceptance_steps": [ + "`src/lib/settings.ts` reads/writes a JSON settings file under `IMAGE_PREVIEWS_STATE_DIR` (default `/directus/extensions/.image-previews`), merged over env defaults; keys: `enabled` (default false), `windowStart`, `windowEnd`, `concurrency`, `presets`, `formats` (`avif,webp`), `onUpload`, `debug`.", + "`GET /image-previews/config` returns effective settings; `PUT /image-previews/config` validates and persists them; invalid payloads return 400 without corrupting the file.", + "`src/lib/state.ts` appends completed `{file_id,preset_key,format}` records to `state.jsonl` and loads them back; a resume helper returns completed pairs + totals.", + "`GET /image-previews/status` returns `{ running, total, done, remaining, generated, hits, failed, rate, eta, lastRun, nextWindow }`.", + "Unit tests cover settings merge/defaults/validation, config round-trip, state append/resume, status aggregation.", + "`npm test` green and `npm run typecheck` 0 errors." + ], + "passes": true, + "blocked_by": [ + "F02" + ], + "notes": [ + "No native Directus extension-settings store exists; a JSON file under a persisted writable dir is the pragmatic choice.", + "All state is file-backed (PM2 cluster, 8 workers — never in-memory)." + ] + }, + { + "feature_id": "F04", + "title": "Preset registry (system + settings, dynamic) and the in-process warmer core", + "acceptance_steps": [ + "`src/lib/presets.ts` returns the merged preset list in controller order (`[...SYSTEM_ASSET_ALLOW_LIST, ...storage_asset_presets]`) with first-match-wins on duplicate keys.", + "System preset objects come from an `@directus/api` constant import if available, else the version-pinned copy in `src/lib/system-presets.ts`; the mechanism used is recorded in progress.txt.", + "`src/lib/warmer.ts` calls `new services.AssetsService({ schema }).getAsset(fileId, { transformationParams: , acceptFormat })` then destroys/unpipes the returned stream.", + "For a `format:'auto'` preset the warmer emits BOTH `avif` and `webp`; a fixed-`format` preset emits one; a resize-only preset emits none.", + "Concurrency honours the configured limit; a per-preset/format failure (concurrency error, `IllegalAssetTransformation`, missing dims) is caught, logged, and doesn't abort the rest; the result summarises `{ requested, ok, failed }`.", + "Unit tests assert: merged preset order + first-match-wins; variant selection per format; exact `getAsset` args; concurrency cap; error isolation.", + "`npm test` green and `npm run typecheck` 0 errors." + ], + "passes": true, + "blocked_by": [ + "F03" + ], + "notes": [ + "Passing `{ key }` alone is a no-op — always pass the full preset object.", + "`acceptFormat:'avif'` in a direct call equals a browser `Accept: image/avif` (verified) — reproduces the admin cache file without HTTP." + ] + }, + { + "feature_id": "F05", + "title": "Upload hook + new-preset trigger", + "acceptance_steps": [ + "On `files.upload` where `payload.type` starts with `image/` and `onUpload` is enabled, the hook runs the warmer for configured presets off the response path (fire-and-forget; never blocks or fails the upload).", + "A non-image upload performs no warming; a warmer failure never fails or slows the upload.", + "After uploading a test JPEG through the admin, the expected `__.avif` (and `.webp`) files for each configured `format:'auto'` preset appear in the storage root.", + "When a storage asset preset is added/changed, existing images are warmed for it (`settings.update` hook and/or a manual \"sync presets\" action); re-running is idempotent.", + "Under PM2 cluster the hook fires once per upload (logged once, not per worker)." + ], + "passes": true, + "blocked_by": [ + "F04" + ], + "notes": [ + "`files.upload` is the correct/only post-write event in v11.17.4.", + "New-preset trigger mechanics are goal.md open item #3.", + "Decision: manual sync chosen — POST /run is idempotent and warms existing images for all presets; no settings.update auto-trigger." + ] + }, + { + "feature_id": "F06", + "title": "Diagnostics & manual generation API + observability", + "acceptance_steps": [ + "`POST /image-previews/generate` `{ fileId, keys?, formats?, dryRun? }` generates (or dry-runs) previews for ONE file and returns per `(key,format)` `{ ok, cached, filename, bytes, ms, error }`.", + "`GET /image-previews/resolve-plan?fileId=&key=` returns the resolved transforms array, the **predicted cache filename**, and whether it already exists — WITHOUT generating.", + "`GET /image-previews/ops?limit=` returns a recent-operations ring buffer (timestamp, fileId, key, format, ok, ms, error), capped in size.", + "A `debug` setting toggles verbose structured logs; logs carry ids/metadata only, never file contents.", + "All diagnostics endpoints except `/generate` are read-only and safe to call on a live instance.", + "Unit tests cover resolve-plan prediction (given a preset, the predicted filename equals the warmer's produced filename) and ops-ring-buffer behaviour.", + "`npm test` green and `npm run typecheck` 0 errors." + ], + "passes": true, + "blocked_by": [ + "F04" + ], + "notes": [ + "resolve-plan is the debugging tool that proves hash-match before anything is written.", + "Also the autonomous loop's primary feedback channel — keep responses machine-readable." + ] + }, + { + "feature_id": "F07", + "title": "Admin module UI: progress bar, disk usage, config form, diagnostics panel", + "acceptance_steps": [ + "The \"Image Previews\" module renders and shows a progress bar with `done / total`, remaining, throughput, ETA, plus last-run and next-window times.", + "During a run the page live-updates (polls `/status`); \"Run now\" / \"Cancel\" controls start/stop a run.", + "A disk-usage panel shows total bytes + count of preview files (list the storage root, sum `stat()` sizes of `__` files); a refresh control recomputes it.", + "A config form edits `enabled`, maintenance window, concurrency, preset selection, formats, `onUpload`, `debug`, persisting via `PUT /config`; changes take effect without rebuilding.", + "A diagnostics panel lists recent operations (`/ops`) and lets an operator run generate/dry-run/resolve-plan for a pasted file id.", + "Invalid input is rejected client-side with a clear message; a saved change is reflected after reload.", + "Smoke-verified on the staging stack (notes/screenshots in progress.txt)." + ], + "passes": false, + "blocked_by": [ + "F06" + ], + "notes": [ + "Use Directus UI components / the private-view shell; keep it minimal.", + "Concurrency is the throttle knob here because in-process generation bypasses the HTTP rate limiter." + ] + }, + { + "feature_id": "F08", + "title": "Resumable backfill engine (window-aware, throttled, progress-reporting)", + "acceptance_steps": [ + "The job enumerates `directus_files` with `type` starting `image/` (paginated) and runs the warmer for each file × configured preset × format.", + "It only runs inside the configured maintenance window, or on an explicit manual run; starting outside the window when scheduled is a logged no-op.", + "It honours the configured concurrency cap and appends completed pairs to `state.jsonl`; restarting after a kill resumes from the last completed pair.", + "Progress is observable during a run via `GET /status` and periodic log lines.", + "A second full run over an already-warmed library reports zero new generations (idempotent).", + "A >6000px original or a concurrency error is recorded as `failed` for that pair; the job continues.", + "`POST /run` starts a run, `POST /run/cancel` stops it; a second concurrent run returns 409 (cluster-wide lock file)." + ], + "passes": true, + "blocked_by": [ + "F07" + ], + "notes": [ + "Run state is file-backed; only one run at a time across the cluster.", + "Scheduling: external cron hitting `/run` (default) vs in-process timer — goal.md open item #4." + ] + }, + { + "feature_id": "F09", + "title": "Verification harness: hash-match gate + cold/warm timing + disk counts", + "acceptance_steps": [ + "For one real file, the on-disk filename produced by an admin browser request for `?key=system-medium-cover` is byte-identical to the warmer's non-dry-run output for the same key+format (the avif variant matches the browser's avif request).", + "The same is demonstrated for `system-large-cover` (image field) and `system-small-cover` (image display).", + "A timing check prints cold vs warm `GET /assets/?key=system-medium-cover`; warm is materially faster.", + "Preview-file count and total bytes are reported before/after a backfill chunk and increase by the expected number of (file × preset × format) entries.", + "Findings (observed admin query string, resulting filenames, uuids, timings) are recorded in progress.txt." + ], + "passes": true, + "blocked_by": [ + "F08" + ], + "notes": [ + "Make-or-break gate: if the warmer's filename does not match the admin's, the approach is wrong — revisit before rollout.", + "Also validates the system-preset copy from F04." + ] + }, + { + "feature_id": "F10", + "title": "Ops: fail-open/disabled-by-default, prod rollout + rollback, scheduling, docs, remote-feedback runbook", + "acceptance_steps": [ + "Every entrypoint body is wrapped so a thrown error is logged and swallowed (never propagates into Directus core), and ALL behavior is gated by `enabled` (default false) — a loaded-but-disabled extension is inert, proven by loading it on staging with `enabled=false`.", + "A documented prod rollout procedure exists: backup previous `dist/` → deploy → healthcheck → enable via config; and a rollback that restores `dist/` and disables.", + "A documented scheduling setup runs the backfill inside the maintenance window (cron hitting `POST /image-previews/run`, by default).", + "`README.md` documents purpose, install/build/publish, every setting, presets/formats warmed, run/resume/cancel, disk reporting, and the maintenance window; `AGENTS.md` captures the transform invariants and safety rules.", + "A **remote-feedback runbook** documents the scoped token and/or restricted SSH access for the autonomous loop (staging only, no prod Docker socket, no prod admin token, private network), and the machine-readable endpoints used as the feedback channel.", + "Final end-to-end pass on staging: upload a fresh image → previews appear with no admin-visible delay; a small backfill chunk completes with the progress bar advancing and disk usage updating; then a controlled prod enable with rollback verified." + ], + "passes": false, + "blocked_by": [ + "F09" + ], + "notes": [ + "Promotion to prod is a human action; the loop proposes, a human deploys.", + "Record the exact cron entry, the scoped grants, and the rollback commands verbatim." + ] + } + ] +} diff --git a/.opencode/goal.md b/.opencode/goal.md new file mode 100644 index 0000000..eaaf91b --- /dev/null +++ b/.opencode/goal.md @@ -0,0 +1,249 @@ +# Goal: Directus image preview pre-generation + +Status: SCAFFOLDED — opened 2026-10-25. Standalone extension project (not part of the +landshaft.life app repo). +Location: `/home/rawman/Projects/DirectusExtensions/directus-extension-generate-image-previews` +Target server: `land-nest`, Directus **11.17.4** (self-hosted, PM2 cluster, local disk storage). +Project type: Directus **bundle** extension (app module + api hook + api endpoint). + +## Summary + +Pre-generate Directus image transform ("preview") cache files so the admin UI never pays the +first-request sharp cost. One warmer, several triggers: + +- **upload hook** — after an image upload, warm its previews. +- **new-preset trigger** — when a storage asset preset is added/changed, warm it for existing images. +- **backfill job** — a resumable, scheduled sweep over the existing `directus_files` library, inside a + configurable maintenance window. +- **manual / diagnostics** — generate (or dry-run) previews for a single file on demand, to debug. + +Plus an **admin module** ("Image Previews") with a **progress bar**, **disk usage**, a **config form** +(persisted, no rebuild), and a **diagnostics panel**. + +## Verified mechanism (source-checked against tag v11.17.4) + +The on-disk transform cache file is: + +``` +__. +``` + +written **beside the original** in the storage root (`STORAGE_LOCAL_ROOT=./uploads`). + +- `hash` = `object-hash@3` defaults: sha1/hex, `unorderedArrays:false` (**array order matters**), + `unorderedObjects:true`. Hashed over the **RESOLVED** transform array from `resolvePreset`. +- `resolvePreset` order is fixed: `(1) copy transformationParams.transforms[]`, `(2) push toFormat` + (if `format`/`quality`), `(3) push resize` (+`extract` on focal cover). +- `maybeExtractFormat` picks the last `toFormat` arg as the file extension. +- `AssetsService.getAsset` checks `exists(assetFilename)` first (cache hit → no sharp); on miss runs + sharp and **awaits** `storage.write(...)` before returning. Not draining the returned stream only + leaks an FD (never truncates). +- Called as sudo (`new AssetsService({ schema })`) it needs **no** HTTP request context. + +## Why the naive / community approach FAILS (must not repeat) + +1. **`{ key }` alone is a no-op.** `resolvePreset` never reads `key`. Pass the **full preset object**. +2. **`format:'auto'` mismatch.** Every `system-*` preset is `format:'auto'`. `getFormat` with no + `acceptFormat` → JPEG→`jpg`/PNG→`png`; a browser `Accept: image/avif` → `avif`. Different + `toFormat` arg → different hash → **guaranteed miss**. +3. Disproved myths: the write is awaited; the admin does NOT use `width`/`height`. +4. Raw params resolve to `[toFormat, resize]`; a preset resolves to `[resize, toFormat]` → different hash. + +## Chosen strategy — IN-PROCESS generation (no token, no HTTP, no rate limiter) + +Because this is an internal extension, generate by calling Directus's own service in-process: + +```ts +const assets = new services.AssetsService({ schema }); // sudo +const { stream } = await assets.getAsset(fileId, { + transformationParams: presetObject, // FULL object, not { key } + acceptFormat: 'avif', // or 'webp' +}); +(stream as Readable).destroy?.(); +``` + +`acceptFormat` reproduces exactly what a browser's `Accept` header does, so the cache filename is +byte-identical to the admin/browser read — **without any HTTP request, token, proxy, or rate limiter**. +Generate **two variants** for `format:'auto'` presets (`avif`, `webp`); fixed-`format` presets yield +one; resize-only presets (no `format`/`quality`) yield none. + +HTTP-replay (`GET /assets/{id}?key=…` with a browser `Accept` + auth) is the documented **fallback** +only — it needs a token and rate-limiter pacing, which is why it is not the default. + +## Preset registry (system + settings, dynamic) + +The controller builds its preset list inline as: + +``` +res.locals['shortcuts'] = [...SYSTEM_ASSET_ALLOW_LIST, ...(directus_settings.storage_asset_presets ?? [])] +``` + +and resolves `?key=` with `.find(t => t.key === key)` → **first match wins** (a system key shadows a +custom preset of the same key). There is **no** reusable helper and `GET /settings` exposes **only** +custom presets. + +Our registry (`src/lib/presets.ts`) mirrors this exact list: + +- **System presets**: 6 objects in `SYSTEM_ASSET_ALLOW_LIST` (`system-small|medium|large` × + `cover|contain`), each `format:'auto'`, resize inside `transforms[]`; sourced at runtime from + `@directus/api` **if** importable, else the version-pinned copy `src/lib/system-presets.ts` (v11.17.4). +- **Custom presets**: read `directus_settings.storage_asset_presets` each run; pass the verbatim object. +- **Dynamic**: newly created presets are picked up per run; the new-preset trigger warms them for + existing images. + +## Admin UI (module "Image Previews") + +- **Progress**: bar + `done / total / remaining`, throughput, ETA, last-run, next-window; live during a run. +- **Storage breakdown**: bytes + file counts separately for **preview assets** (`__`), **original + assets**, and the total — raw and human-readable. Computed by listing the storage root and `stat()`-ing + each file (listing yields paths only, no sizes); the state dir is excluded. +- **Config form** (persisted, no rebuild; defaults `enabled:true`, window `01:00–04:00`): `enabled`, + maintenance window, concurrency, preset selection, formats, `onUpload`, `debug`; each field carries a + plain-language explanation. **Run now** ignores the window; **Cancel** is shown only while a run is active. +- **Diagnostics panel**: recent operations, per-file generate/dry-run, resolve-plan viewer. + +## Diagnostics & manual generation (observability) + +- `POST /image-previews/generate` `{ fileId, keys?, formats?, dryRun? }` → generate (or dry-run) for ONE + file; returns per `(key,format)` `{ ok, failed, cached, filename, bytes, ms, error }`. Primary dev tool. +- `GET /image-previews/resolve-plan?fileId=&key=` → resolved transforms + **predicted cache filename** + + whether it already exists, WITHOUT generating. The hash-match debugging tool. +- `GET /image-previews/ops?limit=` → recent-operations ring buffer (timestamp, fileId, key, format, ok, ms, error). +- `debug` setting enables verbose structured logs; logs carry ids/metadata only, never file contents. +- Everything except `/generate` is read-only. + +## Safe deployment & staging (must not crash prod) + +- **Fail-open, inert by default**: every entrypoint is tiny, bodies wrapped in try/catch, hooks never + throw into Directus core, and ALL behavior is gated by `enabled` (default `false`). A loaded-but-disabled + extension does nothing. +- **Local dev**: `directus-extension link` (or the dev compose stack) loads `src/` live for iteration. +- **Staging stack**: a second compose project (`dev/compose.dev.yml`) running the SAME Directus 11.17.4 + image with its own pg+redis, a **restored anonymized dump**, and a **copy of uploads**, on a separate + port — realistic data, zero prod risk. +- **Rollout discipline**: build → verify on staging → back up the previous `dist/` → copy in → healthcheck + → enable via config → rollback = restore previous `dist/` + disable. +- `EXTENSIONS_AUTO_RELOAD` in prod reloads on file change; deploy with it controlled and accept the brief + reload, or restart deliberately. + +## Remote feedback & autonomous operation (security) + +- Feedback channel = the extension's own **machine-readable endpoints** (`/status`, `/ops`, + `/disk-usage`, `/resolve-plan`). Automation reads those, not CMS content. +- **Least privilege**: + - A Directus **admin token = full CMS control**. Do NOT give it to an autonomous loop. Use a scoped + token reaching only `/image-previews/*` (+ `directus_files` metadata read). + - A **Docker socket = root-equivalent**. Never expose the prod socket to automation; keep the loop on staging. + - Deployment access = a dedicated **non-sudo** SSH user with a restricted key (`restrict`, `from=`, + `command=`), or run opencode on the staging host via `handoff`. +- **Network**: staging reachable only over a private network / VPN / tunnel — never public. +- **Prompt-injection**: treat logs, filenames, and image metadata as untrusted input; keep tooling scoped. +- **Promotion to prod is a human action**: the loop proposes, a human merges/deploys. +- Guard long autonomous runs with a cost/time budget. + +## Design + +``` +src/ + hook/index.ts (api) files.upload -> warmer; settings/presets change -> warm existing + endpoint/index.ts (api) routes: /config (GET/PUT), /status, /run, /run/cancel, + /disk-usage, /generate, /resolve-plan, /ops + module/ + index.ts (app) defineModule({ id:'image-previews', name, icon, routes }) + module.vue progress + disk usage + config form + diagnostics panel + lib/ + presets.ts merged registry (system + settings), mirrors controller semantics + system-presets.ts version-pinned SYSTEM_ASSET_ALLOW_LIST copy (v11.17.4) + warmer.ts in-process getAsset(x, { transformationParams, acceptFormat }) + concurrency + backfill.ts enumerate directus_files (image/*), window-aware, resumable + state.ts state.jsonl read/append/resume + settings.ts config file read/write + env defaults + disk.ts sum sizes of __ files (driver.list + stat) + window.ts maintenance-window logic (is-now-in-window, next-window) + diagnostics.ts resolve-plan/dry-run (resolved transforms + predicted filename), ops ring buffer +dev/ + compose.dev.yml local Directus 11.17.4 + postgres + redis, extension linked + seed/ anonymized DB dump + sample images for local/staging runs +``` + +## Non-goals + +- No Directus upgrade (server stays on 11.17.4; license constraint). +- No change to admin preset configuration or to how the admin builds URLs. +- Never hand-write `__.` filenames. +- No CDN/nginx caching work (separate ops concern). No image `quality` policy change. +- Automation never gets prod Docker socket or a full prod admin token. + +## Constraints / risks + +- `ASSETS_TRANSFORM_MAX_CONCURRENT=45` per worker; over it errors. Catch per file/preset and continue + (never fail the upload). +- `ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION` default 6000 is a **pixel dimension** guard in Directus, not a + file-size limit. The extension imposes no limit of its own; raise the Directus env var to process larger + images (watch memory). Upload-size limits (`FILES_MAX_UPLOAD_SIZE`, `MAX_PAYLOAD_SIZE`) are unrelated. +- Local storage write is **non-atomic** → warm only AFTER an upload is committed; serialize so the + upload hook and the backfill never generate the same file concurrently. +- PM2 cluster (8 workers): the upload hook fires on the handling worker; the backfill runs in the worker + that served the run request. Shared state MUST be file/DB-backed (never in-memory). +- In-process backfill competes with live API serving → the maintenance window + concurrency cap govern + that. (HTTP rate limiter is irrelevant in-process.) +- Observed server bottleneck (2026-10-23): `storage:local:responseTime 1206ms`, `pg:responseTime` + 150–738ms; no 503s, no sharp errors. + +## Verification policy + +- `npm run typecheck` → 0 errors; `npm test` (node:test via tsx) green. +- **The hash-match gate (F09)**: for a real file, the on-disk filename produced by a browser/admin + request for `?key=system-medium-cover` is byte-identical to the warmer's output for the same key+format + (+ the resolved extension matches, e.g. `.avif`). +- Cold-vs-warm timing; preview-file count + total bytes before/after a run. +- All feature testing happens on the **staging** stack; prod deploy only at goal completion, via the + documented backup/rollback procedure. + +## Expected files + +- `package.json`, `tsconfig.json`, `.gitignore` +- `src/hook/`, `src/endpoint/`, `src/module/`, `src/lib/`, plus `src/**/*.test.ts` +- `dev/compose.dev.yml`, `dev/seed/` +- `.opencode/goal.md`, `.opencode/feature_list.json`, `.opencode/progress.txt` +- `README.md`, `AGENTS.md` + +## Resolved decisions (2026-10-25) + +1. State/config: JSON files under `/.image-previews` (default + `/directus/uploads/.image-previews`), override with `IMAGE_PREVIEWS_STATE_DIR`. The former + `/directus/extensions/.image-previews` default was rejected — root-owned volume → `EACCES`. +2. System presets: version-pinned copy (`src/lib/system-presets.ts`, verbatim v11.17.4); no + `@directus/api` import needed for presets. +3. New-preset trigger: manual `POST /run` (idempotent) + the module's Run button; no + `settings.update` auto-trigger. +4. Backfill scheduling: external cron → `POST /image-previews/run`. +5. Dev/staging: isolated local compose stack (`dev/compose.dev.yml`). A land-nest staging project is + still to be created for realistic-data runs. +6. Autonomous-loop grants: still to be defined (scoped token / restricted SSH, staging only). + +## Implementation status (2026-10-25) + +Implemented and verified against an isolated Directus 11.17.4: settings/state, presets + warmer, +upload hook, diagnostics, backfill, endpoints. `npm test` 32/32, typecheck clean, build emits both +bundles. **Hash-match gate passed** (a real browser-style asset request produced exactly the +predicted filename). Features F03–F06, F08, F09 pass. + +Outstanding: admin-module browser verification (F07), staging deploy/rollback + prod rollout (F02/F10), +and the F01 publish-to-staging step. + +## Reference: source anchors (v11.17.4) + +- `api/src/services/assets.ts` — `getAsset`, `getAssetSuffix`, filename build, awaited `write`. +- `api/src/utils/transformations.ts` — `resolvePreset`, `getFormat`. +- `api/src/controllers/assets.ts` — inline `shortcuts = [...SYSTEM_ASSET_ALLOW_LIST, ...settings]`, + `acceptFormat` selection from `req.headers.accept`. +- `api/src/constants.ts` — `SYSTEM_ASSET_ALLOW_LIST` (6 presets, `format:'auto'`); + `ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION` default 6000. +- `app/src/utils/get-asset-url.ts`, `app/src/layouts/cards/components/card.vue`, + `app/src/interfaces/file-image/file-image.vue`, `app/src/displays/image/image.vue` — admin URLs: + Files grid `?key=system-medium-cover&v=`; image field `system-large-cover|contain`; + image display `system-small-cover`. +- `packages/storage-driver-local/src/index.ts` — non-atomic `write`; `list()` returns paths only. +- Prior art that does NOT work as-is: `directus-extension-auto-generate-file-transformations` + (utomic-media) — still falls foul of the `auto`→format mismatch unless it forces the format. diff --git a/.opencode/progress.txt b/.opencode/progress.txt new file mode 100644 index 0000000..066354a --- /dev/null +++ b/.opencode/progress.txt @@ -0,0 +1,136 @@ +# progress.txt — directus-extension-generate-image-previews + +Append-only log of decisions, evidence, and learnings. Newest at the bottom. + +## 2026-10-25 — project scaffolded + +- Opened this standalone extension project to pre-generate Directus image previews. +- Source-verified the strategy against Directus **v11.17.4** (see `.opencode/goal.md`). +- Root causes why existing/community extensions do not work: + 1. `resolvePreset` never reads `key` → `{transformationParams:{key}}` yields `transforms=[]` → no-op. + 2. `system-*` presets are `format:'auto'` → `jpg`/`png` server-side vs `avif` in-browser → different hash. + 3. "undrained stream truncates file" — false (write is awaited). + 4. admin uses `?key=system-*`, not raw width/height. +- Admin URLs (v11.17.4 app source): files grid `?key=system-medium-cover&v=`; + image field `system-large-cover|contain`; image display `system-small-cover`. +- Server bottleneck evidence (2026-10-23 log triage): `storage:local:responseTime 1206ms`, + `pg:responseTime` 150–738ms; no 503s, no sharp errors. + +## 2026-10-25 — scope expanded after user direction (v2) + +- **No token needed.** Dropped HTTP-replay as primary. Strategy: **in-process** + `AssetsService.getAsset(id, { transformationParams: , acceptFormat: 'avif'|'webp' })`. + No HTTP → no token, no proxy, no rate limiter. (HTTP-replay kept as documented fallback.) +- **Bundle extension**: api `hook` + api `endpoint` + app `module`. +- **Presets: all, dynamically** — system `SYSTEM_ASSET_ALLOW_LIST` (6) + `directus_settings.storage_asset_presets`, + controller order `[...SYSTEM, ...custom]`, first-match-wins; new presets picked up per run. +- **Admin module**: progress bar, disk usage (`list`+`stat`), config form, diagnostics panel. +- **Config**: maintenance window + concurrency (throttle knob; in-process bypasses HTTP rate limiter), + persisted in JSON. +- Confirmed `acceptFormat:'avif'` in a direct call equals browser `Accept: image/avif`. + +## 2026-10-25 — diagnostics + safety + autonomy (v3) + +- **Diagnostics / manual generation** (F06): `POST /generate` (one file, dry-run supported), + `GET /resolve-plan` (resolved transforms + predicted filename, no write), `GET /ops` (ring buffer). +- **Safe testing** (F02): staging compose stack + deploy/rollback scripts; fail-open, `enabled` default false. +- **Autonomous remote feedback**: staging-only, least privilege (scoped token, no prod Docker socket, + restricted SSH, private network, human-only prod promotion). + +## 2026-10-25 — implementation + local verification (autonomous session) + +Built the full extension locally and smoke-tested against an **isolated** Directus 11.17.4 stack +(`dev/compose.dev.yml`, port 8057, own pg + volumes). No contact with the prod instance. + +### Implemented +- `src/lib/`: settings (env + JSON), state (`state.jsonl`, run state, lock, cancel flag), + presets (pinned system copy + custom merge, controller parity), warmer (in-process `getAsset` with + `acceptFormat` variants, concurrency, per-task error isolation, stream release), window, storage + (dynamic `getStorage` + local-FS fallback), diagnostics (resolve-plan prediction + ops ring buffer), + backfill (paginated, resumable, window-aware, idempotent). +- `src/endpoint/index.ts`: `/status`, `/config` (GET/PUT), `/presets`, `/run`, `/run/cancel`, + `/disk-usage`, `/generate`, `/resolve-plan`, `/ops`; admin-only guard. +- `src/hook/index.ts`: `files.upload` → fire-and-forget warm. +- `src/module/`: admin module (progress bar, disk usage, config form, diagnostics panel, ops table). +- `dev/`: compose stack, `sync-extension.sh`, seed images. + +### Bugs found & fixed during smoke test +1. **Directus coerces env values** (`"true"` → boolean `true`, `"1"` → number). My string-only parsers + threw `e.toLowerCase is not a function`, which silently broke the upload hook. Fixed `settings.ts` + to accept `unknown` and stringify. Regression test added. +2. **State dir EACCES.** `/directus/extensions/.image-previews` (named volume on a non-existent path) + is root-owned; the API runs as `node`. This broke config writes and the backfill lock. Fixed: default + state dir is now `/.image-previews` (i.e. `/directus/uploads/.image-previews`), + which is writable; `IMAGE_PREVIEWS_STATE_DIR` still overrides. +3. **Bind-mount reload gotcha.** `rm -rf` of the bind-mounted extension dir breaks the Docker mount + (Directus removes the extension and never re-adds it). `sync-extension.sh` now updates in place; + restart the dev container after a sync. + +### Verified evidence (local stack) +- `npm run typecheck` → 0 errors. `npm test` → **32/32 pass**. `npm run build` → `dist/app.js` + `dist/api.js`. +- Directus logs: `Loaded extensions: directus-extension-generate-image-previews` (no errors). +- `/status` → `{enabled:true, inWindow:true, locked:false}`; `/presets` → 6 system presets. +- Upload a JPEG → hook warmed **12 files** (6 presets × avif/webp) in `/directus/uploads`. +- **HASH-MATCH GATE PASSED:** after deleting the cached files, a real + `GET /assets/?key=system-medium-cover` with `Accept: image/avif` produced exactly + `__f15aa4b815722867859d2a3fa46ab50834dfc8dc.avif` — byte-identical to `resolve-plan`'s prediction. +- `/generate` dry-run reports `exists:true/false` correctly; `/ops` shows per-task timing. +- Backfill: `total=36` (3 images × 6 presets × 2 formats), `generated=36`, state `state.jsonl` = 36 lines. + Second run: `generated=0, hits=36`, state unchanged, file count unchanged → **idempotent/resumable**. +- Window gating: `/run` outside `03:00–05:00` → `409 outside-window` with `nextWindow`. +- `/disk-usage` → `157633 bytes / 36 files (source: storage)`. +- Cold vs warm (`system-medium-cover`): 114 ms → 16–30 ms. (`system-large-cover`): 304 ms → 36 ms. + +### Decisions recorded +- System presets: version-pinned copy in `src/lib/system-presets.ts` (verbatim from `api/src/constants.ts` + v11.17.4). No `@directus/api` import needed for presets. +- Storage/disk usage: `@directus/api/storage/index` loaded via dynamic import (hidden from the bundler), + with a local-FS fallback; shares the host `getStorage()` singleton. +- New-preset trigger: **manual** via `POST /run` (idempotent) + the module's Run button; no + `settings.update` auto-trigger (avoids heavy background runs on unrelated settings changes). +- Scheduling: external cron → `POST /image-previews/run` (default). +- State/config dir: `/.image-previews` (writable), override with `IMAGE_PREVIEWS_STATE_DIR`. + +### Additional verification (same session) +- `GET /extensions` → bundle registered with entries `module`, `hook`, `endpoint`, all enabled. +- Inert test: with `enabled:false`, uploading an image produced **zero** new preview files (25 → 25). +- Added `dev/compose.staging.yml`, `dev/deploy-staging.sh`, `dev/rollback-staging.sh` (templates; + NOT executed against any remote host). +- Added `storage.test.ts`; total tests now **34/34**. +- Restarted the dev stack after the final build; `/status`, `/disk-usage`, and bundle entries all good. + +### Still open +- Admin module UI not yet verified in a real browser (bundle + entries registered; rendering/polling untested). +- Cancel + concurrent-run 409 not exercised end-to-end (unit-tested only; local runs finish too fast). +- Staging deploy not executed (scripts/templates provided); prod rollout not done. +- F01/F02/F07/F10 remain `passes:false` (F01 publish step, F02 staging run, F07 browser, F10 rollout). + +## 2026-10-25 — feedback round (UI, defaults, custom formats, docs) + +- **Defaults changed**: `enabled` now `true`; maintenance window defaults to `01:00–04:00`. +- **Custom fixed-format presets verified**: `custom-png` (format `png`) generated `.png` and + `resolve-plan` predicted the exact filename (`exists:true`). `custom-auto` generated avif+webp. + An unsupported preset (`format: bmp`) failed with Directus's clear error + ("Expected one of: heic, heif, avif, jpeg, … received bmp") and is surfaced in Recent operations / `/ops`. +- **Effective format fix**: ops/presets now report the preset's real output format (`png`) instead of + `null`; backfill resume keys use the effective format (fixed a re-warm mismatch). +- **Progress accounting fix**: already-cached tasks now count toward `done`/`hits`. Runs report + `128/128` (120 hits + 8 expected failures) instead of `8/128`. After removing the unsupported preset: + `120/120 hits, 0 failed, 0 generated`. +- **Storage reporting**: `/disk-usage` + panel now split **previews** vs **originals** vs **total** + (raw + human): e.g. previews 774.7 KB / 115 files, originals 24.9 MB / 9 files, total 25.6 MB / 124. +- **Human-readable fields** added to `/status` (ISO + human dates, `etaHuman`, `rateHuman`), + `/disk-usage` (`bytesHuman`), `/ops` (`tsHuman`, `msHuman`), `/generate` (`msHuman`). +- **Admin module rewritten with Directus UI kit** (`v-button`, `v-input`, `v-checkbox`, `v-select`, + `v-notice`, `v-progress-linear`), field explanations, preset table with effective formats + shadow + warnings, storage table, error column in Recent operations, and a full collapsible **API reference**. +- **Run now** now ignores the maintenance window; **Cancel** only renders while a run is active. +- **Dimension vs file size clarified** in README/goal/AGENTS: the 6000 limit is a pixel-dimension guard in + Directus, not a file-size limit; the extension imposes none. +- Tests: **40/40** pass; typecheck clean; build OK. + +### Still open +- Admin module UI not yet verified in a real browser (bundle + entries registered; rendering/polling untested). +- Cancel + concurrent-run 409 not exercised end-to-end. +- Staging deploy not executed (scripts/templates provided); prod rollout not done. +- F01/F02/F07/F10 remain `passes:false`. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5e535b7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,129 @@ +# Agent notes — directus-extension-generate-image-previews + +Common confusion points for agents working here. Read before touching the warmer. + +## What this is + +A Directus **bundle** extension that pre-generates image transform ("preview") cache files so the +admin UI never pays the first-request sharp cost. Target server: **Directus 11.17.4** on `land-nest` +(self-hosted, PM2 cluster, `STORAGE_LOCAL_ROOT=./uploads`). + +Bundle entries: an api **hook** (`files.upload`), an api **endpoint** (`/image-previews/*`), and an +app **module** (`image-previews` admin page: progress, disk usage, config). + +## Directus transform invariants (source-verified, tag v11.17.4) — DO NOT REGRESS + +1. **Cached transform filename** = `__.`, + written **beside the original** in the storage root (`./uploads`, no `.cache` subdir). + +2. **Hash = `object-hash@3` defaults**: sha1/hex, `unorderedArrays:false` (**array order matters**), + `unorderedObjects:true`. Hashes the **RESOLVED** transform array from `resolvePreset`, NOT the raw + query/preset object. + +3. **`resolvePreset` order is fixed**: copy `transformationParams.transforms[]` → push `toFormat` + (if `format`/`quality`) → push `resize` (+`extract` on focal cover). Raw params `?width=&height=&fit=&format=` + resolve to `[toFormat, resize]`; a preset resolves to `[resize, toFormat]` → **different hash for a + visually identical result**. + +4. **`{ key }` alone is a NO-OP.** `resolvePreset` never reads `key`. Always pass the **full preset object**. + +5. **`format:'auto'` is the killer trap.** All six `system-*` presets are `format:'auto'`. `getFormat` + with no `acceptFormat` → JPEG→`jpg`, PNG→`png`; a browser `Accept: image/avif` → `avif`. Different + `toFormat` arg → **different hash → guaranteed miss**. + +6. **The write IS awaited.** Not draining the returned stream leaks an FD but never truncates. + Still: destroy/unpipe the returned stream to avoid FD leaks. + +7. `AssetsService.getAsset` runs fine as sudo (`new AssetsService({ schema })`) with **no HTTP request + context** — this is the basis of our in-process strategy. + +## Preset registry semantics (controller parity) + +The controller builds, inline in `api/src/controllers/assets.ts`: + +``` +res.locals['shortcuts'] = [...SYSTEM_ASSET_ALLOW_LIST, ...(directus_settings.storage_asset_presets ?? [])] +``` + +and resolves `?key=` with `.find(t => t.key === key)` → **first match wins** (a system key shadows a +custom preset of the same key). There is **no** reusable helper, and `GET /settings` exposes **only** +custom presets. Our registry (`src/lib/presets.ts`) must mirror this order exactly. + +## What the admin actually requests (verbatim from app source) + +| Surface | URL | +| ------------- | -------------------------------------------------------- | +| Files grid | `?key=system-medium-cover&v=` (cards, crop) | +| Image field | `?key=system-large-cover` / `system-large-contain` | +| Image display | `?key=system-small-cover` | + +No explicit `width`/`height`/`format`. `format:auto` is implicit. `v=` is ignored for hashing. + +## Chosen strategy — IN-PROCESS (no token, no HTTP, no rate limiter) + +```ts +const assets = new services.AssetsService({ schema }); // sudo +const { stream } = await assets.getAsset(fileId, { + transformationParams: presetObject, // FULL object, not { key } + acceptFormat: 'avif', // or 'webp' +}); +(stream as Readable).destroy?.(); +``` + +`acceptFormat` reproduces what a browser's `Accept` header does → byte-identical cache filename with +no HTTP/token. Warm **both** `avif` and `webp` for `format:'auto'` presets. + +HTTP-replay (`GET /assets/{id}?key=…`) is the documented **fallback** only (needs a token + rate-limit +pacing). + +## Config + state + +- Persisted settings + progress state are small JSON files under `/.image-previews` + (default `/directus/uploads/.image-previews`; override `IMAGE_PREVIEWS_STATE_DIR`). Do NOT default to + `/directus/extensions/...` — that path is a root-owned volume and the API runs as `node` → EACCES. +- `state.jsonl` = append-only completed `{file_id,preset_key,format}` → resume. +- All state is **file-backed** (PM2 cluster, 8 workers — never in-memory). + +## Gotchas learned the hard way + +- **Directus coerces env values**: `"true"` arrives as boolean `true`, `"1"` as number. Env parsing must + accept `unknown` and stringify (see `asString` in `settings.ts`). A string-only `.toLowerCase()` call + silently killed the upload hook. +- **Docker bind mounts**: never `rm -rf` the bind-mounted extension directory — it breaks the mount and + Directus removes the extension without re-adding it. Sync contents in place; restart the dev container. +- The dev stack (`dev/compose.dev.yml`) is the only place to test; never load this into prod while + iterating (prod has `EXTENSIONS_AUTO_RELOAD=true`). + + +## Server facts (land-nest) + +- Directus image `directus/directus:11.17.4`; `PUBLIC_URL=https://data.landshaft.life`; + `PM2_INSTANCES=8`; `STORAGE_LOCAL_ROOT=./uploads`; logs at `~/apps/directus/logs`. +- `ASSETS_TRANSFORM_MAX_CONCURRENT=45` per worker → over it errors; catch per file/preset. +- `ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION` default 6000 is a **pixel dimension** guard, not a file-size + limit. The extension imposes no limit; raise the Directus env var to process larger images. +- Local storage writes are **non-atomic** — warm only after an upload is committed. +- `RATE_LIMITER_*` applies to HTTP only → irrelevant to in-process generation (this is why the admin + knob is *concurrency*, not a rate limit). +- Observed bottleneck (2026-10-23 log triage): `storage:local:responseTime 1206ms`, + `pg:responseTime` 150–738ms; **no** 503s, **no** sharp errors. Pre-generation removes the + cold-transform cost; uploads-volume I/O latency is a separate ops concern. + +## Safety (do not crash prod) + +- Every entrypoint body is wrapped so thrown errors are logged and swallowed — never propagate into + Directus core. ALL behaviour is gated by `enabled` (default **false**): a loaded-but-disabled bundle + does nothing. +- Test only on the **staging** stack (`dev/compose.dev.yml`), never the prod instance. +- Rollout = backup previous `dist/` → deploy → healthcheck → enable; rollback = restore `dist/` + disable. +- Automation (autonomous loops) gets **staging only**: a scoped token limited to `/image-previews/*` + (+ `directus_files` metadata read), never a prod admin token; never the prod Docker socket; restricted + non-sudo SSH or opencode-on-host. Promotion to prod is a human action. + +## Conventions + +- Build via `@directus/extensions-sdk` (`npm run build` / `dev` / `link`); bundle emits `dist/app.js` + `dist/api.js`. +- Deploy with `npm run publish` (scp `dist` + `package.json` to land-nest extensions dir). +- Tests: `node:test` via `tsx` (`npm test`). Typecheck: `npm run typecheck`. +- Do not add comments unless asked. Follow the sibling extensions' style. +- Never commit without explicit request. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dbd8691 --- /dev/null +++ b/README.md @@ -0,0 +1,102 @@ +# directus-extension-generate-image-previews + +Pre-generate Directus image transform ("preview") cache files so the admin UI and API never pay the +first-request sharp cost. + +A Directus **bundle**: + +- **upload hook** — warm previews right after an image upload. +- **new-preset trigger** — `Run now` warms every preset (system + custom) for existing images. +- **backfill job** — resumable, window-aware sweep over the existing library. +- **admin module** — progress, storage breakdown, configuration, diagnostics, and API reference. + +Target: self-hosted Directus **11.17.4** (PM2 cluster, local disk storage). + +## Why this approach (read AGENTS.md too) + +The cache file is `__.`. The hash depends on the +RESOLVED transforms, and every `system-*` preset is `format:'auto'` — which resolves differently +server-side (jpg/png) than in a browser (avif). We therefore generate **in-process** with an explicit +`acceptFormat` (`avif` + `webp`), reproducing the browser's cache file byte-for-byte without any HTTP +request. Naive warming (or the popular community extension) silently misses. + +## Install / build / publish + +```bash +npm install +npm run build # emits dist/app.js + dist/api.js +npm run typecheck # 0 errors +npm test # unit tests +npm run dev # watch build +npm run publish # scp dist + package.json to the (staging) deploy target +``` + +## Settings + +Defaults are applied automatically; everything is editable at runtime from the admin module — no rebuild. + +| setting | default | meaning | +| -------------------------- | -------------------------------------- | ---------------------------------------------------------------- | +| `enabled` | `true` | master switch (off = hooks/backfill inert) | +| `onUpload` | `true` | generate previews immediately after an upload | +| `windowStart` / `windowEnd` | `01:00` / `04:00` | scheduled runs only start inside this window (local time) | +| `concurrency` | `1` | parallel generations | +| `formats` | `avif,webp` | output variants **for `format:'auto'` presets only** (all system presets) | +| `presetKeys` | `null` (all) | restrict to specific preset keys | +| `debug` | `false` | verbose generation logs in the Directus server log | +| `IMAGE_PREVIEWS_STATE_DIR` | `/.image-previews` | where settings + progress state live | + +### Presets and formats + +- **System presets** (`system-small|medium|large` × `cover|contain`) always use `format:'auto'`, so they + generate one file per entry in `formats` (default avif + webp), at their built-in sizes (64 / 300 / 800 px). +- **Custom presets** from Settings → File Storage are added automatically. A custom preset with a fixed + `format` (e.g. `png`) ignores `formats` and generates exactly that format. A custom preset with + `format:'auto'` follows `formats`. +- A custom preset whose `key` equals a system key is **shadowed** (system wins, same as Directus) and is + flagged in the admin module. + +## Storage reporting + +`GET /image-previews/disk-usage` (and the admin panel) reports bytes + file counts separately for +**preview assets** and **original assets**, plus the total — in both raw bytes and human-readable form. + +## API endpoints + +All require an admin session or admin token. Human-readable fields accompany machine-readable ones. + +- `GET /image-previews/status` — run progress, rate, ETA, window state, ISO + human dates +- `GET /image-previews/config`, `PUT /image-previews/config` +- `GET /image-previews/presets` — system + custom presets, effective formats, shadowed keys +- `POST /image-previews/run` — start backfill (`{"ignoreWindow":true}` for an immediate run) +- `POST /image-previews/run/cancel` +- `GET /image-previews/disk-usage` — previews vs originals vs total +- `POST /image-previews/generate` — one file (`{fileId, keys?, formats?, dryRun?}`) +- `GET /image-previews/resolve-plan?fileId=&key=` — predicted cache filename, no write +- `GET /image-previews/ops?limit=` — recent operations with timing + error messages + +The admin module includes a full API reference with curl examples. + +## Safe testing & deployment + +Never test against the prod instance. + +- **Local / staging** (`dev/compose.dev.yml`): Directus 11.17.4 + its own postgres + volumes, on separate + ports. Shares nothing with prod. See `dev/README.md`. +- **Fail-open**: entrypoints never throw into Directus core. +- **Rollout**: build → verify on staging → back up previous `dist/` → deploy → healthcheck → enable. + **Rollback**: restore previous `dist/`. +- Scripts: `dev/deploy-staging.sh`, `dev/rollback-staging.sh`, template `dev/compose.staging.yml`. + +## Caveats + +- **Transform dimension guard, not a file-size limit.** Directus rejects transformations of images whose + width or height exceeds `ASSETS_TRANSFORM_IMAGE_MAX_DIMENSION` (default **6000 px**). This is a Directus + env var, not something this extension imposes. To process larger images, raise it on the Directus + instance (and watch memory). Upload size limits (`FILES_MAX_UPLOAD_SIZE`, `MAX_PAYLOAD_SIZE`) are separate. +- Warm only after uploads are committed (local writes are non-atomic). +- In-process generation bypasses the HTTP rate limiter; bound it with `concurrency` + the window. +- Unsupported custom preset formats (e.g. `bmp`) fail at generation time; the error appears in + Recent operations / `/ops`. + +See `.opencode/goal.md` for the full contract and `.opencode/feature_list.json` for the task list. diff --git a/dev/README.md b/dev/README.md new file mode 100644 index 0000000..251d5aa --- /dev/null +++ b/dev/README.md @@ -0,0 +1,48 @@ +# Local dev / staging stack + +Isolated Directus **11.17.4** with its own Postgres and volumes. Nothing is shared with the prod +instance (separate containers, volumes, and port **8057**). + +## Run + +```bash +# from the project root +./dev/sync-extension.sh # build + copy dist/package.json into dev/extension +docker compose -f dev/compose.dev.yml up -d +``` + +Admin: `http://localhost:8057` — `admin@example.com` / `admin12345`. +The extension is loaded from `dev/extension` (bind mount). Check: + +```bash +docker logs image-previews-dev-directus-1 | grep -i "loaded extensions" +``` + +## After changing source + +```bash +./dev/sync-extension.sh +docker compose -f dev/compose.dev.yml restart directus +``` + +(`EXTENSIONS_AUTO_RELOAD` is on, but a restart is the reliable path. Never `rm -rf dev/extension` — +it breaks the bind mount.) + +## Smoke test + +```bash +BASE=http://localhost:8057 +TOKEN=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' \ + -d '{"email":"admin@example.com","password":"admin12345"}' | jq -r '.data.access_token') + +curl -s "$BASE/image-previews/status" -H "Authorization: Bearer $TOKEN" | jq . +curl -s -X POST "$BASE/files" -H "Authorization: Bearer $TOKEN" -F "file=@dev/seed/test.jpg" | jq '.data | {id,filename_disk}' +docker exec image-previews-dev-directus-1 sh -c "ls -1 /directus/uploads | grep '__' | sort" +``` + +## Stop / reset + +```bash +docker compose -f dev/compose.dev.yml down # stop, keep data +docker compose -f dev/compose.dev.yml down -v # stop and wipe DB + uploads +``` diff --git a/dev/compose.dev.yml b/dev/compose.dev.yml new file mode 100644 index 0000000..88332d4 --- /dev/null +++ b/dev/compose.dev.yml @@ -0,0 +1,51 @@ +name: image-previews-dev + +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: directus + POSTGRES_PASSWORD: directus + POSTGRES_DB: directus + volumes: + - db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U directus"] + interval: 5s + timeout: 5s + retries: 20 + + directus: + image: directus/directus:11.17.4 + ports: + - "8057:8055" + depends_on: + db: + condition: service_healthy + environment: + KEY: dev-key + SECRET: dev-secret + ADMIN_EMAIL: admin@example.com + ADMIN_PASSWORD: admin12345 + DB_CLIENT: pg + DB_HOST: db + DB_PORT: "5432" + DB_DATABASE: directus + DB_USER: directus + DB_PASSWORD: directus + STORAGE_LOCATIONS: local + STORAGE_LOCAL_ROOT: /directus/uploads + EXTENSIONS_AUTO_RELOAD: "true" + CACHE_ENABLED: "false" + RATE_LIMITER_ENABLED: "false" + ASSETS_TRANSFORM_MAX_CONCURRENT: "2" + IMAGE_PREVIEWS_ENABLED: "true" + IMAGE_PREVIEWS_CONCURRENCY: "1" + LOG_LEVEL: info + volumes: + - uploads:/directus/uploads + - ./extension:/directus/extensions/directus-extension-generate-image-previews + +volumes: + db-data: + uploads: diff --git a/dev/compose.staging.yml b/dev/compose.staging.yml new file mode 100644 index 0000000..070bddc --- /dev/null +++ b/dev/compose.staging.yml @@ -0,0 +1,58 @@ +# Staging stack template (run on the staging host only — never on prod). +# +# 1. Restore an anonymized DB dump into the `db` service. +# 2. Copy (or rsync) a subset of prod uploads into the `uploads` volume. +# 3. Deploy the extension with dev/deploy-staging.sh, then start this stack. +# +# Keep ports/volumes/network distinct from the prod stack. + +name: land-nest-staging + +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: directus + POSTGRES_PASSWORD: change-me + POSTGRES_DB: directus + volumes: + - db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U directus"] + interval: 5s + timeout: 5s + retries: 20 + + directus: + image: directus/directus:11.17.4 + ports: + - "8056:8055" + depends_on: + db: + condition: service_healthy + environment: + KEY: change-me + SECRET: change-me-to-at-least-32-bytes-long + ADMIN_EMAIL: staging@example.com + ADMIN_PASSWORD: change-me + DB_CLIENT: pg + DB_HOST: db + DB_PORT: "5432" + DB_DATABASE: directus + DB_USER: directus + DB_PASSWORD: change-me + STORAGE_LOCATIONS: local + STORAGE_LOCAL_ROOT: /directus/uploads + EXTENSIONS_AUTO_RELOAD: "true" + CACHE_ENABLED: "false" + RATE_LIMITER_ENABLED: "false" + IMAGE_PREVIEWS_ENABLED: "true" + IMAGE_PREVIEWS_CONCURRENCY: "1" + LOG_LEVEL: info + volumes: + - uploads:/directus/uploads + - ./extensions:/directus/extensions + +volumes: + db-data: + uploads: diff --git a/dev/deploy-staging.sh b/dev/deploy-staging.sh new file mode 100755 index 0000000..1e03ad1 --- /dev/null +++ b/dev/deploy-staging.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Deploy the built extension to a STAGING Directus extensions dir. Never target prod. +# Usage: ./dev/deploy-staging.sh +set -euo pipefail + +HOST="${1:?usage: deploy-staging.sh }" +EXT_ROOT="${2:?usage: deploy-staging.sh }" +NAME="directus-extension-generate-image-previews" +STAMP="$(date +%Y%m%d-%H%M%S)" + +cd "$(dirname "$0")/.." + +npm run build + +ssh "$HOST" "mkdir -p '$EXT_ROOT/$NAME' && if [ -d '$EXT_ROOT/$NAME/dist' ]; then cp -a '$EXT_ROOT/$NAME/dist' '$EXT_ROOT/$NAME/dist.bak-$STAMP'; fi" + +rsync -az --delete dist/ "$HOST:$EXT_ROOT/$NAME/dist/" +scp package.json "$HOST:$EXT_ROOT/$NAME/package.json" + +echo "deployed to $HOST:$EXT_ROOT/$NAME (backup stamp: $STAMP)" +echo "restart staging Directus, then check: docker logs | grep -i 'loaded extensions'" diff --git a/dev/rollback-staging.sh b/dev/rollback-staging.sh new file mode 100755 index 0000000..8b5426e --- /dev/null +++ b/dev/rollback-staging.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Roll back a staging deploy to a previous dist backup. +# Usage: ./dev/rollback-staging.sh +set -euo pipefail + +HOST="${1:?usage: rollback-staging.sh }" +EXT_ROOT="${2:?usage: rollback-staging.sh }" +STAMP="${3:?usage: rollback-staging.sh }" +NAME="directus-extension-generate-image-previews" + +ssh "$HOST" "test -d '$EXT_ROOT/$NAME/dist.bak-$STAMP' && rm -rf '$EXT_ROOT/$NAME/dist' && cp -a '$EXT_ROOT/$NAME/dist.bak-$STAMP' '$EXT_ROOT/$NAME/dist'" + +echo "rolled back $HOST:$EXT_ROOT/$NAME to dist.bak-$STAMP" diff --git a/dev/seed/test-large.jpg b/dev/seed/test-large.jpg new file mode 100644 index 0000000..fa71ca7 Binary files /dev/null and b/dev/seed/test-large.jpg differ diff --git a/dev/seed/test.jpg b/dev/seed/test.jpg new file mode 100644 index 0000000..638d8f3 Binary files /dev/null and b/dev/seed/test.jpg differ diff --git a/dev/sync-extension.sh b/dev/sync-extension.sh new file mode 100755 index 0000000..4ee1a19 --- /dev/null +++ b/dev/sync-extension.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")/.." + +npm run build + +mkdir -p dev/extension +rm -rf dev/extension/dist +cp -r dist dev/extension/dist +cp package.json dev/extension/package.json + +echo "synced dev/extension (in place)" diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..72ba10a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6346 @@ +{ + "name": "directus-extension-generate-image-previews", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "directus-extension-generate-image-previews", + "version": "0.1.0", + "dependencies": { + "object-hash": "^3.0.0" + }, + "devDependencies": { + "@directus/extensions-sdk": "13.0.1", + "@types/node": "^22.13.5", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vue": "^3.5.13" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.11.tgz", + "integrity": "sha512-OZ6+oaK7GMyg3IfCpj8JOmn8n+w2rJbDT0LLfVDTLb1ZFeKDh3AiNELgCMUw175IJHSkLl74hG0M0VLsvx+gag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.11" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@directus/composables": { + "version": "11.1.6", + "resolved": "https://registry.npmjs.org/@directus/composables/-/composables-11.1.6.tgz", + "integrity": "sha512-zm6GCT9KiTZpqRJzA9IqXTWlFVYB4eYiRha0isr1yMTv1vJEv33H8OzBn8nB3ux6GQC0BVUIyPwwAwKdXSvCRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@directus/constants": "13.0.0", + "@directus/utils": "13.0.0", + "axios": "1.7.9", + "lodash-es": "4.17.21", + "nanoid": "5.0.9" + }, + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + }, + "peerDependencies": { + "vue": "^3.4" + } + }, + "node_modules/@directus/constants": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@directus/constants/-/constants-13.0.0.tgz", + "integrity": "sha512-R2uuMEJarlq29IPH9Pqe11U74mDqQWBHWhVy77R7ZrnJeZiiayLFoQTlZS4FN8M+ROVrCkIcsg39meNpxEyvpA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + } + }, + "node_modules/@directus/extensions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@directus/extensions/-/extensions-3.0.1.tgz", + "integrity": "sha512-31fJOIYhZpg0a5yrvMvc8K8G+/o4lpQeOumYdraDylw8oaIm8jzw2vznrooQS5vO3A1c9+QawsENDSZdt72Aow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@directus/constants": "13.0.0", + "@directus/themes": "1.0.7", + "@directus/types": "13.0.0", + "@directus/utils": "13.0.0", + "@types/express": "4.17.21", + "fs-extra": "11.2.0", + "lodash-es": "4.17.21", + "zod": "3.24.1" + }, + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + }, + "peerDependencies": { + "knex": "3", + "pino": "9", + "vue": "^3.4", + "vue-router": "4" + }, + "peerDependenciesMeta": { + "knex": { + "optional": true + }, + "pino": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, + "node_modules/@directus/extensions-sdk": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/@directus/extensions-sdk/-/extensions-sdk-13.0.1.tgz", + "integrity": "sha512-knEBNGhpogIrElnqwkvuw4K384URR9yeJhbWe9aPJk68z1eDfjpNggzJqQKhDwkHYeMWR+GcDs32Itkq/zimEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@directus/composables": "11.1.6", + "@directus/constants": "13.0.0", + "@directus/extensions": "3.0.1", + "@directus/themes": "1.0.7", + "@directus/types": "13.0.0", + "@directus/utils": "13.0.0", + "@rollup/plugin-commonjs": "25.0.8", + "@rollup/plugin-json": "6.1.0", + "@rollup/plugin-node-resolve": "15.3.1", + "@rollup/plugin-replace": "5.0.7", + "@rollup/plugin-terser": "0.4.4", + "@rollup/plugin-virtual": "3.0.2", + "@vitejs/plugin-vue": "4.6.2", + "chalk": "5.4.1", + "commander": "10.0.1", + "esbuild": "0.17.19", + "execa": "7.2.0", + "fs-extra": "11.2.0", + "inquirer": "12.3.0", + "ora": "6.3.1", + "rollup": "3.29.5", + "rollup-plugin-esbuild": "5.0.0", + "rollup-plugin-styles": "4.0.0", + "semver": "7.6.3", + "vite": "4.5.5", + "vue": "3.5.13" + }, + "bin": { + "directus-extension": "cli.js" + }, + "engines": { + "node": ">=12.20.0" + }, + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/@vue/compiler-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", + "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/shared": "3.5.13", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/@vue/compiler-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", + "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/@vue/compiler-sfc": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", + "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "@vue/compiler-core": "3.5.13", + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.11", + "postcss": "^8.4.48", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/@vue/compiler-ssr": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", + "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/@vue/reactivity": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", + "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.13" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/@vue/runtime-core": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", + "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/shared": "3.5.13" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/@vue/runtime-dom": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", + "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.13", + "@vue/runtime-core": "3.5.13", + "@vue/shared": "3.5.13", + "csstype": "^3.1.3" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/@vue/server-renderer": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", + "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "vue": "3.5.13" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/@vue/shared": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", + "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@directus/extensions-sdk/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@directus/extensions-sdk/node_modules/vue": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", + "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.13", + "@vue/compiler-sfc": "3.5.13", + "@vue/runtime-dom": "3.5.13", + "@vue/server-renderer": "3.5.13", + "@vue/shared": "3.5.13" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@directus/schema": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@directus/schema/-/schema-13.0.0.tgz", + "integrity": "sha512-QrL+HQ6BWyoyQSXNoPNhaFE7OrK/9jcT10TP86qKxWwLt1BQ+yxgWqar53Hh3i4r/dA25qCIXhm9I/SLWqe9SA==", + "dev": true, + "license": "BUSL-1.1", + "dependencies": { + "knex": "3.1.0" + }, + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + } + }, + "node_modules/@directus/system-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@directus/system-data/-/system-data-3.0.0.tgz", + "integrity": "sha512-CwhZ/eRWqgmikxNgJONsYzdxGSr1us/35o+fTcTjh9YsLYBzV3d/jNzeU7JjAR1+i9AfFOwrnwQyraPmmS2nrw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + } + }, + "node_modules/@directus/themes": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@directus/themes/-/themes-1.0.7.tgz", + "integrity": "sha512-RBsl+vCKvbuvc0eJVXN8hDsXJwPGPl4yLOnnQiV7L0wW/h27W4jTB0Zf5mdyg+OBVSAxLhGStH5hUDmqWZJLIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@directus/utils": "13.0.0", + "@sinclair/typebox": "0.34.13", + "decamelize": "6.0.0", + "flat": "6.0.1", + "lodash-es": "4.17.21" + }, + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + }, + "peerDependencies": { + "@unhead/vue": "1", + "pinia": "2", + "vue": "^3.4" + } + }, + "node_modules/@directus/types": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@directus/types/-/types-13.0.0.tgz", + "integrity": "sha512-e1zcdk/vCYVTqo3uXVPPU5zJoCWcPHLihRz3ihn8cq/HVrTwAYAKpSjYCsvL9/cmjggubozocgGqfte/+HQ23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@directus/constants": "13.0.0", + "@directus/schema": "13.0.0", + "@types/geojson": "7946.0.15" + }, + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + }, + "peerDependencies": { + "knex": "3", + "vue": "^3.4" + }, + "peerDependenciesMeta": { + "knex": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/@directus/utils": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@directus/utils/-/utils-13.0.0.tgz", + "integrity": "sha512-2ZP2PUEQhW0fm4CNt8AoMEoS9DBMd/1NWTdA85OGIfjZiu3RUAaVgk9r3KPtt4Xr5UNLkxp/hIZXH0aUzfdMyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@directus/constants": "13.0.0", + "@directus/system-data": "3.0.0", + "date-fns": "4.1.0", + "fs-extra": "11.2.0", + "joi": "17.13.3", + "js-yaml": "4.1.0", + "lodash-es": "4.17.21", + "micromustache": "8.0.3" + }, + "funding": { + "url": "https://github.com/directus/directus?sponsor=1" + }, + "peerDependencies": { + "vue": "^3.4" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz", + "integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz", + "integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.1.21", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", + "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz", + "integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.23", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz", + "integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/external-editor": "^1.0.3", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz", + "integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz", + "integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz", + "integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", + "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz", + "integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.23", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz", + "integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", + "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^4.3.2", + "@inquirer/confirm": "^5.1.21", + "@inquirer/editor": "^4.2.23", + "@inquirer/expand": "^4.0.23", + "@inquirer/input": "^4.3.1", + "@inquirer/number": "^3.0.23", + "@inquirer/password": "^4.0.23", + "@inquirer/rawlist": "^4.1.11", + "@inquirer/search": "^3.2.2", + "@inquirer/select": "^4.4.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz", + "integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz", + "integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", + "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/figures": "^1.0.15", + "@inquirer/type": "^3.0.10", + "yoctocolors-cjs": "^2.1.3" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz", + "integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", + "integrity": "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz", + "integrity": "sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.5.1.tgz", + "integrity": "sha512-P9ZvnfouRu4MSC+ilERaw6FmZs8zqKhpHxEs4aGDGWQAMzQGXnhvCSvoNwokfNvdt7t3Y5Y4p6gStQgezeIqug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@sideway/address": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", + "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.13", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.13.tgz", + "integrity": "sha512-ceVKqyCEgC355Kw0s/0tyfY9MzMQINSykJ/pG2w6YnaZyrcjV48svZpr8lVZrYgWjzOmrIPBhQRAtr/7eJpA5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cssnano": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/cssnano/-/cssnano-5.0.0.tgz", + "integrity": "sha512-z98V7ICNAojxj9YV9+Q8qV+F7fW0poLWJRjed9tu7KNdYzHwAvLOAsTMI8xWjkOY9yzO+HmMxRRixlIvRsZwXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss": "^8" + } + }, + "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/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.9.tgz", + "integrity": "sha512-QP2ESEe/ImWY0HDwNAnK9PvEffUyhLTnWkk7KXzHfyeWAnlrDe1fN77bXl6ia8KT3wPlmA7t9/VPRpnf4Ex9sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.15", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.15.tgz", + "integrity": "sha512-9oSxFzDCT2Rj6DfcHF8G++jxBKS7mBqXl5xrRW+Kbvjry6Uduya2iiwqHPhVXpasAVMBYKkEPGgKhd3+/HZ6xA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@unhead/dom": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.11.20.tgz", + "integrity": "sha512-jgfGYdOH+xHJF/j8gudjsYu3oIjFyXhCWcgKaw3vQnT616gSqyqnGQGOItL+BQtQZACKNISwIfx5PuOtztMKLA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@unhead/schema": "1.11.20", + "@unhead/shared": "1.11.20" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/schema": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.11.20.tgz", + "integrity": "sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "hookable": "^5.5.3", + "zhead": "^2.2.4" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/shared": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.11.20.tgz", + "integrity": "sha512-1MOrBkGgkUXS+sOKz/DBh4U20DNoITlJwpmvSInxEUNhghSNb56S0RnaHRq0iHkhrO/cDgz2zvfdlRpoPLGI3w==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@unhead/schema": "1.11.20", + "packrup": "^0.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/vue": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.11.20.tgz", + "integrity": "sha512-sqQaLbwqY9TvLEGeq8Fd7+F2TIuV3nZ5ihVISHjWpAM3y7DwNWRU7NmT9+yYT+2/jw1Vjwdkv5/HvDnvCLrgmg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@unhead/schema": "1.11.20", + "@unhead/shared": "1.11.20", + "hookable": "^5.5.3", + "unhead": "1.11.20" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "vue": ">=2.7 || >=3" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz", + "integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.9", + "@vue/shared": "3.5.44", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz", + "integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz", + "integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.9", + "@vue/compiler-core": "3.5.44", + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-ssr": "3.5.44", + "@vue/shared": "3.5.44", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.29", + "source-map-js": "^1.2.2" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz", + "integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@vue/reactivity": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.44.tgz", + "integrity": "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.44.tgz", + "integrity": "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz", + "integrity": "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.44", + "@vue/runtime-core": "3.5.44", + "@vue/shared": "3.5.44", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.44.tgz", + "integrity": "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/shared": "3.5.44" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz", + "integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.4.0.tgz", + "integrity": "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.32", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.32.tgz", + "integrity": "sha512-UvuxFU9P3hM29LrQdwu0cD6w6/+SFr+UlB9s/knI7+O2rK9xFxKUwSDWxGdbbqQ9/V7BJ5s0NI5nQAE4uJp3gQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.29.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.4.tgz", + "integrity": "sha512-2TcbbiwXEq+N0kD36tykpCfyyQigUXmuwNQbnPp4Xk4PX9JOqVIKjJlIDjpH7uY6tmRGoAd+apeT1p60T8wsdA==", + "dev": true, + "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.11.29", + "caniuse-lite": "^1.0.30001816", + "electron-to-chromium": "^1.5.459", + "node-releases": "^2.0.59", + "update-browserslist-db": "^1.3.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001819", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001819.tgz", + "integrity": "sha512-LFUqIIAwpzsIuevnKnsR9qnStBcgzPShTX61lBwN9uFkkf8o8zLiebEjbW8AYtZiF+1YKDWfdt/pWDtMuG8Xwg==", + "dev": true, + "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/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", + "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.10.0.tgz", + "integrity": "sha512-AidJptpBJmjTclAp9BkLwJi0T93fo5epJnbaZslpg6QVzpHjAiveF55mE9AcUJiGMqRHgMDY8soMsQtuNYMHfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "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/css-declaration-sorter": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", + "integrity": "sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", + "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.0.1", + "domhandler": "^4.3.1", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "5.1.15", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-5.1.15.tgz", + "integrity": "sha512-j+BKgDcLDQA+eDifLx0EO4XSA56b7uut3BQFH+wbSaSTuGLuiyTa/wbRYthUXX8LC9mLg+WWKe8h+qJuwTAbHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^5.2.14", + "lilconfig": "^2.0.3", + "yaml": "^1.10.2" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-preset-default": { + "version": "5.2.14", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.2.14.tgz", + "integrity": "sha512-t0SFesj/ZV2OTylqQVOrFgEh5uanxbO6ZAdeCrNsUQ6fVuXwYTxJPNAGvGTxHbD68ldIJNec7PyYZDBrfDQ+6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-declaration-sorter": "^6.3.1", + "cssnano-utils": "^3.1.0", + "postcss-calc": "^8.2.3", + "postcss-colormin": "^5.3.1", + "postcss-convert-values": "^5.1.3", + "postcss-discard-comments": "^5.1.2", + "postcss-discard-duplicates": "^5.1.0", + "postcss-discard-empty": "^5.1.1", + "postcss-discard-overridden": "^5.1.0", + "postcss-merge-longhand": "^5.1.7", + "postcss-merge-rules": "^5.1.4", + "postcss-minify-font-values": "^5.1.0", + "postcss-minify-gradients": "^5.1.1", + "postcss-minify-params": "^5.1.4", + "postcss-minify-selectors": "^5.2.1", + "postcss-normalize-charset": "^5.1.0", + "postcss-normalize-display-values": "^5.1.0", + "postcss-normalize-positions": "^5.1.1", + "postcss-normalize-repeat-style": "^5.1.1", + "postcss-normalize-string": "^5.1.0", + "postcss-normalize-timing-functions": "^5.1.0", + "postcss-normalize-unicode": "^5.1.1", + "postcss-normalize-url": "^5.1.0", + "postcss-normalize-whitespace": "^5.1.1", + "postcss-ordered-values": "^5.1.3", + "postcss-reduce-initial": "^5.1.2", + "postcss-reduce-transforms": "^5.1.0", + "postcss-svgo": "^5.1.0", + "postcss-unique-selectors": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/cssnano-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz", + "integrity": "sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-6.0.0.tgz", + "integrity": "sha512-Fv96DCsdOgB6mdGl67MT5JaTNKRzrzill5OH5s8bjYJXVlcXyPYGyPsUkWyGV5p1TXI5esYIYMMeDJL0hEIwaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.464", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.464.tgz", + "integrity": "sha512-nRa65aFge40viPAKxMKULIY8tcXKwKboJWNn4RoKwyWXJ6SP7BnrDGKUTzc64818jIciH0PK7LSF8QzZes19GA==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esm": { + "version": "3.2.25", + "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", + "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/flat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-6.0.1.tgz", + "integrity": "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz", + "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getopts": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", + "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/inquirer": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.3.0.tgz", + "integrity": "sha512-3NixUXq+hM8ezj2wc7wC37b32/rHq1MwNZDYdvx+d6jokOD+r+i8Q4Pkylh9tISYP114A128LCX8RKhopC5RfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.2", + "@inquirer/prompts": "^7.2.1", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "mute-stream": "^2.0.0", + "run-async": "^3.0.0", + "rxjs": "^7.8.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/interpret": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.17.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.17.0.tgz", + "integrity": "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/joi": { + "version": "17.13.3", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", + "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.3.0", + "@hapi/topo": "^5.1.0", + "@sideway/address": "^4.1.5", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/knex": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.1.0.tgz", + "integrity": "sha512-GLoII6hR0c4ti243gMs5/1Rb3B+AjwMOfjYm97pu0FOQa7JH56hgBxYf5WK2525ceSbBY1cjeZ9yk99GPMB6Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "2.0.19", + "commander": "^10.0.0", + "debug": "4.3.4", + "escalade": "^3.1.1", + "esm": "^3.2.25", + "get-package-type": "^0.1.0", + "getopts": "2.3.0", + "interpret": "^2.2.0", + "lodash": "^4.17.21", + "pg-connection-string": "2.6.2", + "rechoir": "^0.8.0", + "resolve-from": "^5.0.0", + "tarn": "^3.0.2", + "tildify": "2.0.0" + }, + "bin": { + "knex": "bin/cli.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "better-sqlite3": { + "optional": true + }, + "mysql": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-native": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", + "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "is-unicode-supported": "^1.1.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromustache": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/micromustache/-/micromustache-8.0.3.tgz", + "integrity": "sha512-SXjrEPuYNtWq0reR9LR2nHdzdQx/3re9HPcDGjm00L7hi2RsH5KMRBhYEBvPdyQC51RW/2TznjwX/sQLPPyHNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/userpixel/micromustache/blob/master/.github/FUNDING.yml" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz", + "integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/node-releases": { + "version": "2.0.60", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.60.tgz", + "integrity": "sha512-OmO8+GM1JUZqXET720VrJCwkjUCXmCMQm0Z3UDDRbfNLHS4NEAvY60j5jiFv0tkKw2PNNlqOLhahVmwJIOLDIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-6.3.1.tgz", + "integrity": "sha512-ERAyNnZOfqM+Ao3RAvIXkYh5joP220yf59gVe2X/cI6SiCxIdi4c9HZKZD8R6q/RDXEje1THBju6iExiSsgJaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.0.0", + "cli-cursor": "^4.0.0", + "cli-spinners": "^2.6.1", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^1.1.0", + "log-symbols": "^5.1.0", + "stdin-discarder": "^0.1.0", + "strip-ansi": "^7.0.1", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/packrup": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/packrup/-/packrup-0.1.2.tgz", + "integrity": "sha512-ZcKU7zrr5GlonoS9cxxrb5HVswGnyj6jQvwFBa6p5VFw7G71VAHcUKL5wyZSU/ECtPM/9gacWxy2KFQKt1gMNA==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.20", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.4.tgz", + "integrity": "sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.9", + "postcss-value-parser": "^4.2.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" + } + }, + "node_modules/postcss-colormin": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.3.1.tgz", + "integrity": "sha512-UsWQG0AqTFQmpBegeLLc1+c3jIqBNB0zlDGRWR+dQ3pRKJL1oeMzyqmH3o2PIfn9MBdNrVPWhDbT769LxCTLJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "colord": "^2.9.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-convert-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz", + "integrity": "sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-comments": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz", + "integrity": "sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz", + "integrity": "sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-empty": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz", + "integrity": "sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz", + "integrity": "sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-longhand": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz", + "integrity": "sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^5.1.1" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-merge-rules": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.1.4.tgz", + "integrity": "sha512-0R2IuYpgU93y9lhVbO/OylTtKMVcHb67zjWIfCiKR9rWL3GUk1677LAqD/BcHizukdZEjT8Ru3oHRoAYoJy44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^3.1.0", + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz", + "integrity": "sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz", + "integrity": "sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "colord": "^2.9.1", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-params": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz", + "integrity": "sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz", + "integrity": "sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.7.tgz", + "integrity": "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.7.tgz", + "integrity": "sha512-wWBro/7hJ40ye6TPiXkUQoXKCw23xSsjriUvqd7Uls9Sso3oLiON1flc18cCdnjgCRXw4fBVqUJJeOdDTh3P3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz", + "integrity": "sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz", + "integrity": "sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz", + "integrity": "sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz", + "integrity": "sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz", + "integrity": "sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz", + "integrity": "sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz", + "integrity": "sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz", + "integrity": "sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew==", + "dev": true, + "license": "MIT", + "dependencies": { + "normalize-url": "^6.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz", + "integrity": "sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-ordered-values": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz", + "integrity": "sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-utils": "^3.1.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.1.2.tgz", + "integrity": "sha512-dE/y2XRaqAi6OvjzD22pjTUQ8eOfc6m/natGHgKFBK9DxFmIm69YmaRVQrGgFlEfc1HePIurY0TmDeROK05rIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz", + "integrity": "sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.5.tgz", + "integrity": "sha512-TEHFzb5KRDKaOgJ4TYE+0jyJtFbjLvkdVOu0fZ2b6MTWzoqZd4Le+61aEppWkvTlYePgOX4ATL1wsAIPMLTw6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz", + "integrity": "sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^2.7.0" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz", + "integrity": "sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.0.5" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/resolve": { + "version": "1.22.13", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.13.tgz", + "integrity": "sha512-Dj/cW9zBV2aRj6XzCxuGIs3ESZKv02jixne3q6jFZYZ3/Tg09gHHviSMDL4kObh9pqSahdRHEnDoaP2gn8YLhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.17.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", + "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-esbuild": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-esbuild/-/rollup-plugin-esbuild-5.0.0.tgz", + "integrity": "sha512-1cRIOHAPh8WQgdQQyyvFdeOdxuiyk+zB5zJ5+YOwrZP4cJ0MT3Fs48pQxrZeyZHcn+klFherytILVfE4aYrneg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "debug": "^4.3.4", + "es-module-lexer": "^1.0.5", + "joycon": "^3.1.1", + "jsonc-parser": "^3.2.0" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.10.1", + "rollup": "^1.20.0 || ^2.0.0 || ^3.0.0" + } + }, + "node_modules/rollup-plugin-styles": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-styles/-/rollup-plugin-styles-4.0.0.tgz", + "integrity": "sha512-A2K2sao84OsTmDxXG83JTCdXWrmgvQkkI38XDat46rdtpGMRm9tSYqeCdlwwGDJF4kKIafhV1mUidqu8MxUGig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^4.1.2", + "@types/cssnano": "^5.0.0", + "cosmiconfig": "^7.0.1", + "cssnano": "^5.0.15", + "fs-extra": "^10.0.0", + "icss-utils": "^5.1.0", + "mime-types": "^2.1.34", + "p-queue": "^6.6.2", + "postcss": "^8.4.5", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "query-string": "^7.1.0", + "resolve": "^1.21.0", + "source-map-js": "^1.0.1", + "tslib": "^2.3.1" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "rollup": "^2.63.0" + } + }, + "node_modules/rollup-plugin-styles/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/rollup-plugin-styles/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/rollup-plugin-styles/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/run-async": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", + "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "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/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", + "dev": true, + "license": "MIT" + }, + "node_modules/stdin-discarder": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", + "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylehacks": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-5.1.1.tgz", + "integrity": "sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.21.4", + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >=14.0" + }, + "peerDependencies": { + "postcss": "^8.2.15" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svgo": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.4.tgz", + "integrity": "sha512-2GJ4h3rl13qYTdwllaK6QlL8tG+UrM8626V2Ylcd/yBUv2Y/EwLsYisVV6UDCRmlk+C74G8nMpxJCk0RWPdDCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^7.2.0", + "css-select": "^4.1.3", + "css-tree": "^1.1.3", + "csso": "^4.2.0", + "picocolors": "^1.0.0", + "sax": "^1.5.0", + "stable": "^0.1.8" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/tarn": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.1.2.tgz", + "integrity": "sha512-3RTvqKZcK/17jnJ8rMKFXbyNogywTs1z0gVPPwFsJGX46rkmUHOdIaSQ/aVO1rS7nH+soiXiWk7rvUXxndm8Dg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/terser": { + "version": "5.51.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.51.2.tgz", + "integrity": "sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tildify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", + "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/darwin-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/freebsd-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ia32": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-loong64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-mips64el": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-ppc64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-riscv64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/linux-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/netbsd-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-arm64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-ia32": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/@esbuild/win32-x64": { + "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" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/tsx/node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unhead": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-1.11.20.tgz", + "integrity": "sha512-3AsNQC0pjwlLqEYHLjtichGWankK8yqmocReITecmpB1H0aOabeESueyy+8X1gyJx4ftZVwo9hqQ4O3fPWffCA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@unhead/dom": "1.11.20", + "@unhead/schema": "1.11.20", + "@unhead/shared": "1.11.20", + "hookable": "^5.5.3" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", + "dev": true, + "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/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", + "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/vite/node_modules/rollup": { + "version": "3.30.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.30.0.tgz", + "integrity": "sha512-kQvGasUgN+AlWGliFn2POSajRQEsULVYFGTvOZmK06d7vCD+YhZztt70kGk3qaeAXeWYL5eO7zx+rAubBc55eA==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/vue": { + "version": "3.5.44", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz", + "integrity": "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.44", + "@vue/compiler-sfc": "3.5.44", + "@vue/runtime-dom": "3.5.44", + "@vue/server-renderer": "3.5.44", + "@vue/shared": "3.5.44" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "peer": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "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/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", + "integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zhead": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", + "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/zod": { + "version": "3.24.1", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz", + "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2eaff78 --- /dev/null +++ b/package.json @@ -0,0 +1,63 @@ +{ + "name": "directus-extension-generate-image-previews", + "description": "Pre-generate Directus image transforms: on upload, on demand, and via a scheduled resumable backfill, with an admin UI (progress, disk usage, maintenance window)", + "author": { + "name": "Rawman Grey", + "email": "rawman@locus.im", + "url": "https://locus.im" + }, + "icon": "extension", + "version": "0.1.0", + "keywords": [ + "directus", + "directus-extension", + "directus-extension-bundle" + ], + "type": "module", + "files": [ + "dist" + ], + "directus:extension": { + "type": "bundle", + "path": { + "app": "dist/app.js", + "api": "dist/api.js" + }, + "entries": [ + { + "type": "module", + "name": "image-previews", + "source": "src/module/index.ts" + }, + { + "type": "hook", + "name": "image-previews-hook", + "source": "src/hook/index.ts" + }, + { + "type": "endpoint", + "name": "image-previews-endpoint", + "source": "src/endpoint/index.ts" + } + ], + "host": "^10.10.0 || ^11.0.0" + }, + "scripts": { + "build": "directus-extension build", + "dev": "directus-extension build -w --no-minify", + "link": "directus-extension link", + "typecheck": "tsc --noEmit", + "test": "node --import tsx --test src/lib/*.test.ts", + "publish": "scp -r ./dist ./package.json land-nest:/home/oak/apps/directus/extensions/directus-extension-generate-image-previews/" + }, + "devDependencies": { + "@directus/extensions-sdk": "13.0.1", + "@types/node": "^22.13.5", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vue": "^3.5.13" + }, + "dependencies": { + "object-hash": "^3.0.0" + } +} diff --git a/src/endpoint/index.ts b/src/endpoint/index.ts new file mode 100644 index 0000000..7a5e117 --- /dev/null +++ b/src/endpoint/index.ts @@ -0,0 +1,312 @@ +import { defineEndpoint } from '@directus/extensions-sdk'; +import type { Request, Response, NextFunction } from 'express'; +import { runBackfill, requestCancel } from '../lib/backfill'; +import { computePlan, getOps, recordOp } from '../lib/diagnostics'; +import { humanBytes, humanDuration, humanMs, toIso } from '../lib/format'; +import { buildRegistry, effectiveFormatsForPreset, formatsForPreset, loadCustomPresets } from '../lib/presets'; +import { mergeSettings, readSettings, writeSettings } from '../lib/settings'; +import { isLocked, readRun } from '../lib/state'; +import { previewExists, storageUsage } from '../lib/storage'; +import { SYSTEM_PRESETS } from '../lib/system-presets'; +import { isInWindow, nextWindow } from '../lib/window'; +import { warmFile } from '../lib/warmer'; + +const FILE_FIELDS = [ + 'id', + 'filename_disk', + 'type', + 'width', + 'height', + 'focal_point_x', + 'focal_point_y', + 'storage', +]; + +function asyncHandler(handler: (req: Request, res: Response) => Promise) { + return (req: Request, res: Response, next: NextFunction) => { + handler(req, res).catch(next); + }; +} + +function bucket(bytes: number, count: number) { + return { bytes, count, bytesHuman: humanBytes(bytes) }; +} + +export default defineEndpoint({ + id: 'image-previews', + handler: (router, context) => { + const { services, database, getSchema, env, logger } = context; + + router.use((req: Request, res: Response, next: NextFunction) => { + const accountability = (req as Request & { accountability?: { admin?: boolean } }).accountability; + + if (accountability?.admin === true) return next(); + + res.status(403).json({ error: 'admin access required' }); + }); + + router.get( + '/status', + asyncHandler(async (_req, res) => { + const settings = await readSettings(env); + const run = await readRun(env); + const next = nextWindow(settings.windowStart, settings.windowEnd); + + res.json({ + enabled: settings.enabled, + window: { start: settings.windowStart, end: settings.windowEnd }, + inWindow: isInWindow(settings.windowStart, settings.windowEnd), + nextWindow: next?.toISOString() ?? null, + nextWindowHuman: next?.toLocaleString() ?? null, + locked: await isLocked(env), + run: run + ? { + ...run, + startedAtIso: toIso(run.startedAt), + finishedAtIso: toIso(run.finishedAt), + startedAtHuman: run.startedAt ? new Date(run.startedAt).toLocaleString() : null, + finishedAtHuman: run.finishedAt ? new Date(run.finishedAt).toLocaleString() : null, + etaHuman: humanDuration(run.eta), + rateHuman: `${Math.round(run.rate)} items/min`, + } + : null, + }); + }), + ); + + router.get( + '/config', + asyncHandler(async (_req, res) => { + res.json(await readSettings(env)); + }), + ); + + router.put( + '/config', + asyncHandler(async (req, res) => { + try { + const current = await readSettings(env); + const merged = mergeSettings(current, req.body ?? {}); + const saved = await writeSettings(merged, env); + res.json(saved); + } catch (error) { + res.status(400).json({ error: error instanceof Error ? error.message : String(error) }); + } + }), + ); + + router.get( + '/presets', + asyncHandler(async (_req, res) => { + const schema = await getSchema(); + const settings = await readSettings(env); + const custom = await loadCustomPresets(new services['SettingsService']({ schema })); + const presets = buildRegistry(custom, null); + const systemKeys = new Set(SYSTEM_PRESETS.map((preset) => preset.key)); + const shadowedKeys = custom + .filter((preset) => systemKeys.has(preset.key)) + .map((preset) => preset.key); + + res.json({ + shadowedKeys, + presets: presets.map((preset) => ({ + key: preset.key, + format: preset.format ?? null, + effectiveFormats: effectiveFormatsForPreset(preset, settings.formats), + system: systemKeys.has(preset.key), + enabled: settings.presetKeys === null || settings.presetKeys.includes(preset.key), + })), + }); + }), + ); + + router.post( + '/run', + asyncHandler(async (req, res) => { + const settings = await readSettings(env); + const ignoreWindow = req.body?.ignoreWindow === true; + + if (!settings.enabled) { + res.status(409).json({ error: 'disabled' }); + return; + } + + if (!ignoreWindow && !isInWindow(settings.windowStart, settings.windowEnd)) { + const next = nextWindow(settings.windowStart, settings.windowEnd); + res.status(409).json({ + error: 'outside-window', + nextWindow: next?.toISOString() ?? null, + nextWindowHuman: next?.toLocaleString() ?? null, + }); + return; + } + + if (await isLocked(env)) { + res.status(409).json({ error: 'already-running' }); + return; + } + + const presetKeys = Array.isArray(req.body?.presets) ? (req.body.presets as string[]) : undefined; + + void runBackfill( + { services, database, getSchema, env, logger }, + { presetKeys, ignoreWindow }, + ).catch((error) => logger.error(error, 'image-previews: run failed')); + + res.status(202).json({ started: true }); + }), + ); + + router.post( + '/run/cancel', + asyncHandler(async (_req, res) => { + await requestCancel(env); + res.json({ cancelling: true }); + }), + ); + + router.get( + '/disk-usage', + asyncHandler(async (_req, res) => { + const usage = await storageUsage(env); + + res.json({ + previews: bucket(usage.previews.bytes, usage.previews.count), + originals: bucket(usage.originals.bytes, usage.originals.count), + total: bucket(usage.total.bytes, usage.total.count), + source: usage.source, + }); + }), + ); + + router.get( + '/ops', + asyncHandler(async (req, res) => { + const limit = Number(req.query.limit) || 50; + + res.json({ + ops: getOps(limit).map((op) => ({ + ...op, + tsIso: toIso(op.ts), + tsHuman: new Date(op.ts).toLocaleString(), + msHuman: humanMs(op.ms), + })), + }); + }), + ); + + router.post( + '/generate', + asyncHandler(async (req, res) => { + const fileId = req.body?.fileId; + + if (typeof fileId !== 'string' || fileId.length === 0) { + res.status(400).json({ error: 'fileId is required' }); + return; + } + + const settings = await readSettings(env); + const schema = await getSchema(); + const custom = await loadCustomPresets(new services['SettingsService']({ schema })); + const presetKeys = Array.isArray(req.body?.keys) ? (req.body.keys as string[]) : settings.presetKeys; + const formats = Array.isArray(req.body?.formats) ? (req.body.formats as string[]) : settings.formats; + const presets = buildRegistry(custom, presetKeys); + + const items = new services['ItemsService']('directus_files', { schema }); + + if (req.body?.dryRun === true) { + const file = await items.readOne(fileId, { fields: FILE_FIELDS }); + const plans = []; + + for (const preset of presets) { + for (const format of formatsForPreset(preset, formats)) { + const plan = await computePlan(file, preset, format); + plans.push({ + key: preset.key, + format, + effectiveFormat: plan.format, + filename: plan.filename, + exists: await previewExists(env, file.storage ?? null, plan.filename), + transforms: plan.transforms, + }); + } + } + + res.json({ dryRun: true, fileId, plans }); + return; + } + + const assets = new services['AssetsService']({ schema }); + const started = Date.now(); + const summary = await warmFile(assets, fileId, presets, { + concurrency: settings.concurrency, + formats, + onResult: (result) => + recordOp({ + ts: Date.now(), + fileId, + key: result.key, + format: result.format, + ok: result.ok, + ms: result.ms, + error: result.error, + }), + }); + + res.json({ + fileId, + ms: Date.now() - started, + msHuman: humanMs(Date.now() - started), + ...summary, + results: summary.results.map((result) => ({ ...result, msHuman: humanMs(result.ms) })), + }); + }), + ); + + router.get( + '/resolve-plan', + asyncHandler(async (req, res) => { + const fileId = typeof req.query.fileId === 'string' ? req.query.fileId : ''; + const key = typeof req.query.key === 'string' ? req.query.key : ''; + + if (!fileId || !key) { + res.status(400).json({ error: 'fileId and key are required' }); + return; + } + + const settings = await readSettings(env); + const schema = await getSchema(); + const custom = await loadCustomPresets(new services['SettingsService']({ schema })); + const presets = buildRegistry(custom, null); + const preset = presets.find((candidate) => candidate.key === key); + + if (!preset) { + const systemKeys = SYSTEM_PRESETS.map((candidate) => candidate.key); + const shadowed = custom.some((candidate) => candidate.key === key); + res.status(404).json({ + error: `unknown preset: ${key}`, + reason: shadowed && systemKeys.includes(key) ? 'shadowed-by-system' : 'not-found', + }); + return; + } + + const items = new services['ItemsService']('directus_files', { schema }); + const file = await items.readOne(fileId, { fields: FILE_FIELDS }); + const plans = []; + + for (const format of formatsForPreset(preset, settings.formats)) { + const plan = await computePlan(file, preset, format); + plans.push({ + format, + effectiveFormat: plan.format, + filename: plan.filename, + exists: await previewExists(env, file.storage ?? null, plan.filename), + transforms: plan.transforms, + }); + } + + res.json({ fileId, key, plans }); + }), + ); + }, +}); diff --git a/src/hook/index.ts b/src/hook/index.ts new file mode 100644 index 0000000..6807dc4 --- /dev/null +++ b/src/hook/index.ts @@ -0,0 +1,56 @@ +import { defineHook } from '@directus/extensions-sdk'; +import { recordOp } from '../lib/diagnostics'; +import { buildRegistry, isImageType, loadCustomPresets } from '../lib/presets'; +import { readSettings } from '../lib/settings'; +import { warmFile } from '../lib/warmer'; + +interface UploadMeta { + key?: string; + collection?: string; + payload?: { type?: string | null; collection?: string }; +} + +export default defineHook(({ action }, { services, getSchema, env, logger }) => { + action('files.upload', (meta) => { + void warmUpload(meta as UploadMeta).catch((error) => + logger.error(error, 'image-previews: upload warm failed'), + ); + }); + + async function warmUpload(meta: UploadMeta): Promise { + const settings = await readSettings(env); + + if (!settings.enabled || !settings.onUpload) return; + + const fileId = meta?.key; + const payload = meta?.payload; + + if (!fileId || !payload) return; + if (payload.collection && payload.collection !== 'directus_files') return; + if (!isImageType(payload.type)) return; + + const schema = await getSchema(); + const custom = await loadCustomPresets(new services['SettingsService']({ schema })); + const presets = buildRegistry(custom, settings.presetKeys); + + if (presets.length === 0) return; + + const assets = new services['AssetsService']({ schema }); + const summary = await warmFile(assets, fileId, presets, { + concurrency: settings.concurrency, + formats: settings.formats, + onResult: (result) => + recordOp({ + ts: Date.now(), + fileId, + key: result.key, + format: result.format, + ok: result.ok, + ms: result.ms, + error: result.error, + }), + }); + + if (settings.debug) logger.info({ fileId, summary }, 'image-previews: warmed upload'); + } +}); diff --git a/src/lib/backfill.ts b/src/lib/backfill.ts new file mode 100644 index 0000000..5891698 --- /dev/null +++ b/src/lib/backfill.ts @@ -0,0 +1,206 @@ +import { recordOp } from './diagnostics'; +import { buildRegistry, effectiveFormatsForPreset, isImageType, loadCustomPresets, tasksPerFile } from './presets'; +import { readSettings } from './settings'; +import { + acquireLock, + appendCompleted, + clearCancel, + completedKeyOf, + isCancelled, + loadCompleted, + readRun, + releaseLock, + requestCancel, + writeRun, +} from './state'; +import type { RunState } from './types'; +import { errorMessage, warmFile } from './warmer'; +import { isInWindow } from './window'; + +export interface BackfillDeps { + services: Record; + database: any; + getSchema: () => Promise; + env: Record; + logger: any; +} + +export interface BackfillOptions { + presetKeys?: string[] | null; + ignoreWindow?: boolean; +} + +export interface BackfillOutcome { + status: 'completed' | 'skipped' | 'failed'; + reason?: string; + run?: RunState; +} + +const PAGE_SIZE = 100; + +let inProcessRunning = false; + +async function countImageFiles(database: any): Promise { + try { + const rows = await database('directus_files').where('type', 'like', 'image/%').count({ count: '*' }); + const row = Array.isArray(rows) ? rows[0] : rows; + return Number(row?.count) || 0; + } catch { + return 0; + } +} + +export async function runBackfill(deps: BackfillDeps, options: BackfillOptions = {}): Promise { + if (inProcessRunning) return { status: 'skipped', reason: 'already-running' }; + + const settings = await readSettings(deps.env); + + if (!settings.enabled) return { status: 'skipped', reason: 'disabled' }; + + if (!options.ignoreWindow && !isInWindow(settings.windowStart, settings.windowEnd)) { + return { status: 'skipped', reason: 'outside-window' }; + } + + if (!(await acquireLock(deps.env))) return { status: 'skipped', reason: 'already-running' }; + + inProcessRunning = true; + await clearCancel(deps.env); + + try { + const schema = await deps.getSchema(); + const settingsService = new deps.services['SettingsService']({ schema }); + const custom = await loadCustomPresets(settingsService); + const presetKeys = options.presetKeys !== undefined ? options.presetKeys : settings.presetKeys; + const presets = buildRegistry(custom, presetKeys); + const formats = settings.formats; + const perFile = tasksPerFile(presets, formats); + const totalFiles = await countImageFiles(deps.database); + + const run: RunState = { + running: true, + startedAt: Date.now(), + finishedAt: null, + total: totalFiles * perFile, + done: 0, + generated: 0, + hits: 0, + failed: 0, + rate: 0, + eta: null, + cancelled: false, + }; + + await writeRun(run, deps.env); + + const completed = await loadCompleted(deps.env); + const items = new deps.services['ItemsService']('directus_files', { schema }); + const assets = new deps.services['AssetsService']({ schema }); + const started = Date.now(); + let page = 1; + + while (true) { + const files = await items.readByQuery({ + filter: { type: { _starts_with: 'image/' } }, + fields: [ 'id', 'filename_disk', 'type', 'width', 'height', 'focal_point_x', 'focal_point_y', 'storage' ], + limit: PAGE_SIZE, + page, + sort: [ 'id' ], + }); + + if (!Array.isArray(files) || files.length === 0) break; + + for (const file of files) { + if (await isCancelled(deps.env)) { + run.cancelled = true; + break; + } + + if (!isImageType(file.type)) continue; + + const pending = presets.filter((preset) => + effectiveFormatsForPreset(preset, formats).some( + (format) => !completed.has(completedKeyOf(file.id, preset.key, format)), + ), + ); + + const pendingTasks = pending.reduce( + (total, preset) => total + effectiveFormatsForPreset(preset, formats).length, + 0, + ); + const alreadyDone = perFile - pendingTasks; + + if (alreadyDone > 0) { + run.hits += alreadyDone; + run.done += alreadyDone; + } + + if (pending.length === 0) { + await writeRun(run, deps.env); + continue; + } + + const summary = await warmFile(assets, file.id, pending, { + concurrency: settings.concurrency, + formats, + onResult: (result) => + recordOp({ + ts: Date.now(), + fileId: file.id, + key: result.key, + format: result.format, + ok: result.ok, + ms: result.ms, + error: result.error, + }), + }); + + for (const result of summary.results) { + if (result.ok) { + completed.add(completedKeyOf(file.id, result.key, result.format)); + await appendCompleted( + { file_id: file.id, preset_key: result.key, format: result.format, ts: Date.now() }, + deps.env, + ); + run.generated += 1; + } else { + run.failed += 1; + } + + run.done += 1; + } + + const elapsedMinutes = (Date.now() - started) / 60000; + run.rate = elapsedMinutes > 0 ? run.done / elapsedMinutes : 0; + run.eta = run.rate > 0 ? Math.round(((run.total - run.done) / run.rate) * 60) : null; + await writeRun(run, deps.env); + } + + if (run.cancelled || files.length < PAGE_SIZE) break; + page += 1; + } + + run.running = false; + run.finishedAt = Date.now(); + await writeRun(run, deps.env); + + return { status: 'completed', run }; + } catch (error) { + deps.logger?.error?.(error, 'image-previews: backfill failed'); + const existing = await readRun(deps.env); + + if (existing) { + existing.running = false; + existing.finishedAt = Date.now(); + existing.error = errorMessage(error); + await writeRun(existing, deps.env); + } + + return { status: 'failed', reason: errorMessage(error) }; + } finally { + inProcessRunning = false; + await releaseLock(deps.env); + await clearCancel(deps.env); + } +} + +export { requestCancel }; diff --git a/src/lib/diagnostics.test.ts b/src/lib/diagnostics.test.ts new file mode 100644 index 0000000..a74a34f --- /dev/null +++ b/src/lib/diagnostics.test.ts @@ -0,0 +1,77 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { computePlan, getOps, recordOp, type PlanFile } from './diagnostics'; +import type { Preset } from './types'; + +const file: PlanFile = { + filename_disk: 'abc123.jpg', + type: 'image/jpeg', + width: 1200, + height: 800, + focal_point_x: null, + focal_point_y: null, + storage: 'local', +}; + +const preset: Preset = { + key: 'system-small-cover', + format: 'auto', + transforms: [ [ 'resize', { width: 64, height: 64, fit: 'cover' } ] ], +}; + +function utilsWith(format: string | undefined) { + return { + resolvePreset: () => + format ? [ [ 'toFormat', format, { quality: undefined } ], [ 'resize', { width: 64 } ] ] : [ [ 'resize', { width: 64 } ] ], + maybeExtractFormat: () => format, + }; +} + +test('computePlan predicts a hashed filename with the resolved extension', async () => { + const plan = await computePlan(file, preset, 'avif', { + utils: utilsWith('avif'), + hashFn: () => 'deadbeef', + }); + + assert.equal(plan.filename, 'abc123__deadbeef.avif'); + assert.equal(plan.format, 'avif'); + assert.equal(plan.transforms.length, 2); +}); + +test('computePlan without a toFormat keeps the original extension but still hashes the resize', async () => { + const plan = await computePlan(file, { key: 'resize-only', width: 100 }, null, { + utils: utilsWith(undefined), + hashFn: () => 'unused', + }); + + assert.equal(plan.filename, 'abc123__unused.jpg'); + assert.equal(plan.format, null); +}); + +test('computePlan passes acceptFormat into the transformation set', async () => { + let seen: Record | null = null; + + await computePlan(file, preset, 'webp', { + utils: { + resolvePreset: (transformation) => { + seen = transformation; + return [ [ 'toFormat', 'webp', {} ] ]; + }, + maybeExtractFormat: () => 'webp', + }, + hashFn: () => 'x', + }); + + assert.equal(seen?.['acceptFormat'], 'webp'); + assert.ok(seen?.['transformationParams']); +}); + +test('ops ring buffer is newest-first and bounded', () => { + recordOp({ ts: 1, fileId: 'a', key: 'k', format: 'avif', ok: true, ms: 1 }); + recordOp({ ts: 2, fileId: 'b', key: 'k', format: 'avif', ok: false, ms: 2, error: 'x' }); + + const ops = getOps(1); + assert.equal(ops.length, 1); + assert.equal(ops[0]?.fileId, 'b'); + assert.equal(getOps(0).length, 0); +}); diff --git a/src/lib/diagnostics.ts b/src/lib/diagnostics.ts new file mode 100644 index 0000000..9b4eb69 --- /dev/null +++ b/src/lib/diagnostics.ts @@ -0,0 +1,97 @@ +import { basename, extname } from 'node:path'; +import objectHash from 'object-hash'; +import type { Preset } from './types'; + +export interface PlanFile { + filename_disk: string; + type?: string | null; + width?: number | null; + height?: number | null; + focal_point_x?: number | null; + focal_point_y?: number | null; + storage?: string | null; +} + +export interface PlanResult { + filename: string; + format: string | null; + transforms: unknown[]; +} + +interface TransformUtils { + resolvePreset(transformation: Record, file: PlanFile): unknown[]; + maybeExtractFormat(transforms: unknown[]): string | undefined; +} + +export interface PlanDeps { + utils?: TransformUtils; + hashFn?: (value: unknown) => string; +} + +async function loadTransformUtils(): Promise { + const importer = new Function('return import("@directus/api/utils/transformations")') as () => Promise< + Record + >; + const module = await importer(); + const namespace = (module['TransformationUtils'] as Record | undefined) ?? module; + + const resolvePreset = (namespace['resolvePreset'] ?? module['resolvePreset']) as TransformUtils['resolvePreset']; + const maybeExtractFormat = (namespace['maybeExtractFormat'] ?? module['maybeExtractFormat']) as TransformUtils['maybeExtractFormat']; + + if (typeof resolvePreset !== 'function' || typeof maybeExtractFormat !== 'function') { + throw new Error('Unable to load Directus transformation utils'); + } + + return { resolvePreset, maybeExtractFormat }; +} + +export async function computePlan( + file: PlanFile, + preset: Preset, + acceptFormat: string | null, + deps: PlanDeps = {}, +): Promise { + const utils = deps.utils ?? (await loadTransformUtils()); + const hashFn = deps.hashFn ?? ((value: unknown) => objectHash(value)); + + const transformationParams: Record = { ...preset }; + const transformation: Record = { transformationParams }; + + if (acceptFormat) transformation['acceptFormat'] = acceptFormat; + + const transforms = utils.resolvePreset(transformation, file); + const suffix = transforms.length > 0 ? `__${hashFn(transforms)}` : ''; + const newFormat = utils.maybeExtractFormat(transforms); + const originalExtension = extname(file.filename_disk); + const base = basename(file.filename_disk, originalExtension); + const extension = newFormat ? `.${newFormat}` : originalExtension; + + return { + filename: `${base}${suffix}${extension}`, + format: newFormat ?? null, + transforms, + }; +} + +export interface OpRecord { + ts: number; + fileId: string; + key: string; + format: string | null; + ok: boolean; + ms: number; + error?: string; +} + +const MAX_OPS = 200; +const ops: OpRecord[] = []; + +export function recordOp(op: OpRecord): void { + ops.unshift(op); + + if (ops.length > MAX_OPS) ops.length = MAX_OPS; +} + +export function getOps(limit = 50): OpRecord[] { + return ops.slice(0, Math.max(0, Math.min(limit, MAX_OPS))); +} diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts new file mode 100644 index 0000000..6fb0f33 --- /dev/null +++ b/src/lib/format.test.ts @@ -0,0 +1,31 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { humanBytes, humanDuration, humanMs, toIso } from './format'; + +test('humanBytes formats sizes', () => { + assert.equal(humanBytes(0), '0 B'); + assert.equal(humanBytes(512), '512 B'); + assert.equal(humanBytes(1024), '1.0 KB'); + assert.equal(humanBytes(1536), '1.5 KB'); + assert.equal(humanBytes(1024 * 1024), '1.0 MB'); +}); + +test('humanMs formats durations', () => { + assert.equal(humanMs(250), '250 ms'); + assert.equal(humanMs(1500), '1.5 s'); + assert.equal(humanMs(65000), '1 min 5 s'); +}); + +test('humanDuration formats seconds', () => { + assert.equal(humanDuration(null), '—'); + assert.equal(humanDuration(30), '30 s'); + assert.equal(humanDuration(90), '1 min 30 s'); + assert.equal(humanDuration(3700), '1 h 1 min'); +}); + +test('toIso returns null for empty values', () => { + assert.equal(toIso(null), null); + assert.equal(toIso(undefined), null); + assert.equal(toIso(0), null); + assert.equal(toIso(1792880701827), new Date(1792880701827).toISOString()); +}); diff --git a/src/lib/format.ts b/src/lib/format.ts new file mode 100644 index 0000000..050b330 --- /dev/null +++ b/src/lib/format.ts @@ -0,0 +1,34 @@ +export function humanBytes(bytes: number): string { + if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'; + + const units = [ 'B', 'KB', 'MB', 'GB', 'TB', 'PB' ]; + const exponent = Math.min(units.length - 1, Math.floor(Math.log(bytes) / Math.log(1024))); + const value = bytes / Math.pow(1024, exponent); + + return `${value.toFixed(exponent === 0 ? 0 : 1)} ${units[exponent]}`; +} + +export function humanMs(ms: number): string { + if (!Number.isFinite(ms) || ms < 0) return '—'; + if (ms < 1000) return `${Math.round(ms)} ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)} s`; + return `${Math.floor(ms / 60000)} min ${Math.round((ms % 60000) / 1000)} s`; +} + +export function humanDuration(seconds: number | null | undefined): string { + if (seconds === null || seconds === undefined || !Number.isFinite(seconds) || seconds < 0) return '—'; + if (seconds < 60) return `${Math.round(seconds)} s`; + + const minutes = Math.floor(seconds / 60); + const rest = Math.round(seconds % 60); + + if (minutes < 60) return rest > 0 ? `${minutes} min ${rest} s` : `${minutes} min`; + + const hours = Math.floor(minutes / 60); + return `${hours} h ${minutes % 60} min`; +} + +export function toIso(value: number | null | undefined): string | null { + if (!value || !Number.isFinite(value)) return null; + return new Date(value).toISOString(); +} diff --git a/src/lib/presets.test.ts b/src/lib/presets.test.ts new file mode 100644 index 0000000..f55beb2 --- /dev/null +++ b/src/lib/presets.test.ts @@ -0,0 +1,63 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { buildRegistry, effectiveFormatsForPreset, formatsForPreset, mergePresets, tasksPerFile, uniqueByKey } from './presets'; +import { SYSTEM_PRESETS } from './system-presets'; +import type { Preset } from './types'; + +test('system presets are the six expected keys', () => { + assert.deepEqual( + SYSTEM_PRESETS.map((preset) => preset.key), + [ + 'system-small-cover', + 'system-small-contain', + 'system-medium-cover', + 'system-medium-contain', + 'system-large-cover', + 'system-large-contain', + ], + ); +}); + +test('mergePresets appends custom after system', () => { + const custom: Preset[] = [ { key: 'custom', width: 100 } ]; + const merged = mergePresets(SYSTEM_PRESETS, custom); + assert.equal(merged.length, SYSTEM_PRESETS.length + 1); + assert.equal(merged[merged.length - 1]?.key, 'custom'); +}); + +test('uniqueByKey keeps the first occurrence (controller parity)', () => { + const custom: Preset[] = [ { key: 'system-small-cover', width: 999 } ]; + const merged = uniqueByKey(mergePresets(SYSTEM_PRESETS, custom)); + const small = merged.find((preset) => preset.key === 'system-small-cover'); + assert.equal(small?.width, undefined); + assert.equal(merged.filter((preset) => preset.key === 'system-small-cover').length, 1); +}); + +test('buildRegistry selects by key or returns all', () => { + const all = buildRegistry([], null); + assert.equal(all.length, 6); + + const selected = buildRegistry([], [ 'system-medium-cover' ]); + assert.deepEqual(selected.map((preset) => preset.key), [ 'system-medium-cover' ]); +}); + +test('formatsForPreset emits two variants for auto and one otherwise', () => { + const auto: Preset = { key: 'a', format: 'auto' }; + const fixed: Preset = { key: 'b', format: 'webp' }; + const none: Preset = { key: 'c', width: 100 }; + + assert.deepEqual(formatsForPreset(auto, [ 'avif', 'webp' ]), [ 'avif', 'webp' ]); + assert.deepEqual(formatsForPreset(fixed, [ 'avif', 'webp' ]), [ null ]); + assert.deepEqual(formatsForPreset(none, [ 'avif', 'webp' ]), [ null ]); +}); + +test('effectiveFormatsForPreset reports the real output format', () => { + assert.deepEqual(effectiveFormatsForPreset({ key: 'a', format: 'auto' }, [ 'avif', 'webp' ]), [ 'avif', 'webp' ]); + assert.deepEqual(effectiveFormatsForPreset({ key: 'b', format: 'png' }, [ 'avif', 'webp' ]), [ 'png' ]); + assert.deepEqual(effectiveFormatsForPreset({ key: 'c', width: 100 }, [ 'avif', 'webp' ]), [ null ]); +}); + +test('tasksPerFile counts variants across presets', () => { + assert.equal(tasksPerFile(SYSTEM_PRESETS, [ 'avif', 'webp' ]), 12); + assert.equal(tasksPerFile(SYSTEM_PRESETS, [ 'avif' ]), 6); +}); diff --git a/src/lib/presets.ts b/src/lib/presets.ts new file mode 100644 index 0000000..3712239 --- /dev/null +++ b/src/lib/presets.ts @@ -0,0 +1,58 @@ +import { SYSTEM_PRESETS } from './system-presets'; +import type { Preset } from './types'; + +export function mergePresets(system: Preset[], custom: Preset[]): Preset[] { + return [ ...system, ...custom ]; +} + +export function uniqueByKey(presets: Preset[]): Preset[] { + const seen = new Set(); + + return presets.filter((preset) => { + if (seen.has(preset.key)) return false; + seen.add(preset.key); + return true; + }); +} + +export function selectPresets(presets: Preset[], keys: string[] | null): Preset[] { + if (keys === null) return presets; + + const wanted = new Set(keys); + return presets.filter((preset) => wanted.has(preset.key)); +} + +export function buildRegistry(custom: Preset[], keys: string[] | null): Preset[] { + const merged = uniqueByKey(mergePresets(SYSTEM_PRESETS, custom)); + return selectPresets(merged, keys); +} + +export function formatsForPreset(preset: Preset, formats: string[]): (string | null)[] { + if (preset.format === 'auto') return formats.length > 0 ? [ ...formats ] : [ 'avif', 'webp' ]; + return [ null ]; +} + +export function effectiveFormat(preset: Preset, variant: string | null): string | null { + return variant ?? preset.format ?? null; +} + +export function effectiveFormatsForPreset(preset: Preset, formats: string[]): (string | null)[] { + return formatsForPreset(preset, formats).map((variant) => effectiveFormat(preset, variant)); +} + +export function tasksPerFile(presets: Preset[], formats: string[]): number { + return presets.reduce((total, preset) => total + formatsForPreset(preset, formats).length, 0); +} + +export function isImageType(type: unknown): boolean { + return typeof type === 'string' && type.startsWith('image/'); +} + +export async function loadCustomPresets(settingsService: { + readSingleton(query?: Record): Promise | null>; +}): Promise { + const settings = await settingsService.readSingleton({ fields: [ 'storage_asset_presets' ] }); + const list = settings?.['storage_asset_presets']; + + return Array.isArray(list) ? (list as Preset[]) : []; +} diff --git a/src/lib/settings.test.ts b/src/lib/settings.test.ts new file mode 100644 index 0000000..87fbd5c --- /dev/null +++ b/src/lib/settings.test.ts @@ -0,0 +1,69 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { envDefaults, mergeSettings, validateSettings } from './settings'; + +test('envDefaults applies safe defaults', () => { + const settings = envDefaults({}); + assert.equal(settings.enabled, true); + assert.equal(settings.windowStart, '01:00'); + assert.equal(settings.windowEnd, '04:00'); + assert.equal(settings.concurrency, 1); + assert.deepEqual(settings.formats, [ 'avif', 'webp' ]); + assert.equal(settings.onUpload, true); + assert.equal(settings.presetKeys, null); +}); + +test('envDefaults parses overrides', () => { + const settings = envDefaults({ + IMAGE_PREVIEWS_ENABLED: 'true', + IMAGE_PREVIEWS_CONCURRENCY: '4', + IMAGE_PREVIEWS_FORMATS: 'webp', + IMAGE_PREVIEWS_PRESETS: 'system-small-cover, system-large-cover', + IMAGE_PREVIEWS_ON_UPLOAD: '0', + }); + + assert.equal(settings.enabled, true); + assert.equal(settings.concurrency, 4); + assert.deepEqual(settings.formats, [ 'webp' ]); + assert.deepEqual(settings.presetKeys, [ 'system-small-cover', 'system-large-cover' ]); + assert.equal(settings.onUpload, false); +}); + +test('envDefaults tolerates Directus-coerced booleans and numbers', () => { + const settings = envDefaults({ + IMAGE_PREVIEWS_ENABLED: true, + IMAGE_PREVIEWS_CONCURRENCY: 3, + IMAGE_PREVIEWS_ON_UPLOAD: false, + }); + + assert.equal(settings.enabled, true); + assert.equal(settings.concurrency, 3); + assert.equal(settings.onUpload, false); +}); + +test('mergeSettings keeps base values for absent overrides', () => { + const base = envDefaults({}); + const merged = mergeSettings(base, { enabled: true }); + assert.equal(merged.enabled, true); + assert.equal(merged.concurrency, base.concurrency); + assert.deepEqual(merged.formats, base.formats); +}); + +test('validateSettings rejects invalid concurrency', () => { + const base = envDefaults({}); + assert.throws(() => validateSettings({ ...base, concurrency: 0 }), /concurrency/); + assert.throws(() => validateSettings({ ...base, concurrency: 99 }), /concurrency/); +}); + +test('validateSettings rejects invalid window and format', () => { + const base = envDefaults({}); + assert.throws(() => validateSettings({ ...base, windowStart: '25:00' }), /windowStart/); + assert.throws(() => validateSettings({ ...base, formats: [ 'bmp' ] }), /unsupported format/); +}); + +test('validateSettings normalises a valid payload', () => { + const base = envDefaults({}); + const result = validateSettings({ ...base, windowStart: '03:00', windowEnd: '05:30' }); + assert.equal(result.windowStart, '03:00'); + assert.equal(result.windowEnd, '05:30'); +}); diff --git a/src/lib/settings.ts b/src/lib/settings.ts new file mode 100644 index 0000000..7f41b34 --- /dev/null +++ b/src/lib/settings.ts @@ -0,0 +1,156 @@ +import { mkdir, readFile, writeFile } from 'node:fs/promises'; +import { dirname, isAbsolute, join } from 'node:path'; + +export interface Settings { + enabled: boolean; + windowStart: string | null; + windowEnd: string | null; + concurrency: number; + presetKeys: string[] | null; + formats: string[]; + onUpload: boolean; + debug: boolean; +} + +export interface EnvLike { + [ key: string ]: unknown; +} + +const DEFAULT_STATE_SUBDIR = '.image-previews'; +const DEFAULT_WINDOW_START = '01:00'; +const DEFAULT_WINDOW_END = '04:00'; + +function asString(value: unknown): string | null { + if (value === undefined || value === null) return null; + if (typeof value === 'string') return value; + if (typeof value === 'number' || typeof value === 'boolean') return String(value); + return null; +} + +export function stateDir(env: EnvLike = process.env): string { + const configured = asString(env['IMAGE_PREVIEWS_STATE_DIR']); + + if (configured && configured.length > 0) return configured; + + const root = asString(env['STORAGE_LOCAL_ROOT']) || '/directus/uploads'; + const absoluteRoot = isAbsolute(root) ? root : join(process.cwd(), root); + + return join(absoluteRoot, DEFAULT_STATE_SUBDIR); +} + +function parseBool(value: unknown, fallback: boolean): boolean { + const raw = asString(value); + if (raw === null || raw.trim() === '') return fallback; + return [ '1', 'true', 'yes', 'on' ].includes(raw.trim().toLowerCase()); +} + +function parseList(value: unknown): string[] | null { + const raw = asString(value); + if (raw === null || raw.trim() === '') return null; + + return raw + .split(',') + .map((part) => part.trim()) + .filter(Boolean); +} + +function parseNumber(value: unknown, fallback: number): number { + const raw = asString(value); + if (raw === null || raw.trim() === '') return fallback; + const parsed = Number(raw); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +export function envDefaults(env: EnvLike = process.env): Settings { + return { + enabled: parseBool(env['IMAGE_PREVIEWS_ENABLED'], true), + windowStart: asString(env['IMAGE_PREVIEWS_WINDOW_START']) ?? DEFAULT_WINDOW_START, + windowEnd: asString(env['IMAGE_PREVIEWS_WINDOW_END']) ?? DEFAULT_WINDOW_END, + concurrency: parseNumber(env['IMAGE_PREVIEWS_CONCURRENCY'], 1), + presetKeys: parseList(env['IMAGE_PREVIEWS_PRESETS']), + formats: parseList(env['IMAGE_PREVIEWS_FORMATS']) ?? [ 'avif', 'webp' ], + onUpload: parseBool(env['IMAGE_PREVIEWS_ON_UPLOAD'], true), + debug: parseBool(env['IMAGE_PREVIEWS_DEBUG'], false), + }; +} + +export function settingsPath(env: EnvLike = process.env): string { + return join(stateDir(env), 'settings.json'); +} + +export function mergeSettings(base: Settings, override: Partial): Settings { + return { + ...base, + ...override, + formats: override.formats ?? base.formats, + presetKeys: override.presetKeys !== undefined ? override.presetKeys : base.presetKeys, + }; +} + +export async function readSettings(env: EnvLike = process.env): Promise { + const defaults = envDefaults(env); + + try { + const raw = await readFile(settingsPath(env), 'utf8'); + const parsed = JSON.parse(raw) as Partial; + return validateSettings(mergeSettings(defaults, parsed)); + } catch { + return defaults; + } +} + +export async function writeSettings(settings: Settings, env: EnvLike = process.env): Promise { + const validated = validateSettings(settings); + const target = settingsPath(env); + await mkdir(dirname(target), { recursive: true }); + await writeFile(target, `${JSON.stringify(validated, null, 2)}\n`, 'utf8'); + return validated; +} + +const TIME_PATTERN = /^([01]\d|2[0-3]):[0-5]\d$/; +const ALLOWED_FORMATS = [ 'avif', 'webp', 'jpg', 'jpeg', 'png' ]; + +export function validateSettings(settings: Settings): Settings { + if (typeof settings.enabled !== 'boolean') throw new Error('enabled must be a boolean'); + if (typeof settings.onUpload !== 'boolean') throw new Error('onUpload must be a boolean'); + if (typeof settings.debug !== 'boolean') throw new Error('debug must be a boolean'); + + if (settings.windowStart !== null && !TIME_PATTERN.test(settings.windowStart)) { + throw new Error('windowStart must be HH:MM or null'); + } + + if (settings.windowEnd !== null && !TIME_PATTERN.test(settings.windowEnd)) { + throw new Error('windowEnd must be HH:MM or null'); + } + + const concurrency = Number(settings.concurrency); + + if (!Number.isInteger(concurrency) || concurrency < 1 || concurrency > 32) { + throw new Error('concurrency must be an integer between 1 and 32'); + } + + if (!Array.isArray(settings.formats) || settings.formats.length === 0) { + throw new Error('formats must be a non-empty array'); + } + + for (const format of settings.formats) { + if (!ALLOWED_FORMATS.includes(format)) throw new Error(`unsupported format: ${format}`); + } + + if (settings.presetKeys !== null) { + if (!Array.isArray(settings.presetKeys) || settings.presetKeys.some((key) => typeof key !== 'string')) { + throw new Error('presetKeys must be an array of strings or null'); + } + } + + return { + enabled: settings.enabled, + windowStart: settings.windowStart, + windowEnd: settings.windowEnd, + concurrency, + presetKeys: settings.presetKeys, + formats: [ ...settings.formats ], + onUpload: settings.onUpload, + debug: settings.debug, + }; +} diff --git a/src/lib/state.test.ts b/src/lib/state.test.ts new file mode 100644 index 0000000..aeb7b62 --- /dev/null +++ b/src/lib/state.test.ts @@ -0,0 +1,95 @@ +import assert from 'node:assert/strict'; +import { appendFile, mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { test } from 'node:test'; +import { + acquireLock, + appendCompleted, + completedKeyOf, + isCancelled, + loadCompleted, + readRun, + releaseLock, + requestCancel, + writeRun, +} from './state'; +import type { RunState } from './types'; + +async function withTempEnv(run: (env: Record) => Promise): Promise { + const dir = await mkdtemp(join(tmpdir(), 'image-previews-')); + + try { + await run({ IMAGE_PREVIEWS_STATE_DIR: dir }); + } finally { + await rm(dir, { recursive: true, force: true }); + } +} + +test('appendCompleted + loadCompleted round-trips and resumes', async () => { + await withTempEnv(async (env) => { + const empty = await loadCompleted(env); + assert.equal(empty.size, 0); + + await appendCompleted({ file_id: 'f1', preset_key: 'system-small-cover', format: 'avif', ts: 1 }, env); + await appendCompleted({ file_id: 'f1', preset_key: 'system-small-cover', format: 'webp', ts: 2 }, env); + + const completed = await loadCompleted(env); + assert.equal(completed.size, 2); + assert.ok(completed.has(completedKeyOf('f1', 'system-small-cover', 'avif'))); + assert.ok(completed.has(completedKeyOf('f1', 'system-small-cover', 'webp'))); + }); +}); + +test('loadCompleted tolerates corrupt lines', async () => { + await withTempEnv(async (env) => { + await appendFile(join(env['IMAGE_PREVIEWS_STATE_DIR'] as string, 'state.jsonl'), 'not-json\n', 'utf8'); + await appendCompleted({ file_id: 'f2', preset_key: 'k', format: null, ts: 1 }, env); + + const completed = await loadCompleted(env); + assert.equal(completed.size, 1); + }); +}); + +test('run state round-trips', async () => { + await withTempEnv(async (env) => { + assert.equal(await readRun(env), null); + + const run: RunState = { + running: true, + startedAt: 1, + finishedAt: null, + total: 10, + done: 2, + generated: 2, + hits: 0, + failed: 0, + rate: 1, + eta: 8, + cancelled: false, + }; + + await writeRun(run, env); + const loaded = await readRun(env); + assert.equal(loaded?.done, 2); + assert.equal(loaded?.total, 10); + }); +}); + +test('lock is exclusive and releasable', async () => { + await withTempEnv(async (env) => { + assert.equal(await acquireLock(env), true); + assert.equal(await acquireLock(env), false); + await releaseLock(env); + assert.equal(await acquireLock(env), true); + await releaseLock(env); + }); +}); + +test('cancel flag toggles', async () => { + await withTempEnv(async (env) => { + assert.equal(await isCancelled(env), false); + await requestCancel(env); + assert.equal(await isCancelled(env), true); + }); +}); diff --git a/src/lib/state.ts b/src/lib/state.ts new file mode 100644 index 0000000..09be302 --- /dev/null +++ b/src/lib/state.ts @@ -0,0 +1,107 @@ +import { appendFile, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { stateDir, type EnvLike } from './settings'; +import type { CompletedRecord, RunState } from './types'; + +const STALE_LOCK_MS = 6 * 60 * 60 * 1000; + +export function completedKey(record: CompletedRecord): string { + return completedKeyOf(record.file_id, record.preset_key, record.format); +} + +export function completedKeyOf(fileId: string, presetKey: string, format: string | null): string { + return `${fileId}::${presetKey}::${format ?? ''}`; +} + +export function stateFilePath(env: EnvLike, name: string): string { + return join(stateDir(env), name); +} + +export async function ensureStateDir(env: EnvLike): Promise { + await mkdir(stateDir(env), { recursive: true }); +} + +export async function loadCompleted(env: EnvLike): Promise> { + const completed = new Set(); + + try { + const raw = await readFile(stateFilePath(env, 'state.jsonl'), 'utf8'); + + for (const line of raw.split('\n')) { + if (!line.trim()) continue; + + try { + completed.add(completedKey(JSON.parse(line) as CompletedRecord)); + } catch { + continue; + } + } + } catch { + return completed; + } + + return completed; +} + +export async function appendCompleted(record: CompletedRecord, env: EnvLike): Promise { + await ensureStateDir(env); + await appendFile(stateFilePath(env, 'state.jsonl'), `${JSON.stringify(record)}\n`, 'utf8'); +} + +export async function readRun(env: EnvLike): Promise { + try { + return JSON.parse(await readFile(stateFilePath(env, 'run.json'), 'utf8')) as RunState; + } catch { + return null; + } +} + +export async function writeRun(run: RunState, env: EnvLike): Promise { + await ensureStateDir(env); + await writeFile(stateFilePath(env, 'run.json'), `${JSON.stringify(run, null, 2)}\n`, 'utf8'); +} + +async function lockAge(env: EnvLike): Promise { + try { + const info = JSON.parse(await readFile(stateFilePath(env, 'run.lock'), 'utf8')) as { ts?: number }; + return Date.now() - (Number(info.ts) || 0); + } catch { + return null; + } +} + +export async function isLocked(env: EnvLike): Promise { + const age = await lockAge(env); + return age !== null && age < STALE_LOCK_MS; +} + +export async function acquireLock(env: EnvLike): Promise { + await ensureStateDir(env); + + if (await isLocked(env)) return false; + + await writeFile(stateFilePath(env, 'run.lock'), JSON.stringify({ ts: Date.now(), pid: process.pid }), 'utf8'); + return true; +} + +export async function releaseLock(env: EnvLike): Promise { + await rm(stateFilePath(env, 'run.lock'), { force: true }); +} + +export async function requestCancel(env: EnvLike): Promise { + await ensureStateDir(env); + await writeFile(stateFilePath(env, 'cancel.flag'), String(Date.now()), 'utf8'); +} + +export async function clearCancel(env: EnvLike): Promise { + await rm(stateFilePath(env, 'cancel.flag'), { force: true }); +} + +export async function isCancelled(env: EnvLike): Promise { + try { + await stat(stateFilePath(env, 'cancel.flag')); + return true; + } catch { + return false; + } +} diff --git a/src/lib/storage.test.ts b/src/lib/storage.test.ts new file mode 100644 index 0000000..36bd1ad --- /dev/null +++ b/src/lib/storage.test.ts @@ -0,0 +1,42 @@ +import assert from 'node:assert/strict'; +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { test } from 'node:test'; +import { isPreviewPath, isStatePath, storageUsage } from './storage'; + +test('isPreviewPath matches transform cache files only', () => { + assert.equal(isPreviewPath('id__0123456789abcdef0123456789abcdef01234567.avif'), true); + assert.equal(isPreviewPath('id__0123456789abcdef0123456789abcdef01234567.webp'), true); + assert.equal(isPreviewPath('id.jpg'), false); + assert.equal(isPreviewPath('state.jsonl'), false); +}); + +test('isStatePath detects the state directory', () => { + assert.equal(isStatePath('.image-previews/settings.json'), true); + assert.equal(isStatePath('.image-previews'), true); + assert.equal(isStatePath('abc.jpg'), false); +}); + +test('storageUsage splits previews and originals, excluding state', async () => { + const dir = await mkdtemp(join(tmpdir(), 'previews-usage-')); + + try { + await writeFile(join(dir, 'a__0123456789abcdef0123456789abcdef01234567.avif'), 'x'.repeat(10)); + await writeFile(join(dir, 'a.jpg'), 'y'.repeat(99)); + await mkdir(join(dir, '.image-previews')); + await writeFile(join(dir, '.image-previews', 'settings.json'), 'z'.repeat(500)); + + const usage = await storageUsage({ STORAGE_LOCAL_ROOT: dir, STORAGE_LOCATIONS: 'local' }); + + assert.equal(usage.source, 'local'); + assert.equal(usage.previews.count, 1); + assert.equal(usage.previews.bytes, 10); + assert.equal(usage.originals.count, 1); + assert.equal(usage.originals.bytes, 99); + assert.equal(usage.total.count, 2); + assert.equal(usage.total.bytes, 109); + } finally { + await rm(dir, { recursive: true, force: true }); + } +}); diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 0000000..c429c2d --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,162 @@ +import { readdir, stat as fsStat } from 'node:fs/promises'; +import { isAbsolute, join } from 'node:path'; +import type { EnvLike } from './settings'; + +const PREVIEW_PATTERN = /__[0-9a-f]{8,}\.[a-z0-9]+$/i; +const STATE_DIR_NAME = '.image-previews'; + +function asString(value: unknown): string | null { + if (value === undefined || value === null) return null; + if (typeof value === 'string') return value; + if (typeof value === 'number' || typeof value === 'boolean') return String(value); + return null; +} + +export function isPreviewPath(path: string): boolean { + return PREVIEW_PATTERN.test(path); +} + +export function isStatePath(path: string): boolean { + return path === STATE_DIR_NAME || path.startsWith(`${STATE_DIR_NAME}/`) || path.includes(`/${STATE_DIR_NAME}/`); +} + +async function loadStorageManager(): Promise | null> { + try { + const importer = new Function('return import("@directus/api/storage/index")') as () => Promise>; + const module = await importer(); + const getStorage = module['getStorage'] as (() => Promise>) | undefined; + if (!getStorage) return null; + return await getStorage(); + } catch { + return null; + } +} + +function localStorageRoot(env: EnvLike): string { + const root = asString(env['STORAGE_LOCAL_ROOT']) || './uploads'; + return isAbsolute(root) ? root : join(process.cwd(), root); +} + +export interface Bucket { + bytes: number; + count: number; +} + +export interface StorageUsage { + previews: Bucket; + originals: Bucket; + total: Bucket; + source: 'storage' | 'local'; +} + +function emptyUsage(source: 'storage' | 'local'): StorageUsage { + return { + previews: { bytes: 0, count: 0 }, + originals: { bytes: 0, count: 0 }, + total: { bytes: 0, count: 0 }, + source, + }; +} + +function add(usage: StorageUsage, path: string, size: number): void { + if (isPreviewPath(path)) { + usage.previews.bytes += size; + usage.previews.count += 1; + } else { + usage.originals.bytes += size; + usage.originals.count += 1; + } + + usage.total.bytes += size; + usage.total.count += 1; +} + +async function walkLocal(root: string, usage: StorageUsage): Promise { + let entries; + + try { + entries = await readdir(root, { withFileTypes: true }); + } catch { + return; + } + + for (const entry of entries) { + const full = join(root, entry.name); + + if (entry.isDirectory()) { + if (entry.name === STATE_DIR_NAME) continue; + await walkLocal(full, usage); + continue; + } + + if (!entry.isFile()) continue; + + try { + const info = await fsStat(full); + add(usage, entry.name, info.size); + } catch { + continue; + } + } +} + +export async function storageUsage(env: EnvLike): Promise { + const manager = await loadStorageManager(); + + if (manager) { + const usage = emptyUsage('storage'); + + const locations = (asString(env['STORAGE_LOCATIONS']) || 'local') + .split(',') + .map((part) => part.trim()) + .filter(Boolean); + + for (const location of locations) { + try { + const driver = (manager['location'] as (name: string) => Record)(location); + const list = (driver['list'] as () => AsyncIterable)(); + + for await (const path of list) { + if (isStatePath(path)) continue; + + try { + const info = (await (driver['stat'] as (p: string) => Promise<{ size: number }>)(path)) as { + size: number; + }; + add(usage, path, Number(info?.size) || 0); + } catch { + continue; + } + } + } catch { + continue; + } + } + + return usage; + } + + const usage = emptyUsage('local'); + await walkLocal(localStorageRoot(env), usage); + return usage; +} + +export async function previewExists(env: EnvLike, location: string | null, filename: string): Promise { + const manager = await loadStorageManager(); + + if (manager && location) { + try { + const driver = (manager['location'] as (name: string) => Record)(location); + return await (driver['exists'] as (p: string) => Promise)(filename); + } catch { + return null; + } + } + + try { + await fsStat(join(localStorageRoot(env), filename)); + return true; + } catch { + return false; + } +} diff --git a/src/lib/system-presets.ts b/src/lib/system-presets.ts new file mode 100644 index 0000000..4f63ecf --- /dev/null +++ b/src/lib/system-presets.ts @@ -0,0 +1,36 @@ +import type { Preset } from './types'; + +export const SYSTEM_PRESETS: Preset[] = [ + { + key: 'system-small-cover', + format: 'auto', + transforms: [ [ 'resize', { width: 64, height: 64, fit: 'cover' } ] ], + }, + { + key: 'system-small-contain', + format: 'auto', + transforms: [ [ 'resize', { width: 64, fit: 'contain' } ] ], + }, + { + key: 'system-medium-cover', + format: 'auto', + transforms: [ [ 'resize', { width: 300, height: 300, fit: 'cover' } ] ], + }, + { + key: 'system-medium-contain', + format: 'auto', + transforms: [ [ 'resize', { width: 300, fit: 'contain' } ] ], + }, + { + key: 'system-large-cover', + format: 'auto', + transforms: [ [ 'resize', { width: 800, height: 800, fit: 'cover' } ] ], + }, + { + key: 'system-large-contain', + format: 'auto', + transforms: [ [ 'resize', { width: 800, fit: 'contain' } ] ], + }, +]; + +export const DEFAULT_FORMATS = [ 'avif', 'webp' ]; diff --git a/src/lib/types.ts b/src/lib/types.ts new file mode 100644 index 0000000..fe47c3c --- /dev/null +++ b/src/lib/types.ts @@ -0,0 +1,50 @@ +export interface Preset { + key: string; + format?: string; + quality?: number | string; + width?: number | string; + height?: number | string; + fit?: string; + withoutEnlargement?: boolean; + focal_point_x?: number | string | null; + focal_point_y?: number | string | null; + transforms?: unknown[]; + [key: string]: unknown; +} + +export interface WarmResultItem { + key: string; + format: string | null; + ok: boolean; + ms: number; + error?: string; +} + +export interface WarmSummary { + requested: number; + ok: number; + failed: number; + results: WarmResultItem[]; +} + +export interface CompletedRecord { + file_id: string; + preset_key: string; + format: string | null; + ts: number; +} + +export interface RunState { + running: boolean; + startedAt: number | null; + finishedAt: number | null; + total: number; + done: number; + generated: number; + hits: number; + failed: number; + rate: number; + eta: number | null; + cancelled: boolean; + error?: string; +} diff --git a/src/lib/warmer.test.ts b/src/lib/warmer.test.ts new file mode 100644 index 0000000..a6240cd --- /dev/null +++ b/src/lib/warmer.test.ts @@ -0,0 +1,92 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import type { Preset } from './types'; +import { warmFile, type AssetsLike } from './warmer'; + +const presets: Preset[] = [ + { key: 'system-small-cover', format: 'auto', transforms: [ [ 'resize', { width: 64, height: 64, fit: 'cover' } ] ] }, + { key: 'custom-fixed', format: 'webp', width: 200 }, +]; + +test('warmFile passes the full preset object and the accept variant', async () => { + const calls: Array> = []; + + const assets: AssetsLike = { + async getAsset(_id, transformation) { + calls.push(transformation ?? {}); + return { stream: { destroy() {} } }; + }, + }; + + const summary = await warmFile(assets, 'file-1', presets, { concurrency: 1, formats: [ 'avif', 'webp' ] }); + + assert.equal(summary.requested, 3); + assert.equal(summary.ok, 3); + assert.equal(calls.length, 3); + + const autoCalls = calls.filter((call) => (call['transformationParams'] as Preset).key === 'system-small-cover'); + assert.equal(autoCalls.length, 2); + assert.deepEqual( + autoCalls.map((call) => call['acceptFormat']).sort(), + [ 'avif', 'webp' ], + ); + assert.ok((autoCalls[0]?.['transformationParams'] as Preset).transforms); + + const fixed = calls.find((call) => (call['transformationParams'] as Preset).key === 'custom-fixed'); + assert.equal(fixed?.['acceptFormat'], undefined); +}); + +test('warmFile isolates per-task errors', async () => { + const assets: AssetsLike = { + async getAsset(_id, transformation) { + const preset = transformation?.['transformationParams'] as Preset; + if (preset.key === 'custom-fixed') throw new Error('boom'); + return { stream: { destroy() {} } }; + }, + }; + + const summary = await warmFile(assets, 'file-1', presets, { concurrency: 1, formats: [ 'avif', 'webp' ] }); + + assert.equal(summary.ok, 2); + assert.equal(summary.failed, 1); + assert.equal(summary.results.find((result) => result.key === 'custom-fixed')?.error, 'boom'); +}); + +test('warmFile respects the concurrency cap', async () => { + let active = 0; + let maxActive = 0; + + const assets: AssetsLike = { + async getAsset() { + active += 1; + maxActive = Math.max(maxActive, active); + await new Promise((resolve) => setTimeout(resolve, 10)); + active -= 1; + return { stream: { destroy() {} } }; + }, + }; + + await warmFile(assets, 'file-1', presets, { concurrency: 2, formats: [ 'avif', 'webp' ] }); + + assert.ok(maxActive <= 2, `maxActive=${maxActive}`); +}); + +test('warmFile releases the returned stream', async () => { + let destroyed = 0; + + const assets: AssetsLike = { + async getAsset() { + return { + stream: { + destroy() { + destroyed += 1; + }, + }, + }; + }, + }; + + await warmFile(assets, 'file-1', [ presets[0] as Preset ], { concurrency: 1, formats: [ 'avif' ] }); + + assert.equal(destroyed, 1); +}); diff --git a/src/lib/warmer.ts b/src/lib/warmer.ts new file mode 100644 index 0000000..89af8b1 --- /dev/null +++ b/src/lib/warmer.ts @@ -0,0 +1,110 @@ +import { effectiveFormat, formatsForPreset } from './presets'; +import type { Preset, WarmResultItem, WarmSummary } from './types'; + +export interface AssetsLike { + getAsset( + id: string, + transformation?: Record, + range?: unknown, + deferStream?: boolean, + ): Promise<{ stream?: unknown }>; +} + +export interface WarmOptions { + concurrency: number; + formats: string[]; + onResult?: (result: WarmResultItem) => void; +} + +interface WarmTask { + preset: Preset; + format: string | null; +} + +export function errorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + if (typeof error === 'string') return error; + + try { + return JSON.stringify(error); + } catch { + return String(error); + } +} + +function releaseStream(stream: unknown): void { + const candidate = stream as { destroy?: () => void; unpipe?: () => void } | undefined; + + if (candidate && typeof candidate.destroy === 'function') { + candidate.destroy(); + } else if (candidate && typeof candidate.unpipe === 'function') { + candidate.unpipe(); + } +} + +export async function warmFile( + assets: AssetsLike, + fileId: string, + presets: Preset[], + options: WarmOptions, +): Promise { + const tasks: WarmTask[] = []; + + for (const preset of presets) { + for (const format of formatsForPreset(preset, options.formats)) { + tasks.push({ preset, format }); + } + } + + const results: WarmResultItem[] = new Array(tasks.length); + let cursor = 0; + + const workerCount = Math.max(1, Math.min(options.concurrency, tasks.length || 1)); + + const workers = Array.from({ length: workerCount }, async () => { + while (cursor < tasks.length) { + const index = cursor; + cursor += 1; + const task = tasks[index]; + + if (!task) break; + + const transformation: Record = { transformationParams: task.preset }; + + if (task.format) transformation['acceptFormat'] = task.format; + + const started = Date.now(); + const outputFormat = effectiveFormat(task.preset, task.format); + let result: WarmResultItem; + + try { + const asset = await assets.getAsset(fileId, transformation); + releaseStream(asset?.stream); + result = { key: task.preset.key, format: outputFormat, ok: true, ms: Date.now() - started }; + } catch (error) { + result = { + key: task.preset.key, + format: outputFormat, + ok: false, + ms: Date.now() - started, + error: errorMessage(error), + }; + } + + results[index] = result; + options.onResult?.(result); + } + }); + + await Promise.all(workers); + + const settled = results.filter((result): result is WarmResultItem => Boolean(result)); + const ok = settled.filter((result) => result.ok).length; + + return { + requested: tasks.length, + ok, + failed: settled.length - ok, + results: settled, + }; +} diff --git a/src/lib/window.test.ts b/src/lib/window.test.ts new file mode 100644 index 0000000..d0c667a --- /dev/null +++ b/src/lib/window.test.ts @@ -0,0 +1,47 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { isInWindow, nextWindow, parseMinutes } from './window'; + +function at(hours: number, minutes: number): Date { + return new Date(2026, 9, 25, hours, minutes, 0, 0); +} + +test('parseMinutes parses HH:MM', () => { + assert.equal(parseMinutes('00:00'), 0); + assert.equal(parseMinutes('03:30'), 210); + assert.equal(parseMinutes('23:59'), 1439); + assert.equal(parseMinutes(null), null); + assert.equal(parseMinutes('bad'), null); +}); + +test('no window means always inside', () => { + assert.equal(isInWindow(null, null, at(12, 0)), true); + assert.equal(isInWindow('03:00', null, at(12, 0)), true); +}); + +test('daytime window', () => { + assert.equal(isInWindow('03:00', '05:00', at(2, 59)), false); + assert.equal(isInWindow('03:00', '05:00', at(3, 0)), true); + assert.equal(isInWindow('03:00', '05:00', at(4, 59)), true); + assert.equal(isInWindow('03:00', '05:00', at(5, 0)), false); +}); + +test('overnight window', () => { + assert.equal(isInWindow('23:00', '02:00', at(23, 30)), true); + assert.equal(isInWindow('23:00', '02:00', at(1, 30)), true); + assert.equal(isInWindow('23:00', '02:00', at(12, 0)), false); +}); + +test('nextWindow returns the upcoming start', () => { + const next = nextWindow('03:00', '05:00', at(12, 0)); + assert.ok(next); + assert.equal(next?.getDate(), 26); + assert.equal(next?.getHours(), 3); +}); + +test('nextWindow today when still before the start', () => { + const next = nextWindow('03:00', '05:00', at(1, 0)); + assert.ok(next); + assert.equal(next?.getDate(), 25); + assert.equal(next?.getHours(), 3); +}); diff --git a/src/lib/window.ts b/src/lib/window.ts new file mode 100644 index 0000000..d1e48cd --- /dev/null +++ b/src/lib/window.ts @@ -0,0 +1,32 @@ +export function parseMinutes(value: string | null): number | null { + if (value === null) return null; + const match = /^([01]\d|2[0-3]):([0-5]\d)$/.exec(value); + if (!match) return null; + return Number(match[1]) * 60 + Number(match[2]); +} + +export function isInWindow(start: string | null, end: string | null, now: Date = new Date()): boolean { + const from = parseMinutes(start); + const to = parseMinutes(end); + + if (from === null || to === null) return true; + if (from === to) return true; + + const current = now.getHours() * 60 + now.getMinutes(); + + if (from < to) return current >= from && current < to; + + return current >= from || current < to; +} + +export function nextWindow(start: string | null, end: string | null, now: Date = new Date()): Date | null { + const from = parseMinutes(start); + if (from === null || parseMinutes(end) === null) return null; + + const next = new Date(now); + next.setHours(Math.floor(from / 60), from % 60, 0, 0); + + if (next.getTime() <= now.getTime()) next.setDate(next.getDate() + 1); + + return next; +} diff --git a/src/module/index.ts b/src/module/index.ts new file mode 100644 index 0000000..393da38 --- /dev/null +++ b/src/module/index.ts @@ -0,0 +1,14 @@ +import { defineModule } from '@directus/extensions-sdk'; +import ModuleComponent from './module.vue'; + +export default defineModule({ + id: 'image-previews', + name: 'Image Previews', + icon: 'image', + routes: [ + { + path: '', + component: ModuleComponent, + }, + ], +}); diff --git a/src/module/module.vue b/src/module/module.vue new file mode 100644 index 0000000..7e35517 --- /dev/null +++ b/src/module/module.vue @@ -0,0 +1,579 @@ + + + + + diff --git a/src/shims.d.ts b/src/shims.d.ts new file mode 100644 index 0000000..7e7372c --- /dev/null +++ b/src/shims.d.ts @@ -0,0 +1,11 @@ +declare module '*.vue' { + import type { DefineComponent } from 'vue'; + + const component: DefineComponent<{}, {}, any>; + export default component; +} + +declare module 'object-hash' { + const hash: (value: unknown) => string; + export default hash; +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..64735af --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2019", + "lib": ["ES2019", "DOM"], + "moduleResolution": "node", + "strict": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "noUncheckedIndexedAccess": true, + "noUnusedParameters": true, + "alwaysStrict": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "resolveJsonModule": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "rootDir": "./src" + }, + "include": ["./src/**/*.ts"] +}