diff --git a/.gitignore b/.gitignore index 8ea6e02..1f1ac63 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,7 @@ data.json .DS_Store *.env + +# Test output artifacts +tests/vault-output/ +PLAN.md diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index b5d9f84..0000000 --- a/PLAN.md +++ /dev/null @@ -1,119 +0,0 @@ -# Plan: Automated Testing for OpenAugi Plugin - -## Problem - -Currently all testing is manual: open Obsidian, create notes, run commands, inspect output. This means: -- Claude Code can't verify changes without you manually testing -- No regression safety net -- Slow iteration cycle - -## Analysis: Obsidian API Coupling - -After reviewing every service, here's the coupling breakdown: - -| Layer | Services | Obsidian APIs Used | -|-------|----------|--------------------| -| **Pure logic** (no Obsidian) | `OpenAIService`, `sanitizeFilename`, `BacklinkMapper`, `estimateTokens` | None - just `fetch` and string ops | -| **Light coupling** | `FileService` | `Vault.adapter.exists`, `Vault.createFolder`, `Vault.create` | -| **Heavy coupling** | `DistillService`, `ContextGatheringService` | `MetadataCache`, `vault.read`, `vault.getMarkdownFiles`, link resolution, Dataview API | - -## Strategy: Mock Obsidian, Test Against Real Files - -We create a **mock Obsidian API** backed by Node.js `fs`, pointed at a **test vault** with fixture markdown files. This lets us test ~80% of logic without Obsidian running. - -No E2E/Electron testing for now — that's complex, fragile, and low-value compared to service-level testing. - -## What We'll Build - -### 1. Add Vitest (test framework) - -- Install `vitest` as dev dependency -- Add `npm test` and `npm run test:watch` scripts -- Configure to handle TypeScript, mock the `obsidian` module globally - -### 2. Create mock Obsidian API (`tests/mocks/obsidian-mock.ts`) - -A filesystem-backed mock that simulates: -- **`TFile`** — wraps real files with `path`, `basename`, `extension`, `stat` -- **`Vault`** — `read()`, `create()`, `getMarkdownFiles()`, `adapter.exists()`, `createFolder()`, `getAbstractFileByPath()` — all backed by Node.js `fs` -- **`MetadataCache`** — parses `[[wikilinks]]` from markdown files to build `resolvedLinks`, `getFileCache()`, and `getFirstLinkpathDest()` -- **`App`** — ties vault + metadataCache together -- **`Notice`** — no-op (just logs) - -This is ~150 lines of code and gives us the ability to test DistillService, ContextGatheringService, and FileService against real markdown files. - -### 3. Create test vault with fixtures (`tests/vault/`) - -A small set of markdown files committed to the repo that cover key scenarios: - -``` -tests/vault/ -├── Root Note.md # Has [[links]] to other notes -├── Linked Note A.md # Forward-linked from root -├── Linked Note B.md # Forward-linked from root -├── Backlink Source.md # Links back to Root Note -├── Journal Note.md # Has ### 2026-02-20 date headers -├── Dataview Note.md # Has ```dataview blocks -├── Collection Note.md # Has - [x] [[checked]] checkboxes -├── Context Note.md # Has context: section -├── Special Characters!.md # Tests filename sanitization -├── Deeply Linked/ -│ └── Deep Note.md # Tests depth traversal -└── Excluded Folder/ - └── Should Skip.md # Tests folder exclusion -``` - -### 4. Write tests - -**Pure unit tests** (no mocks needed): -- `filename-utils.test.ts` — `sanitizeFilename`, `BacklinkMapper.processBacklinks` -- `openai-service.test.ts` — prompt construction, `extractCustomContext`, response parsing (mock `fetch`) - -**Integration tests** (with mock Obsidian): -- `distill-service.test.ts` — link extraction, backlink discovery, content aggregation, journal filtering, date parsing -- `context-gathering-service.test.ts` — BFS traversal, character limits, folder exclusion, backlink snippets -- `file-service.test.ts` — file creation, collision handling, session folders, backlink mapping - -### 5. npm scripts - -```json -{ - "test": "vitest run", - "test:watch": "vitest", - "test:coverage": "vitest run --coverage" -} -``` - -## What This Enables - -After setup, Claude Code can: -1. Make a code change -2. Run `npm test` -3. See if anything broke -4. Add new test cases for edge cases discovered during development - -You can also add test notes to `tests/vault/` to capture edge cases you find during manual testing — they become permanent regression tests. - -## File Changes Summary - -| Action | File | -|--------|------| -| Install | `vitest` dev dependency | -| Create | `vitest.config.ts` — test config | -| Create | `tests/mocks/obsidian-mock.ts` — mock Obsidian API | -| Create | `tests/vault/*.md` — ~10 fixture notes | -| Create | `tests/filename-utils.test.ts` | -| Create | `tests/openai-service.test.ts` | -| Create | `tests/distill-service.test.ts` | -| Create | `tests/file-service.test.ts` | -| Create | `tests/context-gathering-service.test.ts` | -| Update | `package.json` — add test scripts + vitest dep | -| Update | `tsconfig.json` — exclude tests from build | -| Update | `.gitignore` — add `tests/vault-output/` for test artifacts | - -## Not in Scope (for now) - -- **E2E testing with real Obsidian** — requires Playwright + Electron orchestration, fragile -- **OpenAI API integration tests** — would cost real API credits; we mock `fetch` instead -- **UI modal testing** — Obsidian modals are tightly coupled to the DOM -- **Task dispatch testing** — requires tmux, hard to automate in CI diff --git a/package-lock.json b/package-lock.json index bda7ec5..50c7d1e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "open-augi", - "version": "1.0.0", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "open-augi", - "version": "1.0.0", + "version": "0.5.0", "license": "MIT", "devDependencies": { "@types/node": "^16.18.126", @@ -16,32 +16,25 @@ "esbuild": "^0.17.3", "obsidian": "^1.8.7", "tslib": "2.4.0", - "typescript": "^4.7.4" + "typescript": "^4.7.4", + "vitest": "^3.2.4" } }, - "node_modules/@codemirror/state": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", - "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "@marijn/find-cluster-break": "^1.0.0" - } - }, - "node_modules/@codemirror/view": { - "version": "6.38.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", - "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@codemirror/state": "^6.5.0", - "crelt": "^1.0.6", - "style-mod": "^4.1.0", - "w3c-keyname": "^2.2.4" + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { @@ -316,6 +309,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz", @@ -333,6 +343,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/openbsd-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz", @@ -350,6 +377,23 @@ "node": ">=12" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.17.3", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz", @@ -418,121 +462,12 @@ "node": ">=12" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@marijn/find-cluster-break": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", - "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", - "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", @@ -572,6 +507,367 @@ "node": ">= 8" } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/codemirror": { "version": "5.60.8", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", @@ -582,6 +878,13 @@ "@types/tern": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -805,92 +1108,120 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "dev": true, - "license": "ISC", - "peer": true - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", "dev": true, "license": "MIT", - "peer": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" }, - "engines": { - "node": ">=0.4.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", "dev": true, "license": "MIT", - "peer": true, + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "tinyrainbow": "^2.0.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "url": "https://opencollective.com/vitest" } }, - "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==", + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", "dev": true, "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "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", - "peer": true, "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://opencollective.com/vitest" } }, - "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==", + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", "dev": true, - "license": "Python-2.0", - "peer": true + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } }, "node_modules/array-union": { "version": "2.1.0", @@ -902,24 +1233,14 @@ "node": ">=8" } }, - "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==", + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", - "peer": true - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": ">=12" } }, "node_modules/braces": { @@ -948,87 +1269,41 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", "dev": true, "license": "MIT", - "peer": true, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "node": ">=18" } }, - "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==", + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, "license": "MIT", - "peer": true, - "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", - "peer": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "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", - "peer": true, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" + "node": ">= 16" } }, "node_modules/debug": { @@ -1049,13 +1324,15 @@ } } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, "license": "MIT", - "peer": true + "engines": { + "node": ">=6" + } }, "node_modules/dir-glob": { "version": "3.0.1", @@ -1070,19 +1347,12 @@ "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "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": "Apache-2.0", - "peer": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } + "license": "MIT" }, "node_modules/esbuild": { "version": "0.17.3", @@ -1122,78 +1392,6 @@ "@esbuild/win32-x64": "0.17.3" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", - "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", - "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.1", - "@humanwhocodes/config-array": "^0.13.0", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -1250,79 +1448,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -1356,24 +1481,25 @@ "node": ">=4.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, "license": "MIT", - "peer": true + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } }, "node_modules/fast-glob": { "version": "3.3.3", @@ -1405,22 +1531,6 @@ "node": ">= 6" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -1431,18 +1541,22 @@ "reusify": "^1.0.4" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, "license": "MIT", - "peer": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/fill-range": { @@ -1458,56 +1572,21 @@ "node": ">=8" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "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", - "peer": true, - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", - "dev": true, - "license": "ISC", - "peer": true - }, - "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", - "peer": true - }, "node_modules/functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", @@ -1515,60 +1594,6 @@ "dev": true, "license": "MIT" }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -1590,25 +1615,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -1619,56 +1625,6 @@ "node": ">= 4" } }, - "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", - "peer": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8.19" - } - }, - "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", - "peer": true, - "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", - "peer": true - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1702,114 +1658,30 @@ "node": ">=0.12.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "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", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC", - "peer": true - }, - "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", - "peer": true, "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -1834,20 +1706,6 @@ "node": ">=8.6" } }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/moment": { "version": "2.29.4", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", @@ -1865,13 +1723,24 @@ "dev": true, "license": "MIT" }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "peer": true + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } }, "node_modules/obsidian": { "version": "1.8.7", @@ -1888,117 +1757,6 @@ "@codemirror/view": "^6.0.0" } }, - "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", - "peer": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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", - "peer": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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", - "peer": true, - "engines": { - "node": ">=8" - } - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -2009,6 +1767,30 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2022,26 +1804,33 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "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", - "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" + "node": "^10 || ^12 || >=14" } }, "node_modules/queue-microtask": { @@ -2078,17 +1867,6 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "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", - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -2100,22 +1878,49 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, - "license": "ISC", - "peer": true, + "license": "MIT", "dependencies": { - "glob": "^7.1.3" + "@types/estree": "1.0.8" }, "bin": { - "rimraf": "bin.js" + "rollup": "dist/bin/rollup" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" } }, "node_modules/run-parallel": { @@ -2155,30 +1960,12 @@ "node": ">=10" } }, - "node_modules/shebang-command": { + "node_modules/siginfo": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true, - "license": "MIT", - "peer": true, - "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", - "peer": true, - "engines": { - "node": ">=8" - } + "license": "ISC" }, "node_modules/slash": { "version": "3.0.0", @@ -2190,63 +1977,116 @@ "node": ">=8" } }, - "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==", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, + "license": "BSD-3-Clause", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, "license": "MIT", - "peer": true, - "engines": { - "node": ">=8" + "dependencies": { + "js-tokens": "^9.0.1" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/style-mod": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", - "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==", + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", "dev": true, - "license": "MIT", - "peer": true + "license": "MIT" }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "has-flag": "^4.0.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { - "node": ">=8" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, "node_modules/to-regex-range": { "version": "5.0.1", @@ -2291,34 +2131,6 @@ "dev": true, "license": "0BSD" }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -2333,73 +2145,634 @@ "node": ">=4.2.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/w3c-keyname": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", - "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "node_modules/vite": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", - "peer": true - }, - "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", - "peer": true, "dependencies": { - "isexe": "^2.0.0" + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { - "node-which": "bin/node-which" + "vite": "bin/vite.js" }, "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "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", - "peer": true - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" + "node": "^20.19.0 || >=22.12.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" } } } diff --git a/package.json b/package.json index 7dc100f..225aa65 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,8 @@ "scripts": { "dev": "node esbuild.config.mjs", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", + "test": "vitest run", + "test:watch": "vitest", "version": "node version-bump.mjs && git add manifest.json versions.json" }, "keywords": [], @@ -19,6 +21,7 @@ "esbuild": "^0.17.3", "obsidian": "^1.8.7", "tslib": "2.4.0", - "typescript": "^4.7.4" + "typescript": "^4.7.4", + "vitest": "^3.2.4" } } diff --git a/tests/context-gathering-service.test.ts b/tests/context-gathering-service.test.ts new file mode 100644 index 0000000..3d03ccc --- /dev/null +++ b/tests/context-gathering-service.test.ts @@ -0,0 +1,264 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { ContextGatheringService } from '../src/services/context-gathering-service'; +import { DistillService } from '../src/services/distill-service'; +import { OpenAIService } from '../src/services/openai-service'; +import { DEFAULT_SETTINGS } from '../src/types/settings'; +import { createMockApp, createTestTFile } from './mocks/obsidian-mock'; +import * as path from 'path'; + +const VAULT_DIR = path.resolve(__dirname, 'vault'); + +describe('ContextGatheringService', () => { + let app: ReturnType; + let distillService: DistillService; + let service: ContextGatheringService; + + beforeEach(() => { + app = createMockApp(VAULT_DIR); + const openAIService = new OpenAIService('test-key', 'gpt-5'); + distillService = new DistillService(app as any, openAIService, DEFAULT_SETTINGS); + service = new ContextGatheringService(app as any, distillService, DEFAULT_SETTINGS); + }); + + describe('gatherContext - linked-notes mode', () => { + it('discovers root note and its forward links at depth 1', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + + const result = await service.gatherContext({ + sourceMode: 'linked-notes', + rootNote: rootFile, + linkDepth: 1, + maxCharacters: 100000, + excludeFolders: [], + filterRecentSectionsOnly: false, + includeBacklinks: false, + backlinkContextLines: 0, + }); + + const names = result.notes.map(n => n.file.basename); + expect(names).toContain('Root Note'); + expect(names).toContain('Linked Note A'); + expect(names).toContain('Linked Note B'); + expect(result.totalNotes).toBeGreaterThanOrEqual(3); + }); + + it('respects depth limit', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + + const result = await service.gatherContext({ + sourceMode: 'linked-notes', + rootNote: rootFile, + linkDepth: 1, + maxCharacters: 100000, + excludeFolders: [], + filterRecentSectionsOnly: false, + includeBacklinks: false, + backlinkContextLines: 0, + }); + + // At depth 1, we should NOT see "Deep Note" (which is depth 2: Root → A → Deep) + const names = result.notes.map(n => n.file.basename); + expect(names).not.toContain('Deep Note'); + }); + + it('discovers deeper links at depth 2', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + + const result = await service.gatherContext({ + sourceMode: 'linked-notes', + rootNote: rootFile, + linkDepth: 2, + maxCharacters: 100000, + excludeFolders: [], + filterRecentSectionsOnly: false, + includeBacklinks: false, + backlinkContextLines: 0, + }); + + // At depth 2, "Deep Note" should be discovered (Root → Linked Note A → Deep Note) + const names = result.notes.map(n => n.file.basename); + expect(names).toContain('Deep Note'); + }); + + it('includes backlinks when enabled', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + + const result = await service.gatherContext({ + sourceMode: 'linked-notes', + rootNote: rootFile, + linkDepth: 1, + maxCharacters: 100000, + excludeFolders: [], + filterRecentSectionsOnly: false, + includeBacklinks: true, + backlinkContextLines: 0, + }); + + const names = result.notes.map(n => n.file.basename); + // "Backlink Source" links TO Root Note, so should be discovered + expect(names).toContain('Backlink Source'); + + // Verify the backlink note is marked as such + const backlinkNote = result.notes.find(n => n.file.basename === 'Backlink Source'); + expect(backlinkNote?.isBacklink).toBe(true); + expect(backlinkNote?.discoveredVia).toContain('backlink'); + }); + + it('respects character limit', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + + // Use a very small character limit + const result = await service.gatherContext({ + sourceMode: 'linked-notes', + rootNote: rootFile, + linkDepth: 2, + maxCharacters: 200, // Very small — root note alone should exceed this + excludeFolders: [], + filterRecentSectionsOnly: false, + includeBacklinks: false, + backlinkContextLines: 0, + }); + + // Some notes should be excluded due to size limit + const excludedNotes = result.notes.filter(n => !n.included); + expect(excludedNotes.length).toBeGreaterThan(0); + }); + + it('throws when root note is missing in linked-notes mode', async () => { + await expect( + service.gatherContext({ + sourceMode: 'linked-notes', + linkDepth: 1, + maxCharacters: 100000, + excludeFolders: [], + filterRecentSectionsOnly: false, + }) + ).rejects.toThrow('Root note required'); + }); + }); + + describe('folder exclusion', () => { + it('excludes notes from specified folders', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + + const result = await service.gatherContext({ + sourceMode: 'linked-notes', + rootNote: rootFile, + linkDepth: 3, + maxCharacters: 100000, + excludeFolders: ['Excluded Folder'], + filterRecentSectionsOnly: false, + includeBacklinks: true, + backlinkContextLines: 0, + }); + + const includedNames = result.notes + .filter(n => n.included) + .map(n => n.file.basename); + expect(includedNames).not.toContain('Should Skip'); + }); + }); + + describe('aggregateContent', () => { + it('aggregates forward links and backlinks separately', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + + const gathered = await service.gatherContext({ + sourceMode: 'linked-notes', + rootNote: rootFile, + linkDepth: 1, + maxCharacters: 100000, + excludeFolders: [], + filterRecentSectionsOnly: false, + includeBacklinks: true, + backlinkContextLines: 0, + }); + + expect(gathered.aggregatedContent).toContain('# Note:'); + expect(gathered.totalCharacters).toBeGreaterThan(0); + }); + + it('includes backlink snippets in aggregated content', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + + const gathered = await service.gatherContext({ + sourceMode: 'linked-notes', + rootNote: rootFile, + linkDepth: 1, + maxCharacters: 100000, + excludeFolders: [], + filterRecentSectionsOnly: false, + includeBacklinks: true, + backlinkContextLines: 0, + }); + + // Backlink content should be in the aggregated output + if (gathered.notes.some(n => n.isBacklink)) { + expect(gathered.aggregatedContent).toContain('# Backlink:'); + } + }); + }); + + describe('gatherContext - recent-activity mode', () => { + it('throws when time window is missing', async () => { + await expect( + service.gatherContext({ + sourceMode: 'recent-activity', + linkDepth: 1, + maxCharacters: 100000, + excludeFolders: [], + filterRecentSectionsOnly: false, + }) + ).rejects.toThrow('Time window required'); + }); + }); + + describe('note metadata', () => { + it('assigns correct depth to discovered notes', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + + const result = await service.gatherContext({ + sourceMode: 'linked-notes', + rootNote: rootFile, + linkDepth: 2, + maxCharacters: 100000, + excludeFolders: [], + filterRecentSectionsOnly: false, + includeBacklinks: false, + backlinkContextLines: 0, + }); + + const rootEntry = result.notes.find(n => n.file.basename === 'Root Note'); + expect(rootEntry?.depth).toBe(0); + + const linkedA = result.notes.find(n => n.file.basename === 'Linked Note A'); + expect(linkedA?.depth).toBe(1); + + const deepNote = result.notes.find(n => n.file.basename === 'Deep Note'); + if (deepNote) { + expect(deepNote.depth).toBe(2); + } + }); + + it('includes discoveredVia metadata', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + + const result = await service.gatherContext({ + sourceMode: 'linked-notes', + rootNote: rootFile, + linkDepth: 1, + maxCharacters: 100000, + excludeFolders: [], + filterRecentSectionsOnly: false, + includeBacklinks: false, + backlinkContextLines: 0, + }); + + const rootEntry = result.notes.find(n => n.file.basename === 'Root Note'); + expect(rootEntry?.discoveredVia).toBe('root'); + + const linkedEntry = result.notes.find(n => n.file.basename === 'Linked Note A'); + expect(linkedEntry?.discoveredVia).toContain('linked from'); + }); + }); +}); diff --git a/tests/distill-service.test.ts b/tests/distill-service.test.ts new file mode 100644 index 0000000..73536e5 --- /dev/null +++ b/tests/distill-service.test.ts @@ -0,0 +1,240 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { DistillService } from '../src/services/distill-service'; +import { OpenAIService } from '../src/services/openai-service'; +import { DEFAULT_SETTINGS } from '../src/types/settings'; +import { createMockApp, createTestTFile } from './mocks/obsidian-mock'; +import * as path from 'path'; + +const VAULT_DIR = path.resolve(__dirname, 'vault'); + +describe('DistillService', () => { + let app: ReturnType; + let openAIService: OpenAIService; + let service: DistillService; + + beforeEach(() => { + app = createMockApp(VAULT_DIR); + openAIService = new OpenAIService('test-key', 'gpt-5'); + service = new DistillService(app as any, openAIService, DEFAULT_SETTINGS); + }); + + describe('getLinkedNotes', () => { + it('extracts forward links from a note', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + const linked = await service.getLinkedNotes(rootFile); + + const basenames = linked.map(f => f.basename); + expect(basenames).toContain('Linked Note A'); + expect(basenames).toContain('Linked Note B'); + }); + + it('does not include the root file itself', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + const linked = await service.getLinkedNotes(rootFile); + + const paths = linked.map(f => f.path); + expect(paths).not.toContain('Root Note.md'); + }); + + it('deduplicates linked files', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + const linked = await service.getLinkedNotes(rootFile); + + const paths = linked.map(f => f.path); + const uniquePaths = [...new Set(paths)]; + expect(paths.length).toBe(uniquePaths.length); + }); + + it('extracts only checked items from collection notes', async () => { + const collectionFile = createTestTFile(VAULT_DIR, 'Collection Note.md'); + const linked = await service.getLinkedNotes(collectionFile); + + const basenames = linked.map(f => f.basename); + // Only [x] items should be included + expect(basenames).toContain('Linked Note A'); + expect(basenames).toContain('Journal Note'); + // Unchecked items should NOT be included + expect(basenames).not.toContain('Linked Note B'); + expect(basenames).not.toContain('Backlink Source'); + }); + + it('handles notes with no links', async () => { + const deepNote = createTestTFile(VAULT_DIR, 'Deeply Linked/Deep Note.md'); + const linked = await service.getLinkedNotes(deepNote); + expect(linked).toHaveLength(0); + }); + }); + + describe('getBacklinksForFile', () => { + it('finds notes that link TO the target file', () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + const backlinks = service.getBacklinksForFile(rootFile); + + const basenames = backlinks.map(f => f.basename); + expect(basenames).toContain('Backlink Source'); + expect(basenames).toContain('Special Characters!'); + }); + + it('returns empty array for notes with no backlinks', () => { + const excludedFile = createTestTFile(VAULT_DIR, 'Excluded Folder/Should Skip.md'); + const backlinks = service.getBacklinksForFile(excludedFile); + expect(backlinks).toHaveLength(0); + }); + }); + + describe('getBacklinkSnippets', () => { + it('extracts context around backlink references', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + const backlinkSource = createTestTFile(VAULT_DIR, 'Backlink Source.md'); + + const snippets = await service.getBacklinkSnippets(rootFile, backlinkSource, 0); + + expect(snippets.length).toBeGreaterThan(0); + // Header section mode (0) should include the section header + expect(snippets[0].snippet).toContain('Section About Root'); + expect(snippets[0].snippet).toContain('Root Note'); + }); + + it('returns empty for files with no links to target', async () => { + const rootFile = createTestTFile(VAULT_DIR, 'Root Note.md'); + const deepNote = createTestTFile(VAULT_DIR, 'Deeply Linked/Deep Note.md'); + + const snippets = await service.getBacklinkSnippets(rootFile, deepNote, 0); + expect(snippets).toHaveLength(0); + }); + }); + + describe('aggregateContent', () => { + it('combines content from multiple files', async () => { + const files = [ + createTestTFile(VAULT_DIR, 'Linked Note A.md'), + createTestTFile(VAULT_DIR, 'Linked Note B.md'), + ]; + + const result = await service.aggregateContent(files); + + expect(result.content).toContain('# Note: Linked Note A'); + expect(result.content).toContain('# Note: Linked Note B'); + expect(result.content).toContain('atomic note-taking'); + expect(result.content).toContain('Zettelkasten'); + expect(result.sourceNotes).toContain('Linked Note A'); + expect(result.sourceNotes).toContain('Linked Note B'); + }); + + it('deduplicates files by path', async () => { + const fileA = createTestTFile(VAULT_DIR, 'Linked Note A.md'); + const fileDuplicate = createTestTFile(VAULT_DIR, 'Linked Note A.md'); + + const result = await service.aggregateContent([fileA, fileDuplicate]); + + // Should only appear once + const occurrences = (result.content.match(/# Note: Linked Note A/g) || []).length; + expect(occurrences).toBe(1); + }); + + it('strips dataview queries from output', async () => { + const files = [createTestTFile(VAULT_DIR, 'Dataview Note.md')]; + const result = await service.aggregateContent(files); + + expect(result.content).not.toContain('```dataview'); + expect(result.content).not.toContain('TABLE file.mtime'); + expect(result.content).toContain('Regular content after the dataview block'); + }); + }); + + describe('journal date filtering', () => { + it('identifies journal-style notes with date headers', () => { + // Access private method via any cast + const content = '# Daily\n\n### 2026-02-25\nToday entry\n\n### 2026-02-20\nOlder entry'; + const result = (service as any).isJournalStyleNote(content); + expect(result).toBe(true); + }); + + it('rejects non-journal notes', () => { + const content = '# Regular Note\n\nJust some content without date headers.'; + const result = (service as any).isJournalStyleNote(content); + expect(result).toBe(false); + }); + + it('extracts date sections correctly', () => { + const content = '# Header\n\n### 2026-02-25\nRecent\n\n### 2026-01-15\nOlder'; + const sections = (service as any).getDateSections(content); + + expect(sections.length).toBe(3); // header + 2 dated sections + expect(sections[0].date).toBeNull(); // undated header + expect(sections[1].date).toBeInstanceOf(Date); + expect(sections[2].date).toBeInstanceOf(Date); + }); + + it('filters content by date range', () => { + const content = '# Daily Journal\n\n### 2026-02-25\nRecent entry\n\n### 2025-01-01\nVery old entry'; + const filtered = (service as any).extractContentByDateRange(content, 30); + + expect(filtered).toContain('Recent entry'); + expect(filtered).not.toContain('Very old entry'); + }); + }); + + describe('extractDateFromFilename', () => { + it('extracts date from YYYY-MM-DD prefix', () => { + const date = (service as any).extractDateFromFilename('2026-02-25 My Note'); + expect(date).toBeInstanceOf(Date); + expect(date.getFullYear()).toBe(2026); + expect(date.getMonth()).toBe(1); // 0-indexed + expect(date.getDate()).toBe(25); + }); + + it('returns null for non-date filenames', () => { + const date = (service as any).extractDateFromFilename('My Regular Note'); + expect(date).toBeNull(); + }); + }); + + describe('dataview query handling', () => { + it('detects dataview queries in content', () => { + const content = 'Some text\n\n```dataview\nLIST FROM #tag\n```\n\nMore text'; + expect((service as any).containsDataviewQuery(content)).toBe(true); + }); + + it('returns false for content without dataview', () => { + const content = 'Regular markdown content\n\n```javascript\nconsole.log("hi")\n```'; + expect((service as any).containsDataviewQuery(content)).toBe(false); + }); + + it('extracts dataview query strings', () => { + const content = 'Text\n\n```dataview\nLIST FROM #tag\n```\n\n```dataview\nTABLE file.name\n```'; + const queries = (service as any).extractDataviewQueries(content); + expect(queries).toHaveLength(2); + expect(queries[0]).toContain('LIST FROM #tag'); + expect(queries[1]).toContain('TABLE file.name'); + }); + + it('strips dataview queries from content', () => { + const content = 'Before\n\n```dataview\nLIST\n```\nAfter'; + const stripped = (service as any).stripDataviewQueries(content); + expect(stripped).not.toContain('```dataview'); + expect(stripped).toContain('Before'); + expect(stripped).toContain('After'); + }); + }); + + describe('tag stripping', () => { + it('strips simple tags', () => { + const result = (service as any).stripTags('Some text #tag more text'); + expect(result).not.toContain('#tag'); + expect(result).toContain('Some text'); + expect(result).toContain('more text'); + }); + + it('strips nested tags', () => { + const result = (service as any).stripTags('Content #nested/tag here'); + expect(result).not.toContain('#nested/tag'); + }); + + it('preserves markdown headers', () => { + const result = (service as any).stripTags('## Header\n\nContent #tag'); + expect(result).toContain('## Header'); + expect(result).not.toContain('#tag'); + }); + }); +}); diff --git a/tests/file-service.test.ts b/tests/file-service.test.ts new file mode 100644 index 0000000..1fc08f6 --- /dev/null +++ b/tests/file-service.test.ts @@ -0,0 +1,217 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { FileService } from '../src/services/file-service'; +import { MockApp } from './mocks/obsidian-mock'; +import { TranscriptResponse, DistillResponse } from '../src/types/transcript'; +import * as path from 'path'; +import * as fs from 'fs'; + +const BASE_OUTPUT_DIR = path.resolve(__dirname, 'vault-output'); +let testCounter = 0; + +describe('FileService', () => { + let app: MockApp; + let service: FileService; + let outputDir: string; + + beforeEach(() => { + // Use unique subdirectory per test to avoid parallel cleanup races + testCounter++; + outputDir = path.join(BASE_OUTPUT_DIR, `run-${testCounter}-${Date.now()}`); + fs.mkdirSync(outputDir, { recursive: true }); + + app = new MockApp(outputDir); + service = new FileService( + app as any, + 'OpenAugi/Summaries', + 'OpenAugi/Notes', + 'OpenAugi/Published' + ); + }); + + describe('ensureDirectoriesExist', () => { + it('creates all output directories', async () => { + await service.ensureDirectoriesExist(); + + expect(fs.existsSync(path.join(outputDir, 'OpenAugi/Summaries'))).toBe(true); + expect(fs.existsSync(path.join(outputDir, 'OpenAugi/Notes'))).toBe(true); + expect(fs.existsSync(path.join(outputDir, 'OpenAugi/Published'))).toBe(true); + }); + + it('is idempotent (safe to call multiple times)', async () => { + await service.ensureDirectoriesExist(); + await service.ensureDirectoriesExist(); + + expect(fs.existsSync(path.join(outputDir, 'OpenAugi/Summaries'))).toBe(true); + }); + }); + + describe('writeTranscriptFiles', () => { + const mockTranscriptData: TranscriptResponse = { + summary: 'This transcript discusses [[Atomic Notes]] and [[Knowledge Management]].', + notes: [ + { title: 'Atomic Notes', content: 'Each note should contain one idea. See [[Knowledge Management]].' }, + { title: 'Knowledge Management', content: 'Systems for organizing knowledge. Related to [[Atomic Notes]].' }, + ], + tasks: [ + '- [ ] Review [[Atomic Notes]] methodology', + '- [ ] Set up Zettelkasten system', + ], + }; + + it('creates summary file with backlinks', async () => { + await service.writeTranscriptFiles('My Transcript', mockTranscriptData); + + const summaryPath = path.join(outputDir, 'OpenAugi/Summaries/My Transcript - summary.md'); + expect(fs.existsSync(summaryPath)).toBe(true); + + const content = fs.readFileSync(summaryPath, 'utf-8'); + expect(content).toContain('Atomic Notes'); + expect(content).toContain('Knowledge Management'); + }); + + it('creates atomic note files in session folder', async () => { + await service.writeTranscriptFiles('My Transcript', mockTranscriptData); + + const notesDir = path.join(outputDir, 'OpenAugi/Notes'); + const sessionFolders = fs.readdirSync(notesDir); + expect(sessionFolders.length).toBe(1); + expect(sessionFolders[0]).toMatch(/^Transcript \d{4}-\d{2}-\d{2}/); + + const sessionPath = path.join(notesDir, sessionFolders[0]); + const noteFiles = fs.readdirSync(sessionPath); + expect(noteFiles).toContain('Atomic Notes.md'); + expect(noteFiles).toContain('Knowledge Management.md'); + }); + + it('includes tasks section in summary', async () => { + await service.writeTranscriptFiles('My Transcript', mockTranscriptData); + + const summaryPath = path.join(outputDir, 'OpenAugi/Summaries/My Transcript - summary.md'); + const content = fs.readFileSync(summaryPath, 'utf-8'); + expect(content).toContain('## Tasks'); + expect(content).toContain('Review'); + }); + + it('processes backlinks in note content', async () => { + await service.writeTranscriptFiles('My Transcript', mockTranscriptData); + + const notesDir = path.join(outputDir, 'OpenAugi/Notes'); + const sessionFolders = fs.readdirSync(notesDir); + const sessionPath = path.join(notesDir, sessionFolders[0]); + + const atomicContent = fs.readFileSync(path.join(sessionPath, 'Atomic Notes.md'), 'utf-8'); + // Backlinks should reference sanitized filenames + expect(atomicContent).toContain('[[Knowledge Management]]'); + }); + + it('sanitizes filenames with special characters', async () => { + const dataWithSpecialChars: TranscriptResponse = { + summary: 'Summary of [[Note: Special]]', + notes: [ + { title: 'Note: Special', content: 'Content with special title' }, + ], + tasks: [], + }; + + await service.writeTranscriptFiles('Test', dataWithSpecialChars); + + const notesDir = path.join(outputDir, 'OpenAugi/Notes'); + const sessionFolders = fs.readdirSync(notesDir); + const sessionPath = path.join(notesDir, sessionFolders[0]); + const noteFiles = fs.readdirSync(sessionPath); + + // Colon should be sanitized + expect(noteFiles.some(f => f.includes(':'))).toBe(false); + expect(noteFiles.some(f => f.includes('Note'))).toBe(true); + }); + }); + + describe('writeDistilledFiles', () => { + const mockDistillData: DistillResponse = { + summary: 'Distilled insights about [[Topic A]] and [[Topic B]].', + notes: [ + { title: 'Topic A', content: 'Deep analysis of A' }, + { title: 'Topic B', content: 'Overview of B, see also [[Topic A]]' }, + ], + tasks: ['- [ ] Follow up on Topic A'], + sourceNotes: ['Root Note', 'Linked Note A'], + }; + + it('creates distilled summary file', async () => { + // Create a mock TFile for the root + const { createTestTFile } = await import('./mocks/obsidian-mock'); + // We need a file that exists in the output dir, so create it first + fs.mkdirSync(path.join(outputDir, 'test'), { recursive: true }); + fs.writeFileSync(path.join(outputDir, 'test/My Root.md'), 'root content'); + const rootFile = createTestTFile(outputDir, 'test/My Root.md'); + + const summaryPath = await service.writeDistilledFiles(rootFile, mockDistillData); + + expect(summaryPath).toContain('My Root - distilled'); + expect(fs.existsSync(path.join(outputDir, summaryPath))).toBe(true); + }); + + it('creates atomic notes in session folder', async () => { + const { createTestTFile } = await import('./mocks/obsidian-mock'); + fs.mkdirSync(path.join(outputDir, 'test'), { recursive: true }); + fs.writeFileSync(path.join(outputDir, 'test/Root.md'), 'root'); + const rootFile = createTestTFile(outputDir, 'test/Root.md'); + + await service.writeDistilledFiles(rootFile, mockDistillData); + + const notesDir = path.join(outputDir, 'OpenAugi/Notes'); + const sessionFolders = fs.readdirSync(notesDir); + expect(sessionFolders.length).toBe(1); + expect(sessionFolders[0]).toMatch(/^Distill Root/); + + const sessionPath = path.join(notesDir, sessionFolders[0]); + const noteFiles = fs.readdirSync(sessionPath); + expect(noteFiles).toContain('Topic A.md'); + expect(noteFiles).toContain('Topic B.md'); + }); + + it('includes source notes in summary', async () => { + const { createTestTFile } = await import('./mocks/obsidian-mock'); + fs.mkdirSync(path.join(outputDir, 'test'), { recursive: true }); + fs.writeFileSync(path.join(outputDir, 'test/Root.md'), 'root'); + const rootFile = createTestTFile(outputDir, 'test/Root.md'); + + const summaryPath = await service.writeDistilledFiles(rootFile, mockDistillData); + const content = fs.readFileSync(path.join(outputDir, summaryPath), 'utf-8'); + + expect(content).toContain('## Source Notes'); + expect(content).toContain('[[Root Note]]'); + expect(content).toContain('[[Linked Note A]]'); + }); + }); + + describe('writePublishedPost', () => { + it('creates published file with frontmatter', async () => { + const content = '# My Great Post\n\nThis is the blog post content.'; + const filePath = await service.writePublishedPost(content, ['Source A', 'Source B'], 'default'); + + expect(filePath).toContain('Published'); + const fullContent = fs.readFileSync(path.join(outputDir, filePath), 'utf-8'); + + expect(fullContent).toContain('---'); + expect(fullContent).toContain('type: published-post'); + expect(fullContent).toContain('status: draft'); + expect(fullContent).toContain('[[Source A]]'); + expect(fullContent).toContain('# My Great Post'); + }); + + it('extracts title from first heading', async () => { + const content = '# Test Title\n\nContent here.'; + const filePath = await service.writePublishedPost(content, ['Source'], 'default'); + + expect(filePath).toContain('Test Title'); + }); + + it('falls back to source note name when no heading', async () => { + const content = 'No heading, just content.'; + const filePath = await service.writePublishedPost(content, ['My Source Note'], 'default'); + + expect(filePath).toContain('My Source Note'); + }); + }); +}); diff --git a/tests/filename-utils.test.ts b/tests/filename-utils.test.ts new file mode 100644 index 0000000..11df0ad --- /dev/null +++ b/tests/filename-utils.test.ts @@ -0,0 +1,125 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { sanitizeFilename, BacklinkMapper, createFileWithCollisionHandling } from '../src/utils/filename-utils'; +import { MockVault } from './mocks/obsidian-mock'; +import * as path from 'path'; +import * as fs from 'fs'; + +const BASE_OUTPUT_DIR = path.resolve(__dirname, 'vault-output'); +let collisionTestDir: string; +let collisionTestCounter = 0; + +describe('sanitizeFilename', () => { + it('passes through clean filenames unchanged', () => { + expect(sanitizeFilename('My Note')).toBe('My Note'); + expect(sanitizeFilename('simple-name')).toBe('simple-name'); + }); + + it('replaces backslash', () => { + expect(sanitizeFilename('path\\to\\file')).toBe('path - to - file'); + }); + + it('replaces forward slash', () => { + expect(sanitizeFilename('path/to/file')).toBe('path - to - file'); + }); + + it('replaces colon', () => { + expect(sanitizeFilename('Note: Important')).toBe('Note - Important'); + }); + + it('replaces asterisk', () => { + expect(sanitizeFilename('Note*star')).toBe('Note - star'); + }); + + it('replaces question mark', () => { + expect(sanitizeFilename('Is this a note?')).toBe('Is this a note - '); + }); + + it('replaces double quotes', () => { + expect(sanitizeFilename('He said "hello"')).toBe('He said - hello - '); + }); + + it('replaces angle brackets', () => { + expect(sanitizeFilename('')).toBe(' - tag - '); + }); + + it('replaces pipe', () => { + expect(sanitizeFilename('A | B')).toBe('A - B'); + }); + + it('handles multiple special characters', () => { + const result = sanitizeFilename('Note: "Important" | value'); + expect(result).not.toMatch(/[\\/:*?"<>|]/); + }); +}); + +describe('BacklinkMapper', () => { + let mapper: BacklinkMapper; + + beforeEach(() => { + mapper = new BacklinkMapper(); + }); + + it('maps registered titles to sanitized filenames in backlinks', () => { + mapper.registerTitle('My Important Note', 'My Important Note'); + mapper.registerTitle('Note: Special', 'Note - Special'); + + const result = mapper.processBacklinks('See [[Note: Special]] and [[My Important Note]]'); + expect(result).toBe('See [[Note - Special]] and [[My Important Note]]'); + }); + + it('falls back to sanitizeFilename for unregistered titles', () => { + const result = mapper.processBacklinks('See [[Unregistered: Note]]'); + expect(result).toBe('See [[Unregistered - Note]]'); + }); + + it('handles content with no backlinks', () => { + const result = mapper.processBacklinks('Plain text with no links'); + expect(result).toBe('Plain text with no links'); + }); + + it('handles multiple backlinks in same line', () => { + mapper.registerTitle('A', 'a-sanitized'); + mapper.registerTitle('B', 'b-sanitized'); + + const result = mapper.processBacklinks('Links: [[A]] and [[B]]'); + expect(result).toBe('Links: [[a-sanitized]] and [[b-sanitized]]'); + }); +}); + +describe('createFileWithCollisionHandling', () => { + beforeEach(() => { + collisionTestCounter++; + collisionTestDir = path.join(BASE_OUTPUT_DIR, `collision-${collisionTestCounter}-${Date.now()}`); + fs.mkdirSync(collisionTestDir, { recursive: true }); + }); + + it('creates file at original path when no collision', async () => { + const vault = new MockVault(collisionTestDir); + const result = await createFileWithCollisionHandling(vault as any, 'test-note.md', 'Hello'); + expect(result).toBe('test-note.md'); + + const content = fs.readFileSync(path.join(collisionTestDir, 'test-note.md'), 'utf-8'); + expect(content).toBe('Hello'); + }); + + it('appends -1 when file already exists', async () => { + const vault = new MockVault(collisionTestDir); + fs.writeFileSync(path.join(collisionTestDir, 'note.md'), 'existing'); + + const result = await createFileWithCollisionHandling(vault as any, 'note.md', 'New content'); + expect(result).toBe('note-1.md'); + + const content = fs.readFileSync(path.join(collisionTestDir, 'note-1.md'), 'utf-8'); + expect(content).toBe('New content'); + }); + + it('increments counter for multiple collisions', async () => { + const vault = new MockVault(collisionTestDir); + fs.writeFileSync(path.join(collisionTestDir, 'note.md'), 'v1'); + fs.writeFileSync(path.join(collisionTestDir, 'note-1.md'), 'v2'); + fs.writeFileSync(path.join(collisionTestDir, 'note-2.md'), 'v3'); + + const result = await createFileWithCollisionHandling(vault as any, 'note.md', 'v4'); + expect(result).toBe('note-3.md'); + }); +}); diff --git a/tests/mocks/obsidian-mock.ts b/tests/mocks/obsidian-mock.ts new file mode 100644 index 0000000..f71b66e --- /dev/null +++ b/tests/mocks/obsidian-mock.ts @@ -0,0 +1,287 @@ +/** + * Filesystem-backed mock of the Obsidian API. + * Reads/writes real markdown files from a test vault directory so we can + * test DistillService, FileService, ContextGatheringService, etc. without + * running Obsidian. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { TFile } from './obsidian-module'; + +// ─── Link Parsing ──────────────────────────────────────────────────────────── + +interface ParsedLink { + link: string; // The raw link text (e.g., "Note A" or "Note A|alias") + original: string; // The full match including brackets + position: { start: { line: number; col: number }; end: { line: number; col: number } }; +} + +/** Parse [[wikilinks]] from markdown content */ +function parseWikilinks(content: string): ParsedLink[] { + const links: ParsedLink[] = []; + const lines = content.split('\n'); + + for (let lineNum = 0; lineNum < lines.length; lineNum++) { + const line = lines[lineNum]; + const regex = /\[\[(.*?)\]\]/g; + let match; + while ((match = regex.exec(line)) !== null) { + const rawLink = match[1]; + // Strip alias: [[path|alias]] → path + const linkPath = rawLink.includes('|') ? rawLink.split('|')[0] : rawLink; + links.push({ + link: linkPath, + original: match[0], + position: { + start: { line: lineNum, col: match.index }, + end: { line: lineNum, col: match.index + match[0].length }, + }, + }); + } + } + + return links; +} + +// ─── MockTFile helpers ─────────────────────────────────────────────────────── + +/** Create a TFile from a real filesystem path relative to vault root */ +function createMockTFile(vaultRoot: string, relativePath: string): TFile { + const absPath = path.join(vaultRoot, relativePath); + let mtime = Date.now(); + try { + const stat = fs.statSync(absPath); + mtime = stat.mtimeMs; + } catch { /* file may not exist yet for output tests */ } + const file = new TFile(relativePath, mtime); + file.stat.size = (() => { + try { return fs.statSync(absPath).size; } catch { return 0; } + })(); + return file; +} + +// ─── MockVaultAdapter ──────────────────────────────────────────────────────── + +class MockVaultAdapter { + constructor(private vaultRoot: string) {} + + async exists(filePath: string): Promise { + return fs.existsSync(path.join(this.vaultRoot, filePath)); + } + + async stat(filePath: string): Promise<{ mtime: number; ctime: number; size: number } | null> { + const absPath = path.join(this.vaultRoot, filePath); + try { + const stat = fs.statSync(absPath); + return { mtime: stat.mtimeMs, ctime: stat.ctimeMs, size: stat.size }; + } catch { + return null; + } + } +} + +// ─── MockVault ─────────────────────────────────────────────────────────────── + +export class MockVault { + adapter: MockVaultAdapter; + private vaultRoot: string; + + constructor(vaultRoot: string) { + this.vaultRoot = vaultRoot; + this.adapter = new MockVaultAdapter(vaultRoot); + } + + /** Read a file's content */ + async read(file: TFile): Promise { + const absPath = path.join(this.vaultRoot, file.path); + return fs.readFileSync(absPath, 'utf-8'); + } + + /** Create a file */ + async create(filePath: string, content: string): Promise { + const absPath = path.join(this.vaultRoot, filePath); + const dir = path.dirname(absPath); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(absPath, content, 'utf-8'); + return createMockTFile(this.vaultRoot, filePath); + } + + /** Create a folder */ + async createFolder(folderPath: string): Promise { + const absPath = path.join(this.vaultRoot, folderPath); + if (!fs.existsSync(absPath)) { + fs.mkdirSync(absPath, { recursive: true }); + } + } + + /** Get all markdown files in the vault */ + getMarkdownFiles(): TFile[] { + const files: TFile[] = []; + const walk = (dir: string, relative: string) => { + const entries = fs.readdirSync(dir, { withFileTypes: true }); + for (const entry of entries) { + const relPath = relative ? `${relative}/${entry.name}` : entry.name; + if (entry.isDirectory()) { + walk(path.join(dir, entry.name), relPath); + } else if (entry.name.endsWith('.md')) { + files.push(createMockTFile(this.vaultRoot, relPath)); + } + } + }; + walk(this.vaultRoot, ''); + return files; + } + + /** Get a file by its exact path */ + getAbstractFileByPath(filePath: string): TFile | null { + const absPath = path.join(this.vaultRoot, filePath); + if (fs.existsSync(absPath)) { + return createMockTFile(this.vaultRoot, filePath); + } + return null; + } +} + +// ─── MockMetadataCache ─────────────────────────────────────────────────────── + +export class MockMetadataCache { + /** resolvedLinks[sourcePath][targetPath] = linkCount */ + resolvedLinks: Record> = {}; + private vaultRoot: string; + private vault: MockVault; + + constructor(vaultRoot: string, vault: MockVault) { + this.vaultRoot = vaultRoot; + this.vault = vault; + this.buildLinkGraph(); + } + + /** Build the resolved links graph by scanning all markdown files */ + private buildLinkGraph(): void { + const files = this.vault.getMarkdownFiles(); + for (const file of files) { + const absPath = path.join(this.vaultRoot, file.path); + const content = fs.readFileSync(absPath, 'utf-8'); + const links = parseWikilinks(content); + + if (!this.resolvedLinks[file.path]) { + this.resolvedLinks[file.path] = {}; + } + + for (const link of links) { + const resolved = this.resolveLink(link.link, file.path); + if (resolved) { + this.resolvedLinks[file.path][resolved.path] = + (this.resolvedLinks[file.path][resolved.path] || 0) + 1; + } + } + } + } + + /** Resolve a link path to a TFile (mimics Obsidian's shortest-path resolution) */ + private resolveLink(linkPath: string, sourcePath: string): TFile | null { + // Add .md extension if not present + let searchPath = linkPath; + if (!searchPath.endsWith('.md')) { + searchPath += '.md'; + } + + // Try exact path first + const exactFile = this.vault.getAbstractFileByPath(searchPath); + if (exactFile) return exactFile; + + // Try relative to source file's directory + const sourceDir = path.dirname(sourcePath); + const relativePath = sourceDir === '.' ? searchPath : `${sourceDir}/${searchPath}`; + const relativeFile = this.vault.getAbstractFileByPath(relativePath); + if (relativeFile) return relativeFile; + + // Obsidian-style: search all files for basename match + const baseName = searchPath.split('/').pop(); + if (baseName) { + const allFiles = this.vault.getMarkdownFiles(); + const match = allFiles.find(f => f.path.endsWith(baseName) || f.path.endsWith(`/${baseName}`)); + if (match) return match; + } + + return null; + } + + /** Get cached metadata for a file (links extracted from content) */ + getFileCache(file: TFile): { links?: ParsedLink[]; embeds?: ParsedLink[] } | null { + const absPath = path.join(this.vaultRoot, file.path); + try { + const content = fs.readFileSync(absPath, 'utf-8'); + const links = parseWikilinks(content); + // For simplicity, embeds use the same format as links (Obsidian uses ![[embed]]) + const embedRegex = /!\[\[(.*?)\]\]/g; + const embeds: ParsedLink[] = []; + const lines = content.split('\n'); + for (let lineNum = 0; lineNum < lines.length; lineNum++) { + let match; + while ((match = embedRegex.exec(lines[lineNum])) !== null) { + const rawLink = match[1]; + const linkPath = rawLink.includes('|') ? rawLink.split('|')[0] : rawLink; + embeds.push({ + link: linkPath, + original: match[0], + position: { + start: { line: lineNum, col: match.index }, + end: { line: lineNum, col: match.index + match[0].length }, + }, + }); + } + } + return { links, embeds: embeds.length > 0 ? embeds : undefined }; + } catch { + return null; + } + } + + /** Resolve a link path to a TFile (public API matching Obsidian's) */ + getFirstLinkpathDest(linkPath: string, sourcePath: string): TFile | null { + return this.resolveLink(linkPath, sourcePath); + } +} + +// ─── MockApp ───────────────────────────────────────────────────────────────── + +export class MockApp { + vault: MockVault; + metadataCache: MockMetadataCache; + workspace: { onLayoutReady: (cb: () => void) => void; getActiveFile: () => null; getLeaf: () => any }; + plugins: { plugins: Record }; + fileManager: { processFrontMatter: () => Promise }; + + constructor(vaultRoot: string) { + this.vault = new MockVault(vaultRoot); + this.metadataCache = new MockMetadataCache(vaultRoot, this.vault); + this.workspace = { + onLayoutReady: (cb) => cb(), + getActiveFile: () => null, + getLeaf: () => ({ openFile: async () => {} }), + }; + this.plugins = { plugins: {} }; + this.fileManager = { processFrontMatter: async () => {} }; + } +} + +// ─── Helper to create a fresh test vault ───────────────────────────────────── + +/** + * Create a MockApp pointed at a vault directory. + * Use for tests that need the full Obsidian API mock. + */ +export function createMockApp(vaultRoot: string): MockApp { + return new MockApp(vaultRoot); +} + +/** + * Create a TFile from a vault-relative path (for use in test assertions). + */ +export function createTestTFile(vaultRoot: string, relativePath: string): TFile { + return createMockTFile(vaultRoot, relativePath); +} diff --git a/tests/mocks/obsidian-module.ts b/tests/mocks/obsidian-module.ts new file mode 100644 index 0000000..818641d --- /dev/null +++ b/tests/mocks/obsidian-module.ts @@ -0,0 +1,70 @@ +/** + * Stub module that replaces `import { ... } from 'obsidian'` in tests. + * Provides minimal class/type stubs so service code can import without error. + * The real mock implementations (MockVault, MockApp, etc.) are in obsidian-mock.ts. + */ + +export class TFile { + path: string; + basename: string; + extension: string; + stat: { mtime: number; ctime: number; size: number }; + + constructor(path: string, mtime?: number) { + this.path = path; + this.extension = path.split('.').pop() || ''; + this.basename = path.split('/').pop()?.replace(`.${this.extension}`, '') || ''; + this.stat = { mtime: mtime || Date.now(), ctime: mtime || Date.now(), size: 0 }; + } +} + +export class TAbstractFile { + path: string = ''; +} + +export class Vault {} + +export class MetadataCache {} + +export class App {} + +export class Plugin {} + +export class Component {} + +export class Modal { + app: any; + constructor(app: any) { this.app = app; } + open() {} + close() {} + onOpen() {} + onClose() {} +} + +export class PluginSettingTab { + app: any; + plugin: any; + constructor(app: any, plugin: any) { this.app = app; this.plugin = plugin; } + display() {} + hide() {} +} + +export class Setting { + constructor(_el: any) {} + setName(_n: string) { return this; } + setDesc(_d: string) { return this; } + addText(_cb: any) { return this; } + addToggle(_cb: any) { return this; } + addDropdown(_cb: any) { return this; } + addButton(_cb: any) { return this; } +} + +export class Notice { + constructor(message: string, _timeout?: number) { + // Silent in tests — uncomment for debugging: + // console.log('[Notice]', message); + } +} + +export class MarkdownView {} +export class WorkspaceLeaf {} diff --git a/tests/openai-service.test.ts b/tests/openai-service.test.ts new file mode 100644 index 0000000..1a0d142 --- /dev/null +++ b/tests/openai-service.test.ts @@ -0,0 +1,235 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { OpenAIService } from '../src/services/openai-service'; + +describe('OpenAIService', () => { + let service: OpenAIService; + + beforeEach(() => { + service = new OpenAIService('test-api-key', 'gpt-5'); + }); + + describe('constructor', () => { + it('stores API key and model', () => { + // Access private fields via any cast (testing internals) + expect((service as any).apiKey).toBe('test-api-key'); + expect((service as any).model).toBe('gpt-5'); + }); + }); + + describe('extractCustomContext (via prompt building)', () => { + // We test this through the prompt generation methods since extractCustomContext is private + + it('includes context: section in transcript prompt', () => { + const content = 'Some transcript\n\ncontext:\nFocus on tech topics\n\nMore content'; + const prompt = (service as any).getPrompt(content); + expect(prompt).toContain('USER CONTEXT'); + expect(prompt).toContain('Focus on tech topics'); + }); + + it('includes fenced context: section in transcript prompt', () => { + const content = 'Some content\n\n```context:\nOnly extract action items\n```\n\nMore stuff'; + const prompt = (service as any).getPrompt(content); + expect(prompt).toContain('USER CONTEXT'); + expect(prompt).toContain('Only extract action items'); + }); + + it('handles content without context section', () => { + const content = 'Just a regular transcript with no special context'; + const prompt = (service as any).getPrompt(content); + expect(prompt).not.toContain('USER CONTEXT'); + expect(prompt).toContain('Just a regular transcript'); + }); + }); + + describe('getPrompt', () => { + it('includes the transcript content', () => { + const prompt = (service as any).getPrompt('My voice note about productivity'); + expect(prompt).toContain('Transcript:\nMy voice note about productivity'); + }); + + it('includes atomic notes instructions', () => { + const prompt = (service as any).getPrompt('test'); + expect(prompt).toContain('Atomic Notes'); + expect(prompt).toContain('self-contained'); + }); + + it('includes task extraction instructions', () => { + const prompt = (service as any).getPrompt('test'); + expect(prompt).toContain('Tasks'); + expect(prompt).toContain('actionable'); + }); + + it('includes AUGI command handling', () => { + const prompt = (service as any).getPrompt('test'); + expect(prompt).toContain('AUGI'); + expect(prompt).toContain('auggie'); + }); + }); + + describe('getDistillPrompt', () => { + it('includes content to distill', () => { + const prompt = (service as any).getDistillPrompt('Note content here'); + expect(prompt).toContain('Content to Distill:\nNote content here'); + }); + + it('uses custom prompt when provided', () => { + const custom = 'You are a custom processor. Do something special.'; + const prompt = (service as any).getDistillPrompt('content', custom); + expect(prompt).toContain(custom); + // Should NOT contain the default distill instructions + expect(prompt).not.toContain('expert knowledge curator'); + }); + + it('uses default prompt when no custom prompt', () => { + const prompt = (service as any).getDistillPrompt('content'); + expect(prompt).toContain('expert knowledge curator'); + }); + + it('appends custom context from content', () => { + const content = 'Notes here\n\ncontext:\nFocus on architecture\n\nMore notes'; + const prompt = (service as any).getDistillPrompt(content); + expect(prompt).toContain('USER CONTEXT'); + expect(prompt).toContain('Focus on architecture'); + }); + }); + + describe('getPublishPrompt', () => { + it('includes content to transform', () => { + const prompt = (service as any).getPublishPrompt('Blog content'); + expect(prompt).toContain('Content to Transform:\nBlog content'); + }); + + it('uses default publish prompt', () => { + const prompt = (service as any).getPublishPrompt('content'); + expect(prompt).toContain('publishable blog post'); + expect(prompt).toContain('PRESERVE'); + }); + + it('uses custom prompt when provided', () => { + const custom = 'Write a newsletter instead.'; + const prompt = (service as any).getPublishPrompt('content', custom); + expect(prompt).toContain('Write a newsletter instead.'); + expect(prompt).not.toContain('publishable blog post'); + }); + }); + + describe('parseTranscript', () => { + it('throws when API key is not set', async () => { + const noKeyService = new OpenAIService('', 'gpt-5'); + await expect(noKeyService.parseTranscript('content')).rejects.toThrow('OpenAI API key not set'); + }); + + it('calls OpenAI API with correct parameters', async () => { + const mockResponse = { + ok: true, + json: async () => ({ + choices: [{ + message: { + content: JSON.stringify({ + summary: 'Test summary', + notes: [{ title: 'Note 1', content: 'Content 1' }], + tasks: ['- [ ] Task 1'] + }), + refusal: null + } + }] + }) + }; + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any); + + const result = await service.parseTranscript('Test transcript'); + + expect(fetchSpy).toHaveBeenCalledOnce(); + const [url, options] = fetchSpy.mock.calls[0]; + expect(url).toBe('https://api.openai.com/v1/chat/completions'); + expect((options as any).method).toBe('POST'); + expect(JSON.parse((options as any).body).model).toBe('gpt-5'); + + expect(result.summary).toBe('Test summary'); + expect(result.notes).toHaveLength(1); + expect(result.tasks).toHaveLength(1); + + fetchSpy.mockRestore(); + }); + + it('throws on API error response', async () => { + const mockResponse = { + ok: false, + status: 401, + statusText: 'Unauthorized', + json: async () => ({ error: { message: 'Invalid API key' } }) + }; + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any); + + await expect(service.parseTranscript('content')).rejects.toThrow('OpenAI API error'); + + fetchSpy.mockRestore(); + }); + }); + + describe('distillContent', () => { + it('throws when API key is not set', async () => { + const noKeyService = new OpenAIService('', 'gpt-5'); + await expect(noKeyService.distillContent('content')).rejects.toThrow('OpenAI API key not set'); + }); + + it('parses structured response correctly', async () => { + const mockResponse = { + ok: true, + json: async () => ({ + choices: [{ + message: { + content: JSON.stringify({ + summary: 'Distilled summary about [[Topic A]]', + notes: [ + { title: 'Topic A', content: 'Deep dive into A' }, + { title: 'Topic B', content: 'Overview of B' }, + ], + tasks: [] + }), + refusal: null + } + }] + }) + }; + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any); + + const result = await service.distillContent('aggregated content'); + + expect(result.summary).toContain('Topic A'); + expect(result.notes).toHaveLength(2); + expect(result.sourceNotes).toEqual([]); // Initialized as empty + expect(result.tasks).toEqual([]); + + fetchSpy.mockRestore(); + }); + }); + + describe('publishContent', () => { + it('returns plain text content', async () => { + const mockResponse = { + ok: true, + json: async () => ({ + choices: [{ + message: { + content: '# My Blog Post\n\nThis is the published content.', + refusal: null + } + }] + }) + }; + + const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as any); + + const result = await service.publishContent('raw notes'); + + expect(result).toContain('# My Blog Post'); + expect(result).toContain('published content'); + + fetchSpy.mockRestore(); + }); + }); +}); diff --git a/tests/vault/Backlink Source.md b/tests/vault/Backlink Source.md new file mode 100644 index 0000000..19b1219 --- /dev/null +++ b/tests/vault/Backlink Source.md @@ -0,0 +1,10 @@ +# Backlink Source + +## Section About Root + +This section references the [[Root Note]] because it is important for testing backlink discovery. + +## Unrelated Section + +This section has nothing to do with the root note. +It discusses other topics entirely. diff --git a/tests/vault/Collection Note.md b/tests/vault/Collection Note.md new file mode 100644 index 0000000..32123cb --- /dev/null +++ b/tests/vault/Collection Note.md @@ -0,0 +1,8 @@ +# Collection of Notes + +This is a collection note with checkboxes. + +- [x] [[Linked Note A]] +- [ ] [[Linked Note B]] +- [x] [[Journal Note]] +- [ ] [[Backlink Source]] diff --git a/tests/vault/Context Note.md b/tests/vault/Context Note.md new file mode 100644 index 0000000..61fceb2 --- /dev/null +++ b/tests/vault/Context Note.md @@ -0,0 +1,11 @@ +# Note with Custom Context + +This note has custom context instructions for AI processing. + +Some content about software architecture patterns. + +```context: +Only extract notes about design patterns. Ignore implementation details. +``` + +More content about specific patterns like Observer, Strategy, and Factory. diff --git a/tests/vault/Dataview Note.md b/tests/vault/Dataview Note.md new file mode 100644 index 0000000..cdd65e9 --- /dev/null +++ b/tests/vault/Dataview Note.md @@ -0,0 +1,14 @@ +# Notes with Dataview + +This note contains a dataview query that should be stripped from output. + +```dataview +TABLE file.mtime as "Modified" +FROM "/" +SORT file.mtime DESC +LIMIT 10 +``` + +Regular content after the dataview block. + +Also links to [[Linked Note A]]. diff --git a/tests/vault/Deeply Linked/Deep Note.md b/tests/vault/Deeply Linked/Deep Note.md new file mode 100644 index 0000000..8353446 --- /dev/null +++ b/tests/vault/Deeply Linked/Deep Note.md @@ -0,0 +1,8 @@ +# Deep Note + +This note is at depth 2 from the root: +Root Note → Linked Note A → Deep Note + +It contains insights about deep work and focused concentration. + +No further links from here (leaf node). diff --git a/tests/vault/Excluded Folder/Should Skip.md b/tests/vault/Excluded Folder/Should Skip.md new file mode 100644 index 0000000..d550569 --- /dev/null +++ b/tests/vault/Excluded Folder/Should Skip.md @@ -0,0 +1,4 @@ +# Should Be Excluded + +This note lives in a folder that should be excluded from discovery. +It should never appear in gathered context. diff --git a/tests/vault/Journal Note.md b/tests/vault/Journal Note.md new file mode 100644 index 0000000..aea7019 --- /dev/null +++ b/tests/vault/Journal Note.md @@ -0,0 +1,17 @@ +# Daily Journal + +### 2026-02-25 +Worked on setting up automated tests for the Obsidian plugin. +Made good progress on the mock layer. + +### 2026-02-20 +Started thinking about test architecture. +Reviewed several approaches for testing Electron apps. + +### 2026-01-15 +This is an old entry that should be filtered out by time window. +Did some unrelated work on another project. + +### 2025-12-01 +Very old entry. Should definitely be filtered out. +Working on something completely different. diff --git a/tests/vault/Linked Note A.md b/tests/vault/Linked Note A.md new file mode 100644 index 0000000..74d9f91 --- /dev/null +++ b/tests/vault/Linked Note A.md @@ -0,0 +1,7 @@ +# Linked Note A + +This note contains ideas about atomic note-taking. + +Key insight: each note should contain exactly one idea. + +See also [[Deeply Linked/Deep Note]] for more depth. diff --git a/tests/vault/Linked Note B.md b/tests/vault/Linked Note B.md new file mode 100644 index 0000000..1785ce0 --- /dev/null +++ b/tests/vault/Linked Note B.md @@ -0,0 +1,10 @@ +# Linked Note B + +This note discusses knowledge management systems. + +The Zettelkasten method emphasizes: +- Atomic notes +- Linking between ideas +- Building a network of knowledge + +Related: [[Linked Note A]] diff --git a/tests/vault/Root Note.md b/tests/vault/Root Note.md new file mode 100644 index 0000000..dae2f20 --- /dev/null +++ b/tests/vault/Root Note.md @@ -0,0 +1,8 @@ +This is the root note for testing linked note discovery. + +It links to [[Linked Note A]] and [[Linked Note B]]. + +It also has an embed: ![[Linked Note A]] + +context: +Focus on extracting key concepts about testing diff --git a/tests/vault/Special Characters!.md b/tests/vault/Special Characters!.md new file mode 100644 index 0000000..bed58d8 --- /dev/null +++ b/tests/vault/Special Characters!.md @@ -0,0 +1,6 @@ +# Note with Special: Characters* in "Title" + +This note tests filename sanitization. +It contains characters that are invalid in filenames: \ / : * ? " < > | + +Links to [[Root Note]]. diff --git a/tsconfig.json b/tsconfig.json index 2c097c5..60e306d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,5 +21,9 @@ "include": [ "**/*.ts", "src/**/*.ts" + ], + "exclude": [ + "tests/**/*.ts", + "vitest.config.ts" ] } diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..4ff3c30 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; +import path from 'path'; + +export default defineConfig({ + resolve: { + alias: { + // Mock the obsidian module globally so imports don't fail + 'obsidian': path.resolve(__dirname, 'tests/mocks/obsidian-module.ts'), + }, + }, + test: { + globals: true, + include: ['tests/**/*.test.ts'], + }, +});