feat(cli): add @docsearch/cli for MCP setup and search (#2911)
This commit is contained in:
parent
40ca749cf4
commit
218eac3062
33 changed files with 3495 additions and 43 deletions
|
|
@ -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
|
||||
|
|
|
|||
275
bun.lock
275
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=="],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
7
packages/docsearch-cli/CHANGELOG.md
Normal file
7
packages/docsearch-cli/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# @docsearch/cli
|
||||
|
||||
## 0.0.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 710b410: Add the DocSearch CLI for installing and querying the hosted DocSearch MCP.
|
||||
104
packages/docsearch-cli/README.md
Normal file
104
packages/docsearch-cli/README.md
Normal file
|
|
@ -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 <url> 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)
|
||||
57
packages/docsearch-cli/package.json
Normal file
57
packages/docsearch-cli/package.json
Normal file
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
75
packages/docsearch-cli/src/__tests__/cli.test.ts
Normal file
75
packages/docsearch-cli/src/__tests__/cli.test.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
99
packages/docsearch-cli/src/__tests__/commands.test.ts
Normal file
99
packages/docsearch-cli/src/__tests__/commands.test.ts
Normal file
|
|
@ -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/);
|
||||
});
|
||||
});
|
||||
180
packages/docsearch-cli/src/cli.ts
Normal file
180
packages/docsearch-cli/src/cli.ts
Normal file
|
|
@ -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<number> {
|
||||
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<number> {
|
||||
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<SetupScope> {
|
||||
if (request.scope) {
|
||||
return Promise.resolve(request.scope);
|
||||
}
|
||||
|
||||
if (request.yes) {
|
||||
return Promise.resolve('project');
|
||||
}
|
||||
|
||||
return promptScope();
|
||||
}
|
||||
|
||||
async function resolveSetupAgents(
|
||||
request: SetupRequest,
|
||||
context: ReturnType<typeof createPathContext>,
|
||||
scope: SetupScope,
|
||||
): Promise<SetupAgent[]> {
|
||||
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<typeof parseArgs>): Promise<number> {
|
||||
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';
|
||||
}
|
||||
283
packages/docsearch-cli/src/commands.ts
Normal file
283
packages/docsearch-cli/src/commands.ts
Normal file
|
|
@ -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<string, boolean | string>;
|
||||
positionals: string[];
|
||||
}
|
||||
|
||||
export interface ToolRequest {
|
||||
endpoint: string;
|
||||
json: boolean;
|
||||
toolName: string;
|
||||
toolArguments: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface SetupRequest {
|
||||
all: boolean;
|
||||
agents: SetupAgent[];
|
||||
endpoint: string;
|
||||
scope?: SetupScope;
|
||||
yes: boolean;
|
||||
}
|
||||
|
||||
const AGENT_FLAGS: Record<string, SetupAgent> = {
|
||||
'--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<string, boolean | string> = {};
|
||||
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 <library> <query> [--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 <library-or-product> [--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 <docset-id[,docset-id...]> <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<string, boolean | string>): SetupScope | undefined {
|
||||
if (options['--project']) {
|
||||
return 'project';
|
||||
}
|
||||
|
||||
if (options['--global']) {
|
||||
return 'global';
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function readStringOption(options: Record<string, boolean | string>, key: string): string | undefined {
|
||||
const value = options[key];
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}
|
||||
|
||||
function readEndpoint(options: Record<string, boolean | string>): 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<string, boolean | string>, 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<string, unknown>): Record<string, unknown> {
|
||||
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<string, boolean | string>): 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<string, boolean | string>,
|
||||
allowed: ReadonlySet<string>,
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
21
packages/docsearch-cli/src/constants.ts
Normal file
21
packages/docsearch-cli/src/constants.ts
Normal file
|
|
@ -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';
|
||||
}
|
||||
6
packages/docsearch-cli/src/errors.ts
Normal file
6
packages/docsearch-cli/src/errors.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export class UsageError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'UsageError';
|
||||
}
|
||||
}
|
||||
5
packages/docsearch-cli/src/index.ts
Normal file
5
packages/docsearch-cli/src/index.ts
Normal file
|
|
@ -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();
|
||||
76
packages/docsearch-cli/src/mcp/__tests__/client.test.ts
Normal file
76
packages/docsearch-cli/src/mcp/__tests__/client.test.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
27
packages/docsearch-cli/src/mcp/__tests__/format.test.ts
Normal file
27
packages/docsearch-cli/src/mcp/__tests__/format.test.ts
Normal file
|
|
@ -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`);
|
||||
});
|
||||
});
|
||||
55
packages/docsearch-cli/src/mcp/client.ts
Normal file
55
packages/docsearch-cli/src/mcp/client.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
toolName: string;
|
||||
}
|
||||
|
||||
export async function callDocSearchTool({
|
||||
endpoint,
|
||||
toolArguments,
|
||||
toolName,
|
||||
}: CallDocSearchToolOptions): Promise<CallToolResult> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
29
packages/docsearch-cli/src/mcp/format.ts
Normal file
29
packages/docsearch-cli/src/mcp/format.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
61
packages/docsearch-cli/src/setup/__tests__/prompt.test.ts
Normal file
61
packages/docsearch-cli/src/setup/__tests__/prompt.test.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
303
packages/docsearch-cli/src/setup/__tests__/setup.test.ts
Normal file
303
packages/docsearch-cli/src/setup/__tests__/setup.test.ts
Normal file
|
|
@ -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<string> {
|
||||
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<string, { url: string }>;
|
||||
};
|
||||
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);
|
||||
});
|
||||
});
|
||||
154
packages/docsearch-cli/src/setup/__tests__/writers.test.ts
Normal file
154
packages/docsearch-cli/src/setup/__tests__/writers.test.ts
Normal file
|
|
@ -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<string> {
|
||||
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<string, Record<string, unknown>>;
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
181
packages/docsearch-cli/src/setup/agents.ts
Normal file
181
packages/docsearch-cli/src/setup/agents.ts
Normal file
|
|
@ -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<string, unknown>;
|
||||
};
|
||||
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<SetupAgent, AgentConfig> {
|
||||
return agents(context);
|
||||
}
|
||||
|
||||
function agents(context: PathContext): Record<SetupAgent, AgentConfig> {
|
||||
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')],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
326
packages/docsearch-cli/src/setup/prompt.ts
Normal file
326
packages/docsearch-cli/src/setup/prompt.ts
Normal file
|
|
@ -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<SetupScope> {
|
||||
if (isInteractive()) {
|
||||
return promptScopeInteractive();
|
||||
}
|
||||
|
||||
return promptScopeByLine();
|
||||
}
|
||||
|
||||
function promptScopeInteractive(): Promise<SetupScope> {
|
||||
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<SetupScope>((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<SetupScope> {
|
||||
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<SetupAgent>();
|
||||
|
||||
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<SetupAgent[]> {
|
||||
if (isInteractive()) {
|
||||
return promptAgentsInteractive(choices);
|
||||
}
|
||||
|
||||
return promptAgentsByLine(choices);
|
||||
}
|
||||
|
||||
function promptAgentsInteractive(choices: AgentChoice[]): Promise<SetupAgent[]> {
|
||||
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<SetupAgent[]>((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<SetupAgent[]> {
|
||||
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<string> {
|
||||
initStdin();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
lineResolvers.push(resolve);
|
||||
process.stdin.resume();
|
||||
pumpLines();
|
||||
});
|
||||
}
|
||||
220
packages/docsearch-cli/src/setup/setup.ts
Normal file
220
packages/docsearch-cli/src/setup/setup.ts
Normal file
|
|
@ -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> = {}): 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<string> {
|
||||
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<boolean> {
|
||||
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<SetupResult[]> {
|
||||
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<SetupAgent[]> {
|
||||
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<SetupResult> {
|
||||
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 };
|
||||
57
packages/docsearch-cli/src/setup/templates.ts
Normal file
57
packages/docsearch-cli/src/setup/templates.ts
Normal file
|
|
@ -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.
|
||||
`;
|
||||
}
|
||||
308
packages/docsearch-cli/src/setup/writers.ts
Normal file
308
packages/docsearch-cli/src/setup/writers.ts
Normal file
|
|
@ -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 = '<!-- algolia-docsearch:start -->';
|
||||
const SECTION_END = '<!-- algolia-docsearch:end -->';
|
||||
|
||||
export async function pathExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isMissingFileError(error)) {
|
||||
return false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveConfigPath(candidates: string[]): Promise<string> {
|
||||
for (const candidate of candidates) {
|
||||
if (await pathExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
export async function upsertJsonServerEntry(
|
||||
filePath: string,
|
||||
configKey: string,
|
||||
entry: Record<string, unknown>,
|
||||
): 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, unknown>): 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<string, unknown>,
|
||||
): 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<void> {
|
||||
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<string, unknown>,
|
||||
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<string, unknown> {
|
||||
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<string> {
|
||||
try {
|
||||
return await readFile(filePath, 'utf-8');
|
||||
} catch (error) {
|
||||
if (isMissingFileError(error)) {
|
||||
return '';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeFileAtomic(filePath: string, content: string): Promise<void> {
|
||||
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<string> {
|
||||
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';
|
||||
}
|
||||
56
packages/docsearch-cli/src/ui/__tests__/markdown.test.ts
Normal file
56
packages/docsearch-cli/src/ui/__tests__/markdown.test.ts
Normal file
|
|
@ -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('```');
|
||||
});
|
||||
});
|
||||
34
packages/docsearch-cli/src/ui/__tests__/render.test.ts
Normal file
34
packages/docsearch-cli/src/ui/__tests__/render.test.ts
Normal file
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
196
packages/docsearch-cli/src/ui/markdown.ts
Normal file
196
packages/docsearch-cli/src/ui/markdown.ts
Normal file
|
|
@ -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 : [''];
|
||||
}
|
||||
189
packages/docsearch-cli/src/ui/render.ts
Normal file
189
packages/docsearch-cli/src/ui/render.ts
Normal file
|
|
@ -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: '<library> <query>', description: "One-shot search across a library's docs", name: 'docs' },
|
||||
{ args: '<library>', description: 'Find matching docsets for a product', name: 'resolve' },
|
||||
{
|
||||
args: '<docset-id[,docset-id...]> <query>',
|
||||
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('<command>')} ${meta('[options]')}`,
|
||||
'',
|
||||
heading('Commands'),
|
||||
` ${cmd('setup')} Configure the DocSearch MCP for your AI agents`,
|
||||
` ${cmd('docs')} ${color.cyan('<library> <query>')} One-shot search across a library's docs`,
|
||||
` ${cmd('resolve')} ${color.cyan('<library>')} Find matching docsets for a product`,
|
||||
` ${cmd('query')} ${color.cyan('<docset-id[,docset-id...]> <query>')} 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('<n>')} Limit documentation results`,
|
||||
` ${color.green('--max-docsets')} ${color.cyan('<n>')} Limit docsets searched by ${cmd('docs')}`,
|
||||
` ${color.green('--top-n')} ${color.cyan('<n>')} Limit docsets returned by ${cmd('resolve')}`,
|
||||
` ${color.green('--json')} Print raw MCP results as JSON`,
|
||||
'',
|
||||
heading('Global options'),
|
||||
` ${color.green('--endpoint')} ${color.cyan('<url>')} 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)}`;
|
||||
}
|
||||
51
packages/docsearch-cli/src/ui/spinner.ts
Normal file
51
packages/docsearch-cli/src/ui/spinner.ts
Normal file
|
|
@ -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.
|
||||
}
|
||||
65
packages/docsearch-cli/src/ui/theme.ts
Normal file
65
packages/docsearch-cli/src/ui/theme.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
22
packages/docsearch-cli/tsconfig.json
Normal file
22
packages/docsearch-cli/tsconfig.json
Normal file
|
|
@ -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__/**"
|
||||
]
|
||||
}
|
||||
8
packages/docsearch-cli/vitest.config.mts
Normal file
8
packages/docsearch-cli/vitest.config.mts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'node',
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue