diff --git a/.circleci/config.yml b/.circleci/config.yml index c9692d59..f0d04ffd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -33,6 +33,7 @@ aliases: mkdir -p packages/docsearch-modal/dist mkdir -p packages/docsearch-sidepanel/dist mkdir -p packages/docsearch-sidepanel-js/dist + mkdir -p packages/docsearch-cli/dist mkdir -p adapters/docusaurus-theme-search-algolia/lib cp -R /tmp/workspace/packages/docsearch-react/dist packages/docsearch-react @@ -42,6 +43,7 @@ aliases: cp -R /tmp/workspace/packages/docsearch-modal/dist packages/docsearch-modal cp -R /tmp/workspace/packages/docsearch-sidepanel/dist packages/docsearch-sidepanel cp -R /tmp/workspace/packages/docsearch-sidepanel-js/dist packages/docsearch-sidepanel-js + cp -R /tmp/workspace/packages/docsearch-cli/dist packages/docsearch-cli cp -R /tmp/workspace/adapters/docusaurus-theme-search-algolia/lib adapters/docusaurus-theme-search-algolia defaults: &defaults @@ -76,6 +78,7 @@ jobs: name: Build and Size command: | bun run build + bun run --filter @docsearch/cli test:pack - run: name: Move dist folders to workspace command: | @@ -88,6 +91,7 @@ jobs: mkdir -p /tmp/workspace/packages/docsearch-modal/dist mkdir -p /tmp/workspace/packages/docsearch-sidepanel/dist mkdir -p /tmp/workspace/packages/docsearch-sidepanel-js/dist + mkdir -p /tmp/workspace/packages/docsearch-cli/dist mkdir -p /tmp/workspace/adapters/docusaurus-theme-search-algolia/lib cp -R packages/docsearch-react/dist /tmp/workspace/packages/docsearch-react @@ -97,6 +101,7 @@ jobs: cp -R packages/docsearch-modal/dist /tmp/workspace/packages/docsearch-modal cp -R packages/docsearch-sidepanel/dist /tmp/workspace/packages/docsearch-sidepanel cp -R packages/docsearch-sidepanel-js/dist /tmp/workspace/packages/docsearch-sidepanel-js + cp -R packages/docsearch-cli/dist /tmp/workspace/packages/docsearch-cli cp -R adapters/docusaurus-theme-search-algolia/lib /tmp/workspace/adapters/docusaurus-theme-search-algolia - persist_to_workspace: root: *workspace_root diff --git a/bun.lock b/bun.lock index 2389a16c..8cde9ad3 100644 --- a/bun.lock +++ b/bun.lock @@ -108,6 +108,23 @@ "vite": "^6.0.7", }, }, + "packages/docsearch-cli": { + "name": "@docsearch/cli", + "version": "0.0.0", + "bin": { + "docsearch": "./dist/index.js", + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "jsonc-parser": "^3.3.1", + "picocolors": "^1.1.1", + }, + "devDependencies": { + "@types/node": "^26.1.1", + "typescript": "5.7.3", + "vitest": "3.0.2", + }, + }, "packages/docsearch-core": { "name": "@docsearch/core", "version": "4.6.0", @@ -687,6 +704,8 @@ "@discoveryjs/json-ext": ["@discoveryjs/json-ext@0.5.7", "", {}, "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw=="], + "@docsearch/cli": ["@docsearch/cli@workspace:packages/docsearch-cli"], + "@docsearch/core": ["@docsearch/core@workspace:packages/docsearch-core"], "@docsearch/css": ["@docsearch/css@workspace:packages/docsearch-css"], @@ -847,6 +866,8 @@ "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], + "@hono/node-server": ["@hono/node-server@1.19.14", "", { "peerDependencies": { "hono": "^4" } }, "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw=="], + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], @@ -899,6 +920,8 @@ "@mdx-js/react": ["@mdx-js/react@3.1.1", "", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="], "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], @@ -1291,7 +1314,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], "@types/node-forge": ["@types/node-forge@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw=="], @@ -1405,7 +1428,7 @@ "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], @@ -1425,7 +1448,7 @@ "ajv": ["ajv@6.12.6", "", { "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" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], - "ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], @@ -1553,7 +1576,7 @@ "bluebird": ["bluebird@3.7.2", "", {}, "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg=="], - "body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], "bonjour-service": ["bonjour-service@1.3.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "multicast-dns": "^7.2.5" } }, "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA=="], @@ -1707,7 +1730,7 @@ "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], - "content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -1715,7 +1738,7 @@ "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], "copy-webpack-plugin": ["copy-webpack-plugin@11.0.0", "", { "dependencies": { "fast-glob": "^3.2.11", "glob-parent": "^6.0.1", "globby": "^13.1.1", "normalize-path": "^3.0.0", "schema-utils": "^4.0.0", "serialize-javascript": "^6.0.0" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ=="], @@ -1727,6 +1750,8 @@ "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + "cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], @@ -2017,6 +2042,8 @@ "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], "exec-buffer": ["exec-buffer@3.2.0", "", { "dependencies": { "execa": "^0.7.0", "p-finally": "^1.0.0", "pify": "^3.0.0", "rimraf": "^2.5.4", "tempfile": "^2.0.0" } }, "sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA=="], @@ -2029,7 +2056,9 @@ "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], - "express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.5.2", "", { "dependencies": { "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-5Kb34ipNX694DH48vN9irak1Qx30nb0PLYHXfJgw4YEjiC3ZEmZJhwOp+VfiCYwFzvFTdB9QkArYS5kXa2cx2A=="], "ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="], @@ -2083,7 +2112,7 @@ "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - "finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], "find-cache-dir": ["find-cache-dir@4.0.0", "", { "dependencies": { "common-path-prefix": "^3.0.0", "pkg-dir": "^7.0.0" } }, "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg=="], @@ -2113,7 +2142,7 @@ "framer-motion": ["framer-motion@12.24.0", "", { "dependencies": { "motion-dom": "^12.24.0", "motion-utils": "^12.23.28", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-ggTMRkIDPc76lHmM+dRT1MmVfFV6t/y+jkWjWuzR7FG5xRvnAAl/5wFPjzSkLE8Nu5E5uIQRCNxmIXtWJVo6XQ=="], - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "from": ["from@0.1.7", "", {}, "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g=="], @@ -2239,6 +2268,8 @@ "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + "hono": ["hono@4.12.28", "", {}, "sha512-YwUvVpSF7m1yOblFPrU3Hbo8XhPheBoiyfGuII6z19LnOr6JpDnyyp7LFNrfV56wS8tpvtBFGRISHN02pDdLOA=="], + "hookable": ["hookable@6.0.1", "", {}, "sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw=="], "hookified": ["hookified@1.15.0", "", {}, "sha512-51w+ZZGt7Zw5q7rM3nC4t3aLn/xvKDETsXqMczndvwyVQhAHfUmUuFBRFcos8Iyebtk7OAE9dL26wFNzZVVOkw=="], @@ -2339,6 +2370,8 @@ "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], + "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + "ipaddr.js": ["ipaddr.js@2.3.0", "", {}, "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg=="], "irregular-plurals": ["irregular-plurals@3.5.0", "", {}, "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ=="], @@ -2427,6 +2460,8 @@ "is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], "is-regexp": ["is-regexp@1.0.0", "", {}, "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA=="], @@ -2481,6 +2516,8 @@ "joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="], + "jose": ["jose@6.2.3", "", {}, "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw=="], + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], @@ -2501,10 +2538,14 @@ "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], "jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ=="], @@ -2651,7 +2692,7 @@ "mdn-data": ["mdn-data@2.12.2", "", {}, "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA=="], - "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], "memfs": ["memfs@4.51.1", "", { "dependencies": { "@jsonjoy.com/json-pack": "^1.11.0", "@jsonjoy.com/util": "^1.9.0", "glob-to-regex.js": "^1.0.1", "thingies": "^2.5.0", "tree-dump": "^1.0.3", "tslib": "^2.0.0" } }, "sha512-Eyt3XrufitN2ZL9c/uIRMyDwXanLI88h/L3MoWqNY747ha3dMR9dWqp8cRT5ntjZ0U1TNuq4U91ZXK0sMBjYOQ=="], @@ -2659,7 +2700,7 @@ "merge": ["merge@1.2.1", "", {}, "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ=="], - "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -2791,7 +2832,7 @@ "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], - "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], @@ -2959,6 +3000,8 @@ "pinkie-promise": ["pinkie-promise@2.0.1", "", { "dependencies": { "pinkie": "^2.0.0" } }, "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + "pkg-dir": ["pkg-dir@7.0.0", "", { "dependencies": { "find-up": "^6.3.0" } }, "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA=="], "playwright": ["playwright@1.49.1", "", { "dependencies": { "playwright-core": "1.49.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA=="], @@ -3177,9 +3220,9 @@ "randombytes": ["randombytes@2.1.0", "", { "dependencies": { "safe-buffer": "^5.1.0" } }, "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ=="], - "range-parser": ["range-parser@1.2.0", "", {}, "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A=="], + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], @@ -3315,6 +3358,8 @@ "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + "rrweb-cssom": ["rrweb-cssom@0.8.0", "", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], "rtlcss": ["rtlcss@4.3.0", "", { "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0", "postcss": "^8.4.21", "strip-json-comments": "^3.1.1" }, "bin": { "rtlcss": "bin/rtlcss.js" } }, "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig=="], @@ -3363,7 +3408,7 @@ "semver-truncate": ["semver-truncate@1.1.2", "", { "dependencies": { "semver": "^5.3.0" } }, "sha512-V1fGg9i4CL3qesB6U0L6XAm4xOJiHmt4QAacazumuasc03BvtFGIMCduv01JWQ69Nv+JST9TqhSCiJoxoY031w=="], - "send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], "serialize-javascript": ["serialize-javascript@6.0.2", "", { "dependencies": { "randombytes": "^2.1.0" } }, "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g=="], @@ -3371,7 +3416,7 @@ "serve-index": ["serve-index@1.9.1", "", { "dependencies": { "accepts": "~1.3.4", "batch": "0.6.1", "debug": "2.6.9", "escape-html": "~1.0.3", "http-errors": "~1.6.2", "mime-types": "~2.1.17", "parseurl": "~1.3.2" } }, "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw=="], - "serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], @@ -3649,7 +3694,7 @@ "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], - "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], @@ -3673,7 +3718,7 @@ "undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="], - "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], @@ -3841,6 +3886,8 @@ "zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], @@ -3907,6 +3954,8 @@ "@isaacs/fs-minipass/minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + "@jest/types/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "@keyv/bigmap/keyv": ["keyv@5.5.5", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ=="], "@manypkg/find-root/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], @@ -3919,6 +3968,8 @@ "@manypkg/get-packages/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@modelcontextprotocol/sdk/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="], "@quansync/fs/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], @@ -3957,6 +4008,36 @@ "@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "@types/body-parser/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/bonjour/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/connect/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/connect-history-api-fallback/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/express-serve-static-core/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/fs-extra/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/glob/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/http-proxy/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/jsonfile/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/node-forge/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/sax/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/send/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/serve-static/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/sockjs/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/ws/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.20.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.20.0", "@typescript-eslint/types": "8.20.0", "@typescript-eslint/typescript-estree": "8.20.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA=="], "@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.52.0", "", {}, "sha512-LWQV1V4q9V4cT4H5JCIx3481iIFxH1UkVk+ZkGGAV1ZGcjGI9IoFOfg3O6ywz8QqCDEp7Inlg6kovMofsNRaGg=="], @@ -3977,7 +4058,7 @@ "@vitest/utils/@vitest/pretty-format": ["@vitest/pretty-format@3.0.2", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-yBohcBw/T/p0/JRgYD+IYcjCmuHzjC3WLAKsVE4/LwiubzZkE8N49/xIQ/KGQwDRA8PaviF8IRO8JMWMngdVVQ=="], - "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "accepts/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "ajv-formats/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], @@ -4009,9 +4090,11 @@ "bl/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "body-parser/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], - "body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + "body-parser/iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + + "body-parser/qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], "boxen/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], @@ -4047,6 +4130,8 @@ "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "compression/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], + "configstore/write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="], "copy-webpack-plugin/globby": ["globby@13.2.2", "", { "dependencies": { "dir-glob": "^3.0.1", "fast-glob": "^3.3.0", "ignore": "^5.2.4", "merge2": "^1.4.1", "slash": "^4.0.0" } }, "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w=="], @@ -4139,6 +4224,8 @@ "eslint-plugin-react/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "eval/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "exec-buffer/execa": ["execa@0.7.0", "", { "dependencies": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", "is-stream": "^1.1.0", "npm-run-path": "^2.0.0", "p-finally": "^1.0.0", "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } }, "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw=="], "exec-buffer/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], @@ -4149,13 +4236,7 @@ "executable/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], - "express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], - - "express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], - - "express/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + "express/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "ext-list/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], @@ -4165,8 +4246,6 @@ "file-loader/schema-utils": ["schema-utils@3.3.0", "", { "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } }, "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg=="], - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "from2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], @@ -4209,8 +4288,12 @@ "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], + "jest-util/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "jest-worker/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "loader-utils/json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], @@ -4409,8 +4492,6 @@ "proxy-addr/ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], - "raw-body/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "rc/strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "react-router/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], @@ -4427,20 +4508,26 @@ "rimraf/glob": ["glob@7.2.3", "", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "seek-bzip/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], "semver-truncate/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "send/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + "send/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "serve-handler/bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="], + "serve-handler/content-disposition": ["content-disposition@0.5.2", "", {}, "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA=="], + "serve-handler/mime-types": ["mime-types@2.1.18", "", { "dependencies": { "mime-db": "~1.33.0" } }, "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ=="], "serve-handler/path-to-regexp": ["path-to-regexp@3.3.0", "", {}, "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw=="], + "serve-handler/range-parser": ["range-parser@1.2.0", "", {}, "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A=="], + + "serve-index/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "serve-index/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "serve-index/http-errors": ["http-errors@1.6.3", "", { "dependencies": { "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", "statuses": ">= 1.4.0 < 2" } }, "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A=="], @@ -4499,6 +4586,10 @@ "tsdown/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "type-is/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + "typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.20.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.20.0", "@typescript-eslint/types": "8.20.0", "@typescript-eslint/typescript-estree": "8.20.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA=="], "unconfig-core/quansync": ["quansync@1.0.0", "", {}, "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA=="], @@ -4527,10 +4618,10 @@ "webpack-dev-middleware/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "webpack-dev-middleware/range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "webpack-dev-middleware/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], + "webpack-dev-server/express": ["express@4.22.1", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "~1.20.3", "content-disposition": "~0.5.4", "content-type": "~1.0.4", "cookie": "~0.7.1", "cookie-signature": "~1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "~1.3.1", "fresh": "~0.5.2", "http-errors": "~2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "~0.1.12", "proxy-addr": "~2.0.7", "qs": "~6.14.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "~0.19.0", "serve-static": "~1.16.2", "setprototypeof": "1.2.0", "statuses": "~2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g=="], + "webpack-dev-server/open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], "webpack-dev-server/schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], @@ -4589,6 +4680,8 @@ "@eslint/eslintrc/import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "@jest/types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@manypkg/find-root/find-up/locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], "@manypkg/find-root/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], @@ -4599,6 +4692,8 @@ "@manypkg/get-packages/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], + "@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "@radix-ui/react-popper/@floating-ui/react-dom/@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="], "@svgr/core/cosmiconfig/import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -4641,6 +4736,36 @@ "@testing-library/jest-dom/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "@types/body-parser/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/bonjour/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/connect-history-api-fallback/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/connect/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/fs-extra/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/glob/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/http-proxy/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/jsonfile/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/node-forge/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/sax/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/send/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/serve-static/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/sockjs/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "@types/ws/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], "@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.52.0", "", { "dependencies": { "@typescript-eslint/types": "8.52.0", "eslint-visitor-keys": "^4.2.1" } }, "sha512-ink3/Zofus34nmBsPjow63FP5M7IGff0RKAgqR6+CFpdk22M7aLwC9gOcLGYqr7MczLPzZVERW9hRog3O4n1sQ=="], @@ -4649,6 +4774,8 @@ "@typescript-eslint/utils/@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + "accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "autoprefixer/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], @@ -4659,6 +4786,8 @@ "babel-loader/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "babel-loader/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "babel-loader/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], "bin-build/execa/cross-spawn": ["cross-spawn@5.1.0", "", { "dependencies": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A=="], @@ -4711,7 +4840,7 @@ "bl/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - "body-parser/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "body-parser/qs/side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], "boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -4729,6 +4858,8 @@ "copy-webpack-plugin/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "copy-webpack-plugin/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "copy-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], "core-js-compat/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], @@ -4743,6 +4874,8 @@ "css-minimizer-webpack-plugin/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "css-minimizer-webpack-plugin/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "css-minimizer-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], "css-select/domutils/dom-serializer": ["dom-serializer@1.4.1", "", { "dependencies": { "domelementtype": "^2.0.1", "domhandler": "^4.2.0", "entities": "^2.0.0" } }, "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag=="], @@ -4789,6 +4922,8 @@ "duplexify/readable-stream/string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + "eval/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "exec-buffer/execa/cross-spawn": ["cross-spawn@5.1.0", "", { "dependencies": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", "which": "^1.2.9" } }, "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A=="], "exec-buffer/execa/get-stream": ["get-stream@3.0.0", "", {}, "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ=="], @@ -4801,9 +4936,7 @@ "exec-buffer/rimraf/glob": ["glob@7.2.3", "", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "from2/readable-stream/isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], @@ -4847,10 +4980,16 @@ "imagemin/globby/glob": ["glob@7.2.3", "", { "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" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "jest-util/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "jest-worker/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "mdast-util-gfm-autolink-literal/micromark-util-character/micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], "mini-css-extract-plugin/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "mini-css-extract-plugin/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "mini-css-extract-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], "node-fetch/whatwg-url/tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], @@ -4943,10 +5082,12 @@ "renderkid/htmlparser2/entities": ["entities@2.2.0", "", {}, "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + "send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], "serve-handler/mime-types/mime-db": ["mime-db@1.33.0", "", {}, "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="], + "serve-index/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "serve-index/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "serve-index/http-errors/depd": ["depd@1.1.2", "", {}, "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ=="], @@ -4985,12 +5126,18 @@ "tar-stream/readable-stream/string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], + "terser-webpack-plugin/jest-worker/@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + "terser-webpack-plugin/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "terser-webpack-plugin/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "terser-webpack-plugin/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "terser-webpack-plugin/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + "type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + "unrun/rolldown/@oxc-project/types": ["@oxc-project/types@0.106.0", "", {}, "sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg=="], "unrun/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.58", "", { "os": "android", "cpu": "arm64" }, "sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug=="], @@ -5031,12 +5178,40 @@ "webpack-dev-middleware/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "webpack-dev-middleware/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "webpack-dev-middleware/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], + "webpack-dev-server/express/accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "webpack-dev-server/express/body-parser": ["body-parser@1.20.4", "", { "dependencies": { "bytes": "~3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "~1.2.0", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "on-finished": "~2.4.1", "qs": "~6.14.0", "raw-body": "~2.5.3", "type-is": "~1.6.18", "unpipe": "~1.0.0" } }, "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA=="], + + "webpack-dev-server/express/content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "webpack-dev-server/express/cookie-signature": ["cookie-signature@1.0.7", "", {}, "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="], + + "webpack-dev-server/express/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "webpack-dev-server/express/finalhandler": ["finalhandler@1.3.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "~2.4.1", "parseurl": "~1.3.3", "statuses": "~2.0.2", "unpipe": "~1.0.0" } }, "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg=="], + + "webpack-dev-server/express/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "webpack-dev-server/express/merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "webpack-dev-server/express/path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + + "webpack-dev-server/express/send": ["send@0.19.2", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "~0.5.2", "http-errors": "~2.0.1", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "~2.4.1", "range-parser": "~1.2.1", "statuses": "~2.0.2" } }, "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg=="], + + "webpack-dev-server/express/serve-static": ["serve-static@1.16.3", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "~0.19.1" } }, "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA=="], + + "webpack-dev-server/express/type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + "webpack-dev-server/open/define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], "webpack-dev-server/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "webpack-dev-server/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "webpack-dev-server/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], "webpack/browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.9.11", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ=="], @@ -5051,6 +5226,8 @@ "webpack/schema-utils/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + "webpack/schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], + "webpack/schema-utils/ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], "widest-line/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], @@ -5121,6 +5298,8 @@ "bin-wrapper/download/p-event/p-timeout": ["p-timeout@2.0.1", "", { "dependencies": { "p-finally": "^1.0.0" } }, "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA=="], + "body-parser/qs/side-channel/side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + "boxen/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], "copy-webpack-plugin/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -5165,12 +5344,24 @@ "stylelint/cosmiconfig/import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "terser-webpack-plugin/jest-worker/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "terser-webpack-plugin/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "update-notifier/boxen/string-width/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], "webpack-dev-middleware/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + "webpack-dev-server/express/accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "webpack-dev-server/express/body-parser/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "webpack-dev-server/express/body-parser/raw-body": ["raw-body@2.5.3", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.4.24", "unpipe": "~1.0.0" } }, "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA=="], + + "webpack-dev-server/express/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "webpack-dev-server/express/type-is/media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + "webpack-dev-server/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "webpack/schema-utils/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], diff --git a/package.json b/package.json index e62e6bd7..ff7a9d11 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ ], "type": "module", "scripts": { - "build": "bun run --sequential build:stage:base build:stage:react build:stage:consumers build:stage:adapter website:build", + "build": "bun run --sequential build:stage:base build:stage:react build:stage:consumers build:stage:adapter build:pkg:cli website:build", "build:all": "bun run --sequential build website:build", "build:pkg:core": "bun --filter @docsearch/core build", "build:pkg:css": "bun --filter @docsearch/css build", @@ -18,6 +18,7 @@ "build:pkg:sidepanel-js": "bun --filter @docsearch/sidepanel-js build", "build:pkg:modal": "bun --filter @docsearch/modal build", "build:pkg:adapter": "bun --filter @docsearch/docusaurus-adapter build", + "build:pkg:cli": "bun --filter @docsearch/cli build", "build:stage:base": "bun run --parallel build:pkg:core build:pkg:css", "build:stage:react": "bun run build:pkg:react", "build:stage:consumers": "bun run --parallel build:pkg:js build:pkg:sidepanel build:pkg:sidepanel-js build:pkg:modal", diff --git a/packages/docsearch-cli/CHANGELOG.md b/packages/docsearch-cli/CHANGELOG.md new file mode 100644 index 00000000..d86b7b87 --- /dev/null +++ b/packages/docsearch-cli/CHANGELOG.md @@ -0,0 +1,7 @@ +# @docsearch/cli + +## 0.0.1 + +### Patch Changes + +- 710b410: Add the DocSearch CLI for installing and querying the hosted DocSearch MCP. diff --git a/packages/docsearch-cli/README.md b/packages/docsearch-cli/README.md new file mode 100644 index 00000000..3c0cc019 --- /dev/null +++ b/packages/docsearch-cli/README.md @@ -0,0 +1,104 @@ +# DocSearch CLI + +Install the hosted [DocSearch MCP](https://docsearch.algolia.com/mcp) in your AI coding agents and search public developer documentation from the terminal. + +## Run without installing + +```sh +npx @docsearch/cli +``` + +Or install the `docsearch` command globally: + +```sh +npm install --global @docsearch/cli +``` + +Requires Node.js 20 or later. + +## Set up the DocSearch MCP + +Run the interactive installer: + +```sh +docsearch setup +``` + +The installer asks whether to configure the current project or your global agent settings, then lets you select any detected agents. It supports: + +- Cursor +- Claude Code +- Codex +- OpenCode +- Gemini CLI + +Skip the prompts with explicit flags: + +```sh +docsearch setup --project --cursor --claude --yes +docsearch setup --global --all --yes +``` + +Project setup locates the repository root before writing configuration. Re-running setup is idempotent and preserves unrelated configuration. + +## Search documentation + +Use `docs` for most questions: + +```sh +docsearch docs Next.js "how do middleware matchers work" +``` + +For explicit docset selection, resolve a product and then query its docset: + +```sh +docsearch resolve "Algolia InstantSearch React" +docsearch query repo/algolia/instantsearch "configure the React search client" +``` + +Limit the returned results when needed: + +```sh +docsearch docs Next.js "cache revalidation" --max-results 3 --max-docsets 2 +docsearch resolve Next.js --top-n 3 +``` + +Pass `--json` to write the raw MCP result as JSON: + +```sh +docsearch docs Next.js "cache revalidation" --json +``` + +Machine-readable results are written to stdout. Progress and errors are written to stderr. + +## Options + +```text +--project Install into the current repository +--global Install into user-level agent settings +--all Configure every supported agent +--cursor Configure Cursor +--claude Configure Claude Code +--codex Configure Codex +--opencode Configure OpenCode +--gemini Configure Gemini CLI +--yes, -y Run setup non-interactively +--endpoint Override the hosted DocSearch MCP endpoint +--json Print raw MCP results as JSON +``` + +Set [`NO_COLOR`](https://no-color.org/) to disable terminal colors. + +## Development + +From the DocSearch monorepo root: + +```sh +bun install +bun run --filter @docsearch/cli build +bun run --filter @docsearch/cli test +``` + +## License + +[MIT](https://github.com/algolia/docsearch/blob/v5/LICENSE) diff --git a/packages/docsearch-cli/package.json b/packages/docsearch-cli/package.json new file mode 100644 index 00000000..3fdb2464 --- /dev/null +++ b/packages/docsearch-cli/package.json @@ -0,0 +1,57 @@ +{ + "name": "@docsearch/cli", + "description": "Command-line tools for installing and querying DocSearch MCP.", + "version": "0.0.2", + "license": "MIT", + "type": "module", + "homepage": "https://docsearch.algolia.com", + "repository": { + "type": "git", + "url": "git+https://github.com/algolia/docsearch.git", + "directory": "packages/docsearch-cli" + }, + "author": { + "name": "Algolia, Inc.", + "url": "https://www.algolia.com" + }, + "bugs": { + "url": "https://github.com/algolia/docsearch/issues" + }, + "bin": { + "docsearch": "./dist/index.js" + }, + "files": [ + "dist/" + ], + "keywords": [ + "algolia", + "cli", + "documentation", + "docsearch", + "mcp" + ], + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "bun run clean && tsc --project tsconfig.json", + "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "prepack": "bun run build", + "test": "vitest run --config vitest.config.mts", + "test:pack": "npm pack --dry-run", + "watch": "tsc --project tsconfig.json --watch" + }, + "engines": { + "node": ">=20" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.29.0", + "jsonc-parser": "^3.3.1", + "picocolors": "^1.1.1" + }, + "devDependencies": { + "@types/node": "^26.1.1", + "typescript": "5.7.3", + "vitest": "3.0.2" + } +} diff --git a/packages/docsearch-cli/src/__tests__/cli.test.ts b/packages/docsearch-cli/src/__tests__/cli.test.ts new file mode 100644 index 00000000..0bfaf90e --- /dev/null +++ b/packages/docsearch-cli/src/__tests__/cli.test.ts @@ -0,0 +1,75 @@ +// @vitest-environment node + +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { runCli } from '../cli'; +import { callDocSearchTool } from '../mcp/client'; + +vi.mock('../mcp/client.js', () => ({ + callDocSearchTool: vi.fn(), +})); + +interface CapturedOutput { + stderr: string; + stdout: string; +} + +function captureOutput(): CapturedOutput { + const output: CapturedOutput = { stderr: '', stdout: '' }; + vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => { + output.stdout += String(chunk); + return true; + }); + vi.spyOn(process.stderr, 'write').mockImplementation((chunk) => { + output.stderr += String(chunk); + return true; + }); + return output; +} + +afterEach(() => { + vi.clearAllMocks(); + vi.restoreAllMocks(); +}); + +describe('runCli', () => { + it('returns a non-zero exit for MCP tool errors while preserving JSON output', async () => { + const result: CallToolResult = { + content: [{ text: 'Invalid query', type: 'text' }], + isError: true, + }; + vi.mocked(callDocSearchTool).mockResolvedValue(result); + const output = captureOutput(); + + const exitCode = await runCli(['resolve', 'Next.js', '--json']); + + expect(exitCode).toBe(1); + expect(JSON.parse(output.stdout)).toMatchObject({ isError: true }); + expect(output.stderr).toBe(''); + }); + + it('reports MCP tool errors in human-readable mode', async () => { + const result: CallToolResult = { + content: [{ text: 'Invalid query', type: 'text' }], + isError: true, + }; + vi.mocked(callDocSearchTool).mockResolvedValue(result); + const output = captureOutput(); + + const exitCode = await runCli(['resolve', 'Next.js']); + + expect(exitCode).toBe(1); + expect(output.stderr).toContain('Invalid query'); + }); + + it('uses exit code 2 for command usage errors', async () => { + const output = captureOutput(); + + const exitCode = await runCli(['resolve', 'Next.js', '--max-results', '2']); + + expect(exitCode).toBe(2); + expect(output.stderr).toContain('not valid for the resolve command'); + expect(callDocSearchTool).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/docsearch-cli/src/__tests__/commands.test.ts b/packages/docsearch-cli/src/__tests__/commands.test.ts new file mode 100644 index 00000000..4594906b --- /dev/null +++ b/packages/docsearch-cli/src/__tests__/commands.test.ts @@ -0,0 +1,99 @@ +// @vitest-environment node + +import { describe, expect, it } from 'vitest'; + +import { buildSetupRequest, buildToolRequest, parseArgs } from '../commands'; +import { DEFAULT_MCP_ENDPOINT, TOOL_QUERY_DOCS, TOOL_RESOLVE_DOCSET, TOOL_SEARCH_DOCS } from '../constants'; + +describe('buildToolRequest', () => { + it('maps docs to the one-shot DocSearch MCP tool', () => { + const request = buildToolRequest( + 'docs', + parseArgs(['Next.js', 'middleware matcher config', '--max-results', '3', '--json']), + ); + + expect(request).toEqual({ + endpoint: DEFAULT_MCP_ENDPOINT, + json: true, + toolName: TOOL_SEARCH_DOCS, + toolArguments: { + library: 'Next.js', + query: 'middleware matcher config', + maxResults: 3, + }, + }); + }); + + it('maps resolve to the docset resolver tool', () => { + const request = buildToolRequest('resolve', parseArgs(['Algolia', 'InstantSearch', '--top-n', '4'])); + + expect(request).toEqual({ + endpoint: DEFAULT_MCP_ENDPOINT, + json: false, + toolName: TOOL_RESOLVE_DOCSET, + toolArguments: { + query: 'Algolia InstantSearch', + topN: 4, + }, + }); + }); + + it('maps query to explicit docset lookups', () => { + const request = buildToolRequest( + 'query', + parseArgs(['nextjs,react', 'server components', '--endpoint', 'https://example.com/mcp']), + ); + + expect(request).toEqual({ + endpoint: 'https://example.com/mcp', + json: false, + toolName: TOOL_QUERY_DOCS, + toolArguments: { + docsetIds: ['nextjs', 'react'], + query: 'server components', + }, + }); + }); + + it('supports equals syntax for value options', () => { + const request = buildToolRequest('resolve', parseArgs(['Next.js', '--top-n=2'])); + + expect(request.toolArguments).toEqual({ + query: 'Next.js', + topN: 2, + }); + }); + + it('rejects empty docset IDs', () => { + expect(() => buildToolRequest('query', parseArgs([',', 'search query']))).toThrow(/docset ID/i); + }); + + it('rejects options that do not belong to the command', () => { + expect(() => buildToolRequest('resolve', parseArgs(['Next.js', '--max-results', '2']))).toThrow( + /not valid for the resolve command/, + ); + }); + + it('rejects non-HTTP endpoints', () => { + expect(() => buildToolRequest('resolve', parseArgs(['Next.js', '--endpoint', 'file:///tmp/mcp']))).toThrow( + /HTTP or HTTPS/, + ); + }); +}); + +describe('buildSetupRequest', () => { + it('maps setup flags to selected agents and project scope', () => { + expect(buildSetupRequest(parseArgs(['--cursor', '--claude', '--project', '--yes']))).toEqual({ + agents: ['claude', 'cursor'].sort(), + all: false, + endpoint: DEFAULT_MCP_ENDPOINT, + scope: 'project', + yes: true, + }); + }); + + it('rejects positional setup arguments and unrelated flags', () => { + expect(() => buildSetupRequest(parseArgs(['cursor']))).toThrow(/Unexpected setup argument/); + expect(() => buildSetupRequest(parseArgs(['--json']))).toThrow(/not valid for the setup command/); + }); +}); diff --git a/packages/docsearch-cli/src/cli.ts b/packages/docsearch-cli/src/cli.ts new file mode 100644 index 00000000..03d1aa2c --- /dev/null +++ b/packages/docsearch-cli/src/cli.ts @@ -0,0 +1,180 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { + buildSetupRequest, + buildToolRequest, + parseArgs, + type QueryCommandName, + type SetupRequest, +} from './commands.js'; +import { CLI_VERSION } from './constants.js'; +import { UsageError } from './errors.js'; +import { callDocSearchTool } from './mcp/client.js'; +import { formatToolResult, getToolResultText } from './mcp/format.js'; +import type { SetupAgent, SetupScope } from './setup/agents.js'; +import { PromptCancelledError, promptAgents, promptScope } from './setup/prompt.js'; +import { buildAgentChoices, createPathContext, detectAgents, findProjectRoot, setupDocSearch } from './setup/setup.js'; +import { renderMarkdown } from './ui/markdown.js'; +import { + renderError, + renderHelp, + renderLanding, + renderNotice, + renderResultHeader, + renderSetupResults, +} from './ui/render.js'; +import { startSpinner } from './ui/spinner.js'; + +export async function runCli(argv: string[] = process.argv.slice(2)): Promise { + const [commandName, ...commandArgs] = argv; + + try { + if (!commandName) { + process.stdout.write(renderLanding()); + return 0; + } + + if (commandName === 'help' || commandName === '--help' || commandName === '-h') { + process.stdout.write(renderHelp()); + return 0; + } + + if (commandName === 'version' || commandName === '--version' || commandName === '-v') { + process.stdout.write(`${CLI_VERSION}\n`); + return 0; + } + + const parsedArgs = parseArgs(commandArgs); + if (parsedArgs.options['--help'] || parsedArgs.options['-h']) { + process.stdout.write(renderHelp()); + return 0; + } + + if (commandName === 'setup') { + return await runSetup(buildSetupRequest(parsedArgs)); + } + + if (isQueryCommand(commandName)) { + return await runQuery(commandName, parsedArgs); + } + + throw new UsageError(`Unknown command: ${commandName}.`); + } catch (error) { + if (error instanceof PromptCancelledError) { + process.stderr.write(renderNotice('Setup cancelled. No changes were made.')); + return error.exitCode; + } + + const message = error instanceof Error ? error.message : String(error); + process.stderr.write(renderError(message)); + if (error instanceof UsageError) { + process.stderr.write(renderHelp()); + } + return error instanceof UsageError ? 2 : 1; + } +} + +async function runSetup(request: SetupRequest): Promise { + const baseContext = createPathContext(); + const scope = await resolveScope(request); + const cwd = scope === 'project' ? await findProjectRoot(baseContext.cwd, baseContext.homeDir) : baseContext.cwd; + const context = { ...baseContext, cwd }; + const agents = await resolveSetupAgents(request, context, scope); + + if (agents.length === 0) { + process.stdout.write(renderNotice('No agents selected. Re-run `docsearch setup` and pick at least one agent.')); + return 0; + } + + const results = await setupDocSearch({ + agents, + cwd: context.cwd, + endpoint: request.endpoint, + env: context.env, + homeDir: context.homeDir, + scope, + }); + + process.stdout.write( + renderSetupResults(results, { + endpoint: request.endpoint, + location: scope === 'project' ? context.cwd : undefined, + scope, + }), + ); + return 0; +} + +function resolveScope(request: SetupRequest): Promise { + if (request.scope) { + return Promise.resolve(request.scope); + } + + if (request.yes) { + return Promise.resolve('project'); + } + + return promptScope(); +} + +async function resolveSetupAgents( + request: SetupRequest, + context: ReturnType, + scope: SetupScope, +): Promise { + if (request.all) { + return buildAgentChoices(context, []).map((choice) => choice.name); + } + + if (request.agents.length > 0) { + return request.agents; + } + + const detected = await detectAgents(scope, context); + if (request.yes) { + if (detected.length === 0) { + throw new UsageError('No supported agents were detected. Pass an agent flag such as --cursor, or use --all.'); + } + return detected; + } + + return promptAgents(buildAgentChoices(context, detected)); +} + +async function runQuery(commandName: QueryCommandName, parsedArgs: ReturnType): Promise { + const request = buildToolRequest(commandName, parsedArgs); + const subject = describeQuery(commandName, parsedArgs.positionals); + + if (request.json) { + const result = await callDocSearchTool(request); + process.stdout.write(formatToolResult(result, true)); + return result.isError === true ? 1 : 0; + } + + const spinner = startSpinner(`Searching ${subject}…`); + try { + const result = await callDocSearchTool(request); + if (result.isError === true) { + throw new Error(getToolResultText(result) || 'DocSearch MCP returned an error.'); + } + spinner.succeed(`Results for ${subject}`); + process.stdout.write(renderResultHeader(commandName, subject)); + process.stdout.write(renderMarkdown(formatToolResult(result, false))); + return 0; + } catch (error) { + spinner.fail(`Search failed for ${subject}`); + throw error; + } +} + +function describeQuery(commandName: QueryCommandName, positionals: string[]): string { + if (commandName === 'docs') { + const [library] = positionals; + return library ?? 'docs'; + } + + return positionals.join(' ').trim() || commandName; +} + +function isQueryCommand(commandName: string): commandName is QueryCommandName { + return commandName === 'docs' || commandName === 'resolve' || commandName === 'query'; +} diff --git a/packages/docsearch-cli/src/commands.ts b/packages/docsearch-cli/src/commands.ts new file mode 100644 index 00000000..593dd1e1 --- /dev/null +++ b/packages/docsearch-cli/src/commands.ts @@ -0,0 +1,283 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { DEFAULT_MCP_ENDPOINT, TOOL_QUERY_DOCS, TOOL_RESOLVE_DOCSET, TOOL_SEARCH_DOCS } from './constants.js'; +import { UsageError } from './errors.js'; +import type { SetupAgent, SetupScope } from './setup/agents.js'; + +export type QueryCommandName = 'docs' | 'query' | 'resolve'; +export type CommandName = QueryCommandName | 'help' | 'setup' | 'version'; + +export interface ParsedArgs { + options: Record; + positionals: string[]; +} + +export interface ToolRequest { + endpoint: string; + json: boolean; + toolName: string; + toolArguments: Record; +} + +export interface SetupRequest { + all: boolean; + agents: SetupAgent[]; + endpoint: string; + scope?: SetupScope; + yes: boolean; +} + +const AGENT_FLAGS: Record = { + '--claude': 'claude', + '--codex': 'codex', + '--cursor': 'cursor', + '--gemini': 'gemini', + '--opencode': 'opencode', +}; + +const BOOLEAN_OPTIONS = new Set([ + '--all', + '--claude', + '--codex', + '--cursor', + '--gemini', + '--global', + '--help', + '--json', + '--opencode', + '--project', + '--yes', + '-h', + '-y', +]); + +const VALUE_OPTIONS = new Set(['--endpoint', '--max-docsets', '--max-results', '--top-n']); +const HELP_OPTIONS = new Set(['--help', '-h']); +const QUERY_COMMON_OPTIONS = new Set(['--endpoint', '--help', '--json', '-h']); +const SETUP_OPTIONS = new Set([ + '--all', + '--claude', + '--codex', + '--cursor', + '--endpoint', + '--gemini', + '--global', + '--help', + '--opencode', + '--project', + '--yes', + '-h', + '-y', +]); + +export function parseArgs(argv: string[]): ParsedArgs { + const options: Record = {}; + const positionals: string[] = []; + + for (let idx = 0; idx < argv.length; idx++) { + const token = argv[idx]; + const equalsIdx = token.startsWith('--') ? token.indexOf('=') : -1; + const option = equalsIdx > 0 ? token.slice(0, equalsIdx) : token; + const inlineValue = equalsIdx > 0 ? token.slice(equalsIdx + 1) : undefined; + + if (VALUE_OPTIONS.has(option)) { + const value = inlineValue ?? argv[idx + 1]; + if (!value || value.startsWith('-')) { + throw new UsageError(`Missing value for ${option}.`); + } + options[option] = value; + if (inlineValue === undefined) { + idx++; + } + } else if (BOOLEAN_OPTIONS.has(option)) { + if (inlineValue !== undefined) { + throw new UsageError(`${option} does not accept a value.`); + } + options[option] = true; + } else if (option.startsWith('-')) { + throw new UsageError(`Unknown option: ${option}.`); + } else { + positionals.push(token); + } + } + + return { options, positionals }; +} + +export function buildToolRequest(commandName: QueryCommandName, args: ParsedArgs): ToolRequest { + validateToolOptions(commandName, args.options); + const endpoint = readEndpoint(args.options); + const json = Boolean(args.options['--json']); + const maxResults = readNumberOption(args.options, '--max-results'); + + switch (commandName) { + case 'docs': { + const [library, ...queryParts] = args.positionals; + const query = queryParts.join(' ').trim(); + if (!library || !query) { + throw new UsageError('Usage: docsearch docs [--json]'); + } + + return { + endpoint, + json, + toolName: TOOL_SEARCH_DOCS, + toolArguments: removeUndefined({ + library, + query, + maxResults, + maxDocsets: readNumberOption(args.options, '--max-docsets'), + }), + }; + } + + case 'resolve': { + const query = args.positionals.join(' ').trim(); + if (!query) { + throw new UsageError('Usage: docsearch resolve [--json]'); + } + + return { + endpoint, + json, + toolName: TOOL_RESOLVE_DOCSET, + toolArguments: removeUndefined({ + query, + topN: readNumberOption(args.options, '--top-n'), + }), + }; + } + + case 'query': { + const [docsetId, ...queryParts] = args.positionals; + const query = queryParts.join(' ').trim(); + if (!docsetId || !query) { + throw new UsageError('Usage: docsearch query [--json]'); + } + + const docsetIds = docsetId + .split(',') + .map((value) => value.trim()) + .filter(Boolean); + if (docsetIds.length === 0) { + throw new UsageError('At least one non-empty docset ID is required.'); + } + + return { + endpoint, + json, + toolName: TOOL_QUERY_DOCS, + toolArguments: removeUndefined({ + docsetIds, + query, + maxResults, + }), + }; + } + + default: { + const exhaustive: never = commandName; + return exhaustive; + } + } +} + +export function buildSetupRequest(args: ParsedArgs): SetupRequest { + validateOptions(args.options, SETUP_OPTIONS, 'setup'); + if (args.positionals.length > 0) { + throw new UsageError(`Unexpected setup argument: ${args.positionals[0]}.`); + } + + const agents = Object.entries(AGENT_FLAGS) + .filter(([flag]) => Boolean(args.options[flag])) + .map(([, agent]) => agent); + + if (args.options['--project'] && args.options['--global']) { + throw new UsageError('Use either --project or --global, not both.'); + } + + return { + agents: uniqueAgents(agents), + all: Boolean(args.options['--all']), + endpoint: readEndpoint(args.options), + scope: resolveScopeFlag(args.options), + yes: Boolean(args.options['--yes'] || args.options['-y']), + }; +} + +function resolveScopeFlag(options: Record): SetupScope | undefined { + if (options['--project']) { + return 'project'; + } + + if (options['--global']) { + return 'global'; + } + + return undefined; +} + +function readStringOption(options: Record, key: string): string | undefined { + const value = options[key]; + return typeof value === 'string' ? value : undefined; +} + +function readEndpoint(options: Record): string { + const value = readStringOption(options, '--endpoint') ?? DEFAULT_MCP_ENDPOINT; + let url: URL; + try { + url = new URL(value); + } catch { + throw new UsageError(`--endpoint must be a valid HTTP(S) URL.`); + } + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new UsageError(`--endpoint must use HTTP or HTTPS.`); + } + return url.toString(); +} + +function readNumberOption(options: Record, key: string): number | undefined { + const value = readStringOption(options, key); + if (value === undefined) { + return undefined; + } + + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 1) { + throw new UsageError(`${key} must be a positive integer.`); + } + + return parsed; +} + +function removeUndefined(values: Record): Record { + return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined)); +} + +function uniqueAgents(agents: SetupAgent[]): SetupAgent[] { + return [...new Set(agents)]; +} + +function validateToolOptions(commandName: QueryCommandName, options: Record): void { + const allowed = new Set(QUERY_COMMON_OPTIONS); + if (commandName === 'docs') { + allowed.add('--max-docsets'); + allowed.add('--max-results'); + } else if (commandName === 'resolve') { + allowed.add('--top-n'); + } else { + allowed.add('--max-results'); + } + validateOptions(options, allowed, commandName); +} + +function validateOptions( + options: Record, + allowed: ReadonlySet, + commandName: string, +): void { + for (const option of Object.keys(options)) { + if (!allowed.has(option) && !HELP_OPTIONS.has(option)) { + throw new UsageError(`${option} is not valid for the ${commandName} command.`); + } + } +} diff --git a/packages/docsearch-cli/src/constants.ts b/packages/docsearch-cli/src/constants.ts new file mode 100644 index 00000000..c8ce6358 --- /dev/null +++ b/packages/docsearch-cli/src/constants.ts @@ -0,0 +1,21 @@ +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const CLI_NAME = 'docsearch'; +export const CLI_VERSION = readPackageVersion(); +export const DEFAULT_MCP_ENDPOINT = 'https://mcp.algolia.com/1/docsearch/mcp'; +export const DOCSEARCH_MCP_SERVER_NAME = 'algolia-docsearch'; + +export const TOOL_SEARCH_DOCS = 'algolia_docsearch_search_docs'; +export const TOOL_RESOLVE_DOCSET = 'algolia_docsearch_resolve_docset'; +export const TOOL_QUERY_DOCS = 'algolia_docsearch_query_docs'; + +function readPackageVersion(): string { + const currentDirectory = dirname(fileURLToPath(import.meta.url)); + const packageJson = JSON.parse(readFileSync(join(currentDirectory, '..', 'package.json'), 'utf-8')) as { + version?: string; + }; + + return packageJson.version ?? '0.0.0'; +} diff --git a/packages/docsearch-cli/src/errors.ts b/packages/docsearch-cli/src/errors.ts new file mode 100644 index 00000000..7879e50c --- /dev/null +++ b/packages/docsearch-cli/src/errors.ts @@ -0,0 +1,6 @@ +export class UsageError extends Error { + constructor(message: string) { + super(message); + this.name = 'UsageError'; + } +} diff --git a/packages/docsearch-cli/src/index.ts b/packages/docsearch-cli/src/index.ts new file mode 100644 index 00000000..9ce5ef08 --- /dev/null +++ b/packages/docsearch-cli/src/index.ts @@ -0,0 +1,5 @@ +#!/usr/bin/env node +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { runCli } from './cli.js'; + +process.exitCode = await runCli(); diff --git a/packages/docsearch-cli/src/mcp/__tests__/client.test.ts b/packages/docsearch-cli/src/mcp/__tests__/client.test.ts new file mode 100644 index 00000000..b71c529b --- /dev/null +++ b/packages/docsearch-cli/src/mcp/__tests__/client.test.ts @@ -0,0 +1,76 @@ +// @vitest-environment node +/* eslint-disable max-classes-per-file -- SDK constructors are represented by minimal test doubles. */ + +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { callDocSearchTool } from '../client'; + +const mocks = vi.hoisted(() => ({ + callTool: vi.fn(), + close: vi.fn(), + connect: vi.fn(), + listTools: vi.fn(), +})); + +vi.mock('@modelcontextprotocol/sdk/client/index.js', () => ({ + Client: class { + callTool = mocks.callTool; + close = mocks.close; + connect = mocks.connect; + listTools = mocks.listTools; + }, +})); + +vi.mock('@modelcontextprotocol/sdk/client/streamableHttp.js', () => ({ + StreamableHTTPClientTransport: class {}, +})); + +beforeEach(() => { + mocks.connect.mockResolvedValue(undefined); + mocks.close.mockResolvedValue(undefined); + mocks.listTools.mockResolvedValue({ + tools: [{ name: 'algolia_docsearch_resolve_docset' }], + }); + mocks.callTool.mockResolvedValue({ + content: [{ text: 'result', type: 'text' }], + }); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +describe('callDocSearchTool', () => { + it('applies request timeouts and closes the client', async () => { + await callDocSearchTool({ + endpoint: 'https://example.com/mcp', + toolArguments: { query: 'Next.js' }, + toolName: 'algolia_docsearch_resolve_docset', + }); + + expect(mocks.listTools).toHaveBeenCalledWith(undefined, { timeout: 30_000 }); + expect(mocks.callTool).toHaveBeenCalledWith( + { + arguments: { query: 'Next.js' }, + name: 'algolia_docsearch_resolve_docset', + }, + undefined, + { timeout: 30_000 }, + ); + expect(mocks.close).toHaveBeenCalledOnce(); + }); + + it('closes the client when connection fails', async () => { + mocks.connect.mockRejectedValue(new Error('connection failed')); + + await expect( + callDocSearchTool({ + endpoint: 'https://example.com/mcp', + toolArguments: {}, + toolName: 'algolia_docsearch_resolve_docset', + }), + ).rejects.toThrow('connection failed'); + + expect(mocks.close).toHaveBeenCalledOnce(); + }); +}); diff --git a/packages/docsearch-cli/src/mcp/__tests__/format.test.ts b/packages/docsearch-cli/src/mcp/__tests__/format.test.ts new file mode 100644 index 00000000..d212cb46 --- /dev/null +++ b/packages/docsearch-cli/src/mcp/__tests__/format.test.ts @@ -0,0 +1,27 @@ +// @vitest-environment node + +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; +import { describe, expect, it } from 'vitest'; + +import { formatToolResult } from '../format'; + +describe('formatToolResult', () => { + it('prints text content for normal output', () => { + const result: CallToolResult = { + content: [ + { type: 'text', text: 'First result' }, + { type: 'text', text: 'Second result' }, + ], + }; + + expect(formatToolResult(result, false)).toBe('First result\n\nSecond result\n'); + }); + + it('prints the raw result for JSON output', () => { + const result: CallToolResult = { + content: [{ type: 'text', text: 'Result' }], + }; + + expect(formatToolResult(result, true)).toBe(`${JSON.stringify(result, null, 2)}\n`); + }); +}); diff --git a/packages/docsearch-cli/src/mcp/client.ts b/packages/docsearch-cli/src/mcp/client.ts new file mode 100644 index 00000000..1e3e8b0f --- /dev/null +++ b/packages/docsearch-cli/src/mcp/client.ts @@ -0,0 +1,55 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; + +import { CLI_NAME, CLI_VERSION } from '../constants.js'; + +const MCP_REQUEST_TIMEOUT_MS = 30_000; + +export interface CallDocSearchToolOptions { + endpoint: string; + toolArguments: Record; + toolName: string; +} + +export async function callDocSearchTool({ + endpoint, + toolArguments, + toolName, +}: CallDocSearchToolOptions): Promise { + const client = new Client({ + name: CLI_NAME, + version: CLI_VERSION, + }); + const transport = new StreamableHTTPClientTransport(new URL(endpoint)); + + try { + await client.connect(transport); + + const { tools } = await client.listTools(undefined, { + timeout: MCP_REQUEST_TIMEOUT_MS, + }); + if (!tools.some((tool) => tool.name === toolName)) { + throw new Error(`DocSearch MCP tool "${toolName}" is not available at ${endpoint}.`); + } + + const result = await client.callTool( + { + name: toolName, + arguments: toolArguments, + }, + undefined, + { + timeout: MCP_REQUEST_TIMEOUT_MS, + }, + ); + if (!('content' in result)) { + throw new Error(`DocSearch MCP tool "${toolName}" returned an unsupported result shape.`); + } + + return result as CallToolResult; + } finally { + await client.close(); + } +} diff --git a/packages/docsearch-cli/src/mcp/format.ts b/packages/docsearch-cli/src/mcp/format.ts new file mode 100644 index 00000000..fc7ff59b --- /dev/null +++ b/packages/docsearch-cli/src/mcp/format.ts @@ -0,0 +1,29 @@ +import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; + +type TextContent = { + text: string; + type: 'text'; +}; + +export function formatToolResult(result: CallToolResult, json: boolean): string { + if (json) { + return `${JSON.stringify(result, null, 2)}\n`; + } + + const text = getToolResultText(result); + if (text) { + return `${text}\n`; + } + + return `${JSON.stringify(result, null, 2)}\n`; +} + +export function getToolResultText(result: CallToolResult): string { + const text = result.content + .filter((part): part is TextContent => part.type === 'text') + .map((part) => part.text.trim()) + .filter(Boolean) + .join('\n\n'); + + return text; +} diff --git a/packages/docsearch-cli/src/setup/__tests__/prompt.test.ts b/packages/docsearch-cli/src/setup/__tests__/prompt.test.ts new file mode 100644 index 00000000..aa7b2e80 --- /dev/null +++ b/packages/docsearch-cli/src/setup/__tests__/prompt.test.ts @@ -0,0 +1,61 @@ +// @vitest-environment node + +import { describe, expect, it } from 'vitest'; + +import type { AgentChoice } from '../prompt'; +import { parseAgentSelection, parseScopeSelection, PromptCancelledError } from '../prompt'; + +const CHOICES: AgentChoice[] = [ + { detected: true, displayName: 'Cursor', name: 'cursor' }, + { detected: false, displayName: 'Claude Code', name: 'claude' }, + { detected: false, displayName: 'Codex', name: 'codex' }, +]; + +describe('parseAgentSelection', () => { + it('defaults blank input to detected agents only', () => { + expect(parseAgentSelection('', CHOICES)).toEqual(['cursor']); + }); + + it('selects every agent for "all"', () => { + expect(parseAgentSelection('all', CHOICES)).toEqual(['cursor', 'claude', 'codex']); + }); + + it('selects nothing for "none"', () => { + expect(parseAgentSelection('none', CHOICES)).toEqual([]); + }); + + it('parses a mix of numbers and names without duplicates', () => { + expect(parseAgentSelection('1, claude, 2', CHOICES)).toEqual(['cursor', 'claude']); + }); + + it('rejects unknown tokens', () => { + expect(() => parseAgentSelection('9', CHOICES)).toThrow(/Unknown agent/); + expect(() => parseAgentSelection('vim', CHOICES)).toThrow(/Unknown agent/); + }); +}); + +describe('parseScopeSelection', () => { + it('defaults blank input to project', () => { + expect(parseScopeSelection('')).toBe('project'); + }); + + it('accepts numbers, letters, and names', () => { + expect(parseScopeSelection('1')).toBe('project'); + expect(parseScopeSelection('p')).toBe('project'); + expect(parseScopeSelection('Project')).toBe('project'); + expect(parseScopeSelection('2')).toBe('global'); + expect(parseScopeSelection('g')).toBe('global'); + expect(parseScopeSelection('GLOBAL')).toBe('global'); + }); + + it('rejects unknown scope input', () => { + expect(() => parseScopeSelection('workspace')).toThrow(/Unknown scope/); + }); +}); + +describe('PromptCancelledError', () => { + it('preserves the intended process exit code', () => { + expect(new PromptCancelledError().exitCode).toBe(0); + expect(new PromptCancelledError(130).exitCode).toBe(130); + }); +}); diff --git a/packages/docsearch-cli/src/setup/__tests__/setup.test.ts b/packages/docsearch-cli/src/setup/__tests__/setup.test.ts new file mode 100644 index 00000000..b5735a2e --- /dev/null +++ b/packages/docsearch-cli/src/setup/__tests__/setup.test.ts @@ -0,0 +1,303 @@ +// @vitest-environment node + +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, describe, expect, it } from 'vitest'; + +import { DOCSEARCH_MCP_SERVER_NAME } from '../../constants'; +import type { SetupAgent, SetupScope } from '../agents'; +import { findProjectRoot, setupDocSearch, type SetupOptions } from '../setup'; + +const tempDirs: string[] = []; + +async function createTempDir(): Promise { + const path = await mkdtemp(join(tmpdir(), 'docsearch-cli-')); + tempDirs.push(path); + return path; +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((path) => rm(path, { force: true, recursive: true }))); +}); + +describe('setupDocSearch', () => { + it('installs Cursor MCP config, rule, and skill in project scope', async () => { + const cwd = await createTempDir(); + const homeDir = await createTempDir(); + + const results = await setupDocSearch({ + agents: ['cursor'], + cwd, + endpoint: 'https://example.com/mcp', + env: {}, + homeDir, + scope: 'project', + }); + + expect(results[0]).toMatchObject({ + agent: 'Cursor', + mcpStatus: 'configured', + ruleStatus: 'installed', + skillStatus: 'installed', + }); + + const mcp = JSON.parse(await readFile(join(cwd, '.cursor', 'mcp.json'), 'utf-8')) as { + mcpServers: Record; + }; + expect(mcp.mcpServers[DOCSEARCH_MCP_SERVER_NAME]).toEqual({ + url: 'https://example.com/mcp', + }); + + const rule = await readFile(join(cwd, '.cursor', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.mdc`), 'utf-8'); + expect(rule).toContain('alwaysApply: true'); + expect(rule).toContain('algolia_docsearch_search_docs'); + + const skill = await readFile(join(cwd, '.cursor', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), 'utf-8'); + expect(skill).toMatch(/^---\nname: algolia-docsearch\ndescription:/); + expect(skill).toContain('DocSearch MCP'); + }); + + it('updates an existing Codex TOML config and shared skill idempotently', async () => { + const cwd = await createTempDir(); + const homeDir = await createTempDir(); + const options: SetupOptions = { + agents: ['codex'], + cwd, + endpoint: 'https://example.com/mcp', + env: {}, + homeDir, + scope: 'project' as const, + }; + + await setupDocSearch(options); + const results = await setupDocSearch(options); + + expect(results[0]).toMatchObject({ + agent: 'Codex', + mcpStatus: 'updated', + ruleStatus: 'updated', + skillStatus: 'preserved', + }); + + const config = await readFile(join(cwd, '.codex', 'config.toml'), 'utf-8'); + expect(config.match(/\[mcp_servers\.algolia-docsearch\]/g)).toHaveLength(1); + expect(config).toContain('url = "https://example.com/mcp"'); + + const agentsFile = await readFile(join(cwd, 'AGENTS.md'), 'utf-8'); + expect(agentsFile.match(/algolia-docsearch:start/g)).toHaveLength(1); + + const skill = await readFile(join(cwd, '.agents', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), 'utf-8'); + expect(skill).toContain('Use this skill'); + }); + + it('preserves user edits in dedicated rule and skill files', async () => { + const cwd = await createTempDir(); + const homeDir = await createTempDir(); + const options: SetupOptions = { + agents: ['cursor'], + cwd, + endpoint: 'https://example.com/mcp', + env: {}, + homeDir, + scope: 'project', + }; + + await setupDocSearch(options); + const rulePath = join(cwd, '.cursor', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.mdc`); + const skillPath = join(cwd, '.cursor', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'); + await writeFile(rulePath, `${await readFile(rulePath, 'utf-8')}\nUser rule customization.\n`); + await writeFile(skillPath, `${await readFile(skillPath, 'utf-8')}\nUser skill customization.\n`); + + const [result] = await setupDocSearch(options); + + expect(result).toMatchObject({ + ruleStatus: 'preserved', + skillStatus: 'preserved', + }); + expect(await readFile(rulePath, 'utf-8')).toContain('User rule customization.'); + expect(await readFile(skillPath, 'utf-8')).toContain('User skill customization.'); + }); + + it('updates an existing OpenCode JSONC config in place', async () => { + const cwd = await createTempDir(); + const homeDir = await createTempDir(); + const configPath = join(cwd, 'opencode.jsonc'); + await writeFile( + configPath, + `{ + // Existing OpenCode preferences. + "theme": "system", +} +`, + ); + + const [result] = await setupDocSearch({ + agents: ['opencode'], + cwd, + endpoint: 'https://example.com/mcp', + env: {}, + homeDir, + scope: 'project', + }); + + expect(result.mcpPath).toBe(configPath); + expect(await readFile(configPath, 'utf-8')).toContain('// Existing OpenCode preferences.'); + expect(await readFile(configPath, 'utf-8')).toContain('"algolia-docsearch"'); + await expect(readFile(join(cwd, 'opencode.json'), 'utf-8')).rejects.toMatchObject({ code: 'ENOENT' }); + }); +}); + +interface AgentPathCase { + agent: SetupAgent; + mcp: string; + rule: string; + scope: SetupScope; + skill: string; +} + +const AGENT_PATH_CASES: AgentPathCase[] = [ + { + agent: 'claude', + mcp: '.mcp.json', + rule: join('.claude', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.md`), + scope: 'project', + skill: join('.claude', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), + }, + { + agent: 'cursor', + mcp: join('.cursor', 'mcp.json'), + rule: join('.cursor', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.mdc`), + scope: 'project', + skill: join('.cursor', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), + }, + { + agent: 'codex', + mcp: join('.codex', 'config.toml'), + rule: 'AGENTS.md', + scope: 'project', + skill: join('.agents', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), + }, + { + agent: 'opencode', + mcp: 'opencode.json', + rule: 'AGENTS.md', + scope: 'project', + skill: join('.agents', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), + }, + { + agent: 'gemini', + mcp: join('.gemini', 'settings.json'), + rule: 'GEMINI.md', + scope: 'project', + skill: join('.gemini', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), + }, + { + agent: 'claude', + mcp: '.claude.json', + rule: join('.claude', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.md`), + scope: 'global', + skill: join('.claude', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), + }, + { + agent: 'cursor', + mcp: join('.cursor', 'mcp.json'), + rule: join('.cursor', 'rules', `${DOCSEARCH_MCP_SERVER_NAME}.mdc`), + scope: 'global', + skill: join('.cursor', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), + }, + { + agent: 'codex', + mcp: join('.codex', 'config.toml'), + rule: join('.codex', 'AGENTS.md'), + scope: 'global', + skill: join('.agents', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), + }, + { + agent: 'opencode', + mcp: join('.config', 'opencode', 'opencode.json'), + rule: join('.config', 'opencode', 'AGENTS.md'), + scope: 'global', + skill: join('.agents', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), + }, + { + agent: 'gemini', + mcp: join('.gemini', 'settings.json'), + rule: join('.gemini', 'GEMINI.md'), + scope: 'global', + skill: join('.gemini', 'skills', DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'), + }, +]; + +describe.each(AGENT_PATH_CASES)('$agent $scope paths', ({ agent, mcp, rule, scope, skill }) => { + it('writes every artifact to the expected location', async () => { + const cwd = await createTempDir(); + const homeDir = await createTempDir(); + const [result] = await setupDocSearch({ + agents: [agent], + cwd, + endpoint: 'https://example.com/mcp', + env: {}, + homeDir, + scope, + }); + const base = scope === 'project' ? cwd : homeDir; + + expect(result.mcpPath).toBe(join(base, mcp)); + expect(result.rulePath).toBe(join(base, rule)); + expect(result.skillPath).toBe(join(base, skill)); + }); +}); + +describe('findProjectRoot', () => { + it('walks up to the nearest VCS root', async () => { + const home = await createTempDir(); + const root = join(home, 'repo'); + const nested = join(root, 'packages', 'app', 'src'); + await mkdir(join(root, '.git'), { recursive: true }); + await mkdir(nested, { recursive: true }); + + expect(await findProjectRoot(nested, home)).toBe(root); + }); + + it('prefers a repo-root marker over a nested package.json when there is no VCS', async () => { + const home = await createTempDir(); + const root = join(home, 'repo'); + const pkg = join(root, 'packages', 'app'); + await mkdir(pkg, { recursive: true }); + await writeFile(join(root, 'pnpm-workspace.yaml'), 'packages:\n - packages/*\n'); + await writeFile(join(root, 'package.json'), '{}'); + await writeFile(join(pkg, 'package.json'), '{}'); + + expect(await findProjectRoot(pkg, home)).toBe(root); + }); + + it('falls back to the start directory when nothing is found', async () => { + const home = await createTempDir(); + const start = join(home, 'loose', 'dir'); + await mkdir(start, { recursive: true }); + + expect(await findProjectRoot(start, home)).toBe(start); + }); + + it('does not cross the home directory', async () => { + const home = await createTempDir(); + const start = join(home, 'project'); + await mkdir(start, { recursive: true }); + // A .git in home must not be picked up. + await mkdir(join(home, '.git'), { recursive: true }); + + expect(await findProjectRoot(start, home)).toBe(start); + }); + + it('still finds a VCS root when the project is outside the home directory', async () => { + const home = await createTempDir(); + const root = await createTempDir(); + const nested = join(root, 'packages', 'app'); + await mkdir(join(root, '.git'), { recursive: true }); + await mkdir(nested, { recursive: true }); + + expect(await findProjectRoot(nested, home)).toBe(root); + }); +}); diff --git a/packages/docsearch-cli/src/setup/__tests__/writers.test.ts b/packages/docsearch-cli/src/setup/__tests__/writers.test.ts new file mode 100644 index 00000000..48581e95 --- /dev/null +++ b/packages/docsearch-cli/src/setup/__tests__/writers.test.ts @@ -0,0 +1,154 @@ +// @vitest-environment node + +import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, describe, expect, it } from 'vitest'; + +import { appendTomlServer, upsertJsonServerEntry } from '../writers'; + +const tempDirs: string[] = []; + +async function createTempDir(): Promise { + const path = await mkdtemp(join(tmpdir(), 'docsearch-writers-')); + tempDirs.push(path); + return path; +} + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((path) => rm(path, { force: true, recursive: true }))); +}); + +describe('upsertJsonServerEntry', () => { + it('preserves JSONC comments, trailing commas, and unrelated configuration', async () => { + const cwd = await createTempDir(); + const path = join(cwd, 'opencode.jsonc'); + await writeFile( + path, + `{ + // Keep this comment. + "theme": "system", + "mcp": { + "existing": { + "type": "remote", + "url": "https://example.com/mcp", + }, + }, +} +`, + ); + + await upsertJsonServerEntry(path, 'mcp', { + enabled: true, + type: 'remote', + url: 'https://mcp.algolia.com/1/docsearch/mcp', + }); + + const updated = await readFile(path, 'utf-8'); + expect(updated).toContain('// Keep this comment.'); + expect(updated).toContain('"theme": "system"'); + expect(updated).toContain('"existing"'); + expect(updated).toContain('"algolia-docsearch"'); + expect(updated).toContain('"url": "https://mcp.algolia.com/1/docsearch/mcp"'); + }); + + it('preserves unknown fields on an existing DocSearch entry', async () => { + const cwd = await createTempDir(); + const path = join(cwd, 'mcp.json'); + await writeFile( + path, + `${JSON.stringify({ + mcpServers: { + 'algolia-docsearch': { + headers: { 'X-Custom': 'value' }, + timeout: 15_000, + url: 'https://old.example.com/mcp', + }, + }, + })}\n`, + ); + + const result = await upsertJsonServerEntry(path, 'mcpServers', { + url: 'https://mcp.algolia.com/1/docsearch/mcp', + }); + const updated = JSON.parse(await readFile(path, 'utf-8')) as { + mcpServers: Record>; + }; + + expect(result.alreadyExists).toBe(true); + expect(updated.mcpServers['algolia-docsearch']).toEqual({ + headers: { 'X-Custom': 'value' }, + timeout: 15_000, + url: 'https://mcp.algolia.com/1/docsearch/mcp', + }); + }); + + it('rejects malformed JSONC with the file path in the error', async () => { + const cwd = await createTempDir(); + const path = join(cwd, 'broken.jsonc'); + await writeFile(path, '{ "mcp": {'); + + await expect(upsertJsonServerEntry(path, 'mcp', { url: 'https://example.com' })).rejects.toThrow(path); + }); + + it('rejects an existing configuration section with the wrong shape', async () => { + const cwd = await createTempDir(); + const path = join(cwd, 'mcp.json'); + await writeFile(path, '{ "mcpServers": [] }\n'); + + await expect(upsertJsonServerEntry(path, 'mcpServers', { url: 'https://example.com' })).rejects.toThrow( + '"mcpServers" must be an object', + ); + }); +}); + +describe('appendTomlServer', () => { + it('updates known values while preserving custom keys and nested tables', async () => { + const cwd = await createTempDir(); + const path = join(cwd, 'config.toml'); + await writeFile( + path, + `# [mcp_servers.algolia-docsearch] +[mcp_servers.algolia-docsearch] +url = "https://old.example.com/mcp" # Keep this comment. +enabled = false + +[mcp_servers.algolia-docsearch.http_headers] +X-Custom = "value" + +[mcp_servers.other] +url = "https://other.example.com/mcp" +`, + ); + + const result = await appendTomlServer(path, { + url: 'https://mcp.algolia.com/1/docsearch/mcp', + }); + const updated = await readFile(path, 'utf-8'); + + expect(result.alreadyExists).toBe(true); + expect(updated).toContain('# [mcp_servers.algolia-docsearch]'); + expect(updated).toContain('url = "https://mcp.algolia.com/1/docsearch/mcp" # Keep this comment.'); + expect(updated).toContain('enabled = false'); + expect(updated).toContain('[mcp_servers.algolia-docsearch.http_headers]'); + expect(updated).toContain('X-Custom = "value"'); + expect(updated).toContain('[mcp_servers.other]'); + }); + + it('refuses to edit duplicate DocSearch sections', async () => { + const cwd = await createTempDir(); + const path = join(cwd, 'config.toml'); + await writeFile( + path, + `[mcp_servers.algolia-docsearch] +url = "https://one.example.com" + +[mcp_servers.algolia-docsearch] +url = "https://two.example.com" +`, + ); + + await expect(appendTomlServer(path, { url: 'https://example.com' })).rejects.toThrow(/duplicate/i); + }); +}); diff --git a/packages/docsearch-cli/src/setup/agents.ts b/packages/docsearch-cli/src/setup/agents.ts new file mode 100644 index 00000000..02ac9d3d --- /dev/null +++ b/packages/docsearch-cli/src/setup/agents.ts @@ -0,0 +1,181 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { join } from 'node:path'; + +import { DOCSEARCH_MCP_SERVER_NAME } from '../constants.js'; + +export type SetupAgent = 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode'; +export type SetupScope = 'global' | 'project'; + +export interface PathContext { + cwd: string; + env: NodeJS.ProcessEnv; + homeDir: string; +} + +export interface AgentConfig { + detect: { + globalPaths: string[]; + projectPaths: string[]; + }; + displayName: string; + mcp: { + configKey: string; + globalPaths: string[]; + projectPaths: string[]; + buildEntry: (endpoint: string) => Record; + }; + name: SetupAgent; + rule: + | { + dir: (scope: SetupScope) => string; + filename: string; + kind: 'file'; + } + | { + file: (scope: SetupScope) => string; + kind: 'append'; + }; + skill: { + dir: (scope: SetupScope) => string; + }; +} + +export function getAgent(name: SetupAgent, context: PathContext): AgentConfig { + return agents(context)[name]; +} + +export function getAllAgents(context: PathContext): Record { + return agents(context); +} + +function agents(context: PathContext): Record { + const claudeConfigDirectory = context.env.CLAUDE_CONFIG_DIR ?? join(context.homeDir, '.claude'); + const claudeGlobalMcpPath = context.env.CLAUDE_CONFIG_DIR + ? join(claudeConfigDirectory, '.claude.json') + : join(context.homeDir, '.claude.json'); + + return { + claude: { + name: 'claude', + displayName: 'Claude Code', + mcp: { + projectPaths: [join(context.cwd, '.mcp.json')], + globalPaths: [claudeGlobalMcpPath], + configKey: 'mcpServers', + buildEntry: (endpoint) => ({ type: 'http', url: endpoint }), + }, + rule: { + kind: 'file', + dir: (scope) => + scope === 'global' ? join(claudeConfigDirectory, 'rules') : join(context.cwd, '.claude', 'rules'), + filename: `${DOCSEARCH_MCP_SERVER_NAME}.md`, + }, + skill: { + dir: (scope) => + scope === 'global' ? join(claudeConfigDirectory, 'skills') : join(context.cwd, '.claude', 'skills'), + }, + detect: { + projectPaths: [join(context.cwd, '.mcp.json'), join(context.cwd, '.claude')], + globalPaths: [claudeConfigDirectory, claudeGlobalMcpPath], + }, + }, + cursor: { + name: 'cursor', + displayName: 'Cursor', + mcp: { + projectPaths: [join(context.cwd, '.cursor', 'mcp.json')], + globalPaths: [join(context.homeDir, '.cursor', 'mcp.json')], + configKey: 'mcpServers', + buildEntry: (endpoint) => ({ url: endpoint }), + }, + rule: { + kind: 'file', + dir: (scope) => + scope === 'global' ? join(context.homeDir, '.cursor', 'rules') : join(context.cwd, '.cursor', 'rules'), + filename: `${DOCSEARCH_MCP_SERVER_NAME}.mdc`, + }, + skill: { + dir: (scope) => + scope === 'global' ? join(context.homeDir, '.cursor', 'skills') : join(context.cwd, '.cursor', 'skills'), + }, + detect: { + projectPaths: [join(context.cwd, '.cursor')], + globalPaths: [join(context.homeDir, '.cursor')], + }, + }, + codex: { + name: 'codex', + displayName: 'Codex', + mcp: { + projectPaths: [join(context.cwd, '.codex', 'config.toml')], + globalPaths: [join(context.homeDir, '.codex', 'config.toml')], + configKey: 'mcp_servers', + buildEntry: (endpoint) => ({ url: endpoint }), + }, + rule: { + kind: 'append', + file: (scope) => + scope === 'global' ? join(context.homeDir, '.codex', 'AGENTS.md') : join(context.cwd, 'AGENTS.md'), + }, + skill: { + dir: (scope) => + scope === 'global' ? join(context.homeDir, '.agents', 'skills') : join(context.cwd, '.agents', 'skills'), + }, + detect: { + projectPaths: [join(context.cwd, '.codex')], + globalPaths: [join(context.homeDir, '.codex')], + }, + }, + opencode: { + name: 'opencode', + displayName: 'OpenCode', + mcp: { + projectPaths: [join(context.cwd, 'opencode.json'), join(context.cwd, 'opencode.jsonc')], + globalPaths: [ + join(context.homeDir, '.config', 'opencode', 'opencode.json'), + join(context.homeDir, '.config', 'opencode', 'opencode.jsonc'), + ], + configKey: 'mcp', + buildEntry: (endpoint) => ({ type: 'remote', url: endpoint, enabled: true }), + }, + rule: { + kind: 'append', + file: (scope) => + scope === 'global' + ? join(context.homeDir, '.config', 'opencode', 'AGENTS.md') + : join(context.cwd, 'AGENTS.md'), + }, + skill: { + dir: (scope) => + scope === 'global' ? join(context.homeDir, '.agents', 'skills') : join(context.cwd, '.agents', 'skills'), + }, + detect: { + projectPaths: [join(context.cwd, 'opencode.json'), join(context.cwd, 'opencode.jsonc')], + globalPaths: [join(context.homeDir, '.config', 'opencode')], + }, + }, + gemini: { + name: 'gemini', + displayName: 'Gemini CLI', + mcp: { + projectPaths: [join(context.cwd, '.gemini', 'settings.json')], + globalPaths: [join(context.homeDir, '.gemini', 'settings.json')], + configKey: 'mcpServers', + buildEntry: (endpoint) => ({ httpUrl: endpoint }), + }, + rule: { + kind: 'append', + file: (scope) => + scope === 'global' ? join(context.homeDir, '.gemini', 'GEMINI.md') : join(context.cwd, 'GEMINI.md'), + }, + skill: { + dir: (scope) => + scope === 'global' ? join(context.homeDir, '.gemini', 'skills') : join(context.cwd, '.gemini', 'skills'), + }, + detect: { + projectPaths: [join(context.cwd, '.gemini')], + globalPaths: [join(context.homeDir, '.gemini')], + }, + }, + }; +} diff --git a/packages/docsearch-cli/src/setup/prompt.ts b/packages/docsearch-cli/src/setup/prompt.ts new file mode 100644 index 00000000..969a330e --- /dev/null +++ b/packages/docsearch-cli/src/setup/prompt.ts @@ -0,0 +1,326 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import readline from 'node:readline'; + +import { UsageError } from '../errors.js'; +import { color, isInteractive, symbols } from '../ui/theme.js'; + +import type { SetupAgent, SetupScope } from './agents.js'; + +export class PromptCancelledError extends Error { + constructor(readonly exitCode: number = 0) { + super('Setup cancelled.'); + this.name = 'PromptCancelledError'; + } +} + +export interface AgentChoice { + detected: boolean; + displayName: string; + name: SetupAgent; +} + +interface ScopeChoice { + hint: string; + label: string; + value: SetupScope; +} + +const SCOPE_CHOICES: ScopeChoice[] = [ + { hint: 'the detected repository root', label: 'Project', value: 'project' }, + { hint: 'every project (~/.cursor, ~/.codex, …)', label: 'Global', value: 'global' }, +]; + +export function parseScopeSelection(raw: string): SetupScope { + const normalized = raw.trim().toLowerCase(); + + if (normalized === '' || normalized === '1' || normalized === 'p' || normalized === 'project') { + return 'project'; + } + + if (normalized === '2' || normalized === 'g' || normalized === 'global') { + return 'global'; + } + + throw new UsageError(`Unknown scope: "${raw.trim()}". Choose "project" or "global".`); +} + +export function promptScope(): Promise { + if (isInteractive()) { + return promptScopeInteractive(); + } + + return promptScopeByLine(); +} + +function promptScopeInteractive(): Promise { + const input = process.stdin; + const output = process.stderr; + const wasRaw = input.isRaw === true; + let cursor = 0; + let renderedLines = 0; + + try { + input.setRawMode(true); + } catch { + return promptScopeByLine(); + } + readline.emitKeypressEvents(input); + + return new Promise((resolve, reject) => { + function render(): void { + if (renderedLines > 0) { + output.write(`\u001B[${renderedLines}A`); + } + + const lines = [ + `${color.magenta(symbols.diamond)} ${color.bold('Where should DocSearch be installed?')} ${color.dim('(↑↓ move · enter confirm)')}`, + ...SCOPE_CHOICES.map((choice, index) => { + const active = index === cursor; + const radio = active ? color.green(symbols.checkboxOn) : color.dim(symbols.checkboxOff); + const pointer = active ? color.cyan(symbols.pointer) : ' '; + const label = active ? color.cyan(choice.label) : choice.label; + return `${pointer} ${radio} ${label} ${color.dim(`— ${choice.hint}`)}`; + }), + ]; + + output.write(`${lines.map((line) => `\u001B[K${line}`).join('\n')}\n`); + renderedLines = lines.length; + } + + function cleanup(): void { + input.setRawMode(wasRaw); + input.removeListener('keypress', onKeypress); + output.write('\u001B[?25h'); + input.pause(); + } + + function onKeypress(_input: string, key: readline.Key | undefined): void { + if (!key) { + return; + } + + if (key.name === 'up' || key.name === 'k') { + cursor = (cursor - 1 + SCOPE_CHOICES.length) % SCOPE_CHOICES.length; + render(); + } else if (key.name === 'down' || key.name === 'j') { + cursor = (cursor + 1) % SCOPE_CHOICES.length; + render(); + } else if (key.name === 'return') { + cleanup(); + resolve(SCOPE_CHOICES[cursor].value); + } else if (key.name === 'escape' || (key.ctrl === true && key.name === 'c')) { + cleanup(); + output.write('\n'); + reject(new PromptCancelledError(key.ctrl === true ? 130 : 0)); + } + } + + output.write('\u001B[?25l'); + input.on('keypress', onKeypress); + input.resume(); + render(); + }); +} + +async function promptScopeByLine(): Promise { + const output = process.stderr; + + output.write(`${color.magenta(symbols.diamond)} ${color.bold('Where should DocSearch be installed?')}\n`); + for (const [index, choice] of SCOPE_CHOICES.entries()) { + output.write(` ${color.cyan(String(index + 1))}. ${choice.label} ${color.dim(`— ${choice.hint}`)}\n`); + } + output.write(`${color.dim('Enter "project" or "global" (blank for project):')} `); + + return parseScopeSelection(await readSingleLine()); +} + +export function parseAgentSelection(raw: string, choices: AgentChoice[]): SetupAgent[] { + const normalized = raw.trim().toLowerCase(); + + if (normalized === '') { + return choices.filter((choice) => choice.detected).map((choice) => choice.name); + } + + if (normalized === 'all' || normalized === '*') { + return choices.map((choice) => choice.name); + } + + if (normalized === 'none') { + return []; + } + + const selected = new Set(); + + for (const token of normalized.split(/[\s,]+/).filter(Boolean)) { + const index = Number(token); + if (Number.isInteger(index) && index >= 1 && index <= choices.length) { + selected.add(choices[index - 1].name); + } else { + const match = choices.find((choice) => choice.name === token); + if (!match) { + throw new UsageError( + `Unknown agent: "${token}". Choose a number or one of: ${choices.map((c) => c.name).join(', ')}.`, + ); + } + selected.add(match.name); + } + } + + return [...selected]; +} + +export function promptAgents(choices: AgentChoice[]): Promise { + if (isInteractive()) { + return promptAgentsInteractive(choices); + } + + return promptAgentsByLine(choices); +} + +function promptAgentsInteractive(choices: AgentChoice[]): Promise { + const input = process.stdin; + const output = process.stderr; + const wasRaw = input.isRaw === true; + const selected = choices.map((choice) => choice.detected); + let cursor = 0; + let renderedLines = 0; + + try { + input.setRawMode(true); + } catch { + return promptAgentsByLine(choices); + } + readline.emitKeypressEvents(input); + + return new Promise((resolve, reject) => { + function render(): void { + if (renderedLines > 0) { + output.write(`\u001B[${renderedLines}A`); + } + + const lines = [ + `${color.magenta(symbols.diamond)} ${color.bold('Select agents to configure')} ${color.dim('(↑↓ move · space toggle · a all · enter confirm)')}`, + ...choices.map((choice, index) => { + const box = selected[index] ? color.green(symbols.checkboxOn) : color.dim(symbols.checkboxOff); + const pointer = index === cursor ? color.cyan(symbols.pointer) : ' '; + const name = index === cursor ? color.cyan(choice.displayName) : choice.displayName; + const detected = choice.detected ? color.dim(' (detected)') : ''; + return `${pointer} ${box} ${name}${detected}`; + }), + ]; + + output.write(`${lines.map((line) => `\u001B[K${line}`).join('\n')}\n`); + renderedLines = lines.length; + } + + function cleanup(): void { + input.setRawMode(wasRaw); + input.removeListener('keypress', onKeypress); + output.write('\u001B[?25h'); + input.pause(); + } + + function onKeypress(_input: string, key: readline.Key | undefined): void { + if (!key) { + return; + } + + if (key.name === 'up' || key.name === 'k') { + cursor = (cursor - 1 + choices.length) % choices.length; + render(); + } else if (key.name === 'down' || key.name === 'j') { + cursor = (cursor + 1) % choices.length; + render(); + } else if (key.name === 'space') { + selected[cursor] = !selected[cursor]; + render(); + } else if (key.name === 'a') { + const allSelected = selected.every(Boolean); + selected.fill(!allSelected); + render(); + } else if (key.name === 'return') { + cleanup(); + resolve(choices.filter((_, index) => selected[index]).map((choice) => choice.name)); + } else if (key.name === 'escape' || (key.ctrl === true && key.name === 'c')) { + cleanup(); + output.write('\n'); + reject(new PromptCancelledError(key.ctrl === true ? 130 : 0)); + } + } + + output.write('\u001B[?25l'); + input.on('keypress', onKeypress); + input.resume(); + render(); + }); +} + +async function promptAgentsByLine(choices: AgentChoice[]): Promise { + const output = process.stderr; + + output.write(`${color.magenta(symbols.diamond)} ${color.bold('Select agents to configure')}\n`); + for (const [index, choice] of choices.entries()) { + const detected = choice.detected ? color.dim(' (detected)') : ''; + output.write(` ${color.cyan(String(index + 1))}. ${choice.displayName}${detected}\n`); + } + output.write(`${color.dim('Enter numbers/names (comma-separated), "all", or blank for detected:')} `); + + const raw = await readSingleLine(); + return parseAgentSelection(raw, choices); +} + +// A single persistent line reader shared across prompts. Reading one line at a +// time with fresh listeners loses buffered input between prompts (and can hang +// at EOF), so we buffer stdin once and hand out lines on demand. +let stdinBuffer = ''; +let stdinInitialized = false; +let stdinEnded = false; +const lineResolvers: Array<(line: string) => void> = []; + +function pumpLines(): void { + while (lineResolvers.length > 0) { + const newline = stdinBuffer.indexOf('\n'); + if (newline >= 0) { + const line = stdinBuffer.slice(0, newline).replace(/\r$/, ''); + stdinBuffer = stdinBuffer.slice(newline + 1); + lineResolvers.shift()?.(line); + } else if (stdinEnded) { + const line = stdinBuffer; + stdinBuffer = ''; + lineResolvers.shift()?.(line); + } else { + break; + } + } + + if (lineResolvers.length === 0 && !stdinEnded) { + process.stdin.pause(); + } +} + +function initStdin(): void { + if (stdinInitialized) { + return; + } + + stdinInitialized = true; + process.stdin.setEncoding('utf8'); + process.stdin.on('data', (chunk: string) => { + stdinBuffer += chunk; + pumpLines(); + }); + process.stdin.on('end', () => { + stdinEnded = true; + pumpLines(); + }); +} + +function readSingleLine(): Promise { + initStdin(); + + return new Promise((resolve) => { + lineResolvers.push(resolve); + process.stdin.resume(); + pumpLines(); + }); +} diff --git a/packages/docsearch-cli/src/setup/setup.ts b/packages/docsearch-cli/src/setup/setup.ts new file mode 100644 index 00000000..6eda832a --- /dev/null +++ b/packages/docsearch-cli/src/setup/setup.ts @@ -0,0 +1,220 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { homedir } from 'node:os'; +import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; + +import { DOCSEARCH_MCP_SERVER_NAME } from '../constants.js'; + +import { + getAgent, + getAllAgents, + type AgentConfig, + type PathContext, + type SetupAgent, + type SetupScope, +} from './agents.js'; +import type { AgentChoice } from './prompt.js'; +import { getRuleContent, getSkillContent } from './templates.js'; +import { + appendRuleSection, + appendTomlServer, + pathExists, + resolveConfigPath, + upsertJsonServerEntry, + writeRuleFile, + writeSkill, +} from './writers.js'; + +export interface SetupOptions { + agents: SetupAgent[]; + cwd?: string; + endpoint: string; + env?: NodeJS.ProcessEnv; + homeDir?: string; + scope: SetupScope; +} + +export interface SetupResult { + agent: string; + mcpPath: string; + mcpStatus: string; + rulePath: string; + ruleStatus: string; + skillPath: string; + skillStatus: string; +} + +export function createPathContext(overrides: Partial = {}): PathContext { + return { + cwd: overrides.cwd ?? process.cwd(), + env: overrides.env ?? process.env, + homeDir: overrides.homeDir ?? homedir(), + }; +} + +// Version control roots are the canonical project root: they match how Cursor, +// Claude Code, Codex, etc. resolve project config (from the repo/workspace top). +const VCS_MARKERS = ['.git', '.hg', '.svn']; + +// Markers that only exist at the true repo root, so they beat a nested +// package.json when locating a monorepo root without VCS metadata. +const ROOT_MARKERS = [ + 'pnpm-workspace.yaml', + 'bun.lock', + 'bun.lockb', + 'pnpm-lock.yaml', + 'package-lock.json', + 'yarn.lock', + 'go.work', + 'deno.json', + 'deno.jsonc', + 'turbo.json', + 'nx.json', + 'lerna.json', +]; + +// Walks up from `startDir` (never crossing `homeDir` or the filesystem root) +// and returns the best project root. Falls back to `startDir` when nothing is +// found, so a directory-less invocation still installs into the current folder. +export async function findProjectRoot(startDir: string, homeDir: string): Promise { + const start = resolve(startDir); + const home = resolve(homeDir); + const stopBeforeHome = start !== home && isPathWithin(start, home); + let vcsRoot: string | undefined; + let rootMarkerDir: string | undefined; + let topmostPackage: string | undefined; + + let dir = start; + let reachedFileSystemRoot = false; + while (!reachedFileSystemRoot) { + if (stopBeforeHome && dir === home) { + break; + } + if (vcsRoot === undefined && (await hasAnyMarker(dir, VCS_MARKERS))) { + vcsRoot = dir; + } + if (rootMarkerDir === undefined && (await hasAnyMarker(dir, ROOT_MARKERS))) { + rootMarkerDir = dir; + } + if (await pathExists(join(dir, 'package.json'))) { + topmostPackage = dir; + } + + const parent = dirname(dir); + reachedFileSystemRoot = parent === dir; + dir = parent; + } + + return vcsRoot ?? rootMarkerDir ?? topmostPackage ?? start; +} + +async function hasAnyMarker(dir: string, markers: string[]): Promise { + const found = await Promise.all(markers.map((marker) => pathExists(join(dir, marker)))); + return found.some(Boolean); +} + +function isPathWithin(path: string, parent: string): boolean { + const pathFromParent = relative(parent, path); + return ( + pathFromParent !== '..' && + !pathFromParent.startsWith(`..${process.platform === 'win32' ? '\\' : '/'}`) && + !isAbsolute(pathFromParent) + ); +} + +export async function setupDocSearch(options: SetupOptions): Promise { + const context = createPathContext({ + cwd: options.cwd, + env: options.env, + homeDir: options.homeDir, + }); + const results: SetupResult[] = []; + + for (const agentName of options.agents) { + results.push( + await setupAgent(getAgent(agentName, context), { + endpoint: options.endpoint, + scope: options.scope, + }), + ); + } + + return results; +} + +export async function detectAgents(scope: SetupScope, context: PathContext): Promise { + const detected: SetupAgent[] = []; + const allAgents = getAllAgents(context); + + for (const agent of Object.values(allAgents)) { + const paths = scope === 'global' ? agent.detect.globalPaths : agent.detect.projectPaths; + for (const path of paths) { + if (await pathExists(path)) { + detected.push(agent.name); + break; + } + } + } + + return detected; +} + +export function buildAgentChoices(context: PathContext, detected: SetupAgent[]): AgentChoice[] { + const detectedSet = new Set(detected); + + return Object.values(getAllAgents(context)).map((agent) => ({ + detected: detectedSet.has(agent.name), + displayName: agent.displayName, + name: agent.name, + })); +} + +async function setupAgent(agent: AgentConfig, options: { endpoint: string; scope: SetupScope }): Promise { + const mcpPath = await installMcpConfig(agent, options.endpoint, options.scope); + const rulePath = await installRule(agent, options.scope); + const skill = await writeSkill(agent.skill.dir(options.scope), getSkillContent()); + + return { + agent: agent.displayName, + mcpPath: mcpPath.path, + mcpStatus: mcpPath.alreadyExists ? 'updated' : 'configured', + rulePath: rulePath.path, + ruleStatus: rulePath.status, + skillPath: skill.path, + skillStatus: skill.alreadyExists ? 'preserved' : 'installed', + }; +} + +async function installMcpConfig( + agent: AgentConfig, + endpoint: string, + scope: SetupScope, +): Promise<{ alreadyExists: boolean; path: string }> { + const candidates = scope === 'global' ? agent.mcp.globalPaths : agent.mcp.projectPaths; + const mcpPath = await resolveConfigPath(candidates); + const entry = agent.mcp.buildEntry(endpoint); + + if (mcpPath.endsWith('.toml')) { + const result = await appendTomlServer(mcpPath, entry); + return { alreadyExists: result.alreadyExists, path: mcpPath }; + } + + const result = await upsertJsonServerEntry(mcpPath, agent.mcp.configKey, entry); + return { alreadyExists: result.alreadyExists, path: mcpPath }; +} + +async function installRule(agent: AgentConfig, scope: SetupScope): Promise<{ path: string; status: string }> { + const content = getRuleContent(agent.name); + + if (agent.rule.kind === 'file') { + const rulePath = join(agent.rule.dir(scope), agent.rule.filename); + const result = await writeRuleFile(rulePath, content); + return { path: rulePath, status: result.alreadyExists ? 'preserved' : 'installed' }; + } + + const rulePath = agent.rule.file(scope); + const existed = await pathExists(rulePath); + await appendRuleSection(rulePath, content); + return { path: rulePath, status: existed ? 'updated' : 'installed' }; +} + +export { DOCSEARCH_MCP_SERVER_NAME }; diff --git a/packages/docsearch-cli/src/setup/templates.ts b/packages/docsearch-cli/src/setup/templates.ts new file mode 100644 index 00000000..8b0fe287 --- /dev/null +++ b/packages/docsearch-cli/src/setup/templates.ts @@ -0,0 +1,57 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { DOCSEARCH_MCP_SERVER_NAME } from '../constants.js'; + +import type { SetupAgent } from './agents.js'; + +const BASE_GUIDANCE = `Use DocSearch MCP when the user asks about public developer documentation for a library, framework, SDK, API, CLI tool, or cloud service. + +Prefer the one-shot \`algolia_docsearch_search_docs\` tool for normal lookups: + +1. Put the official product, library, SDK, or platform name in \`library\`. +2. Put the actual documentation question in \`query\`. +3. Answer only from returned documentation chunks and include source links when available. + +Use \`algolia_docsearch_resolve_docset\` followed by \`algolia_docsearch_query_docs\` when a question spans multiple products or when the right documentation set is ambiguous. + +Do not use DocSearch MCP for general programming concepts, local code review, or business logic that is not answered by public docs.`; + +const CURSOR_FRONTMATTER = `---\nalwaysApply: true\n---\n\n`; + +export function getRuleContent(agent: SetupAgent): string { + if (agent === 'cursor') { + return `${CURSOR_FRONTMATTER}${BASE_GUIDANCE}\n`; + } + + return `${BASE_GUIDANCE}\n`; +} + +export function getSkillContent(): string { + return `--- +name: ${DOCSEARCH_MCP_SERVER_NAME} +description: Search current public developer documentation through DocSearch MCP. Use for library, framework, SDK, API, CLI, and cloud service documentation questions. +--- + +# DocSearch MCP + +Use this skill to fetch current public developer documentation through DocSearch MCP before answering documentation questions. + +## When to use + +- The user asks about a library, framework, SDK, API, CLI tool, or cloud service. +- The answer depends on current public docs, configuration, API syntax, migration steps, or version-specific behavior. +- The user explicitly says to use DocSearch MCP. + +## Steps + +1. For most questions, call \`algolia_docsearch_search_docs\`. + - \`library\`: official product or vendor/library name, for example \`Next.js\`, \`Stripe\`, or \`Algolia InstantSearch\`. + - \`query\`: the user's actual documentation question. +2. If the library is ambiguous, call \`algolia_docsearch_resolve_docset\`, pick the best \`docset_id\`, then call \`algolia_docsearch_query_docs\`. +3. Use separate focused queries for unrelated topics. Combined broad queries dilute ranking. +4. Answer from retrieved chunks only. Include source URLs when present. + +## Fallback + +If DocSearch MCP returns no useful results, retry with the official product name and a narrower query. If it still returns nothing, say that the answer was not found in the indexed docs. +`; +} diff --git a/packages/docsearch-cli/src/setup/writers.ts b/packages/docsearch-cli/src/setup/writers.ts new file mode 100644 index 00000000..4e5c47e8 --- /dev/null +++ b/packages/docsearch-cli/src/setup/writers.ts @@ -0,0 +1,308 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { access, mkdir, readFile, realpath, rename, rm, stat, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; + +import { applyEdits, modify, parse, printParseErrorCode, type FormattingOptions, type ParseError } from 'jsonc-parser'; + +import { DOCSEARCH_MCP_SERVER_NAME } from '../constants.js'; +import { UsageError } from '../errors.js'; + +const SECTION_START = ''; +const SECTION_END = ''; + +export async function pathExists(path: string): Promise { + try { + await access(path); + return true; + } catch (error) { + if (isMissingFileError(error)) { + return false; + } + throw error; + } +} + +export async function resolveConfigPath(candidates: string[]): Promise { + for (const candidate of candidates) { + if (await pathExists(candidate)) { + return candidate; + } + } + + return candidates[0]; +} + +export async function upsertJsonServerEntry( + filePath: string, + configKey: string, + entry: Record, +): Promise<{ alreadyExists: boolean }> { + const existing = await readTextFile(filePath); + const source = existing.trim() === '' ? '{}\n' : existing; + const errors: ParseError[] = []; + const parsed = parse(source, errors, { + allowTrailingComma: true, + disallowComments: false, + }) as unknown; + + if (errors.length > 0) { + const firstError = errors[0]; + throw new UsageError( + `Cannot update ${filePath}: invalid JSON/JSONC (${printParseErrorCode(firstError.error)} at offset ${firstError.offset}).`, + ); + } + if (!isPlainObject(parsed)) { + throw new UsageError(`Cannot update ${filePath}: the configuration root must be an object.`); + } + + const section = parsed[configKey]; + if (section !== undefined && !isPlainObject(section)) { + throw new UsageError(`Cannot update ${filePath}: "${configKey}" must be an object.`); + } + + const serverEntry = section?.[DOCSEARCH_MCP_SERVER_NAME]; + if (serverEntry !== undefined && !isPlainObject(serverEntry)) { + throw new UsageError(`Cannot update ${filePath}: "${configKey}.${DOCSEARCH_MCP_SERVER_NAME}" must be an object.`); + } + + const alreadyExists = serverEntry !== undefined; + const mergedEntry = { + ...(serverEntry ?? {}), + ...entry, + }; + const edits = modify(source, [configKey, DOCSEARCH_MCP_SERVER_NAME], mergedEntry, { + formattingOptions: inferFormatting(source), + }); + const updated = ensureFinalNewline(applyEdits(source, edits)); + + await writeFileAtomic(filePath, updated); + return { alreadyExists }; +} + +export function buildTomlServerBlock(entry: Record): string { + return [ + `[mcp_servers.${DOCSEARCH_MCP_SERVER_NAME}]`, + ...Object.entries(entry).map(([key, value]) => `${key} = ${JSON.stringify(value)}`), + '', + ].join('\n'); +} + +export async function appendTomlServer( + filePath: string, + entry: Record, +): Promise<{ alreadyExists: boolean }> { + const existing = await readTextFile(filePath); + const block = buildTomlServerBlock(entry); + const sectionHeader = `[mcp_servers.${DOCSEARCH_MCP_SERVER_NAME}]`; + const headerRegex = new RegExp(`^${escapeRegExp(sectionHeader)}[ \\t]*$`, 'gm'); + const matches = [...existing.matchAll(headerRegex)]; + if (matches.length > 1) { + throw new UsageError(`Cannot update ${filePath}: duplicate ${sectionHeader} sections.`); + } + + const alreadyExists = matches.length === 1; + const content = alreadyExists + ? mergeTomlSection(existing, matches[0], entry, filePath) + : appendBlock(existing, block); + + await writeFileAtomic(filePath, content); + return { alreadyExists }; +} + +export async function writeRuleFile(filePath: string, content: string): Promise<{ alreadyExists: boolean }> { + const alreadyExists = await pathExists(filePath); + if (!alreadyExists) { + await writeFileAtomic(filePath, content); + } + return { alreadyExists }; +} + +export async function appendRuleSection(filePath: string, content: string): Promise { + const existing = await readTextFile(filePath); + const hasStart = existing.includes(SECTION_START); + const hasEnd = existing.includes(SECTION_END); + if (hasStart !== hasEnd) { + throw new UsageError(`Cannot update ${filePath}: incomplete DocSearch managed section.`); + } + + const section = `${SECTION_START}\n${content.trim()}\n${SECTION_END}`; + const updated = + hasStart && hasEnd + ? existing.replace(new RegExp(`${escapeRegExp(SECTION_START)}[\\s\\S]*?${escapeRegExp(SECTION_END)}`), section) + : appendBlock(existing, `${section}\n`); + + await writeFileAtomic(filePath, updated); +} + +export async function writeSkill(skillDir: string, content: string): Promise<{ alreadyExists: boolean; path: string }> { + const skillPath = join(skillDir, DOCSEARCH_MCP_SERVER_NAME, 'SKILL.md'); + const alreadyExists = await pathExists(skillPath); + if (!alreadyExists) { + await writeFileAtomic(skillPath, content); + } + return { alreadyExists, path: skillPath }; +} + +function mergeTomlSection( + existing: string, + headerMatch: RegExpMatchArray, + entry: Record, + filePath: string, +): string { + const startIdx = headerMatch.index; + if (startIdx === undefined) { + throw new UsageError(`Cannot update ${filePath}: failed to locate the DocSearch MCP section.`); + } + + const lineBreak = existing.includes('\r\n') ? '\r\n' : '\n'; + const headerEnd = startIdx + headerMatch[0].length; + const rest = existing.slice(headerEnd); + const nextHeader = /^[ \t]*\[[^\]\r\n]+\][ \t]*(?:#.*)?$/m.exec(rest); + const endIdx = nextHeader?.index === undefined ? existing.length : headerEnd + nextHeader.index; + const lines = existing.slice(startIdx, endIdx).replace(/\s+$/, '').split(/\r?\n/); + + for (const [key, value] of Object.entries(entry)) { + const keyRegex = new RegExp(`^(\\s*)${escapeRegExp(key)}\\s*=`); + const matchingLines = lines + .map((line, index) => ({ index, match: keyRegex.exec(line) })) + .filter((candidate) => candidate.match !== null); + + if (matchingLines.length > 1) { + throw new UsageError(`Cannot update ${filePath}: duplicate "${key}" values in the DocSearch MCP section.`); + } + + const replacement = `${key} = ${toTomlValue(value, filePath)}`; + const matchingLine = matchingLines[0]; + if (matchingLine?.match) { + const comment = readTomlInlineComment(lines[matchingLine.index]); + lines[matchingLine.index] = `${matchingLine.match[1]}${replacement}${comment}`; + } else { + lines.push(replacement); + } + } + + const mergedSection = `${lines.join(lineBreak)}${lineBreak}`; + return ensureFinalNewline(`${existing.slice(0, startIdx)}${mergedSection}${existing.slice(endIdx)}`); +} + +function toTomlValue(value: unknown, filePath: string): string { + if (typeof value === 'string' || typeof value === 'boolean' || typeof value === 'number') { + return JSON.stringify(value); + } + + throw new UsageError(`Cannot update ${filePath}: unsupported TOML value in the DocSearch MCP configuration.`); +} + +function readTomlInlineComment(line: string): string { + let quote: '"' | "'" | undefined; + let escaped = false; + + for (let index = 0; index < line.length; index++) { + const character = line[index]; + if (escaped) { + escaped = false; + } else if (quote === '"' && character === '\\') { + escaped = true; + } else if (quote && character === quote) { + quote = undefined; + } else if (!quote && (character === '"' || character === "'")) { + quote = character; + } else if (!quote && character === '#') { + const whitespace = line.slice(0, index).match(/[ \t]*$/)?.[0] ?? ''; + return `${whitespace || ' '}${line.slice(index)}`; + } + } + + return ''; +} + +function appendBlock(existing: string, block: string): string { + const lineBreak = existing.includes('\r\n') ? '\r\n' : '\n'; + const normalizedBlock = block.replace(/\r?\n/g, lineBreak); + if (!existing) { + return normalizedBlock.endsWith(lineBreak) ? normalizedBlock : `${normalizedBlock}${lineBreak}`; + } + + const separator = existing.endsWith(lineBreak) ? lineBreak : `${lineBreak}${lineBreak}`; + return `${existing}${separator}${ + normalizedBlock.endsWith(lineBreak) ? normalizedBlock : `${normalizedBlock}${lineBreak}` + }`; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function inferFormatting(source: string): FormattingOptions { + const lineBreak = source.includes('\r\n') ? '\r\n' : '\n'; + const indentation = source.match(/^[ \t]+(?=")/m)?.[0]; + const insertSpaces = indentation?.includes('\t') !== true; + + return { + eol: lineBreak, + insertSpaces, + tabSize: insertSpaces ? (indentation?.length ?? 2) : 1, + }; +} + +function isPlainObject(value: unknown): value is Record { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function ensureFinalNewline(content: string): string { + if (content.endsWith('\n')) { + return content; + } + return `${content}\n`; +} + +async function readTextFile(filePath: string): Promise { + try { + return await readFile(filePath, 'utf-8'); + } catch (error) { + if (isMissingFileError(error)) { + return ''; + } + throw error; + } +} + +async function writeFileAtomic(filePath: string, content: string): Promise { + const targetPath = await resolveExistingPath(filePath); + await mkdir(dirname(targetPath), { recursive: true }); + + let mode: number | undefined; + try { + mode = (await stat(targetPath)).mode % 0o1000; + } catch (error) { + if (!isMissingFileError(error)) { + throw error; + } + } + + const tempPath = `${targetPath}.${process.pid}.${Date.now()}.tmp`; + try { + await writeFile(tempPath, content, { + encoding: 'utf-8', + mode, + }); + await rename(tempPath, targetPath); + } finally { + await rm(tempPath, { force: true }); + } +} + +async function resolveExistingPath(filePath: string): Promise { + try { + return await realpath(filePath); + } catch (error) { + if (isMissingFileError(error)) { + return filePath; + } + throw error; + } +} + +function isMissingFileError(error: unknown): error is NodeJS.ErrnoException { + return error instanceof Error && 'code' in error && error.code === 'ENOENT'; +} diff --git a/packages/docsearch-cli/src/ui/__tests__/markdown.test.ts b/packages/docsearch-cli/src/ui/__tests__/markdown.test.ts new file mode 100644 index 00000000..aa6e07d1 --- /dev/null +++ b/packages/docsearch-cli/src/ui/__tests__/markdown.test.ts @@ -0,0 +1,56 @@ +// @vitest-environment node + +import { describe, expect, it } from 'vitest'; + +import { renderMarkdown } from '../markdown'; + +describe('renderMarkdown', () => { + it('renders headings, sources, bullets, and dividers as plain text when color is disabled', () => { + const input = [ + 'Searched docsets:', + '', + '### Configure props', + '', + 'Source: https://example.com/docs', + '', + '- `apiKey`: Your key.', + '- Read the [configuration guide](https://example.com/config).', + '--------------------------------', + ].join('\n'); + + const output = renderMarkdown(input); + + expect(output).toContain('Configure props'); + expect(output).toContain('https://example.com/docs'); + expect(output).toContain('apiKey'); + expect(output).toContain('https://example.com/config'); + expect(output).not.toContain('###'); + expect(output).toMatch(/─{10,}/); + }); + + it('renders resolve metadata as a card with a score bar', () => { + const input = [ + '- title: Next.js Docs', + '- docset_id: repo/vercel/next.js', + '- description: Official documentation.', + '- trustScore: 100', + ].join('\n'); + + const output = renderMarkdown(input); + + expect(output).toContain('Next.js Docs'); + expect(output).toContain('repo/vercel/next.js'); + expect(output).toContain('trust'); + expect(output).toContain('100'); + expect(output).toMatch(/[▰▱#-]{10}/); + }); + + it('keeps code fences as a bordered block without backticks', () => { + const input = ['```ts', 'const x = 1;', '```'].join('\n'); + + const output = renderMarkdown(input); + + expect(output).toContain('const x = 1;'); + expect(output).not.toContain('```'); + }); +}); diff --git a/packages/docsearch-cli/src/ui/__tests__/render.test.ts b/packages/docsearch-cli/src/ui/__tests__/render.test.ts new file mode 100644 index 00000000..5e2b4f08 --- /dev/null +++ b/packages/docsearch-cli/src/ui/__tests__/render.test.ts @@ -0,0 +1,34 @@ +// @vitest-environment node + +import { describe, expect, it } from 'vitest'; + +import { renderHelp, renderLanding, renderLogo } from '../render'; + +describe('renderLanding', () => { + it('includes the logo, tagline, commands, and discovery link', () => { + const output = renderLanding(); + + expect(output).toContain('▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓'); + expect(output).toContain('The open documentation search for AI agents'); + expect(output).toContain('setup'); + expect(output).toContain('docs'); + expect(output).toContain('resolve'); + expect(output).toContain('query'); + expect(output).toContain('https://docsearch.algolia.com'); + }); + + it('renders the logo as multi-line art', () => { + expect(renderLogo().slice(1, -1).split('\n')).toHaveLength(21); + }); + + it('documents setup and query flags', () => { + const output = renderHelp(); + + expect(output).toContain('--all'); + expect(output).toContain('--cursor'); + expect(output).toContain('--max-results'); + expect(output).toContain('--max-docsets'); + expect(output).toContain('--top-n'); + expect(output).toContain('project + detected agents'); + }); +}); diff --git a/packages/docsearch-cli/src/ui/markdown.ts b/packages/docsearch-cli/src/ui/markdown.ts new file mode 100644 index 00000000..0e97b9a8 --- /dev/null +++ b/packages/docsearch-cli/src/ui/markdown.ts @@ -0,0 +1,196 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { color, symbols } from './theme.js'; + +const CODE_FENCE = /^```(.*)$/; +const HORIZONTAL_RULE = /^\s*([-*_]|─){3,}\s*$/; +const HEADING = /^(#{1,6})\s+(.*)$/; +const SOURCE = /^Source:\s+(.+)$/; +const BULLET = /^(\s*)[-*]\s+(.+)$/; +const META_FIELD = /^([A-Za-z][\w]*):\s+([\s\S]*)$/; +const SECTION_LABEL = /^[A-Z][^:`]*:\s*$/; +const TABLE_ROW = /^\s*\|.*\|\s*$/; +const TABLE_SEPARATOR = /^\s*\|[\s:|-]+\|\s*$/; + +const MAX_WIDTH = 88; +const RULE_WIDTH = 56; + +export function renderMarkdown(text: string): string { + const width = Math.max(40, Math.min(process.stdout.columns ?? 80, MAX_WIDTH)); + const lines = text.replace(/\n+$/, '').split('\n'); + const output: string[] = []; + let inCodeBlock = false; + + for (const line of lines) { + const fence = line.match(CODE_FENCE); + if (fence) { + output.push(inCodeBlock ? renderCodeBottom() : renderCodeTop(fence[1].trim())); + inCodeBlock = !inCodeBlock; + } else if (inCodeBlock) { + output.push(renderCodeLine(line)); + } else { + output.push(...renderProseLine(line, width)); + } + } + + return `${output.join('\n')}\n`; +} + +function renderProseLine(line: string, width: number): string[] { + if (line.trim() === '') { + return ['']; + } + + if (HORIZONTAL_RULE.test(line)) { + return [color.dim(symbols.rule.repeat(RULE_WIDTH))]; + } + + const heading = line.match(HEADING); + if (heading) { + return [`${color.magenta(symbols.accent)} ${color.bold(color.cyan(heading[2]))}`]; + } + + const source = line.match(SOURCE); + if (source) { + return [` ${color.dim(symbols.link)} ${color.underline(color.blue(source[1]))}`]; + } + + const bullet = line.match(BULLET); + if (bullet) { + return renderBullet(bullet[2], width); + } + + if (TABLE_ROW.test(line)) { + return [renderTableRow(line)]; + } + + if (SECTION_LABEL.test(line)) { + return [color.bold(line)]; + } + + return wrap(line, width).map((part) => styleInline(part)); +} + +function renderBullet(content: string, width: number): string[] { + const field = content.match(META_FIELD); + if (field && !content.startsWith('`')) { + return renderMetaField(field[1], field[2], width); + } + + const marker = color.cyan(symbols.bullet); + return wrap(content, width - 2).map((part, index) => + index === 0 ? `${marker} ${styleInline(part)}` : ` ${styleInline(part)}`, + ); +} + +function renderMetaField(key: string, value: string, width: number): string[] { + if (key === 'title') { + return [`${color.magenta(symbols.diamond)} ${color.bold(value)}`]; + } + + if (key === 'docset_id') { + return [` ${color.dim('id')} ${color.cyan(value)}`]; + } + + if (/score$/i.test(key)) { + return [renderScore(key.replace(/score$/i, ''), value)]; + } + + if (key === 'description') { + return wrap(value, width - 2).map((part) => ` ${color.dim(part)}`); + } + + const indent = ' '.repeat(key.length + 4); + return wrap(value, width - key.length - 4).map((part, index) => + index === 0 ? ` ${color.dim(key)} ${styleInline(part)}` : `${indent}${styleInline(part)}`, + ); +} + +function renderScore(label: string, value: string): string { + const score = Number(value); + const paddedLabel = color.dim(label.padEnd(11)); + + if (!Number.isFinite(score)) { + return ` ${paddedLabel} ${value}`; + } + + const clamped = Math.max(0, Math.min(100, score)); + const filled = Math.round((clamped / 100) * 10); + const bar = symbols.scoreOn.repeat(filled) + symbols.scoreOff.repeat(10 - filled); + const tint = scoreTint(score); + + return ` ${paddedLabel} ${tint(bar)} ${tint(String(value))}`; +} + +function scoreTint(score: number): (text: string) => string { + if (score >= 80) { + return color.green; + } + + if (score >= 50) { + return color.yellow; + } + + return color.red; +} + +function renderTableRow(line: string): string { + if (TABLE_SEPARATOR.test(line)) { + return color.dim(symbols.rule.repeat(RULE_WIDTH)); + } + + const cells = line + .trim() + .replace(/^\|/, '') + .replace(/\|$/, '') + .split('|') + .map((cell) => styleInline(cell.trim())); + + return ` ${cells.join(color.dim(` ${symbols.codeGutter} `))}`; +} + +function renderCodeTop(language: string): string { + const chip = language ? ` ${color.dim(language)}` : ''; + return `${color.dim(`${symbols.codeTop}${symbols.rule}`)}${chip}`; +} + +function renderCodeLine(line: string): string { + return `${color.dim(`${symbols.codeGutter} `)}${color.gray(line)}`; +} + +function renderCodeBottom(): string { + return color.dim(`${symbols.codeBottom}${symbols.rule}`); +} + +function styleInline(text: string): string { + return text + .replace( + /\[([^\]]+)\]\(([^)]+)\)/g, + (_match, label: string, url: string) => `${color.underline(color.blue(label))}${color.dim(` (${url})`)}`, + ) + .replace(/`([^`]+)`/g, (_match, code: string) => color.cyan(code)) + .replace(/\*\*([^*]+)\*\*/g, (_match, bold: string) => color.bold(bold)); +} + +function wrap(text: string, width: number): string[] { + const limit = Math.max(20, width); + const words = text.split(/\s+/).filter(Boolean); + const lines: string[] = []; + let current = ''; + + for (const word of words) { + if (current === '') { + current = word; + } else if (current.length + 1 + word.length <= limit) { + current += ` ${word}`; + } else { + lines.push(current); + current = word; + } + } + + if (current !== '') { + lines.push(current); + } + + return lines.length > 0 ? lines : ['']; +} diff --git a/packages/docsearch-cli/src/ui/render.ts b/packages/docsearch-cli/src/ui/render.ts new file mode 100644 index 00000000..f6b37b8e --- /dev/null +++ b/packages/docsearch-cli/src/ui/render.ts @@ -0,0 +1,189 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { CLI_NAME, CLI_VERSION } from '../constants.js'; +import type { SetupResult } from '../setup/setup.js'; + +import { brandShade, color, symbols } from './theme.js'; + +const LOGO: string[] = [ + ' ', + ' ░░░░░░░░░░░░░░░░░░░░░░░░░░░', + ' ░░░░░', + ' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░', + ' ░░░░', + ' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░', + ' ░░░░', + ' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░', + ' ░░░', + ' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░', + ' ░░░', + ' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░', + ' ░░░', + ' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░', + ' ░░░░', + ' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░', + ' ░░░', + ' ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ ░░░░', + ' ░░░░░', + ' ░░░░░░░░░░░░░░░░░░░░░░░░░░', + ' ', +]; + +const LOGO_TOP_LEVEL = 1; +const LOGO_BOTTOM_LEVEL = 0; + +export function renderLogo(): string { + const span = Math.max(1, LOGO.length - 1); + const rows = LOGO.map((line, index) => { + const level = LOGO_TOP_LEVEL - (index / span) * (LOGO_TOP_LEVEL - LOGO_BOTTOM_LEVEL); + return brandShade(level)(line); + }); + + return `\n${rows.join('\n')}\n`; +} + +export function renderBanner(): string { + const title = color.bold(color.cyan('DocSearch')); + const subtitle = color.dim('search public docs & set up the DocSearch MCP'); + + return `\n${color.magenta(symbols.diamond)} ${title} ${color.dim(`v${CLI_VERSION}`)}\n ${subtitle}\n`; +} + +interface LandingCommand { + args?: string; + description: string; + name: string; +} + +const LANDING_COMMANDS: LandingCommand[] = [ + { description: 'Configure the DocSearch MCP for your AI agents', name: 'setup' }, + { args: ' ', description: "One-shot search across a library's docs", name: 'docs' }, + { args: '', description: 'Find matching docsets for a product', name: 'resolve' }, + { + args: ' ', + description: 'Search inside specific docsets', + name: 'query', + }, +]; + +export function renderLanding(): string { + const prefix = `${CLI_NAME} `; + const plainLefts = LANDING_COMMANDS.map( + (command) => ` $ ${prefix}${command.name}${command.args ? ` ${command.args}` : ''}`, + ); + const column = Math.max(...plainLefts.map((left) => left.length)) + 3; + + const commandLines = LANDING_COMMANDS.map((command, index) => { + const styledLeft = ` ${color.dim('$')} ${color.dim(prefix.trim())} ${color.green(command.name)}${ + command.args ? ` ${color.cyan(command.args)}` : '' + }`; + const padding = ' '.repeat(Math.max(2, column - plainLefts[index].length)); + return `${styledLeft}${padding}${command.description}`; + }); + + return [ + renderLogo(), + color.dim(' The open documentation search for AI agents'), + '', + ...commandLines, + '', + ` ${color.dim('try:')} ${color.bold(`${CLI_NAME} docs Next.js`)} ${color.dim('"how do middleware matchers work"')}`, + '', + ` ${color.dim('Docs & MCP setup at')} ${color.underline(color.blue('https://docsearch.algolia.com/mcp'))}`, + '', + ].join('\n'); +} + +export function renderHelp(): string { + const heading = (text: string): string => color.bold(color.yellow(text)); + const cmd = (text: string): string => color.green(text); + const meta = (text: string): string => color.dim(text); + + return [ + renderBanner(), + heading('Usage'), + ` ${color.bold(CLI_NAME)} ${color.cyan('')} ${meta('[options]')}`, + '', + heading('Commands'), + ` ${cmd('setup')} Configure the DocSearch MCP for your AI agents`, + ` ${cmd('docs')} ${color.cyan(' ')} One-shot search across a library's docs`, + ` ${cmd('resolve')} ${color.cyan('')} Find matching docsets for a product`, + ` ${cmd('query')} ${color.cyan(' ')} Search inside specific docsets`, + '', + heading('Setup options'), + ` ${color.green('--project')} Install at the detected repository root`, + ` ${color.green('--global')} Install into user-level agent settings`, + ` ${color.green('--all')} Configure every supported agent`, + ` ${color.green('--cursor')}, ${color.green('--claude')} Configure Cursor or Claude Code`, + ` ${color.green('--codex')}, ${color.green('--opencode')} Configure Codex or OpenCode`, + ` ${color.green('--gemini')} Configure Gemini CLI`, + ` ${color.green('--yes')}, ${color.green('-y')} Run setup non-interactively ${meta('(project + detected agents)')}`, + '', + heading('Query options'), + ` ${color.green('--max-results')} ${color.cyan('')} Limit documentation results`, + ` ${color.green('--max-docsets')} ${color.cyan('')} Limit docsets searched by ${cmd('docs')}`, + ` ${color.green('--top-n')} ${color.cyan('')} Limit docsets returned by ${cmd('resolve')}`, + ` ${color.green('--json')} Print raw MCP results as JSON`, + '', + heading('Global options'), + ` ${color.green('--endpoint')} ${color.cyan('')} Override the hosted DocSearch MCP endpoint`, + ` ${color.green('--help')}, ${color.green('-h')} Show help`, + '', + heading('Examples'), + ` ${meta('$')} ${CLI_NAME} setup`, + ` ${meta('$')} ${CLI_NAME} setup ${color.green('--cursor --claude --project')}`, + ` ${meta('$')} ${CLI_NAME} docs Next.js ${color.dim('"how do middleware matchers work"')}`, + ` ${meta('$')} ${CLI_NAME} resolve ${color.dim('"Algolia InstantSearch React"')}`, + ` ${meta('$')} ${CLI_NAME} query nextjs ${color.dim('"middleware matcher config"')} --json`, + '', + ].join('\n'); +} + +export function renderResultHeader(command: string, subject: string): string { + return `\n${color.magenta(symbols.diamond)} ${color.bold(color.cyan(command))} ${color.dim(symbols.arrow)} ${color.bold(subject)}\n`; +} + +export function renderError(message: string): string { + return `${color.red(symbols.cross)} ${color.red(message)}\n`; +} + +export function renderNotice(message: string): string { + return `${color.yellow(symbols.info)} ${message}\n`; +} + +export interface SetupSummaryMeta { + endpoint: string; + location?: string; + scope: string; +} + +export function renderSetupResults(results: SetupResult[], meta: SetupSummaryMeta): string { + if (results.length === 0) { + return renderNotice('No agents selected. Nothing was configured.'); + } + + const label = results.length === 1 ? 'agent' : 'agents'; + const lines: string[] = [ + `\n${color.green(symbols.tick)} ${color.bold(`Configured DocSearch MCP for ${results.length} ${label}`)}`, + ` ${color.dim('scope')} ${meta.scope}`, + ...(meta.location ? [` ${color.dim('root')} ${meta.location}`] : []), + ` ${color.dim('endpoint')} ${color.underline(meta.endpoint)}`, + '', + ]; + + for (const result of results) { + lines.push(`${color.magenta(symbols.diamond)} ${color.bold(result.agent)}`); + lines.push(renderStep('MCP server', result.mcpStatus, result.mcpPath)); + lines.push(renderStep('Rule', result.ruleStatus, result.rulePath)); + lines.push(renderStep('Skill', result.skillStatus, result.skillPath)); + lines.push(''); + } + + lines.push(color.dim(`Restart your agent to load the new configuration.`)); + + return `${lines.join('\n')}\n`; +} + +function renderStep(name: string, status: string, path: string): string { + const badge = status === 'updated' ? color.yellow(status) : color.green(status); + return ` ${color.green(symbols.tick)} ${name} ${color.dim(symbols.bullet)} ${badge}\n ${color.dim(path)}`; +} diff --git a/packages/docsearch-cli/src/ui/spinner.ts b/packages/docsearch-cli/src/ui/spinner.ts new file mode 100644 index 00000000..f3cced7d --- /dev/null +++ b/packages/docsearch-cli/src/ui/spinner.ts @@ -0,0 +1,51 @@ +/* eslint-disable import/no-unresolved -- NodeNext source imports use runtime .js extensions. */ +import { color, symbols } from './theme.js'; + +const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; +const FRAME_INTERVAL_MS = 80; + +export interface Spinner { + fail: (text?: string) => void; + stop: () => void; + succeed: (text?: string) => void; +} + +export function startSpinner(text: string, stream: NodeJS.WriteStream = process.stderr): Spinner { + const animate = stream.isTTY === true && !('NO_COLOR' in process.env); + + if (!animate) { + stream.write(`${color.dim(symbols.bullet)} ${text}\n`); + return { fail: noop, stop: noop, succeed: noop }; + } + + let frame = 0; + stream.write('\u001B[?25l'); + + const timer = setInterval(() => { + frame = (frame + 1) % FRAMES.length; + stream.write(`\r${color.cyan(FRAMES[frame])} ${text}`); + }, FRAME_INTERVAL_MS); + + function clear(): void { + clearInterval(timer); + stream.write('\r\u001B[K\u001B[?25h'); + } + + return { + fail(message) { + clear(); + stream.write(`${color.red(symbols.cross)} ${message ?? text}\n`); + }, + stop() { + clear(); + }, + succeed(message) { + clear(); + stream.write(`${color.green(symbols.tick)} ${message ?? text}\n`); + }, + }; +} + +function noop(): void { + // Intentionally empty: non-animated spinners have nothing to clear. +} diff --git a/packages/docsearch-cli/src/ui/theme.ts b/packages/docsearch-cli/src/ui/theme.ts new file mode 100644 index 00000000..8c06a5da --- /dev/null +++ b/packages/docsearch-cli/src/ui/theme.ts @@ -0,0 +1,65 @@ +import pc from 'picocolors'; + +const colorEnabled = + !('NO_COLOR' in process.env) && (process.stdout.isTTY === true || process.env.FORCE_COLOR !== undefined); + +function paint(fn: (input: string) => string): (text: string) => string { + return (text: string) => (colorEnabled ? fn(text) : text); +} + +export const color = { + blue: paint(pc.blue), + bold: paint(pc.bold), + cyan: paint(pc.cyan), + dim: paint(pc.dim), + gray: paint(pc.gray), + green: paint(pc.green), + magenta: paint(pc.magenta), + red: paint(pc.red), + underline: paint(pc.underline), + yellow: paint(pc.yellow), +}; + +const richUnicode = + process.platform !== 'win32' || process.env.WT_SESSION !== undefined || process.env.TERM_PROGRAM !== undefined; + +export const symbols = { + accent: richUnicode ? '▍' : '|', + arrow: richUnicode ? '→' : '->', + bullet: richUnicode ? '•' : '*', + checkboxOff: richUnicode ? '◯' : '[ ]', + checkboxOn: richUnicode ? '◉' : '[x]', + codeBottom: richUnicode ? '└' : '+', + codeGutter: richUnicode ? '│' : '|', + codeTop: richUnicode ? '┌' : '+', + cross: richUnicode ? '✖' : 'x', + diamond: richUnicode ? '◆' : '*', + info: richUnicode ? 'ℹ' : 'i', + link: richUnicode ? '↗' : '->', + pointer: richUnicode ? '❯' : '>', + rule: richUnicode ? '─' : '-', + scoreOff: richUnicode ? '▱' : '-', + scoreOn: richUnicode ? '▰' : '#', + tick: richUnicode ? '✔' : 'v', +}; + +export function isInteractive(): boolean { + return ( + process.stdin.isTTY === true && process.stderr.isTTY === true && typeof process.stdin.setRawMode === 'function' + ); +} + +const DOCSEARCH_CYAN = [44, 200, 247] as const; +const DOCSEARCH_NEBULA = [84, 104, 255] as const; +const DOCSEARCH_PINK = [248, 44, 170] as const; + +export function brandShade(level: number): (text: string) => string { + const clamped = Math.max(0, Math.min(1, level)); + const [from, to, progress] = + clamped >= 0.5 + ? [DOCSEARCH_NEBULA, DOCSEARCH_PINK, (clamped - 0.5) * 2] + : [DOCSEARCH_CYAN, DOCSEARCH_NEBULA, clamped * 2]; + const [red, green, blue] = from.map((channel, index) => Math.round(channel + (to[index] - channel) * progress)); + + return (text: string) => (colorEnabled ? `\u001B[38;2;${red};${green};${blue}m${text}\u001B[39m` : text); +} diff --git a/packages/docsearch-cli/tsconfig.json b/packages/docsearch-cli/tsconfig.json new file mode 100644 index 00000000..5f64c6ea --- /dev/null +++ b/packages/docsearch-cli/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "declarationMap": false, + "lib": [ + "ES2022" + ], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "target": "ES2022" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "src/**/*.test.ts", + "src/**/__tests__/**" + ] +} diff --git a/packages/docsearch-cli/vitest.config.mts b/packages/docsearch-cli/vitest.config.mts new file mode 100644 index 00000000..7eeb3f85 --- /dev/null +++ b/packages/docsearch-cli/vitest.config.mts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts'], + }, +});