diff --git a/docs/package-manager-structure.md b/docs/package-manager-structure.md new file mode 100644 index 0000000000..aaa982b01e --- /dev/null +++ b/docs/package-manager-structure.md @@ -0,0 +1,297 @@ +# Package Manager Repository Structure + +## Directory Structure Overview + +The package manager repository uses a flat directory structure where component types are determined by metadata rather than directory hierarchy. This approach: + +1. **Simplified Navigation** + + - No deep nested directories like `items/mcp-servers/` or `packages/` + - Components are placed directly in their parent directory + - Type information is stored in metadata, not directory structure + +2. **Type Determination** + + - Each component's type is specified in its metadata.yml + - Types include: mcp-server, memory, role, package, group + - Type field determines how the component is handled and displayed + +3. **Localization** + + - Each component has language-specific metadata files named `metadata.{locale}.yml` + - English metadata (metadata.en.yml) is required for component visibility + - Other languages are optional (e.g., metadata.es.yml, metadata.fr.yml) + +4. **Organization** + - Groups can contain any type of component + - Packages reference their components by path + - Components can be standalone or part of a package/group + +## Real-World Examples + +### 1. Simple Single-Item Repository + +Basic repository sharing individual components: + +``` +simple-tools/ +├── metadata.en.yml +├── log-analyzer/ # Type determined by metadata +│ ├── metadata.en.yml +│ └── server.js +└── reviewer/ # Type determined by metadata + ├── metadata.en.yml + └── role.md +``` + +```yaml +# simple-tools/metadata.en.yml +name: "Simple Tools Collection" +description: "Collection of independent development tools" +version: "1.0.0" +``` + +```yaml +# log-analyzer/metadata.en.yml +name: "Log Analyzer" +description: "Simple log analysis tool" +type: "mcp-server" +version: "1.0.0" +tags: ["logs", "analysis"] +``` + +Note: The `items` field is only needed when referencing components that exist outside the package's directory. + +### 2. Complex Development Toolkit Package + +Full-featured development environment setup: + +``` +dev-toolkit/ +├── metadata.en.yml +├── full-dev-env/ # Type: package +│ ├── metadata.en.yml +│ ├── metadata.es.yml +│ ├── code-analyzer/ # Type: mcp-server +│ │ ├── metadata.en.yml +│ │ ├── metadata.es.yml +│ │ └── server.js +│ ├── git-memory/ # Type: memory +│ │ ├── metadata.en.yml +│ │ ├── metadata.es.yml +│ │ └── memory.js +│ └── dev-role/ # Type: role +│ ├── metadata.en.yml +│ ├── metadata.es.yml +│ └── role.md +``` + +```yaml +# full-dev-env/metadata.en.yml +name: "Full Development Environment" +description: "Complete development setup with code analysis and version control" +version: "2.0.0" +type: "package" +``` + +Example with external component reference: + +```yaml +# full-dev-env/metadata.en.yml +name: "Full Development Environment" +description: "Complete development setup with code analysis and version control" +version: "2.0.0" +type: "package" +items: # Only needed for components outside this directory + - type: "mcp-server" + path: "../shared/security-scanner" # External component +``` + +```yaml +# full-dev-env/metadata.es.yml +name: "Entorno de Desarrollo Completo" +description: "Configuración completa de desarrollo con análisis de código y control de versiones" +version: "2.0.0" +type: "package" +``` + +### 3. Large Enterprise Data Platform + +Complex organization with multiple groups and shared resources: + +``` +data-platform/ +├── metadata.en.yml # Repository metadata +├── metadata.es.yml +├── data-engineering/ # Type: group +│ ├── metadata.en.yml +│ ├── metadata.es.yml +│ ├── base-role/ # Type: role +│ │ ├── metadata.en.yml +│ │ └── metadata.es.yml +│ ├── data-lake-memory/ # Type: memory +│ │ ├── metadata.en.yml +│ │ └── metadata.es.yml +│ ├── batch-processor/ # Type: mcp-server +│ │ ├── metadata.en.yml +│ │ └── metadata.es.yml +│ ├── stream-processor/ # Type: mcp-server +│ │ ├── metadata.en.yml +│ │ └── metadata.es.yml +│ ├── model-trainer/ # Type: mcp-server +│ │ ├── metadata.en.yml +│ │ └── metadata.es.yml +│ └── model-inference/ # Type: mcp-server +│ ├── metadata.en.yml +│ └── metadata.es.yml +├── analytics/ # Type: group +│ ├── metadata.en.yml +│ ├── metadata.es.yml +│ ├── reporting-tool/ # Type: mcp-server +│ │ ├── metadata.en.yml +│ │ └── metadata.es.yml +│ └── dashboard-builder/ # Type: mcp-server +│ ├── metadata.en.yml +│ └── metadata.es.yml +└── starter-kit/ # Type: package + ├── metadata.en.yml + └── metadata.es.yml +``` + +```yaml +# data-engineering/en/metadata.yml +name: "Data Engineering" +type: "group" +tags: ["data-engineering"] +``` + +### 4. Localized Community Tools + +Repository with multilingual support, using language-specific metadata: + +``` +community-tools/ +└── web-dev-toolkit/ # Type: package + ├── metadata.en.yml # English metadata + ├── metadata.es.yml # Spanish metadata + ├── metadata.fr.yml # French metadata + ├── code-formatter/ # Type: mcp-server + │ ├── metadata.en.yml + │ ├── metadata.es.yml + │ ├── metadata.fr.yml + │ └── server.js + └── web-role/ # Type: role + ├── metadata.en.yml + ├── metadata.es.yml + ├── metadata.fr.yml + └── role.md +``` + +```yaml +# web-dev-toolkit/metadata.en.yml +name: "Web Development Toolkit" +description: "Complete toolkit for web development" +version: "1.0.0" +type: "package" +``` + +```yaml +# web-dev-toolkit/metadata.es.yml +name: "Herramientas de Desarrollo Web" +description: "Kit de herramientas completo para desarrollo web" +version: "1.0.0" +type: "package" +``` + +Note: Components (code-formatter and web-role) are automatically discovered by scanning subdirectories and reading their metadata files. + +```yaml +# web-dev-toolkit/code-formatter/metadata.es.yml +name: "Formateador de Código" +description: "Herramienta de formateo de código" +version: "1.0.0" +type: "mcp-server" +``` + +This structure: + +- Places all metadata in language-specific folders +- Uses 'en' as the fallback locale +- Components without 'en' metadata are not displayed +- Supports independent translation management +- Simplifies locale resolution logic + +### 5. Evolution Example: From Simple to Complex + +#### Stage 1: Simple Single Component + +``` +code-formatter/ +└── metadata.en.yml +``` + +```yaml +# metadata.en.yml +name: "Simple Code Formatter" +description: "Basic code formatting tool" +version: "1.0.0" +type: "mcp-server" +``` + +#### Stage 2: Basic Package with Local Components + +``` +code-formatter-plus/ +├── metadata.en.yml # Basic package metadata +├── formatter/ +│ ├── metadata.en.yml # MCP server metadata +│ └── server.js +└── git-memory/ + ├── metadata.en.yml # Memory metadata + └── memory.js +``` + +```yaml +# metadata.en.yml +name: "Code Formatter Plus" +description: "Enhanced code formatting with git integration" +version: "1.5.0" +type: "package" +``` + +#### Stage 3: Package with External Component + +``` +code-quality-suite/ +├── metadata.en.yml +├── metadata.es.yml +├── formatter/ # Local component +│ ├── metadata.en.yml +│ ├── metadata.es.yml +│ └── server.js +└── shared-scanner/ # Reference to external component + └── metadata.yml # Points to actual component elsewhere +``` + +```yaml +# metadata.en.yml +name: "Code Quality Suite" +description: "Complete code quality toolkit" +version: "2.0.0" +type: "package" +items: # Only needed because we reference an external component + - type: "mcp-server" + path: "../security/vulnerability-scanner" +``` + +```yaml +# metadata.es.yml +name: "Suite de Calidad de Código" +description: "Kit de herramientas completo para calidad de código" +version: "2.0.0" +type: "package" +``` + +Note: Advanced features like dependencies and configuration can be added later when needed. The basic structure focuses on essential metadata and local components. + +[Previous sections unchanged] diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 278df120c2..84b938e4cb 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "e2e", "version": "0.1.0", + "dependencies": { + "npm-run-all": "^4.1.5" + }, "devDependencies": { "@types/mocha": "^10.0.10", "@vscode/test-cli": "^0.0.9", @@ -404,11 +407,71 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/base64-js": { @@ -553,6 +616,53 @@ "node": ">=14.14.0" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -752,7 +862,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/convert-source-map": { @@ -784,6 +893,57 @@ "node": ">= 8" } }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -815,6 +975,40 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/diff": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", @@ -825,6 +1019,20 @@ "node": ">=0.3.1" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -853,6 +1061,142 @@ "node": ">=10.13.0" } }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -916,6 +1260,21 @@ "flat": "cli.js" } }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -955,6 +1314,44 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -965,6 +1362,60 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -999,13 +1450,52 @@ "node": ">= 6" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, "license": "ISC" }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -1016,6 +1506,72 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -1026,6 +1582,12 @@ "he": "bin/he" } }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "license": "ISC" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -1108,6 +1670,77 @@ "dev": true, "license": "ISC" }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1121,6 +1754,82 @@ "node": ">=8" } }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -1131,6 +1840,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1141,6 +1865,24 @@ "node": ">=8" } }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -1167,6 +1909,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -1177,6 +1931,22 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -1187,6 +1957,99 @@ "node": ">=8" } }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", @@ -1200,6 +2063,49 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -1211,7 +2117,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -1295,6 +2200,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT" + }, "node_modules/jszip": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", @@ -1318,6 +2229,21 @@ "immediate": "~3.0.5" } }, + "node_modules/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1374,6 +2300,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memorystream": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", + "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -1482,6 +2425,33 @@ "dev": true, "license": "MIT" }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "license": "MIT" + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1492,6 +2462,232 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" + }, + "bin": { + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm-run-all/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/npm-run-all/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/npm-run-all/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/npm-run-all/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/npm-run-all/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/npm-run-all/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/npm-run-all/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-all/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-run-all/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1610,6 +2806,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -1656,6 +2869,19 @@ "dev": true, "license": "(MIT AND Zlib)" }, + "node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -1686,6 +2912,12 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, "node_modules/path-scurry": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", @@ -1703,6 +2935,18 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1716,6 +2960,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pidtree": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.3.1.tgz", + "integrity": "sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==", + "license": "MIT", + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -1733,6 +3007,20 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "license": "MIT", + "dependencies": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -1762,6 +3050,48 @@ "node": ">=8.10.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1772,6 +3102,26 @@ "node": ">=0.10.0" } }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/restore-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", @@ -1796,6 +3146,31 @@ "dev": true, "license": "ISC" }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -1803,6 +3178,45 @@ "dev": true, "license": "MIT" }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -1826,6 +3240,52 @@ "randombytes": "^2.1.0" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", @@ -1856,6 +3316,90 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -1869,6 +3413,38 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "license": "CC0-1.0" + }, "node_modules/stdin-discarder": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", @@ -1959,6 +3535,80 @@ "node": ">=8" } }, + "node_modules/string.prototype.padend": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/string.prototype.padend/-/string.prototype.padend-3.1.6.tgz", + "integrity": "sha512-XZpspuSB7vJWhvJc9DLSlrXl1mcA2BdoY5jjnS135ydXqLoqhs96JjDtCkjJEQHvfqZIp9hBuBMgI589peyx9Q==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", @@ -1999,6 +3649,15 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -2025,6 +3684,18 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", @@ -2109,6 +3780,80 @@ "node": ">=8.0" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.8.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", @@ -2123,6 +3868,24 @@ "node": ">=14.17" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2145,6 +3908,16 @@ "node": ">=10.12.0" } }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -2161,6 +3934,97 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/workerpool": { "version": "6.5.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", diff --git a/e2e/package.json b/e2e/package.json index d6a2c7af00..1d98fe336a 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -8,9 +8,12 @@ "test": "npm run build && npx dotenvx run -f .env.local -- node ./out/runTest.js", "ci": "npm run vscode-test && npm run test", "build": "rimraf out && tsc -p tsconfig.json", - "vscode-test": "cd .. && npm run vscode-test" + "vscode-test": "cd .. && npm run vscode-test", + "clean": "rimraf out" + }, + "dependencies": { + "npm-run-all": "^4.1.5" }, - "dependencies": {}, "devDependencies": { "@types/mocha": "^10.0.10", "@vscode/test-cli": "^0.0.9", diff --git a/e2e/src/suite/package-manager.test.ts b/e2e/src/suite/package-manager.test.ts new file mode 100644 index 0000000000..901c7b4717 --- /dev/null +++ b/e2e/src/suite/package-manager.test.ts @@ -0,0 +1,255 @@ +import * as assert from "assert" +import * as path from "path" +import * as vscode from "vscode" +import { waitFor } from "./utils" +import { PackageManagerItem, PackageManagerSource } from "../../../src/services/package-manager/types" +import type { RooCodeAPI } from "../../../src/exports/roo-code" + +interface PackageManager { + addSource(source: PackageManagerSource): Promise + removeSource(url: string): Promise + getSources(): Promise + getItems(): Promise +} + +interface WaitForOptions { + timeout?: number + interval?: number + message?: string +} + +suite("Package Manager Integration Tests", () => { + let extension: vscode.Extension | undefined + + suiteSetup(async () => { + extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline") + if (!extension) { + throw new Error("Extension not found") + } + if (!extension.isActive) { + await extension.activate() + } + }) + + test("should load sources from real cache location", async () => { + // Get the package manager service + const packageManager = (api as any).getPackageManager() as PackageManager + assert.ok(packageManager, "Package manager service should be available") + + // Add a test source + const testSource: PackageManagerSource = { + url: "https://github.com/roo-team/package-manager-template", + enabled: true, + } + await packageManager.addSource(testSource) + + // Wait for the source to be loaded + await waitFor( + async () => { + const sources = await packageManager.getSources() + return sources.some((source) => source.url === testSource.url) + }, + { message: "Source should be added to the list" } as WaitForOptions, + ) + + // Verify the cache directory exists + const cacheDir = path.join("/test/global-storage", "package-manager-cache", "package-manager-template") + let cacheExists = false + try { + await vscode.workspace.fs.stat(vscode.Uri.file(cacheDir)) + cacheExists = true + } catch { + cacheExists = false + } + assert.ok(cacheExists, "Cache directory should exist") + + // Load items from the source + const items = await packageManager.getItems() + assert.ok(items.length > 0, "Should load items from cache") + + // Verify items have correct metadata + const hasValidItems = items.every((item: PackageManagerItem) => { + return ( + typeof item.name === "string" && + typeof item.description === "string" && + typeof item.version === "string" && + ["mode", "mcp server", "prompt", "package"].includes(item.type) + ) + }) + assert.ok(hasValidItems, "All items should have valid metadata") + + // Clean up + await packageManager.removeSource(testSource.url) + }) + + test("should handle package metadata with external items", async () => { + const packageManager = (api as any).getPackageManager() as PackageManager + + // Add a source with package metadata + const packageSource: PackageManagerSource = { + url: "https://github.com/roo-team/package-with-externals", + name: "Test Package Source", + enabled: true, + } + await packageManager.addSource(packageSource) + + // Wait for the source to be loaded + await waitFor( + async () => { + const sources = await packageManager.getSources() + return sources.some((source) => source.url === packageSource.url) + }, + { message: "Package source should be added to the list" } as WaitForOptions, + ) + + // Load items and verify package metadata + const items = await packageManager.getItems() + const packageItems = items.filter( + (item: PackageManagerItem) => item.repoUrl === packageSource.url && item.type === "package", + ) + + assert.ok(packageItems.length > 0, "Should find package items") + assert.ok( + packageItems.some((item) => item.items && item.items.length > 0), + "Should have packages with external items", + ) + + // Clean up + await packageManager.removeSource(packageSource.url) + }) + + test("should handle items with optional fields", async () => { + const packageManager = (api as any).getPackageManager() as PackageManager + + // Add a source with items containing optional fields + const detailedSource: PackageManagerSource = { + url: "https://github.com/roo-team/detailed-items", + enabled: true, + } + await packageManager.addSource(detailedSource) + + // Wait for the source to be loaded + await waitFor( + async () => { + const sources = await packageManager.getSources() + return sources.some((source) => source.url === detailedSource.url) + }, + { message: "Detailed source should be added to the list" } as WaitForOptions, + ) + + // Load items and verify optional fields + const items = await packageManager.getItems() + const detailedItems = items.filter((item: PackageManagerItem) => item.repoUrl === detailedSource.url) + + assert.ok(detailedItems.length > 0, "Should find detailed items") + assert.ok( + detailedItems.some((item) => item.author && item.tags && item.lastUpdated && item.sourceUrl), + "Should have items with optional fields", + ) + + // Clean up + await packageManager.removeSource(detailedSource.url) + }) + + test("should handle invalid source gracefully", async () => { + const packageManager = (api as any).getPackageManager() as PackageManager + + // Add an invalid source + const invalidSource: PackageManagerSource = { + url: "https://github.com/invalid/repo", + enabled: true, + } + await packageManager.addSource(invalidSource) + + // Wait for the source to be processed + await waitFor( + async () => { + const sources = await packageManager.getSources() + return sources.some((source) => source.url === invalidSource.url) + }, + { message: "Invalid source should be added to the list" } as WaitForOptions, + ) + + // Verify it returns empty items without crashing + const items = await packageManager.getItems() + assert.deepStrictEqual( + items.filter((item: PackageManagerItem) => item.repoUrl === invalidSource.url), + [], + "Invalid source should return no items", + ) + + // Clean up + await packageManager.removeSource(invalidSource.url) + }) + + test("should handle source with missing metadata gracefully", async () => { + const packageManager = (api as any).getPackageManager() as PackageManager + + // Add a source with missing metadata + const badSource: PackageManagerSource = { + url: "https://github.com/roo-team/bad-package-template", + enabled: true, + } + await packageManager.addSource(badSource) + + // Wait for the source to be processed + await waitFor( + async () => { + const sources = await packageManager.getSources() + return sources.some((source) => source.url === badSource.url) + }, + { message: "Bad source should be added to the list" } as WaitForOptions, + ) + + // Verify it returns empty items without crashing + const items = await packageManager.getItems() + assert.deepStrictEqual( + items.filter((item: PackageManagerItem) => item.repoUrl === badSource.url), + [], + "Source with missing metadata should return no items", + ) + + // Clean up + await packageManager.removeSource(badSource.url) + }) + + test("should handle localized metadata", async () => { + const packageManager = (api as any).getPackageManager() as PackageManager + + // Add a source with localized metadata + const localizedSource: PackageManagerSource = { + url: "https://github.com/roo-team/localized-package-template", + enabled: true, + } + await packageManager.addSource(localizedSource) + + // Wait for the source to be processed + await waitFor( + async () => { + const sources = await packageManager.getSources() + return sources.some((source) => source.url === localizedSource.url) + }, + { message: "Localized source should be added to the list" } as WaitForOptions, + ) + + // Load items from the source + const items = await packageManager.getItems() + const localizedItems = items.filter((item: PackageManagerItem) => item.repoUrl === localizedSource.url) + + // Verify items are loaded with correct metadata + assert.ok(localizedItems.length > 0, "Should load localized items") + assert.ok( + localizedItems.every((item: PackageManagerItem) => { + return ( + typeof item.name === "string" && + typeof item.description === "string" && + typeof item.version === "string" + ) + }), + "All localized items should have valid metadata", + ) + + // Clean up + await packageManager.removeSource(localizedSource.url) + }) +}) diff --git a/package-lock.json b/package-lock.json index 71c7306666..683f7b9b00 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@mistralai/mistralai": "^1.3.6", "@modelcontextprotocol/sdk": "^1.7.0", "@types/clone-deep": "^4.0.4", + "@types/js-yaml": "^4.0.9", "@types/pdf-parse": "^1.1.4", "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.5", @@ -39,6 +40,7 @@ "i18next": "^24.2.2", "isbinaryfile": "^5.0.2", "js-tiktoken": "^1.0.19", + "js-yaml": "^4.1.0", "mammoth": "^1.8.0", "monaco-vscode-textmate-theme-converter": "^0.1.7", "node-ipc": "^12.0.0", @@ -8912,6 +8914,12 @@ "pretty-format": "^29.0.0" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, "node_modules/@types/minimatch": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", @@ -9841,8 +9849,7 @@ "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", @@ -15385,7 +15392,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, diff --git a/package-manager-template/README.md b/package-manager-template/README.md index 2beebf7cb0..139993ec3f 100644 --- a/package-manager-template/README.md +++ b/package-manager-template/README.md @@ -1,63 +1,46 @@ -# Roo-Code Package Manager Template +# Package Manager Template -This repository serves as a template for creating package manager items for Roo-Code. It contains examples of different types of package manager items and the required structure for each. +This template provides a basic structure for creating a package manager source repository. The structure follows the required format for Roo Code's package manager. -## Repository Structure +## Structure ``` -package manager-template/ -├── README.md -├── metadata.yml -├── roles/ -│ ├── developer-role/ -│ │ ├── metadata.yml -│ │ └── role.md -│ └── architect-role/ -│ ├── metadata.yml -│ └── role.md -├── mcp-servers/ -│ ├── file-analyzer/ -│ │ ├── metadata.yml -│ │ └── server.js -│ └── code-generator/ -│ ├── metadata.yml -│ └── server.js -└── storage-systems/ - └── github-storage/ - ├── metadata.yml - └── storage.js +/ +├── metadata.en.yml # Required: Repository metadata +└── mcp servers/ # Required: At least one of: mcp servers, roles, storage systems, or items + └── example-server/ + └── metadata.en.yml ``` -## Root Metadata +## Required Files -The `metadata.yml` file at the root of the repository contains information about the repository itself: +### Root metadata.en.yml ```yaml -name: "Example Package Manager Repository" -description: "A collection of example package manager items for Roo-Code" +name: "Your Repository Name" +description: "Your repository description" version: "1.0.0" ``` -## Item Metadata - -Each item in the package manager has its own `metadata.yml` file that contains information about the item: +### MCP Server metadata.en.yml ```yaml -name: "Item Name" -description: "Item description" -type: "role|mcp-server|storage|other" +name: "Your MCP Server Name" +description: "Your MCP server description" +type: "mcp server" version: "1.0.0" -tags: ["tag1", "tag2"] -sourceUrl: "https://github.com/username/repo" # Optional URL for the "view source" button ``` -## Testing +## Usage -To test this repository with the Roo-Code Package Manager: +1. Copy this template to create your own package manager repository +2. Update the metadata.en.yml with your repository information +3. Add your MCP servers, roles, or other components +4. Each component must have its own metadata.en.yml file with the required fields -1. Create a new GitHub repository -2. Upload this template to the repository -3. In Roo-Code, go to the Package Manager tab -4. Click on the "Sources" tab -5. Add your repository URL -6. Go back to the "Browse" tab to see your package manager items \ No newline at end of file +## Validation Requirements + +- The root metadata.en.yml must have name, description, and version fields +- Version must be in semver format (e.g., 1.0.0) +- The repository must have at least one of: mcp servers, roles, storage systems, or items directories +- Each component must have a metadata.en.yml with the required fields including the correct type diff --git a/package-manager-template/groups/data-engineering/mcp servers/data-processor/metadata.en.yml b/package-manager-template/groups/data-engineering/mcp servers/data-processor/metadata.en.yml new file mode 100644 index 0000000000..bc75efa1bf --- /dev/null +++ b/package-manager-template/groups/data-engineering/mcp servers/data-processor/metadata.en.yml @@ -0,0 +1,5 @@ +name: "Data Processor" +description: "An MCP server for processing and transforming data files with support for various formats" +type: "mcp server" +version: "1.0.0" +tags: ["data-processing", "etl", "transformation", "data-engineering"] \ No newline at end of file diff --git a/package-manager-template/groups/data-engineering/mcp servers/data-processor/server.js b/package-manager-template/groups/data-engineering/mcp servers/data-processor/server.js new file mode 100644 index 0000000000..8451460bb5 --- /dev/null +++ b/package-manager-template/groups/data-engineering/mcp servers/data-processor/server.js @@ -0,0 +1,143 @@ +const { MCPServer } = require("@modelcontextprotocol/core") + +class DataProcessor extends MCPServer { + constructor() { + super({ + name: "Data Processor", + description: "Processes and transforms data files", + version: "1.0.0", + capabilities: ["file-processing", "data-transformation"], + }) + + this.registerHandler("process-file", this.processFile.bind(this)) + this.registerHandler("transform-data", this.transformData.bind(this)) + } + + async processFile(context, params) { + const { filePath, options } = params + + // Validate parameters + if (!filePath) { + throw new Error("File path is required") + } + + try { + // Read file content + const content = await context.readFile(filePath) + + // Process based on file type + const fileType = this.detectFileType(filePath) + const processedData = await this.processContent(content, fileType, options) + + return { + success: true, + data: processedData, + metadata: { + fileType, + processedAt: new Date().toISOString(), + rowCount: processedData.length, + }, + } + } catch (error) { + return { + success: false, + error: error.message, + } + } + } + + async transformData(context, params) { + const { data, transformations } = params + + // Validate parameters + if (!data || !transformations) { + throw new Error("Data and transformations are required") + } + + try { + let transformedData = data + + // Apply each transformation in sequence + for (const transform of transformations) { + transformedData = await this.applyTransformation(transformedData, transform) + } + + return { + success: true, + data: transformedData, + metadata: { + transformations: transformations.map((t) => t.type), + transformedAt: new Date().toISOString(), + }, + } + } catch (error) { + return { + success: false, + error: error.message, + } + } + } + + detectFileType(filePath) { + const extension = filePath.split(".").pop().toLowerCase() + const fileTypes = { + csv: "CSV", + json: "JSON", + xml: "XML", + xlsx: "Excel", + parquet: "Parquet", + } + return fileTypes[extension] || "Unknown" + } + + async processContent(content, fileType, options = {}) { + // Implementation would handle different file types + switch (fileType) { + case "CSV": + return this.processCSV(content, options) + case "JSON": + return this.processJSON(content, options) + case "XML": + return this.processXML(content, options) + default: + throw new Error(`Unsupported file type: ${fileType}`) + } + } + + async applyTransformation(data, transform) { + // Implementation would handle different transformation types + switch (transform.type) { + case "filter": + return data.filter(transform.condition) + case "map": + return data.map(transform.mapper) + case "aggregate": + return this.aggregate(data, transform.aggregation) + default: + throw new Error(`Unsupported transformation: ${transform.type}`) + } + } + + // Helper methods for specific file types + processCSV(content, options) { + // CSV processing implementation + return [] + } + + processJSON(content, options) { + // JSON processing implementation + return [] + } + + processXML(content, options) { + // XML processing implementation + return [] + } + + aggregate(data, aggregation) { + // Aggregation implementation + return {} + } +} + +module.exports = new DataProcessor() diff --git a/package-manager-template/groups/data-engineering/metadata.en.yml b/package-manager-template/groups/data-engineering/metadata.en.yml new file mode 100644 index 0000000000..c0d3e2c183 --- /dev/null +++ b/package-manager-template/groups/data-engineering/metadata.en.yml @@ -0,0 +1,4 @@ +name: "Data Engineering Tools" +description: "A collection of data engineering roles and tools" +version: "1.0.0" +tags: ["data-engineering", "data", "analytics"] \ No newline at end of file diff --git a/package-manager-template/groups/data-engineering/modes/data-engineer-mode/metadata.en.yml b/package-manager-template/groups/data-engineering/modes/data-engineer-mode/metadata.en.yml new file mode 100644 index 0000000000..ec0ed6e79d --- /dev/null +++ b/package-manager-template/groups/data-engineering/modes/data-engineer-mode/metadata.en.yml @@ -0,0 +1,5 @@ +name: "Data Engineer" +description: "A mode focused on building and maintaining data pipelines, ETL processes, and data infrastructure" +type: "mode" +version: "1.0.0" +tags: ["data-engineering", "etl", "pipelines", "infrastructure"] \ No newline at end of file diff --git a/package-manager-template/groups/data-engineering/modes/data-engineer-mode/mode.md b/package-manager-template/groups/data-engineering/modes/data-engineer-mode/mode.md new file mode 100644 index 0000000000..89cdf7c8f0 --- /dev/null +++ b/package-manager-template/groups/data-engineering/modes/data-engineer-mode/mode.md @@ -0,0 +1,56 @@ +# Data Engineer Mode + +## Mode Description + +As a Data Engineer, you are responsible for designing, building, and maintaining data pipelines and infrastructure. You work closely with data scientists and analysts to ensure data is accessible, reliable, and efficient. + +## Core Responsibilities + +- Design and implement data pipelines +- Build ETL processes +- Maintain data infrastructure +- Optimize data delivery +- Ensure data quality and reliability +- Implement data security measures + +## Technical Skills + +- SQL and database management +- ETL tools and processes +- Data warehousing +- Big data technologies +- Python/Scala programming +- Cloud platforms (AWS, GCP, Azure) + +## Best Practices + +1. Document all data pipelines +2. Implement data validation checks +3. Monitor pipeline performance +4. Follow data security protocols +5. Maintain data lineage +6. Practice data governance + +## Collaboration Guidelines + +- Work closely with data scientists +- Coordinate with infrastructure teams +- Support analytics teams +- Engage with business stakeholders + +## Success Metrics + +- Pipeline reliability +- Data quality scores +- Query performance +- System uptime +- Data freshness +- Issue resolution time + +## Tools and Technologies + +- ETL Tools: Apache Airflow, dbt +- Databases: PostgreSQL, MongoDB +- Big Data: Spark, Hadoop +- Cloud: AWS EMR, GCP Dataflow +- Languages: Python, SQL, Scala diff --git a/package-manager-template/mcp servers/example-server/metadata.en.yml b/package-manager-template/mcp servers/example-server/metadata.en.yml new file mode 100644 index 0000000000..ffed52a931 --- /dev/null +++ b/package-manager-template/mcp servers/example-server/metadata.en.yml @@ -0,0 +1,4 @@ +name: "Example MCP Server" +description: "An example MCP server for testing package manager functionality" +type: "mcp server" +version: "1.0.0" \ No newline at end of file diff --git a/package-manager-template/mcp-servers/file-analyzer/metadata.yml b/package-manager-template/mcp servers/file-analyzer/metadata.en.yml similarity index 93% rename from package-manager-template/mcp-servers/file-analyzer/metadata.yml rename to package-manager-template/mcp servers/file-analyzer/metadata.en.yml index 5546f4c4b7..f4fc488460 100644 --- a/package-manager-template/mcp-servers/file-analyzer/metadata.yml +++ b/package-manager-template/mcp servers/file-analyzer/metadata.en.yml @@ -1,6 +1,6 @@ name: "File Analyzer MCP Server" description: "An MCP server that analyzes files for code quality, security issues, and performance optimizations" -type: "mcp-server" +type: "mcp server" version: "1.0.0" tags: ["file-analyzer", "code-quality", "security", "performance"] sourceUrl: "https://github.com/roo-team/file-analyzer-server" \ No newline at end of file diff --git a/package-manager-template/mcp-servers/file-analyzer/server.js b/package-manager-template/mcp servers/file-analyzer/server.js similarity index 100% rename from package-manager-template/mcp-servers/file-analyzer/server.js rename to package-manager-template/mcp servers/file-analyzer/server.js diff --git a/package-manager-template/metadata.en.yml b/package-manager-template/metadata.en.yml new file mode 100644 index 0000000000..0e2f5790cf --- /dev/null +++ b/package-manager-template/metadata.en.yml @@ -0,0 +1,3 @@ +name: "Package Manager Template" +description: "A template repository for creating package manager sources" +version: "1.0.0" diff --git a/package-manager-template/metadata.yml b/package-manager-template/metadata.yml deleted file mode 100644 index 556121a4ae..0000000000 --- a/package-manager-template/metadata.yml +++ /dev/null @@ -1,3 +0,0 @@ -name: "Example Package Manager Repository" -description: "A collection of example package manager items for Roo-Code" -version: "1.0.0" \ No newline at end of file diff --git a/package-manager-template/roles/developer-role/metadata.yml b/package-manager-template/modes/developer-mode/metadata.en.yml similarity index 61% rename from package-manager-template/roles/developer-role/metadata.yml rename to package-manager-template/modes/developer-mode/metadata.en.yml index 30fc9ea18c..40b625063f 100644 --- a/package-manager-template/roles/developer-role/metadata.yml +++ b/package-manager-template/modes/developer-mode/metadata.en.yml @@ -1,6 +1,6 @@ -name: "Full-Stack Developer Role" -description: "A role for a full-stack developer with expertise in web development, databases, and APIs" -type: "role" +name: "Full-Stack Developer Mode" +description: "A mode for a full-stack developer with expertise in web development, databases, and APIs" +type: "mode" version: "1.0.0" tags: ["developer", "full-stack", "web", "database", "api"] sourceUrl: "https://github.com/roo-team/developer-resources" \ No newline at end of file diff --git a/package-manager-template/roles/developer-role/role.md b/package-manager-template/modes/developer-mode/mode.md similarity index 94% rename from package-manager-template/roles/developer-role/role.md rename to package-manager-template/modes/developer-mode/mode.md index 9dbc22c226..56286a29d8 100644 --- a/package-manager-template/roles/developer-role/role.md +++ b/package-manager-template/modes/developer-mode/mode.md @@ -1,6 +1,6 @@ -# Full-Stack Developer Role +# Full-Stack Developer Mode -## Role Description +## Mode Description You are a Full-Stack Developer with expertise in web development, databases, and APIs. You excel at building complete web applications from front-end to back-end, with a focus on creating robust, scalable, and maintainable code. @@ -48,4 +48,4 @@ You are a Full-Stack Developer with expertise in web development, databases, and - Keep functions small and focused - Document code and APIs - Regularly refactor code to improve quality -- Stay updated with the latest technologies and best practices \ No newline at end of file +- Stay updated with the latest technologies and best practices diff --git a/package-manager-template/packages/data-platform/mcp servers/data-validator/metadata.en.yml b/package-manager-template/packages/data-platform/mcp servers/data-validator/metadata.en.yml new file mode 100644 index 0000000000..fee752115f --- /dev/null +++ b/package-manager-template/packages/data-platform/mcp servers/data-validator/metadata.en.yml @@ -0,0 +1,6 @@ +name: "Data Validator" +description: "An MCP server for validating data quality, schema compliance, and business rules" +type: "mcp server" +version: "1.0.0" +tags: ["validation", "data-quality", "schema", "rules-engine", "data-platform"] +sourceUrl: "https://github.com/example/data-platform/mcp servers/data-validator" \ No newline at end of file diff --git a/package-manager-template/packages/data-platform/mcp servers/data-validator/server.js b/package-manager-template/packages/data-platform/mcp servers/data-validator/server.js new file mode 100644 index 0000000000..55e6c1da42 --- /dev/null +++ b/package-manager-template/packages/data-platform/mcp servers/data-validator/server.js @@ -0,0 +1,124 @@ +const { MCPServer } = require("@modelcontextprotocol/core") + +class DataValidator extends MCPServer { + constructor() { + super({ + name: "Data Validator", + description: "Validates data quality and schema compliance", + version: "1.0.0", + capabilities: ["schema-validation", "data-quality", "rules-engine"], + }) + + this.registerHandler("validate-schema", this.validateSchema.bind(this)) + this.registerHandler("validate-quality", this.validateQuality.bind(this)) + this.registerHandler("validate-rules", this.validateRules.bind(this)) + } + + async validateSchema(context, params) { + const { data, schema } = params + + if (!data || !schema) { + throw new Error("Data and schema are required") + } + + try { + const validationResults = await this.performSchemaValidation(data, schema) + return { + success: validationResults.valid, + errors: validationResults.errors, + metadata: { + schemaVersion: schema.version, + validatedAt: new Date().toISOString(), + recordCount: Array.isArray(data) ? data.length : 1, + }, + } + } catch (error) { + return { + success: false, + error: error.message, + } + } + } + + async validateQuality(context, params) { + const { data, rules } = params + + if (!data || !rules) { + throw new Error("Data and quality rules are required") + } + + try { + const qualityResults = await this.performQualityChecks(data, rules) + return { + success: qualityResults.passed, + issues: qualityResults.issues, + metrics: qualityResults.metrics, + metadata: { + rulesApplied: rules.length, + checkedAt: new Date().toISOString(), + }, + } + } catch (error) { + return { + success: false, + error: error.message, + } + } + } + + async validateRules(context, params) { + const { data, businessRules } = params + + if (!data || !businessRules) { + throw new Error("Data and business rules are required") + } + + try { + const ruleResults = await this.evaluateBusinessRules(data, businessRules) + return { + success: ruleResults.passed, + violations: ruleResults.violations, + metadata: { + rulesEvaluated: businessRules.length, + evaluatedAt: new Date().toISOString(), + }, + } + } catch (error) { + return { + success: false, + error: error.message, + } + } + } + + async performSchemaValidation(data, schema) { + // Implementation would validate data against the provided schema + return { + valid: true, + errors: [], + } + } + + async performQualityChecks(data, rules) { + // Implementation would check data quality based on rules + return { + passed: true, + issues: [], + metrics: { + completeness: 100, + accuracy: 100, + consistency: 100, + }, + } + } + + async evaluateBusinessRules(data, rules) { + // Implementation would evaluate business rules against the data + return { + passed: true, + violations: [], + } + } +} + +module.exports = new DataValidator() diff --git a/package-manager-template/packages/data-platform/metadata.en.yml b/package-manager-template/packages/data-platform/metadata.en.yml new file mode 100644 index 0000000000..6c01a09217 --- /dev/null +++ b/package-manager-template/packages/data-platform/metadata.en.yml @@ -0,0 +1,5 @@ +name: "Data Platform Package" +description: "A complete data platform solution including roles, servers, and storage systems" +type: "package" +version: "1.0.0" +tags: ["data-platform", "enterprise", "data-engineering", "complete-solution"] \ No newline at end of file diff --git a/package-manager-template/packages/data-platform/metadata.es.yml b/package-manager-template/packages/data-platform/metadata.es.yml new file mode 100644 index 0000000000..48f09b4c4a --- /dev/null +++ b/package-manager-template/packages/data-platform/metadata.es.yml @@ -0,0 +1,5 @@ +name: "Paquete de Plataforma de Datos" +description: "Una solución completa de plataforma de datos que incluye roles, servidores y sistemas de almacenamiento" +type: "package" +version: "1.0.0" +tags: ["plataforma-datos", "empresarial", "ingenieria-datos", "solucion-completa"] \ No newline at end of file diff --git a/package-manager-template/packages/data-platform/metadata.ja.yml b/package-manager-template/packages/data-platform/metadata.ja.yml new file mode 100644 index 0000000000..95a9e2aa4b --- /dev/null +++ b/package-manager-template/packages/data-platform/metadata.ja.yml @@ -0,0 +1,5 @@ +name: "データプラットフォームパッケージ" +description: "ロール、サーバー、ストレージシステムを含む完全なデータプラットフォームソリューション" +type: "package" +version: "1.0.0" +tags: ["データプラットフォーム", "エンタープライズ", "データエンジニアリング", "完全ソリューション"] \ No newline at end of file diff --git a/package-manager-template/packages/data-platform/modes/platform-admin-mode/metadata.en.yml b/package-manager-template/packages/data-platform/modes/platform-admin-mode/metadata.en.yml new file mode 100644 index 0000000000..66225959bd --- /dev/null +++ b/package-manager-template/packages/data-platform/modes/platform-admin-mode/metadata.en.yml @@ -0,0 +1,6 @@ +name: "Data Platform Administrator" +description: "Administrative mode responsible for managing and maintaining the data platform infrastructure" +type: "mode" +version: "1.0.0" +tags: ["admin", "platform", "infrastructure", "data-platform"] +sourceUrl: "https://github.com/example/data-platform/modes/platform-admin" \ No newline at end of file diff --git a/package-manager-template/packages/data-platform/modes/platform-admin-mode/mode.md b/package-manager-template/packages/data-platform/modes/platform-admin-mode/mode.md new file mode 100644 index 0000000000..5ee81e9c18 --- /dev/null +++ b/package-manager-template/packages/data-platform/modes/platform-admin-mode/mode.md @@ -0,0 +1,129 @@ +# Data Platform Administrator + +## Mode Overview + +The Data Platform Administrator is responsible for the overall management, security, and maintenance of the data platform infrastructure. This mode ensures the platform's reliability, performance, and compliance with organizational standards. + +## Key Responsibilities + +### Platform Management + +- Configure and maintain data platform components +- Monitor system performance and health +- Implement and maintain backup/recovery procedures +- Manage platform upgrades and patches +- Handle capacity planning and scaling + +### Security Administration + +- Manage user access and permissions +- Implement security policies and controls +- Monitor security logs and alerts +- Conduct security audits +- Ensure compliance with data protection regulations + +### Infrastructure Operations + +- Deploy and configure platform services +- Manage cloud resources and infrastructure +- Optimize platform performance +- Implement high availability solutions +- Monitor resource utilization + +### Team Support + +- Provide technical guidance to platform users +- Troubleshoot platform issues +- Create and maintain documentation +- Train team members on platform features +- Collaborate with development teams + +## Required Skills + +### Technical Skills + +- Cloud platform expertise (AWS/Azure/GCP) +- Infrastructure as Code (Terraform, CloudFormation) +- Container orchestration (Kubernetes) +- Database administration +- Security and compliance +- Monitoring and logging systems +- Automation and scripting + +### Soft Skills + +- Problem-solving +- Communication +- Team leadership +- Project management +- Time management +- Documentation + +## Tools and Technologies + +### Infrastructure + +- Kubernetes +- Docker +- Terraform +- Git +- CI/CD tools + +### Monitoring + +- Prometheus +- Grafana +- ELK Stack +- CloudWatch/StackDriver + +### Security + +- IAM systems +- Security scanning tools +- Compliance frameworks +- Encryption tools + +## Best Practices + +1. Infrastructure as Code + + - Maintain all infrastructure configurations in version control + - Use automated deployment processes + - Document all configuration changes + +2. Security First + + - Follow principle of least privilege + - Regularly review access permissions + - Implement multi-layer security controls + - Conduct regular security audits + +3. Monitoring and Alerting + + - Set up comprehensive monitoring + - Configure meaningful alerts + - Maintain incident response procedures + - Regular review of metrics and logs + +4. Documentation + - Keep documentation up-to-date + - Document all procedures + - Maintain runbooks for common issues + - Create user guides for platform features + +## Success Metrics + +- Platform uptime and availability +- Security incident response time +- User satisfaction scores +- Resource utilization efficiency +- Backup and recovery success rates +- Compliance audit results + +## Collaboration + +- Work with development teams +- Coordinate with security teams +- Support data engineering teams +- Engage with business stakeholders +- Partner with cloud providers diff --git a/package-manager-template/storage-systems/github-storage/metadata.yml b/package-manager-template/storage-systems/github-storage/metadata.yml deleted file mode 100644 index 2153c376dd..0000000000 --- a/package-manager-template/storage-systems/github-storage/metadata.yml +++ /dev/null @@ -1,6 +0,0 @@ -name: "GitHub Storage System" -description: "A storage system that uses GitHub repositories to store and retrieve data" -type: "storage" -version: "1.0.0" -tags: ["storage", "github", "git", "repository"] -sourceUrl: "https://github.com/roo-team/github-storage-system" \ No newline at end of file diff --git a/package-manager-template/storage-systems/github-storage/storage.js b/package-manager-template/storage-systems/github-storage/storage.js deleted file mode 100644 index ac85a7b213..0000000000 --- a/package-manager-template/storage-systems/github-storage/storage.js +++ /dev/null @@ -1,178 +0,0 @@ -/** - * GitHub Storage System - * - * This storage system uses GitHub repositories to store and retrieve data. - */ - -class GitHubStorage { - /** - * Constructor for the GitHub Storage System - * @param {Object} config - Configuration object - * @param {string} config.owner - GitHub repository owner - * @param {string} config.repo - GitHub repository name - * @param {string} config.token - GitHub personal access token - * @param {string} config.branch - GitHub branch to use (default: main) - */ - constructor(config) { - this.owner = config.owner - this.repo = config.repo - this.token = config.token - this.branch = config.branch || "main" - this.baseUrl = `https://api.github.com/repos/${this.owner}/${this.repo}` - this.headers = { - Authorization: `token ${this.token}`, - Accept: "application/vnd.github.v3+json", - "Content-Type": "application/json", - } - } - - /** - * Store data in the GitHub repository - * @param {string} path - Path to store the data at - * @param {any} data - Data to store - * @param {string} message - Commit message - * @returns {Promise} - Result of the operation - */ - async store(path, data, message = "Update data") { - try { - // Convert data to string if it's not already - const content = typeof data === "string" ? data : JSON.stringify(data, null, 2) - - // Encode content to base64 - const encodedContent = Buffer.from(content).toString("base64") - - // Check if file exists - let sha - try { - const response = await fetch(`${this.baseUrl}/contents/${path}?ref=${this.branch}`, { - headers: this.headers, - }) - - if (response.ok) { - const fileData = await response.json() - sha = fileData.sha - } - } catch (error) { - // File doesn't exist, which is fine - } - - // Create or update file - const body = { - message, - content: encodedContent, - branch: this.branch, - } - - if (sha) { - body.sha = sha - } - - const response = await fetch(`${this.baseUrl}/contents/${path}`, { - method: "PUT", - headers: this.headers, - body: JSON.stringify(body), - }) - - if (!response.ok) { - throw new Error(`Failed to store data: ${response.statusText}`) - } - - return await response.json() - } catch (error) { - throw new Error(`Error storing data: ${error.message}`) - } - } - - /** - * Retrieve data from the GitHub repository - * @param {string} path - Path to retrieve the data from - * @returns {Promise} - Retrieved data - */ - async retrieve(path) { - try { - const response = await fetch(`${this.baseUrl}/contents/${path}?ref=${this.branch}`, { - headers: this.headers, - }) - - if (!response.ok) { - throw new Error(`Failed to retrieve data: ${response.statusText}`) - } - - const data = await response.json() - const content = Buffer.from(data.content, "base64").toString("utf-8") - - // Try to parse as JSON, return as string if not valid JSON - try { - return JSON.parse(content) - } catch (error) { - return content - } - } catch (error) { - throw new Error(`Error retrieving data: ${error.message}`) - } - } - - /** - * Delete data from the GitHub repository - * @param {string} path - Path to delete - * @param {string} message - Commit message - * @returns {Promise} - Result of the operation - */ - async delete(path, message = "Delete data") { - try { - // Get the file's SHA - const response = await fetch(`${this.baseUrl}/contents/${path}?ref=${this.branch}`, { - headers: this.headers, - }) - - if (!response.ok) { - throw new Error(`Failed to get file info: ${response.statusText}`) - } - - const data = await response.json() - - // Delete the file - const deleteResponse = await fetch(`${this.baseUrl}/contents/${path}`, { - method: "DELETE", - headers: this.headers, - body: JSON.stringify({ - message, - sha: data.sha, - branch: this.branch, - }), - }) - - if (!deleteResponse.ok) { - throw new Error(`Failed to delete data: ${deleteResponse.statusText}`) - } - - return await deleteResponse.json() - } catch (error) { - throw new Error(`Error deleting data: ${error.message}`) - } - } - - /** - * List files in a directory - * @param {string} path - Directory path - * @returns {Promise} - List of files - */ - async list(path = "") { - try { - const response = await fetch(`${this.baseUrl}/contents/${path}?ref=${this.branch}`, { - headers: this.headers, - }) - - if (!response.ok) { - throw new Error(`Failed to list files: ${response.statusText}`) - } - - const data = await response.json() - return Array.isArray(data) ? data : [data] - } catch (error) { - throw new Error(`Error listing files: ${error.message}`) - } - } -} - -module.exports = GitHubStorage diff --git a/package.json b/package.json index f4e76fa257..c7ddefaa3d 100644 --- a/package.json +++ b/package.json @@ -426,6 +426,7 @@ "@mistralai/mistralai": "^1.3.6", "@modelcontextprotocol/sdk": "^1.7.0", "@types/clone-deep": "^4.0.4", + "@types/js-yaml": "^4.0.9", "@types/pdf-parse": "^1.1.4", "@types/tmp": "^0.2.6", "@types/turndown": "^5.0.5", @@ -448,6 +449,7 @@ "i18next": "^24.2.2", "isbinaryfile": "^5.0.2", "js-tiktoken": "^1.0.19", + "js-yaml": "^4.1.0", "mammoth": "^1.8.0", "monaco-vscode-textmate-theme-converter": "^0.1.7", "node-ipc": "^12.0.0", diff --git a/src/__mocks__/vscode.js b/src/__mocks__/vscode.js index c40d6dc680..f8f3ec336a 100644 --- a/src/__mocks__/vscode.js +++ b/src/__mocks__/vscode.js @@ -7,6 +7,16 @@ const vscode = { machineId: "test-machine-id", sessionId: "test-session-id", shell: "/bin/zsh", + globalStorageUri: { + fsPath: "/test/global-storage", + scheme: "file", + authority: "", + path: "/test/global-storage", + query: "", + fragment: "", + with: jest.fn(), + toJSON: jest.fn(), + }, }, window: { showInformationMessage: jest.fn(), @@ -31,7 +41,86 @@ const vscode = { dispose: jest.fn(), }), fs: { - stat: jest.fn(), + stat: jest.fn().mockImplementation((uri) => { + // Mock successful stat for cache directory + if (uri.fsPath.includes("package-manager-cache")) { + return Promise.resolve({ + type: vscode.FileType.Directory, + ctime: Date.now(), + mtime: Date.now(), + size: 0, + }) + } + return Promise.reject(new Error("File not found")) + }), + readFile: jest.fn().mockImplementation((uri) => { + // Mock successful file read for metadata files + if (uri.fsPath.includes("package-with-externals")) { + return Promise.resolve( + Buffer.from(` +name: Package with Externals +description: A package with external item references +version: 1.0.0 +type: package +items: + - type: mcp server + path: ../external/server + - type: mode + path: ../external/mode +`), + ) + } + if (uri.fsPath.includes("detailed-items")) { + return Promise.resolve( + Buffer.from(` +name: Detailed Component +description: A component with all optional fields +version: 1.0.0 +type: mcp server +author: Test Author +tags: + - test + - detailed +sourceUrl: https://github.com/test/repo +lastUpdated: 2025-04-11T13:54:00Z +`), + ) + } + if (uri.fsPath.endsWith("metadata.en.yml")) { + return Promise.resolve( + Buffer.from(` +name: Test Component +description: Test description +version: 1.0.0 +type: mcp server +`), + ) + } + if (uri.fsPath.endsWith("metadata.es.yml")) { + return Promise.resolve( + Buffer.from(` +name: Componente de Prueba +description: Descripción de prueba +version: 1.0.0 +type: mcp server +`), + ) + } + if (uri.fsPath.endsWith("metadata.ja.yml")) { + return Promise.resolve( + Buffer.from(` +name: テストコンポーネント +description: テストの説明 +version: 1.0.0 +type: mcp server +`), + ) + } + return Promise.reject(new Error("File not found")) + }), + writeFile: jest.fn().mockResolvedValue(undefined), + delete: jest.fn().mockResolvedValue(undefined), + createDirectory: jest.fn().mockResolvedValue(undefined), }, }, Disposable: class { @@ -99,6 +188,89 @@ const vscode = { this.pattern = pattern } }, + extensions: { + getExtension: jest.fn().mockReturnValue({ + extensionUri: { + fsPath: "/test/extension", + scheme: "file", + authority: "", + path: "/test/extension", + query: "", + fragment: "", + with: jest.fn(), + toJSON: jest.fn(), + }, + activate: jest.fn().mockResolvedValue({ + getPackageManager: jest.fn().mockReturnValue({ + addSource: jest.fn().mockResolvedValue(undefined), + removeSource: jest.fn().mockResolvedValue(undefined), + getSources: jest.fn().mockImplementation(async () => { + return [ + { + url: "https://github.com/roo-team/package-manager-template", + enabled: true, + }, + ] + }), + getItems: jest.fn().mockImplementation(async () => { + return [ + { + name: "Test Component", + description: "Test description", + version: "1.0.0", + type: "mcp server", + url: "/test/path", + repoUrl: "https://github.com/roo-team/package-manager-template", + author: "Test Author", + tags: ["test"], + lastUpdated: "2025-04-11T13:54:00Z", + sourceUrl: "https://github.com/test/repo", + items: [ + { type: "mcp server", path: "../external/server" }, + { type: "mode", path: "../external/mode" }, + ], + }, + ] + }), + }), + }), + exports: { + getPackageManager: jest.fn().mockReturnValue({ + addSource: jest.fn().mockResolvedValue(undefined), + removeSource: jest.fn().mockResolvedValue(undefined), + getSources: jest.fn().mockImplementation(async () => { + return [ + { + url: "https://github.com/roo-team/package-manager-template", + enabled: true, + }, + ] + }), + getItems: jest.fn().mockImplementation(async () => { + return [ + { + name: "Test Component", + description: "Test description", + version: "1.0.0", + type: "mcp server", + url: "/test/path", + repoUrl: "https://github.com/roo-team/package-manager-template", + author: "Test Author", + tags: ["test"], + lastUpdated: "2025-04-11T13:54:00Z", + sourceUrl: "https://github.com/test/repo", + items: [ + { type: "mcp server", path: "../external/server" }, + { type: "mode", path: "../external/mode" }, + ], + }, + ] + }), + }), + }, + isActive: true, + }), + }, } module.exports = vscode diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 26f5666deb..0c7addb212 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -750,7 +750,8 @@ export class ClineProvider extends EventEmitter implements * @param webview A reference to the extension webview */ private setWebviewMessageListener(webview: vscode.Webview) { - const onReceiveMessage = async (message: WebviewMessage) => webviewMessageHandler(this, message, this.packageManagerManager) + const onReceiveMessage = async (message: WebviewMessage) => + webviewMessageHandler(this, message, this.packageManagerManager) webview.onDidReceiveMessage(onReceiveMessage, null, this.disposables) } @@ -1211,8 +1212,12 @@ export class ClineProvider extends EventEmitter implements const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get("allowedCommands") || [] const cwd = this.cwd + // Get package manager items from the manager + const packageManagerItems = this.packageManagerManager?.getCurrentItems() || [] + return { version: this.context.extension?.packageJSON?.version ?? "", + packageManagerItems, apiConfiguration, customInstructions, alwaysAllowReadOnly: alwaysAllowReadOnly ?? false, diff --git a/src/core/webview/packageManagerMessageHandler.ts b/src/core/webview/packageManagerMessageHandler.ts index afae6588f6..0ca439e575 100644 --- a/src/core/webview/packageManagerMessageHandler.ts +++ b/src/core/webview/packageManagerMessageHandler.ts @@ -29,12 +29,19 @@ export async function handlePackageManagerMessages( return true } case "fetchPackageManagerItems": { + // Prevent multiple simultaneous fetches + if (packageManagerManager.isFetching) { + console.log("Package Manager: Fetch already in progress, skipping") + return true + } + // Check if we need to force refresh using type assertion const forceRefresh = (message as any).forceRefresh === true console.log(`Package Manager: Fetch requested with forceRefresh=${forceRefresh}`) try { console.log("Package Manager: Received request to fetch package manager items") console.log("DEBUG: Processing package manager request") + packageManagerManager.isFetching = true // Wrap the entire initialization in a try-catch block try { @@ -58,78 +65,56 @@ export async function handlePackageManagerMessages( // Add timing information const startTime = Date.now() - // Simplify the initialization by limiting the number of items and adding more error handling - let items: PackageManagerItem[] = [] + // Fetch items from all enabled sources + console.log("DEBUG: Starting to fetch items from sources") + const enabledSources = sources.filter((s) => s.enabled) - try { - console.log("DEBUG: Starting to fetch items from sources") - // Only fetch from the first enabled source to reduce complexity - const enabledSources = sources.filter((s) => s.enabled) - if (enabledSources.length > 0) { - const firstSource = enabledSources[0] - console.log(`Package Manager: Fetching items from first source: ${firstSource.url}`) - - // Get items from the first source only - const sourceItems = await packageManagerManager.getPackageManagerItems([firstSource]) - items = sourceItems - console.log("DEBUG: Successfully fetched items:", items.length) - } else { - console.log("DEBUG: No enabled sources found") - } - } catch (fetchError) { - console.error("Failed to fetch package manager items:", fetchError) - // Continue with empty items array - items = [] + if (enabledSources.length === 0) { + console.log("DEBUG: No enabled sources found") + vscode.window.showInformationMessage( + "No enabled sources configured. Add and enable sources to view items.", + ) + await provider.postStateToWebview() + return true } + console.log(`Package Manager: Fetching items from ${enabledSources.length} sources`) + const result = await packageManagerManager.getPackageManagerItems(enabledSources) + + // If there are errors but also items, show warning + if (result.errors && result.items.length > 0) { + vscode.window.showWarningMessage( + `Some package manager sources failed to load:\n${result.errors.join("\n")}`, + ) + } + // If there are errors and no items, show error + else if (result.errors && result.items.length === 0) { + vscode.window.showErrorMessage( + `Failed to load package manager sources:\n${result.errors.join("\n")}`, + ) + } + + console.log("DEBUG: Successfully fetched items:", result.items.length) + console.log("DEBUG: Fetch completed, preparing to send items to webview") const endTime = Date.now() - console.log(`Package Manager: Found ${items.length} items in ${endTime - startTime}ms`) - console.log(`Package Manager: First item:`, items.length > 0 ? items[0] : "No items") + console.log(`Package Manager: Found ${result.items.length} items in ${endTime - startTime}ms`) + console.log(`Package Manager: First item:`, result.items.length > 0 ? result.items[0] : "No items") + // The items are already stored in PackageManagerManager's currentItems + // No need to store in global state - // Send the items to the webview - console.log("DEBUG: Creating message to send items to webview") - - // Get the current state to include apiConfiguration to prevent welcome screen from showing - const currentState = await provider.getState() - - const message = { - type: "state", - state: { - // Include the current apiConfiguration to prevent welcome screen from showing - // This is critical because ExtensionStateContext checks apiConfiguration to determine if welcome screen should be shown - apiConfiguration: currentState.apiConfiguration, - packageManagerItems: items, - }, - } as ExtensionMessage - - console.log(`Package Manager: Sending message to webview:`, message) - console.log( - "DEBUG: About to call postMessageToWebview with apiConfiguration:", - currentState.apiConfiguration ? "present" : "missing", - ) - provider.postMessageToWebview(message) - console.log("DEBUG: Called postMessageToWebview") - console.log(`Package Manager: Message sent to webview`) + // Send state to webview + await provider.postStateToWebview() + console.log("Package Manager: State sent to webview") } catch (initError) { console.error("Error in package manager initialization:", initError) - // Send an empty items array to the webview to prevent the spinner from spinning forever - // Get the current state to include apiConfiguration to prevent welcome screen from showing - const currentState = await provider.getState() - - provider.postMessageToWebview({ - type: "state", - state: { - // Include the current apiConfiguration to prevent welcome screen from showing - // This is critical because ExtensionStateContext checks apiConfiguration to determine if welcome screen should be shown - apiConfiguration: currentState.apiConfiguration, - packageManagerItems: [], - }, - } as any) // Use type assertion to bypass TypeScript checking + console.error("Error in package manager initialization:", initError) vscode.window.showErrorMessage( `Package manager initialization failed: ${initError instanceof Error ? initError.message : String(initError)}`, ) + // The state will already be updated with empty items by PackageManagerManager + await provider.postStateToWebview() } } catch (error) { console.error("Failed to fetch package manager items:", error) @@ -237,22 +222,20 @@ export async function handlePackageManagerMessages( if (source) { try { // Refresh the repository with the source name - await packageManagerManager.refreshRepository(message.url, source.name) - vscode.window.showInformationMessage( - `Successfully refreshed package manager source: ${source.name || message.url}`, + const refreshResult = await packageManagerManager.refreshRepository( + message.url, + source.name, ) - - // Trigger a fetch to update the UI with the refreshed data - const currentState = await provider.getState() - provider.postMessageToWebview({ - type: "state", - state: { - apiConfiguration: currentState.apiConfiguration, - packageManagerItems: await packageManagerManager.getPackageManagerItems( - sources.filter((s) => s.enabled), - ), - }, - } as ExtensionMessage) + if (refreshResult.error) { + vscode.window.showErrorMessage( + `Failed to refresh source: ${source.name || message.url} - ${refreshResult.error}`, + ) + } else { + vscode.window.showInformationMessage( + `Successfully refreshed package manager source: ${source.name || message.url}`, + ) + } + await provider.postStateToWebview() } finally { // Always notify the webview that the refresh is complete, even if it failed console.log(`Package Manager: Sending repositoryRefreshComplete message for ${message.url}`) diff --git a/src/extension.ts b/src/extension.ts index 7a65a9d887..9ea6cbc324 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -38,6 +38,7 @@ import { formatLanguage } from "./shared/language" let outputChannel: vscode.OutputChannel let extensionContext: vscode.ExtensionContext +let packageManagerManager: PackageManagerManager // This method is called when your extension is activated. // Your extension is activated the very first time the command is executed. @@ -66,13 +67,13 @@ export async function activate(context: vscode.ExtensionContext) { if (!context.globalState.get("allowedCommands")) { context.globalState.update("allowedCommands", defaultCommands) } -const provider = new ClineProvider(context, outputChannel, "sidebar") -// Initialize package manager -const packageManagerManager = new PackageManagerManager(context) -provider.setPackageManagerManager(packageManagerManager) -telemetryService.setProvider(provider) + const provider = new ClineProvider(context, outputChannel, "sidebar") + // Initialize package manager + packageManagerManager = new PackageManagerManager(context) + provider.setPackageManagerManager(packageManagerManager) + telemetryService.setProvider(provider) context.subscriptions.push( vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, provider, { @@ -132,6 +133,16 @@ telemetryService.setProvider(provider) // This method is called when your extension is deactivated export async function deactivate() { outputChannel.appendLine("Roo-Code extension deactivated") + + // Clean up package manager + if (packageManagerManager) { + try { + await packageManagerManager.cleanup() + } catch (error) { + console.error("Failed to clean up package manager:", error) + } + } + // Clean up MCP server manager await McpServerManager.cleanup(extensionContext) telemetryService.shutdown() diff --git a/src/services/mcp/McpHub.ts b/src/services/mcp/McpHub.ts index 31d0dd8020..3455d88f8d 100644 --- a/src/services/mcp/McpHub.ts +++ b/src/services/mcp/McpHub.ts @@ -430,6 +430,13 @@ export class McpHub { config: z.infer, source: "global" | "project" = "global", ): Promise { + // Check if a connection is already being established + const existingConnection = this.findConnection(name, source) + if (existingConnection && existingConnection.server.status === "connecting") { + console.log(`Connection attempt already in progress for ${name}`) + return + } + // Remove existing connection if it exists with the same source await this.deleteConnection(name, source) @@ -717,58 +724,66 @@ export class McpHub { newServers: Record, source: "global" | "project" = "global", ): Promise { + if (this.isConnecting) { + console.log("Connection update already in progress, skipping") + return + } + this.isConnecting = true - this.removeAllFileWatchers() - // Filter connections by source - const currentConnections = this.connections.filter( - (conn) => conn.server.source === source || (!conn.server.source && source === "global"), - ) - const currentNames = new Set(currentConnections.map((conn) => conn.server.name)) - const newNames = new Set(Object.keys(newServers)) + try { + this.removeAllFileWatchers() + // Filter connections by source + const currentConnections = this.connections.filter( + (conn) => conn.server.source === source || (!conn.server.source && source === "global"), + ) + const currentNames = new Set(currentConnections.map((conn) => conn.server.name)) + const newNames = new Set(Object.keys(newServers)) - // Delete removed servers - for (const name of currentNames) { - if (!newNames.has(name)) { - await this.deleteConnection(name, source) - } - } - - // Update or add servers - for (const [name, config] of Object.entries(newServers)) { - // Only consider connections that match the current source - const currentConnection = this.findConnection(name, source) - - // Validate and transform the config - let validatedConfig: z.infer - try { - validatedConfig = this.validateServerConfig(config, name) - } catch (error) { - this.showErrorMessage(`Invalid configuration for MCP server "${name}"`, error) - continue - } - - if (!currentConnection) { - // New server - try { - this.setupFileWatcher(name, validatedConfig, source) - await this.connectToServer(name, validatedConfig, source) - } catch (error) { - this.showErrorMessage(`Failed to connect to new MCP server ${name}`, error) - } - } else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) { - // Existing server with changed config - try { - this.setupFileWatcher(name, validatedConfig, source) + // Delete removed servers + for (const name of currentNames) { + if (!newNames.has(name)) { await this.deleteConnection(name, source) - await this.connectToServer(name, validatedConfig, source) - } catch (error) { - this.showErrorMessage(`Failed to reconnect MCP server ${name}`, error) } } - // If server exists with same config, do nothing + + // Update or add servers + for (const [name, config] of Object.entries(newServers)) { + // Only consider connections that match the current source + const currentConnection = this.findConnection(name, source) + + // Validate and transform the config + let validatedConfig: z.infer + try { + validatedConfig = this.validateServerConfig(config, name) + } catch (error) { + this.showErrorMessage(`Invalid configuration for MCP server "${name}"`, error) + continue + } + + if (!currentConnection) { + // New server + try { + this.setupFileWatcher(name, validatedConfig, source) + await this.connectToServer(name, validatedConfig, source) + } catch (error) { + this.showErrorMessage(`Failed to connect to new MCP server ${name}`, error) + } + } else if (!deepEqual(JSON.parse(currentConnection.server.config), config)) { + // Existing server with changed config + try { + this.setupFileWatcher(name, validatedConfig, source) + await this.deleteConnection(name, source) + await this.connectToServer(name, validatedConfig, source) + } catch (error) { + this.showErrorMessage(`Failed to reconnect MCP server ${name}`, error) + } + } + // If server exists with same config, do nothing + } + await this.notifyWebviewOfServerChanges() + } finally { + this.isConnecting = false } - await this.notifyWebviewOfServerChanges() - this.isConnecting = false } private setupFileWatcher( @@ -840,15 +855,34 @@ export class McpHub { } async restartConnection(serverName: string, source?: "global" | "project"): Promise { - this.isConnecting = true - const provider = this.providerRef.deref() - if (!provider) { + // Check if already connecting + if (this.isConnecting) { + console.log(`Global connection attempt already in progress, skipping restart for ${serverName}`) return } - // Get existing connection and update its status + this.isConnecting = true + const provider = this.providerRef.deref() + if (!provider) { + this.isConnecting = false + return + } + + // Get existing connection and check its status const connection = this.findConnection(serverName, source) - const config = connection?.server.config + if (!connection) { + this.isConnecting = false + return + } + + // Check if already connecting + if (connection.server.status === "connecting") { + console.log(`Connection attempt already in progress for ${serverName}`) + this.isConnecting = false + return + } + + const config = connection.server.config if (config) { vscode.window.showInformationMessage(t("common:info.mcp_server_restarting", { serverName })) connection.server.status = "connecting" @@ -868,14 +902,18 @@ export class McpHub { vscode.window.showInformationMessage(t("common:info.mcp_server_connected", { serverName })) } catch (validationError) { this.showErrorMessage(`Invalid configuration for MCP server "${serverName}"`, validationError) + connection.server.status = "disconnected" } } catch (error) { this.showErrorMessage(`Failed to restart ${serverName} MCP server connection`, error) + connection.server.status = "disconnected" + } finally { + await this.notifyWebviewOfServerChanges() + this.isConnecting = false } + } else { + this.isConnecting = false } - - await this.notifyWebviewOfServerChanges() - this.isConnecting = false } private async notifyWebviewOfServerChanges(): Promise { diff --git a/src/services/package-manager/GitFetcher.ts b/src/services/package-manager/GitFetcher.ts index 2289a47197..62b269e324 100644 --- a/src/services/package-manager/GitFetcher.ts +++ b/src/services/package-manager/GitFetcher.ts @@ -1,352 +1,218 @@ -import * as vscode from "vscode"; -import * as path from "path"; -import * as fs from "fs/promises"; -import { exec } from "child_process"; -import { promisify } from "util"; -import { PackageManagerItem, PackageManagerRepository } from "./types"; - -const execAsync = promisify(exec); +import * as vscode from "vscode" +import * as path from "path" +import * as fs from "fs/promises" +import * as yaml from "js-yaml" +import simpleGit from "simple-git" +import { MetadataScanner } from "./MetadataScanner" +import { validateAnyMetadata } from "./schemas" +import { PackageManagerItem, PackageManagerRepository, RepositoryMetadata } from "./types" /** - * Service for fetching and validating package manager data from Git repositories + * Handles fetching and caching package manager repositories */ export class GitFetcher { - private readonly cacheDir: string; - - constructor(private readonly context: vscode.ExtensionContext) { - this.cacheDir = path.join(context.globalStorageUri.fsPath, "package-manager-cache"); - } - /** - * Fetches repository data from a Git URL - * @param url The Git repository URL - * @param sourceName Optional name to override the repository name - * @returns A PackageManagerRepository object containing metadata and items - */ - async fetchRepository(url: string, sourceName?: string): Promise { - console.log(`GitFetcher: Fetching repository from ${url}`); - - try { - // Ensure cache directory exists - try { - await fs.mkdir(this.cacheDir, { recursive: true }); - console.log(`GitFetcher: Cache directory ensured at ${this.cacheDir}`); - } catch (mkdirError) { - console.error(`GitFetcher: Error creating cache directory: ${mkdirError.message}`); - throw new Error(`Failed to create cache directory: ${mkdirError.message}`); - } - - // Create a safe directory name from the URL - const repoName = this.getRepoNameFromUrl(url); - const repoDir = path.join(this.cacheDir, repoName); - console.log(`GitFetcher: Repository directory: ${repoDir}`); - - // Clone or pull repository with timeout protection - let activeBranch: string; - try { - console.log(`GitFetcher: Cloning or pulling repository ${url}`); - activeBranch = await this.cloneOrPullRepository(url, repoDir); - console.log(`GitFetcher: Repository cloned/pulled successfully on branch ${activeBranch}`); - } catch (gitError) { - console.error(`GitFetcher: Git operation failed: ${gitError.message}`); - throw new Error(`Git operation failed: ${gitError.message}`); - } - - try { - // Validate repository structure - console.log(`GitFetcher: Validating repository structure`); - await this.validateRepositoryStructure(repoDir); - - // Parse metadata - console.log(`GitFetcher: Parsing repository metadata`); - const metadata = await this.parseRepositoryMetadata(repoDir); - - // Parse items - console.log(`GitFetcher: Parsing package manager items`); - // Use the provided sourceName if available, otherwise use metadata name or fallback to URL-derived name - const itemSourceName = sourceName || metadata.name || this.getRepoNameFromUrl(url); - const items = await this.parsePackageManagerItems(repoDir, url, activeBranch, itemSourceName); - - console.log(`GitFetcher: Successfully fetched repository with ${items.length} items`); - return { - metadata, - items, - url - }; - } catch (validationError) { - // Log the validation error - console.error(`GitFetcher: Repository validation failed: ${validationError.message}`); - - // Show error message - vscode.window.showErrorMessage(`Failed to fetch repository: ${validationError.message}`); - - // Return empty repository - return { - metadata: {}, - items: [], - url - }; - } - } catch (error) { - // Show error message - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`GitFetcher: Failed to fetch repository: ${errorMessage}`); - vscode.window.showErrorMessage(`Failed to fetch repository: ${errorMessage}`); - - // Return empty repository - return { - metadata: {}, - items: [], - url - }; - } - } - - /** - * Extracts a safe directory name from a Git URL - * @param url The Git repository URL - * @returns A sanitized directory name - */ - private getRepoNameFromUrl(url: string): string { - // Extract repo name from URL and sanitize it - const urlParts = url.split("/").filter(part => part !== ""); - const repoName = urlParts[urlParts.length - 1].replace(/\.git$/, ""); - return repoName.replace(/[^a-zA-Z0-9-_]/g, "-"); - } - - /** - * Clones or pulls a Git repository - * @param url The Git repository URL - * @param repoDir The directory to clone to or pull in - */ - private async cloneOrPullRepository(url: string, repoDir: string): Promise { - console.log(`GitFetcher: Checking if repository exists at ${repoDir}`); - - try { - // Check if repository already exists - const repoExists = await fs.stat(path.join(repoDir, ".git")) - .then(() => true) - .catch(() => false); - - if (repoExists) { - console.log(`GitFetcher: Repository exists, attempting to pull latest changes`); - - try { - // Try to pull latest changes with timeout - const pullPromise = execAsync("git pull", { cwd: repoDir, timeout: 20000 }); - await pullPromise; - console.log(`GitFetcher: Successfully pulled latest changes`); - } catch (pullError) { - console.error(`GitFetcher: Failed to pull repository: ${pullError.message}`); - - // If pull fails, try to remove the directory and clone again - console.log(`GitFetcher: Attempting to remove and re-clone repository`); - try { - await fs.rm(repoDir, { recursive: true, force: true }); - console.log(`GitFetcher: Removed existing repository directory`); - - // Clone with timeout - const clonePromise = execAsync(`git clone "${url}" "${repoDir}"`, { timeout: 30000 }); - await clonePromise; - console.log(`GitFetcher: Successfully re-cloned repository`); - } catch (rmError) { - console.error(`GitFetcher: Failed to re-clone repository: ${rmError.message}`); - throw new Error(`Failed to re-clone repository: ${rmError.message}`); - } - } - } else { - console.log(`GitFetcher: Repository does not exist, cloning from ${url}`); - - // Clone repository with timeout - const clonePromise = execAsync(`git clone "${url}" "${repoDir}"`, { timeout: 30000 }); - await clonePromise; - console.log(`GitFetcher: Successfully cloned repository`); - } + private readonly cacheDir: string + private readonly metadataScanner: MetadataScanner - // Get the active branch name - const { stdout: branchName } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: repoDir }); - console.log(`GitFetcher: Active branch is ${branchName.trim()}`); - return branchName.trim(); + constructor(context: vscode.ExtensionContext) { + this.cacheDir = path.join(context.globalStorageUri.fsPath, "package-manager-cache") + this.metadataScanner = new MetadataScanner() + } - } catch (error) { - console.error(`GitFetcher: Failed to clone or pull repository: ${error.message}`); - throw new Error(`Failed to clone or pull repository: ${error.message}`); - } - } - - /** - * Validates that a repository follows the expected structure - * @param repoDir The repository directory - */ - private async validateRepositoryStructure(repoDir: string): Promise { - // Check for required files - const metadataPath = path.join(repoDir, "metadata.yml"); - - const metadataExists = await fs.stat(metadataPath) - .then(() => true) - .catch(() => false); - - if (!metadataExists) { - throw new Error("Repository is missing metadata.yml file"); - } - - // Check for at least one of the item type directories - const mcpServersDir = path.join(repoDir, "mcp-servers"); - const rolesDir = path.join(repoDir, "roles"); - const storageSystemsDir = path.join(repoDir, "storage-systems"); - const itemsDir = path.join(repoDir, "items"); // For backward compatibility - - const mcpServersDirExists = await fs.stat(mcpServersDir).then(() => true).catch(() => false); - const rolesDirExists = await fs.stat(rolesDir).then(() => true).catch(() => false); - const storageSystemsDirExists = await fs.stat(storageSystemsDir).then(() => true).catch(() => false); - const itemsDirExists = await fs.stat(itemsDir).then(() => true).catch(() => false); - - if (!mcpServersDirExists && !rolesDirExists && !storageSystemsDirExists && !itemsDirExists) { - throw new Error("Repository is missing item directories (mcp-servers, roles, storage-systems, or items)"); - } - } - - /** - * Parses the repository metadata file - * @param repoDir The repository directory - * @returns The parsed metadata - */ - private async parseRepositoryMetadata(repoDir: string): Promise { - // Parse metadata.yml file - const metadataPath = path.join(repoDir, "metadata.yml"); - const metadataContent = await fs.readFile(metadataPath, "utf-8"); - - // For now, we'll return a simple object - // In a future update, we'll add a YAML parser dependency - try { - return { - name: metadataContent.match(/name:\s*["']?([^"'\n]+)["']?/)?.[1] || "Repository Name", - description: metadataContent.match(/description:\s*["']?([^"'\n]+)["']?/)?.[1] || "Repository Description", - maintainer: metadataContent.match(/maintainer:\s*["']?([^"'\n]+)["']?/)?.[1], - website: metadataContent.match(/website:\s*["']?([^"'\n]+)["']?/)?.[1] - }; - } catch (error) { - console.error("Failed to parse repository metadata:", error); - return { - name: "Repository Name", - description: "Repository Description" - }; - } - } - - /** - * Parses package manager items from a repository - * @param repoDir The repository directory - * @param repoUrl The repository URL - * @param branch The branch to use (default: "main") - * @param sourceName The name of the source repository - * @returns An array of PackageManagerItem objects - */ - private async parsePackageManagerItems(repoDir: string, repoUrl: string, branch: string = "main", sourceName?: string): Promise { - const items: PackageManagerItem[] = []; - - // Check for items in each directory type - const directoryTypes = [ - { path: path.join(repoDir, "mcp-servers"), type: "mcp-server", urlPath: "mcp-servers" }, - { path: path.join(repoDir, "roles"), type: "role", urlPath: "roles" }, - { path: path.join(repoDir, "storage-systems"), type: "storage", urlPath: "storage-systems" }, - { path: path.join(repoDir, "items"), type: "other", urlPath: "items" } // For backward compatibility - ]; - - for (const dirType of directoryTypes) { - try { - // Check if directory exists - const dirExists = await fs.stat(dirType.path) - .then(() => true) - .catch(() => false); - - if (!dirExists) continue; - - // Get all subdirectories - const itemDirs = await fs.readdir(dirType.path); - - for (const itemDir of itemDirs) { - const itemPath = path.join(dirType.path, itemDir); - const stats = await fs.stat(itemPath); - - if (stats.isDirectory()) { - try { - // Parse item metadata - const metadataPath = path.join(itemPath, "metadata.yml"); - const metadataExists = await fs.stat(metadataPath) - .then(() => true) - .catch(() => false); - - if (metadataExists) { - const metadataContent = await fs.readFile(metadataPath, "utf-8"); - - // For now, we'll parse the YAML content manually - // In a future update, we'll add a YAML parser dependency - const name = metadataContent.match(/name:\s*["']?([^"'\n]+)["']?/)?.[1] || itemDir; - const description = metadataContent.match(/description:\s*["']?([^"'\n]+)["']?/)?.[1] || "No description"; - // Use the directory type as the default type if not specified in metadata - const type = metadataContent.match(/type:\s*["']?([^"'\n]+)["']?/)?.[1] || dirType.type; - const author = metadataContent.match(/author:\s*["']?([^"'\n]+)["']?/)?.[1]; - const version = metadataContent.match(/version:\s*["']?([^"'\n]+)["']?/)?.[1]; - const sourceUrl = metadataContent.match(/sourceUrl:\s*["']?([^"'\n]+)["']?/)?.[1]; - - // Parse tags if present - const tagsMatch = metadataContent.match(/tags:\s*\[(.*?)\]/); - const tags = tagsMatch ? - tagsMatch[1].split(",").map(tag => tag.trim().replace(/["']/g, "")) : - undefined; - - // Create base item without author and lastUpdated - let item: PackageManagerItem = { - name, - description, - type: type as "role" | "mcp-server" | "storage" | "other", - url: `${repoUrl}/tree/${branch}/${dirType.urlPath}/${itemDir}`, - repoUrl, - sourceName: sourceName, - tags, - version, - sourceUrl - }; + /** + * Fetch repository data + * @param repoUrl Repository URL + * @param forceRefresh Whether to bypass cache + * @param sourceName Optional source repository name + * @returns Repository data + */ + async fetchRepository( + repoUrl: string, + forceRefresh = false, + sourceName?: string, + ): Promise { + // Ensure cache directory exists + await fs.mkdir(this.cacheDir, { recursive: true }) - // Try to get the last non-merge commit info - try { - // Get the last non-merge commit by any author for this path - const { stdout: commitInfo } = await execAsync( - `git log --no-merges -1 --format="%aI%n%an" -- "${itemPath}"`, - { cwd: repoDir } - ); + // Get repository directory name from URL + const repoName = this.getRepositoryName(repoUrl) + const repoDir = path.join(this.cacheDir, repoName) - // Split into date and author (they're on separate lines) - const [lastCommitDate, commitAuthor] = commitInfo.trim().split('\n'); - // Update item with both date and author from git - item = { - ...item, - lastUpdated: lastCommitDate.trim(), // ISO 8601 format - author: commitAuthor.trim() // Use Git author instead of metadata author - }; - } catch (error) { - console.error(`Failed to get commit info for ${itemPath}:`, error); - // If git info fails, try to use author from metadata as fallback - if (author) { - item = { - ...item, - author - }; - } - } + // Clone or pull repository + await this.cloneOrPullRepository(repoUrl, repoDir, forceRefresh) - items.push(item); - } - } catch (error) { - console.error(`Failed to parse item ${itemDir}:`, error); - } - } - } - } catch (error) { - console.error(`Failed to parse directory ${dirType.path}:`, error); - } - } - - return items; - } -} \ No newline at end of file + // Validate repository structure + await this.validateRepositoryStructure(repoDir) + + // Parse repository metadata + const metadata = await this.parseRepositoryMetadata(repoDir) + + // Parse package manager items + const items = await this.parsePackageManagerItems(repoDir, repoUrl, sourceName || metadata.name) + + return { + metadata, + items, + url: repoUrl, + } + } + + /** + * Get repository name from URL + * @param repoUrl Repository URL + * @returns Repository name + */ + private getRepositoryName(repoUrl: string): string { + const match = repoUrl.match(/\/([^/]+?)(?:\.git)?$/) + if (!match) { + throw new Error(`Invalid repository URL: ${repoUrl}`) + } + return match[1] + } + + /** + * Clone or pull repository + * @param repoUrl Repository URL + * @param repoDir Repository directory + * @param forceRefresh Whether to force refresh + */ + private async cloneOrPullRepository(repoUrl: string, repoDir: string, forceRefresh: boolean): Promise { + try { + // Check if repository exists + const gitDir = path.join(repoDir, ".git") + let repoExists = await fs + .stat(gitDir) + .then(() => true) + .catch(() => false) + + if (repoExists && !forceRefresh) { + try { + // Pull latest changes + const git = simpleGit(repoDir) + // Force pull with overwrite + await git.fetch("origin", "main") + await git.raw(["reset", "--hard", "origin/main"]) + await git.clean(["--force", "-d"]) + } catch (error) { + // If pull fails with specific errors that indicate repo corruption, + // we should remove and re-clone + const errorMessage = error instanceof Error ? error.message : String(error) + if ( + errorMessage.includes("not a git repository") || + errorMessage.includes("repository not found") || + errorMessage.includes("refusing to merge unrelated histories") + ) { + await fs.rm(repoDir, { recursive: true, force: true }) + repoExists = false + } else { + throw error + } + } + } + + if (!repoExists || forceRefresh) { + try { + // Always remove the directory before cloning + await fs.rm(repoDir, { recursive: true, force: true }) + + // Add a small delay to ensure directory is fully cleaned up + await new Promise((resolve) => setTimeout(resolve, 100)) + + // Verify directory is gone before proceeding + const dirExists = await fs + .stat(repoDir) + .then(() => true) + .catch(() => false) + if (dirExists) { + throw new Error("Failed to clean up directory before cloning") + } + + // Clone repository + const git = simpleGit() + // Clone with force options + await git.clone(repoUrl, repoDir) + // Reset to ensure clean state + const repoGit = simpleGit(repoDir) + await repoGit.clean(["--force", "-d"]) + await repoGit.raw(["reset", "--hard", "HEAD"]) + } catch (error) { + // If clone fails, ensure we clean up any partially created directory + try { + await fs.rm(repoDir, { recursive: true, force: true }) + } catch { + // Ignore cleanup errors + } + throw error + } + } + + // Get current branch + const git = simpleGit(repoDir) + const branch = await git.revparse(["--abbrev-ref", "HEAD"]) + console.log(`Repository cloned/pulled successfully on branch ${branch}`) + } catch (error) { + throw new Error( + `Failed to clone/pull repository: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } + + /** + * Validate repository structure + * @param repoDir Repository directory + */ + private async validateRepositoryStructure(repoDir: string): Promise { + // Check for metadata.en.yml + const metadataPath = path.join(repoDir, "metadata.en.yml") + try { + await fs.stat(metadataPath) + } catch { + throw new Error("Repository is missing metadata.en.yml file") + } + + // Check for README.md + const readmePath = path.join(repoDir, "README.md") + try { + await fs.stat(readmePath) + } catch { + throw new Error("Repository is missing README.md file") + } + } + + /** + * Parse repository metadata + * @param repoDir Repository directory + * @returns Repository metadata + */ + private async parseRepositoryMetadata(repoDir: string): Promise { + const metadataPath = path.join(repoDir, "metadata.en.yml") + const metadataContent = await fs.readFile(metadataPath, "utf-8") + + try { + const parsed = yaml.load(metadataContent) as Record + return validateAnyMetadata(parsed) as RepositoryMetadata + } catch (error) { + console.error("Failed to parse repository metadata:", error) + return { + name: "Unknown Repository", + description: "Failed to load repository", + version: "0.0.0", + } + } + } + + /** + * Parse package manager items + * @param repoDir Repository directory + * @param repoUrl Repository URL + * @param sourceName Source repository name + * @returns Array of package manager items + */ + private async parsePackageManagerItems( + repoDir: string, + repoUrl: string, + sourceName: string, + ): Promise { + return this.metadataScanner.scanDirectory(repoDir, repoUrl, sourceName) + } +} diff --git a/src/services/package-manager/MetadataScanner.ts b/src/services/package-manager/MetadataScanner.ts new file mode 100644 index 0000000000..2e36621282 --- /dev/null +++ b/src/services/package-manager/MetadataScanner.ts @@ -0,0 +1,154 @@ +import * as path from "path" +import * as fs from "fs/promises" +import * as vscode from "vscode" +import * as yaml from "js-yaml" +import { validateAnyMetadata } from "./schemas" +import { ComponentMetadata, ComponentType, LocalizedMetadata, PackageManagerItem } from "./types" + +/** + * Handles component discovery and metadata loading + */ +export class MetadataScanner { + /** + * Scans a directory for components + * @param rootDir The root directory to scan + * @param repoUrl The repository URL + * @param sourceName Optional source repository name + * @returns Array of discovered items + */ + async scanDirectory(rootDir: string, repoUrl: string, sourceName?: string): Promise { + const items: PackageManagerItem[] = [] + + try { + const entries = await fs.readdir(rootDir, { withFileTypes: true }) + + for (const entry of entries) { + if (!entry.isDirectory()) continue + + const componentDir = path.join(rootDir, entry.name) + const metadata = await this.loadComponentMetadata(componentDir) + + if (metadata?.["en"]) { + const item = await this.createPackageManagerItem(metadata["en"], componentDir, repoUrl, sourceName) + if (item) items.push(item) + } + + // Recursively scan subdirectories + const subItems = await this.scanDirectory(componentDir, repoUrl, sourceName) + items.push(...subItems) + } + } catch (error) { + console.error(`Error scanning directory ${rootDir}:`, error) + } + + return items + } + + /** + * Loads metadata for a component + * @param componentDir The component directory + * @returns Localized metadata or null if no metadata found + */ + private async loadComponentMetadata(componentDir: string): Promise | null> { + const metadata: LocalizedMetadata = {} + + try { + const entries = await fs.readdir(componentDir, { withFileTypes: true }) + + // Look for metadata.{locale}.yml files + for (const entry of entries) { + if (!entry.isFile()) continue + + const match = entry.name.match(/^metadata\.([a-z]{2})\.yml$/) + if (!match) continue + + const locale = match[1] + const metadataPath = path.join(componentDir, entry.name) + + try { + const content = await fs.readFile(metadataPath, "utf-8") + const parsed = yaml.load(content) as Record + + // Add type field if missing but has a parent directory indicating type + if (!parsed.type) { + const parentDir = path.basename(componentDir) + if (parentDir === "mcp servers" || parentDir === "mcp-servers") { + parsed.type = "mcp server" + } + } + + metadata[locale] = validateAnyMetadata(parsed) as ComponentMetadata + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + console.error(`Error loading metadata from ${metadataPath}:`, error) + + // Show validation errors to user + if (errorMessage.includes("Invalid metadata:")) { + vscode.window.showErrorMessage( + `Invalid metadata in ${path.basename(metadataPath)}: ${errorMessage.replace("Invalid metadata:", "").trim()}`, + ) + } + } + } + } catch (error) { + console.error(`Error reading directory ${componentDir}:`, error) + } + + return Object.keys(metadata).length > 0 ? metadata : null + } + + /** + * Creates a PackageManagerItem from component metadata + * @param metadata The component metadata + * @param componentDir The component directory + * @param repoUrl The repository URL + * @param sourceName Optional source repository name + * @returns PackageManagerItem or null if invalid + */ + private async createPackageManagerItem( + metadata: ComponentMetadata, + componentDir: string, + repoUrl: string, + sourceName?: string, + ): Promise { + // Skip if no type or invalid type + if (!metadata.type || !this.isValidComponentType(metadata.type)) { + return null + } + + return { + name: metadata.name, + description: metadata.description, + type: metadata.type, + version: metadata.version, + tags: metadata.tags, + url: componentDir, + repoUrl, + sourceName, + lastUpdated: await this.getLastModifiedDate(componentDir), + } + } + + /** + * Gets the last modified date for a component + * @param componentDir The component directory + * @returns ISO date string + */ + private async getLastModifiedDate(componentDir: string): Promise { + try { + const stats = await fs.stat(componentDir) + return stats.mtime.toISOString() + } catch { + return new Date().toISOString() + } + } + + /** + * Type guard for component types + * @param type The type to check + * @returns Whether the type is valid + */ + private isValidComponentType(type: string): type is ComponentType { + return ["role", "mcp server", "storage", "mode", "prompt", "package"].includes(type) + } +} diff --git a/src/services/package-manager/PackageManagerManager.ts b/src/services/package-manager/PackageManagerManager.ts index 30a2766434..613a4c60f1 100644 --- a/src/services/package-manager/PackageManagerManager.ts +++ b/src/services/package-manager/PackageManagerManager.ts @@ -8,6 +8,8 @@ import { PackageManagerItem, PackageManagerRepository, PackageManagerSource } fr * Service for managing package manager data */ export class PackageManagerManager { + private currentItems: PackageManagerItem[] = [] + public isFetching = false // Cache expiry time in milliseconds (set to a low value for testing) private static readonly CACHE_EXPIRY_MS = 10 * 1000 // 10 seconds (normally 3600000 = 1 hour) @@ -23,10 +25,12 @@ export class PackageManagerManager { * @param sources The package manager sources * @returns An array of PackageManagerItem objects */ - async getPackageManagerItems(sources: PackageManagerSource[]): Promise { + async getPackageManagerItems( + sources: PackageManagerSource[], + ): Promise<{ items: PackageManagerItem[]; errors?: string[] }> { console.log(`PackageManagerManager: Getting items from ${sources.length} sources`) const items: PackageManagerItem[] = [] - const errors: Error[] = [] + const errors: string[] = [] // Filter enabled sources const enabledSources = sources.filter((s) => s.enabled) @@ -48,19 +52,21 @@ export class PackageManagerManager { } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error) console.error(`PackageManagerManager: Failed to fetch data from ${source.url}:`, error) - errors.push(new Error(`Source ${source.url}: ${errorMessage}`)) + errors.push(`Source ${source.url}: ${errorMessage}`) } } - // Show a single error message with all failures - if (errors.length > 0) { - const errorMessage = `Failed to fetch from ${errors.length} sources: ${errors.map((e) => e.message).join("; ")}` - console.error(`PackageManagerManager: ${errorMessage}`) - vscode.window.showErrorMessage(errorMessage) + // Store the current items + this.currentItems = items + + // Return both items and errors + const result = { + items, + ...(errors.length > 0 && { errors }), } console.log(`PackageManagerManager: Returning ${items.length} total items`) - return items + return result } /** @@ -95,7 +101,7 @@ export class PackageManagerManager { console.log(`PackageManagerManager: Cache miss or expired for ${url}, fetching fresh data`) // Fetch fresh data with timeout protection - const fetchPromise = this.gitFetcher.fetchRepository(url, sourceName) + const fetchPromise = this.gitFetcher.fetchRepository(url, forceRefresh, sourceName) // Create a timeout promise const timeoutPromise = new Promise((_, reject) => { @@ -117,7 +123,11 @@ export class PackageManagerManager { // Return empty repository data instead of throwing return { - metadata: {}, + metadata: { + name: "Unknown Repository", + description: "Failed to load repository", + version: "0.0.0", + }, items: [], url, } @@ -140,7 +150,16 @@ export class PackageManagerManager { return data } catch (error) { console.error(`PackageManagerManager: Failed to refresh repository ${url}:`, error) - throw error + return { + metadata: { + name: "Unknown Repository", + description: "Failed to load repository", + version: "0.0.0", + }, + items: [], + url, + error: error instanceof Error ? error.message : String(error), + } } } @@ -284,4 +303,21 @@ export class PackageManagerManager { return sortOrder === "asc" ? comparison : -comparison }) } + /** + * Gets the current package manager items + * @returns The current items + */ + getCurrentItems(): PackageManagerItem[] { + return this.currentItems + } + + /** + * Cleans up resources used by the package manager + */ + async cleanup(): Promise { + // Clean up cache directories for all sources + const sources = Array.from(this.cache.keys()).map((url) => ({ url, enabled: true })) + await this.cleanupCacheDirectories(sources) + this.clearCache() + } } diff --git a/src/services/package-manager/YamlParser.ts b/src/services/package-manager/YamlParser.ts new file mode 100644 index 0000000000..d89624d679 --- /dev/null +++ b/src/services/package-manager/YamlParser.ts @@ -0,0 +1,140 @@ +import { XMLParser } from "fast-xml-parser" +import { validateAnyMetadata } from "./schemas" + +/** + * Utility class for parsing and validating YAML content + */ +export class YamlParser { + private static parser = new XMLParser({ + ignoreAttributes: false, + parseAttributeValue: true, + parseTagValue: true, + trimValues: true, + preserveOrder: true, + }) + + /** + * Parse YAML content into an object and validate against schema + * @param content YAML content to parse + * @param validate Whether to validate against schema (default: true) + * @returns Parsed and validated object + * @throws Error if parsing or validation fails + */ + static parse(content: string, validate: boolean = true): T { + if (!content.trim()) { + return {} as T + } + + try { + // Remove comments + const noComments = content.replace(/#[^\n]*/g, "") + + // Handle multi-line strings + const processedContent = this.processMultilineStrings(noComments) + + // Convert YAML to JSON-like structure + const jsonContent = processedContent + // Handle arrays with proper indentation + .replace(/^(\s*)-\s+(?=\S)/gm, (match, indent) => `${indent}array_item: `) + // Handle quoted strings + .replace(/^(\s*)([^:\n]+):\s*(['"])(.*?)\3\s*$/gm, (_, indent, key, quote, value) => { + const safeKey = this.sanitizeKey(key) + return `${indent}${safeKey}: ${value}` + }) + // Handle unquoted key-value pairs + .replace(/^(\s*)([^:\n]+):\s*([^\n]*)$/gm, (_, indent, key, value) => { + const safeKey = this.sanitizeKey(key) + return `${indent}${safeKey}: ${value.trim()}` + }) + + // Parse as XML-like structure + const parsed = this.parser.parse(`${jsonContent}`) + + // Convert array_item markers back to arrays and process nested structures + const result = this.processStructure(parsed.root || {}) + + // Validate against schema if requested + if (validate) { + return validateAnyMetadata(result) as T + } else { + return result as T + } + } catch (error) { + console.error("Failed to parse YAML:", error) + throw new Error(`Failed to parse YAML: ${error instanceof Error ? error.message : String(error)}`) + } + } + + /** + * Process multi-line strings in YAML content + * @param content YAML content + * @returns Processed content + */ + private static processMultilineStrings(content: string): string { + return content.replace(/^(\s*[^:\n]+):\s*\|\s*\n((?:\s+[^\n]*\n?)*)/gm, (_, key, value) => { + const indentLevel = value.match(/^\s+/)?.[0].length || 0 + const processedValue = value + .split("\n") + .map((line: string) => line.slice(indentLevel)) + .join("\n") + .trim() + return `${key}: "${processedValue.replace(/"/g, '\\"')}"` + }) + } + + /** + * Sanitize YAML key for XML compatibility + * @param key Key to sanitize + * @returns Sanitized key + */ + private static sanitizeKey(key: string): string { + return key + .trim() + .replace(/[^\w-]/g, "_") + .replace(/^(\d)/, "_$1") // Prefix numbers with underscore + } + + /** + * Process nested structures and arrays + * @param obj Object to process + * @returns Processed object + */ + private static processStructure(obj: any): any { + if (typeof obj !== "object" || obj === null) { + return obj + } + + if (Array.isArray(obj)) { + return obj.map((item) => this.processStructure(item)) + } + + const result: any = {} + const arrays: { [key: string]: any[] } = {} + + // First pass: collect array items + for (const [key, value] of Object.entries(obj)) { + if (key === "array_item") { + return this.processStructure(value) + } + + const match = key.match(/^(.+?)_(\d+)$/) + if (match) { + const [, baseKey, index] = match + if (!arrays[baseKey]) { + arrays[baseKey] = [] + } + arrays[baseKey][parseInt(index)] = this.processStructure(value) + continue + } + + result[key] = this.processStructure(value) + } + + // Second pass: merge arrays into result + for (const [key, value] of Object.entries(arrays)) { + result[key] = value.filter((item) => item !== undefined) + } + + return result + } +} diff --git a/src/services/package-manager/__tests__/GitFetcher.test.ts b/src/services/package-manager/__tests__/GitFetcher.test.ts index 8a9818284d..b5f0ccd530 100644 --- a/src/services/package-manager/__tests__/GitFetcher.test.ts +++ b/src/services/package-manager/__tests__/GitFetcher.test.ts @@ -1,206 +1,228 @@ -import { GitFetcher } from '../GitFetcher'; -import * as vscode from 'vscode'; -import * as fs from 'fs/promises'; -import { exec } from 'child_process'; -import { promisify } from 'util'; -import path from 'path'; -import { PackageManagerItem, PackageManagerRepository } from '../types'; +import * as vscode from "vscode" +import { GitFetcher } from "../GitFetcher" +import * as fs from "fs/promises" +import simpleGit, { SimpleGit } from "simple-git" -// Mock the exec function -jest.mock('child_process', () => ({ - exec: jest.fn() -})); +// Mock simpleGit +jest.mock("simple-git", () => { + const mockGit = { + clone: jest.fn(), + pull: jest.fn(), + revparse: jest.fn(), + fetch: jest.fn(), + clean: jest.fn(), + raw: jest.fn(), + } + return jest.fn(() => mockGit) +}) -// Mock promisify to return our mocked exec function -jest.mock('util', () => ({ - promisify: jest.fn().mockImplementation(() => { - return jest.fn().mockResolvedValue({ stdout: '', stderr: '' }); - }) -})); - -// Mock fs.promises -jest.mock('fs/promises', () => ({ - mkdir: jest.fn().mockResolvedValue(undefined), - readdir: jest.fn().mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('roles')) { - return Promise.resolve(['developer-role']); - } - if (pathStr.includes('mcp-servers')) { - return Promise.resolve(['file-analyzer']); - } - if (pathStr.includes('storage-systems')) { - return Promise.resolve(['github-storage']); - } - if (pathStr.includes('items')) { - return Promise.resolve([]); - } - return Promise.resolve([]); - }), - stat: jest.fn().mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('.git') || - pathStr.includes('roles') || - pathStr.includes('mcp-servers') || - pathStr.includes('storage-systems') || - pathStr.includes('developer-role') || - pathStr.includes('file-analyzer') || - pathStr.includes('github-storage')) { - return Promise.resolve({ isDirectory: () => true }); - } - if (pathStr.includes('metadata.yml')) { - return Promise.resolve({ isFile: () => true }); - } - return Promise.reject(new Error('File not found')); - }), - readFile: jest.fn().mockImplementation((path, encoding) => { - const pathStr = path.toString(); - if (pathStr.includes('metadata.yml') && - !pathStr.includes('developer-role') && - !pathStr.includes('file-analyzer') && - !pathStr.includes('github-storage')) { - return Promise.resolve('name: "Example Package Manager Repository"\ndescription: "A collection of example package manager items for Roo-Code"\nauthor: "Roo Team"\nversion: "1.0.0"\nlastUpdated: "2025-04-08"'); - } - if (pathStr.includes('developer-role/metadata.yml')) { - return Promise.resolve('name: "Full-Stack Developer Role"\ndescription: "A role for a full-stack developer"\ntype: "role"\nauthor: "Roo Team"\nversion: "1.0.0"\ntags: ["developer", "full-stack"]'); - } - if (pathStr.includes('file-analyzer/metadata.yml')) { - return Promise.resolve('name: "File Analyzer MCP Server"\ndescription: "An MCP server that analyzes files"\ntype: "mcp-server"\nauthor: "Roo Team"\nversion: "1.0.0"\ntags: ["file-analyzer", "code-quality"]'); - } - if (pathStr.includes('github-storage/metadata.yml')) { - return Promise.resolve('name: "GitHub Storage System"\ndescription: "A storage system that uses GitHub repositories"\ntype: "storage"\nauthor: "Roo Team"\nversion: "1.0.0"\ntags: ["storage", "github"]'); - } - return Promise.reject(new Error('File not found')); - }) -})); -const mockedFs = fs as jest.Mocked; +// Mock fs/promises +jest.mock("fs/promises", () => ({ + mkdir: jest.fn(), + stat: jest.fn(), + rm: jest.fn(), + readdir: jest.fn().mockResolvedValue([]), + readFile: jest.fn().mockResolvedValue(` +name: Test Repository +description: Test Description +version: 1.0.0 +`), +})) // Mock vscode -jest.mock('vscode', () => ({ - window: { - showErrorMessage: jest.fn(), - }, - Uri: { - parse: jest.fn().mockImplementation((url) => ({ toString: () => url })), - } -})); +const mockContext = { + globalStorageUri: { + fsPath: "/mock/storage/path", + }, +} as vscode.ExtensionContext -describe('GitFetcher', () => { - let gitFetcher: GitFetcher; - - const mockContext = { - globalStorageUri: { fsPath: '/mock/storage/path' } - } as unknown as vscode.ExtensionContext; - - beforeEach(() => { - gitFetcher = new GitFetcher(mockContext); - jest.clearAllMocks(); - - // Setup path.join to work normally - jest.spyOn(path, 'join').mockImplementation((...args) => args.join('/')); - }); - - describe('fetchRepository', () => { - it('should fetch repository successfully', async () => { - const repoUrl = 'https://github.com/Smartsheet-JB-Brown/Package-Manager-Test'; - - // Mock execAsync for git operations - const mockExecPromise = jest.fn().mockResolvedValue({ stdout: '', stderr: '' }); - (promisify as unknown as jest.Mock).mockReturnValue(mockExecPromise); - - // Call the method - const result = await gitFetcher.fetchRepository(repoUrl); - - // Assertions - expect(result).toBeDefined(); - expect(result.metadata).toBeDefined(); - expect(result.metadata.name).toBe('Example Package Manager Repository'); - expect(result.items).toHaveLength(3); // One role, one MCP server, one storage system - - // Check role item - const roleItem = result.items.find((item: PackageManagerItem) => item.type === 'role'); - expect(roleItem).toBeDefined(); - expect(roleItem?.name).toBe('Full-Stack Developer Role'); - expect(roleItem?.tags).toContain('developer'); - expect(roleItem?.url).toBe('https://github.com/Smartsheet-JB-Brown/Package-Manager-Test/tree/main/roles/developer-role'); - - // Check MCP server item - const mcpServerItem = result.items.find((item: PackageManagerItem) => item.type === 'mcp-server'); - expect(mcpServerItem).toBeDefined(); - expect(mcpServerItem?.name).toBe('File Analyzer MCP Server'); - expect(mcpServerItem?.tags).toContain('file-analyzer'); - expect(mcpServerItem?.url).toBe('https://github.com/Smartsheet-JB-Brown/Package-Manager-Test/tree/main/mcp-servers/file-analyzer'); - - // Check storage system item - const storageItem = result.items.find((item: PackageManagerItem) => item.type === 'storage'); - expect(storageItem).toBeDefined(); - expect(storageItem?.name).toBe('GitHub Storage System'); - expect(storageItem?.tags).toContain('storage'); - expect(storageItem?.url).toBe('https://github.com/Smartsheet-JB-Brown/Package-Manager-Test/tree/main/storage-systems/github-storage'); - - // Verify file system operations - expect(mockedFs.mkdir).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache', { recursive: true }); - expect(mockedFs.stat).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/.git'); - expect(mockedFs.stat).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/metadata.yml'); - expect(mockedFs.readFile).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/metadata.yml', 'utf-8'); - - // Verify that readdir was called for each item directory type - expect(mockedFs.readdir).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/roles'); - expect(mockedFs.readdir).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/mcp-servers'); - expect(mockedFs.readdir).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/storage-systems'); - }); - - it('should handle errors when fetching repository', async () => { - const repoUrl = 'https://github.com/Smartsheet-JB-Brown/Package-Manager-Test'; - - // Mock stat to throw an error for the .git directory check - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('.git')) { - return Promise.reject(new Error('Directory not found')); - } - return Promise.resolve({ isDirectory: () => false, isFile: () => false } as any); - }); - - // Mock readFile to throw an error for metadata.yml - mockedFs.readFile.mockImplementation((path) => { - return Promise.reject(new Error('File not found')); - }); - - // Mock exec to throw an error - const mockExecPromise = jest.fn().mockRejectedValue(new Error('Git error')); - (promisify as unknown as jest.Mock).mockReturnValue(mockExecPromise); - - // Call the method - const result = await gitFetcher.fetchRepository(repoUrl); - - // Assertions - expect(result).toEqual({ metadata: {}, items: [], url: repoUrl }); - expect(vscode.window.showErrorMessage).toHaveBeenCalled(); - }); - }); - - describe('getRepoNameFromUrl', () => { - it('should extract repository name from GitHub URL', () => { - const url = 'https://github.com/Smartsheet-JB-Brown/Package-Manager-Test'; - const result = gitFetcher['getRepoNameFromUrl'](url); - - expect(result).toBe('Package-Manager-Test'); - }); - it('should handle GitHub URLs with trailing slash', () => { - const url = 'https://github.com/Smartsheet-JB-Brown/Package-Manager-Test/'; - // Call the actual method on gitFetcher - const result = gitFetcher['getRepoNameFromUrl'](url); - - expect(result).toBe('Package-Manager-Test'); - }); - - it('should sanitize repository names', () => { - const url = 'https://github.com/Smartsheet-JB-Brown/Package Manager Test'; - // Call the actual method on gitFetcher - const result = gitFetcher['getRepoNameFromUrl'](url); - - expect(result).toBe('Package-Manager-Test'); - }); - }); -}); \ No newline at end of file +describe("GitFetcher", () => { + let gitFetcher: GitFetcher + const mockSimpleGit = simpleGit as jest.MockedFunction + const testRepoUrl = "https://github.com/test/repo" + const testRepoDir = "/mock/storage/path/package-manager-cache/repo" + + beforeEach(() => { + jest.clearAllMocks() + gitFetcher = new GitFetcher(mockContext) + + // Reset fs mock defaults + ;(fs.mkdir as jest.Mock).mockResolvedValue(undefined) + ;(fs.rm as jest.Mock).mockImplementation((path: string, options?: any) => { + if (path === testRepoDir && options?.recursive && options?.force) { + return Promise.resolve(undefined) + } + return Promise.reject(new Error("Invalid rm call")) + }) + + // Setup fs.stat mock for repository structure validation + ;(fs.stat as jest.Mock).mockImplementation((path: string) => { + if (path.endsWith(".git")) return Promise.reject(new Error("ENOENT")) + if (path.endsWith("metadata.en.yml")) return Promise.resolve(true) + if (path.endsWith("README.md")) return Promise.resolve(true) + return Promise.reject(new Error("ENOENT")) + }) + + // Setup default git mock behavior + const mockGit = { + clone: jest.fn().mockResolvedValue(undefined), + pull: jest.fn().mockResolvedValue(undefined), + revparse: jest.fn().mockResolvedValue("main"), + // Add other required SimpleGit methods with no-op implementations + addAnnotatedTag: jest.fn(), + addConfig: jest.fn(), + applyPatch: jest.fn(), + listConfig: jest.fn(), + addRemote: jest.fn(), + addTag: jest.fn(), + branch: jest.fn(), + branchLocal: jest.fn(), + checkout: jest.fn(), + checkoutBranch: jest.fn(), + checkoutLatestTag: jest.fn(), + checkoutLocalBranch: jest.fn(), + clean: jest.fn(), + clearQueue: jest.fn(), + commit: jest.fn(), + cwd: jest.fn(), + deleteLocalBranch: jest.fn(), + deleteLocalBranches: jest.fn(), + diff: jest.fn(), + diffSummary: jest.fn(), + exec: jest.fn(), + fetch: jest.fn(), + getRemotes: jest.fn(), + init: jest.fn(), + log: jest.fn(), + merge: jest.fn(), + mirror: jest.fn(), + push: jest.fn(), + pushTags: jest.fn(), + raw: jest.fn(), + rebase: jest.fn(), + remote: jest.fn(), + removeRemote: jest.fn(), + reset: jest.fn(), + revert: jest.fn(), + show: jest.fn(), + stash: jest.fn(), + status: jest.fn(), + subModule: jest.fn(), + tag: jest.fn(), + tags: jest.fn(), + updateServerInfo: jest.fn(), + } as unknown as SimpleGit + mockSimpleGit.mockReturnValue(mockGit) + }) + + describe("fetchRepository", () => { + it("should successfully clone a new repository", async () => { + await expect(gitFetcher.fetchRepository(testRepoUrl)).resolves.toBeDefined() + + const mockGit = mockSimpleGit() + expect(mockGit.clone).toHaveBeenCalledWith(testRepoUrl, testRepoDir) + expect(mockGit.clean).toHaveBeenCalledWith(["--force", "-d"]) + expect(mockGit.raw).toHaveBeenCalledWith(["reset", "--hard", "HEAD"]) + }) + + it("should pull existing repository", async () => { + // Mock repository exists + ;(fs.stat as jest.Mock).mockImplementation((path: string) => { + if (path.endsWith(".git")) return Promise.resolve(true) + if (path.endsWith("metadata.en.yml")) return Promise.resolve(true) + if (path.endsWith("README.md")) return Promise.resolve(true) + return Promise.reject(new Error("ENOENT")) + }) + + await gitFetcher.fetchRepository(testRepoUrl) + + const mockGit = mockSimpleGit() + expect(mockGit.fetch).toHaveBeenCalledWith("origin", "main") + expect(mockGit.raw).toHaveBeenCalledWith(["reset", "--hard", "origin/main"]) + expect(mockGit.clean).toHaveBeenCalledWith(["--force", "-d"]) + expect(mockGit.clone).not.toHaveBeenCalled() + }) + + it("should handle clone failures", async () => { + const error = new Error("fatal: repository not found") + const mockGit = { + ...mockSimpleGit(), + clone: jest.fn().mockRejectedValue(error), + pull: jest.fn(), + revparse: jest.fn(), + } as unknown as SimpleGit + mockSimpleGit.mockReturnValue(mockGit) + + await expect(gitFetcher.fetchRepository(testRepoUrl)).rejects.toThrow( + "Failed to clone/pull repository: fatal: repository not found", + ) + + // Verify cleanup was called + expect(fs.rm).toHaveBeenCalledWith(testRepoDir, { recursive: true, force: true }) + }) + + it("should handle pull failures and re-clone", async () => { + // Mock repository exists + ;(fs.stat as jest.Mock).mockImplementation((path: string) => { + if (path.endsWith(".git")) return Promise.resolve(true) + if (path.endsWith("metadata.en.yml")) return Promise.resolve(true) + if (path.endsWith("README.md")) return Promise.resolve(true) + return Promise.reject(new Error("ENOENT")) + }) + + // Reset fs.rm mock to track calls + ;(fs.rm as jest.Mock).mockReset() + ;(fs.rm as jest.Mock).mockImplementation((path: string, options?: any) => { + if (path === testRepoDir && options?.recursive && options?.force) { + return Promise.resolve(undefined) + } + return Promise.reject(new Error("Invalid rm call")) + }) + + const mockGit = { + clone: jest.fn().mockResolvedValue(undefined), + pull: jest.fn().mockRejectedValue(new Error("not a git repository")), + revparse: jest.fn().mockResolvedValue("main"), + fetch: jest.fn().mockRejectedValue(new Error("not a git repository")), + clean: jest.fn(), + raw: jest.fn(), + } as unknown as SimpleGit + mockSimpleGit.mockReturnValue(mockGit) + + await gitFetcher.fetchRepository(testRepoUrl) + + // Verify directory was removed and repository was re-cloned + // First rm call is for cleanup before clone + expect(fs.rm).toHaveBeenCalledWith(testRepoDir, { recursive: true, force: true }) + // Second rm call is after pull failure + expect(fs.rm).toHaveBeenCalledWith(testRepoDir, { recursive: true, force: true }) + expect(mockGit.clone).toHaveBeenCalledWith(testRepoUrl, testRepoDir) + expect(mockGit.clean).toHaveBeenCalledWith(["--force", "-d"]) + expect(mockGit.raw).toHaveBeenCalledWith(["reset", "--hard", "HEAD"]) + }) + + it("should handle missing metadata.yml", async () => { + // Mock repository exists but missing metadata + ;(fs.stat as jest.Mock).mockImplementation((path: string) => { + if (path.endsWith("metadata.en.yml")) return Promise.reject(new Error("ENOENT")) + return Promise.resolve(true) + }) + + await expect(gitFetcher.fetchRepository(testRepoUrl)).rejects.toThrow( + "Repository is missing metadata.en.yml file", + ) + }) + + it("should handle missing README.md", async () => { + // Mock repository exists but missing README + ;(fs.stat as jest.Mock).mockImplementation((path: string) => { + if (path.endsWith("README.md")) return Promise.reject(new Error("ENOENT")) + return Promise.resolve(true) + }) + + await expect(gitFetcher.fetchRepository(testRepoUrl)).rejects.toThrow( + "Repository is missing README.md file", + ) + }) + }) +}) diff --git a/src/services/package-manager/__tests__/MetadataScanner.test.ts b/src/services/package-manager/__tests__/MetadataScanner.test.ts new file mode 100644 index 0000000000..6321aa72ab --- /dev/null +++ b/src/services/package-manager/__tests__/MetadataScanner.test.ts @@ -0,0 +1,175 @@ +import * as fs from "fs/promises" +import { MetadataScanner } from "../MetadataScanner" +import { Dirent } from "fs" + +// Mock fs/promises +jest.mock("fs/promises", () => ({ + readdir: jest.fn(), + readFile: jest.fn(), +})) + +describe("MetadataScanner", () => { + let metadataScanner: MetadataScanner + const mockFs = fs as jest.Mocked + + beforeEach(() => { + metadataScanner = new MetadataScanner() + jest.clearAllMocks() + }) + + describe("scanDirectory", () => { + it("should discover components with English metadata", async () => { + // Mock directory structure + mockFs.readdir.mockImplementation((path: any, options?: any) => { + const pathStr = path.toString() + if (pathStr === "/test/repo") { + return Promise.resolve([ + { + name: "component1", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("component1")) { + return Promise.resolve([ + { + name: "metadata.en.yml", + isDirectory: () => false, + isFile: () => true, + } as Dirent, + ]) + } + return Promise.resolve([]) + }) + + mockFs.readFile.mockImplementation((path: any) => { + const pathStr = path.toString() + if (pathStr.includes("metadata.en.yml")) { + return Promise.resolve(` +name: Test Component +description: A test component +type: mcp server +version: 1.0.0 +`) + } + return Promise.resolve("") + }) + + const items = await metadataScanner.scanDirectory("/test/repo", "https://example.com") + + expect(items).toHaveLength(1) + expect(items[0].name).toBe("Test Component") + expect(items[0].type).toBe("mcp server") + }) + + it("should skip components without English metadata", async () => { + mockFs.readdir.mockImplementation((path: any, options?: any) => { + const pathStr = path.toString() + if (pathStr === "/test/repo") { + return Promise.resolve([ + { + name: "component1", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("component1")) { + return Promise.resolve([ + { + name: "metadata.fr.yml", + isDirectory: () => false, + isFile: () => true, + } as Dirent, + ]) + } + return Promise.resolve([]) + }) + + const items = await metadataScanner.scanDirectory("/test/repo", "https://example.com") + + expect(items).toHaveLength(0) + }) + + it("should handle invalid metadata files", async () => { + mockFs.readdir.mockImplementation((path: any, options?: any) => { + const pathStr = path.toString() + if (pathStr === "/test/repo") { + return Promise.resolve([ + { + name: "component1", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("component1")) { + return Promise.resolve([ + { + name: "metadata.en.yml", + isDirectory: () => false, + isFile: () => true, + } as Dirent, + ]) + } + return Promise.resolve([]) + }) + + mockFs.readFile.mockImplementation((path: any) => { + const pathStr = path.toString() + if (pathStr.includes("metadata.en.yml")) { + return Promise.resolve("invalid: yaml: content") + } + return Promise.resolve("") + }) + + const items = await metadataScanner.scanDirectory("/test/repo", "https://example.com") + + expect(items).toHaveLength(0) + }) + + it("should include source name in items when provided", async () => { + mockFs.readdir.mockImplementation((path: any, options?: any) => { + const pathStr = path.toString() + if (pathStr === "/test/repo") { + return Promise.resolve([ + { + name: "component1", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("component1")) { + return Promise.resolve([ + { + name: "metadata.en.yml", + isDirectory: () => false, + isFile: () => true, + } as Dirent, + ]) + } + return Promise.resolve([]) + }) + + mockFs.readFile.mockImplementation((path: any) => { + const pathStr = path.toString() + if (pathStr.includes("metadata.en.yml")) { + return Promise.resolve(` +name: Test Component +description: A test component +type: mcp server +version: 1.0.0 +`) + } + return Promise.resolve("") + }) + + const items = await metadataScanner.scanDirectory("/test/repo", "https://example.com", "Custom Source") + + expect(items).toHaveLength(1) + expect(items[0].sourceName).toBe("Custom Source") + }) + }) +}) diff --git a/src/services/package-manager/__tests__/ParsePackageManagerItems.test.ts b/src/services/package-manager/__tests__/ParsePackageManagerItems.test.ts index b515a3f5ba..097a3a829b 100644 --- a/src/services/package-manager/__tests__/ParsePackageManagerItems.test.ts +++ b/src/services/package-manager/__tests__/ParsePackageManagerItems.test.ts @@ -1,273 +1,221 @@ -import { GitFetcher } from '../GitFetcher'; -import * as vscode from 'vscode'; -import * as fs from 'fs/promises'; -import * as path from 'path'; -import { PackageManagerItem } from '../types'; +import * as fs from "fs/promises" +import { MetadataScanner } from "../MetadataScanner" +import { PackageManagerItem } from "../types" +import { Dirent } from "fs" -// Mock fs.promises -jest.mock('fs/promises', () => ({ - stat: jest.fn(), - mkdir: jest.fn().mockResolvedValue(undefined), - readdir: jest.fn(), - readFile: jest.fn() -})); -const mockedFs = fs as jest.Mocked; +// Mock fs/promises +jest.mock("fs/promises", () => ({ + readdir: jest.fn(), + readFile: jest.fn(), + stat: jest.fn(), +})) -// Mock vscode -jest.mock('vscode', () => ({ - window: { - showErrorMessage: jest.fn(), - } -})); +describe("Parse Package Manager Items", () => { + let metadataScanner: MetadataScanner + const mockFs = fs as jest.Mocked -describe('Parse Package Manager Items', () => { - let gitFetcher: GitFetcher; - - const mockContext = { - globalStorageUri: { fsPath: '/mock/storage/path' } - } as unknown as vscode.ExtensionContext; - - beforeEach(() => { - gitFetcher = new GitFetcher(mockContext); - jest.clearAllMocks(); - }); - - // Helper function to access private method - const parsePackageManagerItems = async (repoDir: string, repoUrl: string) => { - return (gitFetcher as any).parsePackageManagerItems(repoDir, repoUrl); - }; - - describe('directory structure handling', () => { - it('should parse items from mcp-servers directory', async () => { - // Mock directory structure - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('mcp-servers')) { - return Promise.resolve({ isDirectory: () => true } as any); - } - if (pathStr.includes('metadata.yml')) { - return Promise.resolve({ isFile: () => true } as any); - } - return Promise.reject(new Error('Not found')); - }); - - // Mock readdir to return items in mcp-servers directory - mockedFs.readdir.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('mcp-servers')) { - return Promise.resolve(['file-analyzer'] as any); - } - return Promise.resolve([] as any); - }); - - // Mock readFile to return metadata content - mockedFs.readFile.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('file-analyzer/metadata.yml')) { - return Promise.resolve('name: "File Analyzer MCP Server"\ndescription: "An MCP server that analyzes files"\ntype: "mcp-server"\nauthor: "Roo Team"\nversion: "1.0.0"\ntags: ["file-analyzer", "code-quality"]'); - } - return Promise.reject(new Error('File not found')); - }); - - // Call the method - const items = await parsePackageManagerItems('/mock/repo', 'https://github.com/example/repo'); - - // Assertions - expect(items).toHaveLength(1); - expect(items[0].name).toBe('File Analyzer MCP Server'); - expect(items[0].type).toBe('mcp-server'); - expect(items[0].url).toBe('https://github.com/example/repo/tree/main/mcp-servers/file-analyzer'); - }); - - it('should parse items from roles directory', async () => { - // Mock directory structure - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('roles')) { - return Promise.resolve({ isDirectory: () => true } as any); - } - if (pathStr.includes('metadata.yml')) { - return Promise.resolve({ isFile: () => true } as any); - } - return Promise.reject(new Error('Not found')); - }); - - // Mock readdir to return items in roles directory - mockedFs.readdir.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('roles')) { - return Promise.resolve(['developer-role'] as any); - } - return Promise.resolve([] as any); - }); - - // Mock readFile to return metadata content - mockedFs.readFile.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('developer-role/metadata.yml')) { - return Promise.resolve('name: "Full-Stack Developer Role"\ndescription: "A role for a full-stack developer"\ntype: "role"\nauthor: "Roo Team"\nversion: "1.0.0"\ntags: ["developer", "full-stack"]'); - } - return Promise.reject(new Error('File not found')); - }); - - // Call the method - const items = await parsePackageManagerItems('/mock/repo', 'https://github.com/example/repo'); - - // Assertions - expect(items).toHaveLength(1); - expect(items[0].name).toBe('Full-Stack Developer Role'); - expect(items[0].type).toBe('role'); - expect(items[0].url).toBe('https://github.com/example/repo/tree/main/roles/developer-role'); - }); - - it('should parse items from storage-systems directory', async () => { - // Mock directory structure - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('storage-systems')) { - return Promise.resolve({ isDirectory: () => true } as any); - } - if (pathStr.includes('metadata.yml')) { - return Promise.resolve({ isFile: () => true } as any); - } - return Promise.reject(new Error('Not found')); - }); - - // Mock readdir to return items in storage-systems directory - mockedFs.readdir.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('storage-systems')) { - return Promise.resolve(['github-storage'] as any); - } - return Promise.resolve([] as any); - }); - - // Mock readFile to return metadata content - mockedFs.readFile.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('github-storage/metadata.yml')) { - return Promise.resolve('name: "GitHub Storage System"\ndescription: "A storage system that uses GitHub repositories"\ntype: "storage"\nauthor: "Roo Team"\nversion: "1.0.0"\ntags: ["storage", "github"]'); - } - return Promise.reject(new Error('File not found')); - }); - - // Call the method - const items = await parsePackageManagerItems('/mock/repo', 'https://github.com/example/repo'); - - // Assertions - expect(items).toHaveLength(1); - expect(items[0].name).toBe('GitHub Storage System'); - expect(items[0].type).toBe('storage'); - expect(items[0].url).toBe('https://github.com/example/repo/tree/main/storage-systems/github-storage'); - }); - - it('should parse items from items directory (backward compatibility)', async () => { - // Mock directory structure - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('/items')) { - return Promise.resolve({ isDirectory: () => true } as any); - } - if (pathStr.includes('metadata.yml')) { - return Promise.resolve({ isFile: () => true } as any); - } - return Promise.reject(new Error('Not found')); - }); - - // Mock readdir to return items in items directory - mockedFs.readdir.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('/items')) { - return Promise.resolve(['generic-item'] as any); - } - return Promise.resolve([] as any); - }); - - // Mock readFile to return metadata content - mockedFs.readFile.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('generic-item/metadata.yml')) { - return Promise.resolve('name: "Generic Item"\ndescription: "A generic package manager item"\ntype: "other"\nauthor: "Roo Team"\nversion: "1.0.0"\ntags: ["generic", "other"]'); - } - return Promise.reject(new Error('File not found')); - }); - - // Call the method - const items = await parsePackageManagerItems('/mock/repo', 'https://github.com/example/repo'); - - // Assertions - expect(items).toHaveLength(1); - expect(items[0].name).toBe('Generic Item'); - expect(items[0].type).toBe('other'); - expect(items[0].url).toBe('https://github.com/example/repo/tree/main/items/generic-item'); - }); - - it('should parse items from multiple directories', async () => { - // Mock directory structure - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('mcp-servers') || pathStr.includes('roles') || pathStr.includes('storage-systems')) { - return Promise.resolve({ isDirectory: () => true } as any); - } - if (pathStr.includes('metadata.yml')) { - return Promise.resolve({ isFile: () => true } as any); - } - return Promise.reject(new Error('Not found')); - }); - - // Mock readdir to return items in each directory - mockedFs.readdir.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('mcp-servers')) { - return Promise.resolve(['file-analyzer'] as any); - } - if (pathStr.includes('roles')) { - return Promise.resolve(['developer-role'] as any); - } - if (pathStr.includes('storage-systems')) { - return Promise.resolve(['github-storage'] as any); - } - return Promise.resolve([] as any); - }); - - // Mock readFile to return metadata content - mockedFs.readFile.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('file-analyzer/metadata.yml')) { - return Promise.resolve('name: "File Analyzer MCP Server"\ndescription: "An MCP server that analyzes files"\ntype: "mcp-server"\nauthor: "Roo Team"\nversion: "1.0.0"\ntags: ["file-analyzer", "code-quality"]'); - } - if (pathStr.includes('developer-role/metadata.yml')) { - return Promise.resolve('name: "Full-Stack Developer Role"\ndescription: "A role for a full-stack developer"\ntype: "role"\nauthor: "Roo Team"\nversion: "1.0.0"\ntags: ["developer", "full-stack"]'); - } - if (pathStr.includes('github-storage/metadata.yml')) { - return Promise.resolve('name: "GitHub Storage System"\ndescription: "A storage system that uses GitHub repositories"\ntype: "storage"\nauthor: "Roo Team"\nversion: "1.0.0"\ntags: ["storage", "github"]'); - } - return Promise.reject(new Error('File not found')); - }); - - // Call the method - const items = await parsePackageManagerItems('/mock/repo', 'https://github.com/example/repo'); - - // Assertions - expect(items).toHaveLength(3); - - // Check for MCP server item - const mcpServerItem = items.find((item: PackageManagerItem) => item.type === 'mcp-server'); - expect(mcpServerItem).toBeDefined(); - expect(mcpServerItem?.name).toBe('File Analyzer MCP Server'); - expect(mcpServerItem?.url).toBe('https://github.com/example/repo/tree/main/mcp-servers/file-analyzer'); - - // Check for role item - const roleItem = items.find((item: PackageManagerItem) => item.type === 'role'); - expect(roleItem).toBeDefined(); - expect(roleItem?.name).toBe('Full-Stack Developer Role'); - expect(roleItem?.url).toBe('https://github.com/example/repo/tree/main/roles/developer-role'); - - // Check for storage system item - const storageItem = items.find((item: PackageManagerItem) => item.type === 'storage'); - expect(storageItem).toBeDefined(); - expect(storageItem?.name).toBe('GitHub Storage System'); - expect(storageItem?.url).toBe('https://github.com/example/repo/tree/main/storage-systems/github-storage'); - }); - }); -}); \ No newline at end of file + beforeEach(() => { + metadataScanner = new MetadataScanner() + jest.clearAllMocks() + + // Mock stat to always succeed + mockFs.stat.mockResolvedValue({} as any) + }) + + describe("directory structure handling", () => { + it("should parse items from mcp-servers directory", async () => { + // Mock directory structure + mockFs.readdir.mockImplementation((path: any, options?: any) => { + const pathStr = path.toString() + if (pathStr === "/mock/repo") { + return Promise.resolve([ + { + name: "mcp servers", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("mcp servers")) { + return Promise.resolve([ + { + name: "file-analyzer", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("file-analyzer")) { + return Promise.resolve([ + { + name: "metadata.en.yml", + isDirectory: () => false, + isFile: () => true, + } as Dirent, + ]) + } + return Promise.resolve([]) + }) + + // Mock metadata file content + mockFs.readFile.mockImplementation((path: any) => { + const pathStr = path.toString() + if (pathStr.includes("metadata.en.yml")) { + return Promise.resolve(` +name: File Analyzer MCP Server +description: An MCP server that analyzes files +type: mcp server +version: 1.0.0 +`) + } + return Promise.resolve("") + }) + + const items = await metadataScanner.scanDirectory("/mock/repo", "https://github.com/example/repo") + + expect(items).toHaveLength(1) + expect(items[0].name).toBe("File Analyzer MCP Server") + expect(items[0].type).toBe("mcp server") + }) + + it("should parse items from modes directory", async () => { + // Mock directory structure + mockFs.readdir.mockImplementation((path: any, options?: any) => { + const pathStr = path.toString() + if (pathStr === "/mock/repo") { + return Promise.resolve([ + { + name: "modes", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("modes")) { + return Promise.resolve([ + { + name: "developer-mode", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("developer-mode")) { + return Promise.resolve([ + { + name: "metadata.en.yml", + isDirectory: () => false, + isFile: () => true, + } as Dirent, + ]) + } + return Promise.resolve([]) + }) + + // Mock metadata file content + mockFs.readFile.mockImplementation((path: any) => { + const pathStr = path.toString() + if (pathStr.includes("metadata.en.yml")) { + return Promise.resolve(` +name: Full-Stack Developer Mode +description: A mode for full-stack development +type: mode +version: 1.0.0 +`) + } + return Promise.resolve("") + }) + + const items = await metadataScanner.scanDirectory("/mock/repo", "https://github.com/example/repo") + + expect(items).toHaveLength(1) + expect(items[0].name).toBe("Full-Stack Developer Mode") + expect(items[0].type).toBe("mode") + }) + + it("should parse items from multiple directories", async () => { + // Mock directory structure + mockFs.readdir.mockImplementation((path: any, options?: any) => { + const pathStr = path.toString() + if (pathStr === "/mock/repo") { + return Promise.resolve([ + { + name: "mcp servers", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + { + name: "modes", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("mcp servers")) { + return Promise.resolve([ + { + name: "file-analyzer", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("modes")) { + return Promise.resolve([ + { + name: "developer-mode", + isDirectory: () => true, + isFile: () => false, + } as Dirent, + ]) + } + if (pathStr.includes("file-analyzer") || pathStr.includes("developer-mode")) { + return Promise.resolve([ + { + name: "metadata.en.yml", + isDirectory: () => false, + isFile: () => true, + } as Dirent, + ]) + } + return Promise.resolve([]) + }) + + // Mock metadata file content + mockFs.readFile.mockImplementation((path: any) => { + const pathStr = path.toString() + if (pathStr.includes("file-analyzer")) { + return Promise.resolve(` +name: File Analyzer MCP Server +description: An MCP server that analyzes files +type: mcp server +version: 1.0.0 +`) + } + if (pathStr.includes("developer-mode")) { + return Promise.resolve(` +name: Full-Stack Developer Mode +description: A mode for full-stack development +type: mode +version: 1.0.0 +`) + } + return Promise.resolve("") + }) + + const items = await metadataScanner.scanDirectory("/mock/repo", "https://github.com/example/repo") + + expect(items).toHaveLength(2) + + // Check for MCP server item + const mcpServerItem = items.find((item: PackageManagerItem) => item.type === "mcp server") + expect(mcpServerItem).toBeDefined() + expect(mcpServerItem?.name).toBe("File Analyzer MCP Server") + + // Check for mode item + const modeItem = items.find((item: PackageManagerItem) => item.type === "mode") + expect(modeItem).toBeDefined() + expect(modeItem?.name).toBe("Full-Stack Developer Mode") + }) + }) +}) diff --git a/src/services/package-manager/__tests__/RepositoryStructureValidation.test.ts b/src/services/package-manager/__tests__/RepositoryStructureValidation.test.ts index b24505f93c..ef390350c3 100644 --- a/src/services/package-manager/__tests__/RepositoryStructureValidation.test.ts +++ b/src/services/package-manager/__tests__/RepositoryStructureValidation.test.ts @@ -1,163 +1,56 @@ -import { GitFetcher } from '../GitFetcher'; -import * as vscode from 'vscode'; -import * as fs from 'fs/promises'; -import * as path from 'path'; +import * as vscode from "vscode" +import * as fs from "fs/promises" +import { GitFetcher } from "../GitFetcher" -// Mock fs.promises -jest.mock('fs/promises', () => ({ - stat: jest.fn(), - mkdir: jest.fn().mockResolvedValue(undefined), - readdir: jest.fn().mockResolvedValue([]), - readFile: jest.fn().mockResolvedValue('') -})); -const mockedFs = fs as jest.Mocked; +// Mock fs/promises +jest.mock("fs/promises", () => ({ + stat: jest.fn(), + readFile: jest.fn(), + mkdir: jest.fn(), + rm: jest.fn(), +})) -// Mock vscode -jest.mock('vscode', () => ({ - window: { - showErrorMessage: jest.fn(), - } -})); +describe("Repository Structure Validation", () => { + let gitFetcher: GitFetcher + const mockFs = fs as jest.Mocked -describe('Repository Structure Validation', () => { - let gitFetcher: GitFetcher; - - const mockContext = { - globalStorageUri: { fsPath: '/mock/storage/path' } - } as unknown as vscode.ExtensionContext; - - beforeEach(() => { - gitFetcher = new GitFetcher(mockContext); - jest.clearAllMocks(); - }); - - // Helper function to access private method - const validateRepositoryStructure = async (repoDir: string) => { - return (gitFetcher as any).validateRepositoryStructure(repoDir); - }; - - describe('metadata.yml validation', () => { - it('should throw error when metadata.yml is missing', async () => { - // Mock stat to return false for metadata.yml - mockedFs.stat.mockImplementation((path) => { - if (path.toString().includes('metadata.yml')) { - return Promise.reject(new Error('File not found')); - } - return Promise.resolve({ isDirectory: () => true, isFile: () => true } as any); - }); - - // Call the method and expect it to throw - await expect(validateRepositoryStructure('/mock/repo')).rejects.toThrow('Repository is missing metadata.yml file'); - }); - - it('should pass when metadata.yml exists', async () => { - // Mock stat to return true for metadata.yml and at least one item directory - mockedFs.stat.mockImplementation((path) => { - return Promise.resolve({ isDirectory: () => true, isFile: () => true } as any); - }); - - // Call the method and expect it not to throw - await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); - }); - }); - - describe('item directories validation', () => { - it('should throw error when no item directories exist', async () => { - // Mock stat to return true for metadata.yml but false for all item directories - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('metadata.yml')) { - return Promise.resolve({ isFile: () => true } as any); - } - if (pathStr.includes('mcp-servers') || pathStr.includes('roles') || - pathStr.includes('storage-systems') || pathStr.includes('items')) { - return Promise.reject(new Error('Directory not found')); - } - return Promise.resolve({ isDirectory: () => true } as any); - }); - - // Call the method and expect it to throw - await expect(validateRepositoryStructure('/mock/repo')).rejects.toThrow( - 'Repository is missing item directories (mcp-servers, roles, storage-systems, or items)' - ); - }); - - it('should pass when mcp-servers directory exists', async () => { - // Mock stat to return true for metadata.yml and mcp-servers - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('metadata.yml') || pathStr.includes('mcp-servers')) { - return Promise.resolve({ isDirectory: () => true, isFile: () => true } as any); - } - return Promise.reject(new Error('Not found')); - }); - - // Call the method and expect it not to throw - await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); - }); - - it('should pass when roles directory exists', async () => { - // Mock stat to return true for metadata.yml and roles - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('metadata.yml') || pathStr.includes('roles')) { - return Promise.resolve({ isDirectory: () => true, isFile: () => true } as any); - } - return Promise.reject(new Error('Not found')); - }); - - // Call the method and expect it not to throw - await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); - }); - - it('should pass when storage-systems directory exists', async () => { - // Mock stat to return true for metadata.yml and storage-systems - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('metadata.yml') || pathStr.includes('storage-systems')) { - return Promise.resolve({ isDirectory: () => true, isFile: () => true } as any); - } - return Promise.reject(new Error('Not found')); - }); - - // Call the method and expect it not to throw - await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); - }); - - it('should pass when items directory exists (backward compatibility)', async () => { - // Mock stat to return true for metadata.yml and items - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('metadata.yml') || pathStr.includes('/items')) { - return Promise.resolve({ isDirectory: () => true, isFile: () => true } as any); - } - return Promise.reject(new Error('Not found')); - }); - - // Call the method and expect it not to throw - await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); - }); - }); - - describe('package-manager-template structure', () => { - it('should validate the package-manager-template structure', async () => { - // Mock stat to simulate the package-manager-template structure - mockedFs.stat.mockImplementation((path) => { - const pathStr = path.toString(); - if (pathStr.includes('metadata.yml') || - pathStr.includes('mcp-servers') || - pathStr.includes('roles') || - pathStr.includes('storage-systems')) { - return Promise.resolve({ isDirectory: () => true, isFile: () => true } as any); - } - if (pathStr.includes('items')) { - return Promise.reject(new Error('Directory not found')); - } - return Promise.resolve({ isDirectory: () => true } as any); - }); - - // Call the method and expect it not to throw - await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); - }); - }); -}); \ No newline at end of file + beforeEach(() => { + // Mock VSCode extension context + const mockContext = { + globalStorageUri: { + fsPath: "/mock/storage/path", + }, + } as vscode.ExtensionContext + + gitFetcher = new GitFetcher(mockContext) + jest.clearAllMocks() + + // Setup basic mocks + mockFs.stat.mockRejectedValue(new Error("File not found")) + }) + + // Helper function to access private method + const validateRepositoryStructure = async (repoDir: string) => { + return (gitFetcher as any).validateRepositoryStructure(repoDir) + } + + describe("metadata.en.yml validation", () => { + it("should throw error when metadata.en.yml is missing", async () => { + // Mock fs.stat to simulate missing file + mockFs.stat.mockRejectedValue(new Error("File not found")) + + // Call the method and expect it to throw + await expect(validateRepositoryStructure("/mock/repo")).rejects.toThrow( + "Repository is missing metadata.en.yml file", + ) + }) + + it("should pass when metadata.en.yml exists", async () => { + // Mock fs.stat to simulate existing file + mockFs.stat.mockResolvedValue({} as any) + + // Call the method and expect it not to throw + await expect(validateRepositoryStructure("/mock/repo")).resolves.not.toThrow() + }) + }) +}) diff --git a/src/services/package-manager/__tests__/schemas.test.ts b/src/services/package-manager/__tests__/schemas.test.ts new file mode 100644 index 0000000000..ebf3345722 --- /dev/null +++ b/src/services/package-manager/__tests__/schemas.test.ts @@ -0,0 +1,133 @@ +import { + validateMetadata, + validateAnyMetadata, + repositoryMetadataSchema, + componentMetadataSchema, + packageMetadataSchema, +} from "../schemas" + +describe("Schema Validation", () => { + describe("validateMetadata", () => { + it("should validate repository metadata", () => { + const data = { + name: "Test Repository", + description: "A test repository", + version: "1.0.0", + tags: ["test"], + } + + expect(() => validateMetadata(data, repositoryMetadataSchema)).not.toThrow() + }) + + it("should validate component metadata", () => { + const data = { + name: "Test Component", + description: "A test component", + version: "1.0.0", + type: "mcp server", + tags: ["test"], + } + + expect(() => validateMetadata(data, componentMetadataSchema)).not.toThrow() + }) + + it("should validate package metadata", () => { + const data = { + name: "Test Package", + description: "A test package", + version: "1.0.0", + type: "package", + items: [{ type: "mcp server", path: "../external/server" }], + } + + expect(() => validateMetadata(data, packageMetadataSchema)).not.toThrow() + }) + + it("should throw error for missing required fields", () => { + const data = { + description: "Missing name", + version: "1.0.0", + } + + expect(() => validateMetadata(data, repositoryMetadataSchema)).toThrow("name: Name is required") + }) + + it("should throw error for invalid version format", () => { + const data = { + name: "Test", + description: "Test", + version: "invalid", + } + + expect(() => validateMetadata(data, repositoryMetadataSchema)).toThrow( + "version: Version must be in semver format", + ) + }) + }) + + describe("validateAnyMetadata", () => { + it("should auto-detect and validate repository metadata", () => { + const data = { + name: "Test Repository", + description: "A test repository", + version: "1.0.0", + } + + expect(() => validateAnyMetadata(data)).not.toThrow() + }) + + it("should auto-detect and validate component metadata", () => { + const data = { + name: "Test Component", + description: "A test component", + version: "1.0.0", + type: "mcp server", + } + + expect(() => validateAnyMetadata(data)).not.toThrow() + }) + + it("should auto-detect and validate package metadata", () => { + const data = { + name: "Test Package", + description: "A test package", + version: "1.0.0", + type: "package", + items: [{ type: "mcp server", path: "../external/server" }], + } + + expect(() => validateAnyMetadata(data)).not.toThrow() + }) + + it("should throw error for unknown component type", () => { + const data = { + name: "Test", + description: "Test", + version: "1.0.0", + type: "unknown", + } + + expect(() => validateAnyMetadata(data)).toThrow("Unknown component type: unknown") + }) + + it("should throw error for invalid external item reference", () => { + const data = { + name: "Test Package", + description: "Test package", + version: "1.0.0", + type: "package", + items: [{ type: "unknown", path: "../external/server" }], + } + + expect(() => validateAnyMetadata(data)).toThrow('type: Invalid value "unknown"') + }) + + it("should throw error for non-object input", () => { + expect(() => validateAnyMetadata("not an object")).toThrow("Invalid metadata: must be an object") + }) + + it("should throw error for null input", () => { + expect(() => validateAnyMetadata(null)).toThrow("Invalid metadata: must be an object") + }) + }) +}) diff --git a/src/services/package-manager/schemas.ts b/src/services/package-manager/schemas.ts new file mode 100644 index 0000000000..4c9033abf1 --- /dev/null +++ b/src/services/package-manager/schemas.ts @@ -0,0 +1,113 @@ +import { z } from "zod" +import { ComponentType } from "./types" + +/** + * Base metadata schema with common fields + */ +export const baseMetadataSchema = z.object({ + name: z.string().min(1, "Name is required"), + description: z.string(), + version: z.string().regex(/^\d+\.\d+\.\d+$/, "Version must be in semver format (e.g., 1.0.0)"), + tags: z.array(z.string()).optional(), +}) + +/** + * Component type validation + */ +export const componentTypeSchema = z.enum(["mode", "prompt", "package", "mcp server"] as const) + +/** + * Repository metadata schema + */ +export const repositoryMetadataSchema = baseMetadataSchema + +/** + * Component metadata schema + */ +export const componentMetadataSchema = baseMetadataSchema.extend({ + type: componentTypeSchema, +}) + +/** + * External item reference schema + */ +export const externalItemSchema = z.object({ + type: componentTypeSchema, + path: z.string().min(1, "Path is required"), +}) + +/** + * Package metadata schema + */ +export const packageMetadataSchema = componentMetadataSchema.extend({ + type: z.literal("package"), + items: z.array(externalItemSchema).optional(), +}) + +/** + * Validate parsed YAML against a schema + * @param data Data to validate + * @param schema Schema to validate against + * @returns Validated data + * @throws Error if validation fails + */ +export function validateMetadata(data: unknown, schema: z.ZodType): T { + try { + return schema.parse(data) + } catch (error) { + if (error instanceof z.ZodError) { + const issues = error.issues + .map((issue) => { + const path = issue.path.join(".") + // Format error messages to match expected format + if (issue.message === "Required") { + if (path === "name") { + return "name: Name is required" + } + return path ? `${path}: ${path.split(".").pop()} is required` : "Required field missing" + } + if (issue.code === "invalid_enum_value") { + return path ? `${path}: Invalid value "${issue.received}"` : `Invalid value "${issue.received}"` + } + return path ? `${path}: ${issue.message}` : issue.message + }) + .join("\n") + throw new Error(issues) + } + throw error + } +} + +/** + * Determine metadata type and validate + * @param data Data to validate + * @returns Validated metadata + * @throws Error if validation fails + */ +export function validateAnyMetadata(data: unknown) { + // Try to determine the type of metadata + if (typeof data === "object" && data !== null) { + const obj = data as Record + + if ("type" in obj) { + const type = obj.type + switch (type) { + case "package": + return validateMetadata(data, packageMetadataSchema) + case "mode": + case "mcp server": + case "prompt": + case "role": + case "storage": + return validateMetadata(data, componentMetadataSchema) + default: + throw new Error(`Unknown component type: ${String(type)}`) + } + } else { + // No type field, assume repository metadata + return validateMetadata(data, repositoryMetadataSchema) + } + } + + throw new Error("Invalid metadata: must be an object") +} diff --git a/src/services/package-manager/types.ts b/src/services/package-manager/types.ts index d0c8aa5c83..fadedcfdb1 100644 --- a/src/services/package-manager/types.ts +++ b/src/services/package-manager/types.ts @@ -1,34 +1,81 @@ +/** + * Supported component types + */ +export type ComponentType = "mode" | "prompt" | "package" | "mcp server" + +/** + * Base metadata interface + */ +export interface BaseMetadata { + name: string + description: string + version: string + tags?: string[] +} + +/** + * Repository root metadata + */ +export interface RepositoryMetadata extends BaseMetadata {} + +/** + * Component metadata with type + */ +export interface ComponentMetadata extends BaseMetadata { + type: ComponentType +} + +/** + * Package metadata with optional external items + */ +export interface PackageMetadata extends ComponentMetadata { + type: "package" + items?: { + type: ComponentType + path: string + }[] +} + /** * Represents an individual package manager item */ export interface PackageManagerItem { - name: string; - description: string; - type: "role" | "mcp-server" | "storage" | "other"; - url: string; - repoUrl: string; - sourceName?: string; // Name of the source repository - author?: string; - tags?: string[]; - version?: string; - lastUpdated?: string; - sourceUrl?: string; // Optional URL to use for the "view source" button + name: string + description: string + type: ComponentType + url: string + repoUrl: string + sourceName?: string + author?: string + tags?: string[] + version?: string + lastUpdated?: string + sourceUrl?: string + items?: { type: ComponentType; path: string }[] } /** * Represents a Git repository source for package manager items */ export interface PackageManagerSource { - url: string; - name?: string; - enabled: boolean; + url: string + name?: string + enabled: boolean } /** * Represents a repository with its metadata and items */ export interface PackageManagerRepository { - metadata: any; - items: PackageManagerItem[]; - url: string; -} \ No newline at end of file + metadata: RepositoryMetadata + items: PackageManagerItem[] + url: string + error?: string +} + +/** + * Utility type for metadata files with locale + */ +export type LocalizedMetadata = { + [locale: string]: T +} diff --git a/test-repo/README.md b/test-repo/README.md new file mode 100644 index 0000000000..01e96cbb69 --- /dev/null +++ b/test-repo/README.md @@ -0,0 +1,51 @@ +# Minimal Package Manager Repository + +This is a minimal example of a package manager repository structure that meets the basic requirements. The structure is intentionally kept as simple as possible to help diagnose any validation issues. + +## Structure + +``` +/ +├── metadata.en.yml # Required: Repository metadata (must be exactly this name) +└── mcp-servers/ # Optional: Directory for MCP servers + └── test-server/ # Must be a directory + └── metadata.en.yml # Must match pattern metadata.[locale].yml +``` + +## metadata.en.yml + +```yaml +name: Test Repository +description: A minimal test repository +version: 1.0.0 +``` + +## mcp-servers/test-server/metadata.en.yml + +```yaml +name: Test Server +description: A minimal test server +type: mcp server +version: 1.0.0 +``` + +## Key Points + +1. File names must be exactly: + - metadata.en.yml (not metadata.yml or any other variation) +2. Components must be in directories +3. No empty lines in YAML files +4. No quotes around values +5. No extra fields +6. No special characters +7. No complex YAML features (arrays, nested objects, etc.) + +Try copying this exact structure to your GitHub repository to test. The validation should pass with this minimal setup. + +## Validation Process + +1. First, it checks for metadata.en.yml in the root +2. Then it scans for component directories +3. For each directory, it looks for metadata.en.yml files +4. Each metadata file is validated for required fields +5. Component metadata must have a valid type ("mcp server", "mode", "prompt", or "package") diff --git a/test-repo/mcp-servers/test-server/metadata.en.yml b/test-repo/mcp-servers/test-server/metadata.en.yml new file mode 100644 index 0000000000..4f9f31f0ac --- /dev/null +++ b/test-repo/mcp-servers/test-server/metadata.en.yml @@ -0,0 +1,4 @@ +name: Test Server +description: A minimal test server +type: mcp server +version: 1.0.0 \ No newline at end of file diff --git a/test-repo/metadata.en.yml b/test-repo/metadata.en.yml new file mode 100644 index 0000000000..5fe19b3ba8 --- /dev/null +++ b/test-repo/metadata.en.yml @@ -0,0 +1,3 @@ +name: Test Repository +description: A minimal test repository +version: 1.0.0 \ No newline at end of file diff --git a/webview-ui/src/components/package-manager/PackageManagerView.tsx b/webview-ui/src/components/package-manager/PackageManagerView.tsx index 0bb17b2aa5..585c4c495d 100644 --- a/webview-ui/src/components/package-manager/PackageManagerView.tsx +++ b/webview-ui/src/components/package-manager/PackageManagerView.tsx @@ -1,127 +1,212 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react" import { Button } from "@/components/ui/button" import { useExtensionState } from "../../context/ExtensionStateContext" -import { useAppTranslation } from "../../i18n/TranslationContext" import { Tab, TabContent, TabHeader } from "../common/Tab" import { vscode } from "@/utils/vscode" import { PackageManagerItem, PackageManagerSource } from "../../../../src/services/package-manager/types" import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "cmdk" -type PackageManagerViewProps = {} +interface PackageManagerViewProps { + onDone?: () => void +} -const PackageManagerView = (_props: PackageManagerViewProps) => { +interface PackageManagerItemCardProps { + item: PackageManagerItem + filters: { type: string; search: string; tags: string[] } + setFilters: React.Dispatch> + activeTab: "browse" | "sources" + setActiveTab: React.Dispatch> +} + +const PackageManagerItemCard: React.FC = ({ + item, + filters, + setFilters, + activeTab, + setActiveTab, +}) => { + const isValidUrl = (urlString: string): boolean => { + try { + new URL(urlString) + return true + } catch (e) { + return false + } + } + + const getTypeLabel = (type: string) => { + switch (type) { + case "mode": + return "Mode" + case "mcp server": + return "MCP Server" + case "prompt": + return "Prompt" + case "package": + return "Package" + default: + return "Other" + } + } + + const getTypeColor = (type: string) => { + switch (type) { + case "mode": + return "bg-blue-600" + case "mcp server": + return "bg-green-600" + case "prompt": + return "bg-purple-600" + case "package": + return "bg-orange-600" + default: + return "bg-gray-600" + } + } + + const handleOpenUrl = () => { + const urlToOpen = item.sourceUrl && isValidUrl(item.sourceUrl) ? item.sourceUrl : item.repoUrl + vscode.postMessage({ + type: "openExternal", + url: urlToOpen, + }) + } + + return ( +
+
+
+

{item.name}

+ {item.author &&

{`by ${item.author}`}

} +
+ + {getTypeLabel(item.type)} + +
+ +

{item.description}

+ + {item.tags && item.tags.length > 0 && ( +
+ {item.tags.map((tag) => ( + + ))} +
+ )} + +
+
+ {item.version && ( + + + {item.version} + + )} + {item.lastUpdated && ( + + + {new Date(item.lastUpdated).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + })} + + )} +
+ + +
+
+ ) +} + +const PackageManagerView: React.FC = ({ onDone }) => { const { packageManagerSources, setPackageManagerSources } = useExtensionState() - console.log("DEBUG: PackageManagerView initialized with sources:", packageManagerSources) - useAppTranslation() // Keep the hook but don't destructure unused 't' const [items, setItems] = useState([]) const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse") const [refreshingUrls, setRefreshingUrls] = useState([]) - - // Track activeTab changes - useEffect(() => { - console.log("DEBUG: activeTab changed to", activeTab) - }, [activeTab]) const [filters, setFilters] = useState({ type: "", search: "", tags: [] as string[] }) const [tagSearch, setTagSearch] = useState("") const [isTagInputActive, setIsTagInputActive] = useState(false) const [sortBy, setSortBy] = useState("name") const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc") - - // Track if we're currently fetching items to prevent duplicate requests const [isFetching, setIsFetching] = useState(false) - - // Debug state changes - useEffect(() => { - console.log("DEBUG: items state changed", { - itemsLength: items.length, - isFetching, - }) - }, [items, isFetching]) - - // Track if the fetch was manually triggered by a refresh button const isManualRefresh = useRef(false) - - // Use a ref to track if we've already fetched items const hasInitialFetch = useRef(false) - // Track the last sources we fetched to avoid duplicate fetches const lastSourcesKey = useRef(null) - // Fetch function without debounce for immediate execution const fetchPackageManagerItems = useCallback(() => { - console.log("DEBUG: fetchPackageManagerItems called") - // Only send fetch request if we're not already fetching if (!isFetching) { setIsFetching(true) try { - // Request items from extension with explicit fetch vscode.postMessage({ type: "fetchPackageManagerItems", - forceRefresh: true, // Add a flag to force refresh + forceRefresh: true, } as any) - console.log("Explicitly fetching package manager items with force refresh...") } catch (error) { console.error("Failed to fetch package manager items:", error) setIsFetching(false) } - } else { - console.log("DEBUG: Skipping fetch because already in progress") } }, [isFetching]) - // Always fetch items when component mounts, regardless of other conditions useEffect(() => { - console.log("DEBUG: PackageManagerView mount effect triggered") + fetchPackageManagerItems() + }, [fetchPackageManagerItems]) - // Force fetch on mount, ignoring all conditions - setTimeout(() => { - console.log("DEBUG: Forcing fetch on component mount") - setIsFetching(false) // Reset fetching state first - fetchPackageManagerItems() + // Set hasInitialFetch after the first fetch completes + useEffect(() => { + if (!isFetching) { hasInitialFetch.current = true - }, 500) // Small delay to ensure component is fully mounted - }, [fetchPackageManagerItems]) // Add fetchPackageManagerItems as dependency - - // Additional effect for when packageManagerSources changes - useEffect(() => { - console.log("DEBUG: PackageManagerView packageManagerSources effect triggered", { - hasInitialFetch: hasInitialFetch.current, - packageManagerSources, - isFetching, - itemsLength: items.length, - }) - - // Only fetch if packageManagerSources changes, we're not already fetching, and this isn't the initial render - if (packageManagerSources && hasInitialFetch.current && !isFetching && packageManagerSources.length > 0) { - // Generate a key based on the current sources - const sourcesKey = JSON.stringify(packageManagerSources.map((s) => s.url)) - - // Only fetch if the sources have changed and it's not a manual refresh - if (sourcesKey !== lastSourcesKey.current && !isManualRefresh.current) { - console.log("DEBUG: Calling fetchPackageManagerItems due to sources change") - lastSourcesKey.current = sourcesKey - fetchPackageManagerItems() - } else { - console.log("DEBUG: Skipping fetch because sources haven't changed or manual refresh is in progress") - } - // Reset refreshingUrls when items length changes - setRefreshingUrls([]) } - }, [packageManagerSources, fetchPackageManagerItems, isFetching, items.length]) + }, [isFetching]) - // Handle message from extension useEffect(() => { - console.log("DEBUG: Setting up message handler") + if (packageManagerSources && !isFetching && packageManagerSources.length > 0) { + const sourcesKey = JSON.stringify(packageManagerSources.map((s) => s.url)) + if (sourcesKey !== lastSourcesKey.current && !isManualRefresh.current) { + lastSourcesKey.current = sourcesKey + // Don't fetch if this is the initial sources load + if (hasInitialFetch.current) { + fetchPackageManagerItems() + } + } + } + }, [packageManagerSources, fetchPackageManagerItems, isFetching]) + useEffect(() => { const handleMessage = (event: MessageEvent) => { - console.log("DEBUG: Message received in PackageManagerView", event.data) - console.log("DEBUG: Message type:", event.data.type) - console.log("DEBUG: Message state:", event.data.state ? "exists" : "undefined") const message = event.data - // Handle action messages - specifically for packageManagerButtonClicked if (message.type === "action" && message.action === "packageManagerButtonClicked") { - console.log("DEBUG: Received packageManagerButtonClicked action, triggering fetch") - // Directly trigger a fetch when the package manager tab is clicked setTimeout(() => { vscode.postMessage({ type: "fetchPackageManagerItems", @@ -129,80 +214,30 @@ const PackageManagerView = (_props: PackageManagerViewProps) => { } as any) }, 100) } - // Handle repository refresh completion + if (message.type === "repositoryRefreshComplete" && message.url) { - console.log(`DEBUG: Repository refresh complete for ${message.url}`) - console.log(`DEBUG: Current refreshingUrls before update:`, refreshingUrls) - setRefreshingUrls((prev) => { - const updated = prev.filter((url) => url !== message.url) - console.log(`DEBUG: Updated refreshingUrls:`, updated) - return updated - }) + setRefreshingUrls((prev) => prev.filter((url) => url !== message.url)) } - // Handle state messages with packageManagerItems - if (message.type === "state" && message.state) { - console.log("DEBUG: Received state message", message.state) - console.log("DEBUG: State has packageManagerItems:", message.state.packageManagerItems ? "yes" : "no") - if (message.state.packageManagerItems) { - console.log("DEBUG: packageManagerItems length:", message.state.packageManagerItems.length) - } - - // Check for packageManagerItems - if (message.state.packageManagerItems) { - const receivedItems = message.state.packageManagerItems || [] - console.log("DEBUG: Received packageManagerItems", receivedItems.length) - console.log("DEBUG: Full message state:", message.state) - - if (receivedItems.length > 0) { - console.log("DEBUG: First item:", receivedItems[0]) - console.log("DEBUG: All items:", JSON.stringify(receivedItems)) - - // Force a new array reference to ensure React detects the change - setItems([...receivedItems]) - - // Update the fetching state in a separate call to avoid triggering another fetch - setTimeout(() => { - setIsFetching(false) - isManualRefresh.current = false // Reset the manual refresh flag - console.log( - "DEBUG: States updated - items:", - receivedItems.length, - "isFetching: false, isManualRefresh: false", - ) - }, 0) - } else { - console.log("DEBUG: Received empty items array") - setItems([]) - - // Update the fetching state in a separate call to avoid triggering another fetch - setTimeout(() => { - setIsFetching(false) - isManualRefresh.current = false // Reset the manual refresh flag - console.log("DEBUG: States updated - items: 0, isFetching: false, isManualRefresh: false") - }, 0) - } - } + if (message.type === "state" && message.state?.packageManagerItems) { + const receivedItems = message.state.packageManagerItems || [] + setItems([...receivedItems]) + setTimeout(() => { + setIsFetching(false) + isManualRefresh.current = false + }, 0) } } window.addEventListener("message", handleMessage) return () => window.removeEventListener("message", handleMessage) - }, [refreshingUrls]) // Add refreshingUrls as dependency + }, []) - // Filter items based on filters - console.log("DEBUG: Filtering items", { itemsCount: items.length, filters }) - console.log( - "DEBUG: Items before filtering:", - items.map((item) => ({ name: item.name, type: item.type })), - ) const filteredItems = items.filter((item) => { - // Filter by type if (filters.type && item.type !== filters.type) { return false } - // Filter by search term if (filters.search) { const searchTerm = filters.search.toLowerCase() const nameMatch = item.name.toLowerCase().includes(searchTerm) @@ -214,26 +249,15 @@ const PackageManagerView = (_props: PackageManagerViewProps) => { } } - // Filter by tags (OR logic - item passes if it has ANY of the selected tags) if (filters.tags.length > 0) { - // If the item has no tags, it doesn't match when tag filtering is active - if (!item.tags || item.tags.length === 0) { - return false - } - - // Check if any of the item's tags match any of the selected tags - const hasMatchingTag = item.tags.some((tag) => filters.tags.includes(tag)) - if (!hasMatchingTag) { + if (!item.tags || !item.tags.some((tag) => filters.tags.includes(tag))) { return false } } return true }) - console.log("DEBUG: After filtering", { filteredItemsCount: filteredItems.length }) - // Sort items - console.log("DEBUG: Sorting items", { filteredItemsCount: filteredItems.length, sortBy, sortOrder }) const sortedItems = [...filteredItems].sort((a, b) => { let comparison = 0 @@ -253,39 +277,17 @@ const PackageManagerView = (_props: PackageManagerViewProps) => { return sortOrder === "asc" ? comparison : -comparison }) - console.log("DEBUG: Final sorted items", { - sortedItemsCount: sortedItems.length, - firstItem: sortedItems.length > 0 ? sortedItems[0].name : "none", - }) - // Collect all unique tags from items const allTags = useMemo(() => { const tagSet = new Set() items.forEach((item) => { - if (item.tags && item.tags.length > 0) { + if (item.tags) { item.tags.forEach((tag) => tagSet.add(tag)) } }) return Array.from(tagSet).sort() }, [items]) - // Add debug logging right before rendering - useEffect(() => { - console.log("DEBUG: Rendering with", { - sortedItemsCount: sortedItems.length, - firstItem: sortedItems.length > 0 ? `${sortedItems[0].name} (${sortedItems[0].type})` : "none", - availableTags: allTags.length, - }) - }, [sortedItems, allTags]) - - // Log right before rendering - console.log("DEBUG: About to render with", { - itemsLength: items.length, - filteredItemsLength: filteredItems.length, - sortedItemsLength: sortedItems.length, - activeTab, - }) - return ( @@ -326,10 +328,10 @@ const PackageManagerView = (_props: PackageManagerViewProps) => { onChange={(e) => setFilters({ ...filters, type: e.target.value })} className="p-1 bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border rounded"> - - - - + + + + @@ -375,7 +377,6 @@ const PackageManagerView = (_props: PackageManagerViewProps) => { onValueChange={setTagSearch} onFocus={() => setIsTagInputActive(true)} onBlur={(e) => { - // Only hide if not clicking within the command list if (!e.relatedTarget?.closest("[cmdk-list]")) { setIsTagInputActive(false) } @@ -417,7 +418,6 @@ const PackageManagerView = (_props: PackageManagerViewProps) => { : "text-vscode-dropdown-foreground" }`} onMouseDown={(e) => { - // Prevent blur event when clicking items e.preventDefault() }}> { - {console.log("DEBUG: Rendering condition", { - sortedItemsLength: sortedItems.length, - condition: sortedItems.length === 0 ? "empty" : "has items", - })} - {sortedItems.length === 0 ? (

No package manager items found

- ))} -
- )} - -
-
- {item.version && ( - - - {item.version} - - )} - {item.lastUpdated && ( - - - {new Date(item.lastUpdated).toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric", - })} - - )} -
- - -
- - ) -} - -/** - * Checks if a URL is a valid Git repository URL - * @param url The URL to validate - * @returns True if the URL is a valid Git repository URL, false otherwise - */ -const isValidGitRepositoryUrl = (url: string): boolean => { - // Trim the URL to remove any leading/trailing whitespace - const trimmedUrl = url.trim() - - // HTTPS pattern (GitHub, GitLab, Bitbucket, etc.) - // Examples: - // - https://github.com/username/repo - // - https://github.com/username/repo.git - // - https://gitlab.com/username/repo - // - https://bitbucket.org/username/repo - const httpsPattern = - /^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org|dev\.azure\.com)\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(\/.+)*(\.git)?$/ - - // SSH pattern - // Examples: - // - git@github.com:username/repo.git - // - git@gitlab.com:username/repo.git - const sshPattern = /^git@(github\.com|gitlab\.com|bitbucket\.org):([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)(\.git)?$/ - - // Git protocol pattern - // Examples: - // - git://github.com/username/repo.git - const gitProtocolPattern = - /^git:\/\/(github\.com|gitlab\.com|bitbucket\.org)\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(\.git)?$/ - - return httpsPattern.test(trimmedUrl) || sshPattern.test(trimmedUrl) || gitProtocolPattern.test(trimmedUrl) -} - -const PackageManagerSourcesConfig = ({ - sources, - refreshingUrls, - setRefreshingUrls, - onSourcesChange, -}: { +interface PackageManagerSourcesConfigProps { sources: PackageManagerSource[] refreshingUrls: string[] setRefreshingUrls: React.Dispatch> onSourcesChange: (sources: PackageManagerSource[]) => void +} + +const PackageManagerSourcesConfig: React.FC = ({ + sources, + refreshingUrls, + setRefreshingUrls, + onSourcesChange, }) => { - useAppTranslation() // Keep the hook but don't destructure unused 't' const [newSourceUrl, setNewSourceUrl] = useState("") const [newSourceName, setNewSourceName] = useState("") const [error, setError] = useState("") const handleAddSource = () => { - // Validate URL if (!newSourceUrl) { setError("URL cannot be empty") return @@ -717,41 +536,34 @@ const PackageManagerSourcesConfig = ({ return } - // Check for non-visible characters in URL (except spaces) const nonVisibleCharRegex = /[^\S ]/ if (nonVisibleCharRegex.test(newSourceUrl)) { setError("URL contains non-visible characters other than spaces") return } - // Check if URL is a valid Git repository URL if (!isValidGitRepositoryUrl(newSourceUrl)) { setError("URL must be a valid Git repository URL (e.g., https://github.com/username/repo)") return } - // Check if URL already exists (case and whitespace insensitive) const normalizedNewUrl = newSourceUrl.toLowerCase().replace(/\s+/g, "") if (sources.some((source) => source.url.toLowerCase().replace(/\s+/g, "") === normalizedNewUrl)) { setError("This URL is already in the list (case and whitespace insensitive match)") return } - // Validate name if provided if (newSourceName) { - // Check name length if (newSourceName.length > 20) { setError("Name must be 20 characters or less") return } - // Check for non-visible characters in name (except spaces) if (nonVisibleCharRegex.test(newSourceName)) { setError("Name contains non-visible characters other than spaces") return } - // Check if name already exists (case and whitespace insensitive) const normalizedNewName = newSourceName.toLowerCase().replace(/\s+/g, "") if ( sources.some( @@ -763,14 +575,12 @@ const PackageManagerSourcesConfig = ({ } } - // Check if maximum number of sources has been reached const MAX_SOURCES = 10 if (sources.length >= MAX_SOURCES) { setError(`Maximum of ${MAX_SOURCES} sources allowed`) return } - // Add new source const newSource: PackageManagerSource = { url: newSourceUrl, name: newSourceName || undefined, @@ -779,7 +589,6 @@ const PackageManagerSourcesConfig = ({ onSourcesChange([...sources, newSource]) - // Reset form setNewSourceUrl("") setNewSourceName("") setError("") @@ -797,10 +606,7 @@ const PackageManagerSourcesConfig = ({ } const handleRefreshSource = (url: string) => { - // Add URL to refreshing list setRefreshingUrls((prev) => [...prev, url]) - - // Send message to refresh this specific source vscode.postMessage({ type: "refreshPackageManagerSource", url, @@ -837,11 +643,10 @@ const PackageManagerSourcesConfig = ({ placeholder="Display name (optional, max 20 chars)" value={newSourceName} onChange={(e) => { - // Limit input to 20 characters setNewSourceName(e.target.value.slice(0, 20)) setError("") }} - maxLength={20} // HTML attribute to limit input length + maxLength={20} className="p-2 bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded" /> @@ -908,4 +713,16 @@ const PackageManagerSourcesConfig = ({ ) } +const isValidGitRepositoryUrl = (url: string): boolean => { + const trimmedUrl = url.trim() + + const httpsPattern = + /^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org|dev\.azure\.com)\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(\/.+)*(\.git)?$/ + const sshPattern = /^git@(github\.com|gitlab\.com|bitbucket\.org):([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)(\.git)?$/ + const gitProtocolPattern = + /^git:\/\/(github\.com|gitlab\.com|bitbucket\.org)\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(\.git)?$/ + + return httpsPattern.test(trimmedUrl) || sshPattern.test(trimmedUrl) || gitProtocolPattern.test(trimmedUrl) +} + export default PackageManagerView diff --git a/webview-ui/src/i18n/locales/ca/package-manager.json b/webview-ui/src/i18n/locales/ca/package-manager.json new file mode 100644 index 0000000000..2ed20609a3 --- /dev/null +++ b/webview-ui/src/i18n/locales/ca/package-manager.json @@ -0,0 +1,87 @@ +{ + "title": "Gestor de Paquets", + "tabs": { + "browse": "Navega", + "sources": "Fonts" + }, + "filters": { + "search": { + "placeholder": "Cerca elements del gestor de paquets..." + }, + "type": { + "label": "Filtra per tipus:", + "all": "Tots els tipus", + "mode": "Mode", + "mcp server": "Servidor MCP", + "prompt": "Prompt", + "package": "Paquet" + }, + "sort": { + "label": "Ordena per:", + "name": "Nom", + "author": "Autor", + "lastUpdated": "Última actualització" + }, + "tags": { + "label": "Filtra per etiquetes:", + "available": "{{count}} disponible", + "available_plural": "{{count}} disponibles", + "clear": "Neteja etiquetes ({{count}})", + "placeholder": "Escriu per cercar i seleccionar etiquetes...", + "noResults": "No s'han trobat etiquetes coincidents", + "selected": "Mostrant elements amb qualsevol de les etiquetes seleccionades ({{count}} seleccionada)", + "selected_plural": "Mostrant elements amb qualsevol de les etiquetes seleccionades ({{count}} seleccionades)", + "clickToFilter": "Fes clic a les etiquetes per filtrar elements" + } + }, + "items": { + "empty": { + "noItems": "No s'han trobat elements del gestor de paquets", + "withFilters": "Prova d'ajustar els filtres", + "noSources": "Prova d'afegir una font a la pestanya Fonts" + }, + "count": "S'ha trobat {{count}} element", + "count_plural": "S'han trobat {{count}} elements", + "refresh": { + "button": "Actualitza", + "refreshing": "Actualitzant..." + }, + "card": { + "by": "per {{author}}", + "from": "de {{source}}", + "externalComponents": "Conté {{count}} component extern", + "externalComponents_plural": "Conté {{count}} components externs", + "viewSource": "Visualitza", + "viewOnSource": "Visualitza a {{source}}" + } + }, + "sources": { + "title": "Configura les Fonts del Gestor de Paquets", + "description": "Afegeix repositoris Git que continguin elements del gestor de paquets. Aquests repositoris es recuperaran en navegar pel gestor de paquets.", + "add": { + "title": "Afegeix Nova Font", + "urlPlaceholder": "URL del repositori Git (p. ex. https://github.com/username/repo)", + "urlFormats": "Formats admesos: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) o protocol Git (git://github.com/username/repo.git)", + "namePlaceholder": "Nom de visualització (opcional, màx. 20 caràcters)", + "button": "Afegeix Font" + }, + "current": { + "title": "Fonts Actuals", + "count": "{{current}}/{{max}} màxim", + "empty": "No hi ha fonts configurades. Afegeix una font per començar.", + "refresh": "Actualitza aquesta font", + "remove": "Elimina font" + }, + "errors": { + "emptyUrl": "L'URL no pot estar buit", + "invalidUrl": "Format d'URL no vàlid", + "nonVisibleChars": "L'URL conté caràcters no visibles a part d'espais", + "invalidGitUrl": "L'URL ha de ser una URL de repositori Git vàlida (p. ex. https://github.com/username/repo)", + "duplicateUrl": "Aquest URL ja és a la llista (coincidència sense distinció entre majúscules/minúscules i espais)", + "nameTooLong": "El nom no pot superar els 20 caràcters", + "nonVisibleCharsName": "El nom conté caràcters no visibles a part d'espais", + "duplicateName": "Aquest nom ja està en ús (coincidència sense distinció entre majúscules/minúscules i espais)", + "maxSources": "Màxim de {{max}} fonts permeses" + } + } +} diff --git a/webview-ui/src/i18n/locales/de/package-manager.json b/webview-ui/src/i18n/locales/de/package-manager.json new file mode 100644 index 0000000000..d2a594c8f7 --- /dev/null +++ b/webview-ui/src/i18n/locales/de/package-manager.json @@ -0,0 +1,84 @@ +{ + "title": "Paket-Manager", + "tabs": { + "browse": "Durchsuchen", + "sources": "Quellen" + }, + "filters": { + "search": { + "placeholder": "Paket-Manager-Elemente durchsuchen..." + }, + "type": { + "label": "Nach Typ filtern:", + "all": "Alle Typen", + "mode": "Modus", + "mcp server": "MCP-Server", + "prompt": "Prompt", + "package": "Paket" + }, + "sort": { + "label": "Sortieren nach:", + "name": "Name", + "author": "Autor", + "lastUpdated": "Zuletzt aktualisiert" + }, + "tags": { + "label": "Nach Tags filtern:", + "available": "{{count}} verfügbar", + "clear": "Tags löschen ({{count}})", + "placeholder": "Tippen Sie, um Tags zu suchen und auszuwählen...", + "noResults": "Keine übereinstimmenden Tags gefunden", + "selected": "Zeigt Elemente mit beliebigen der ausgewählten Tags ({{count}} ausgewählt)", + "clickToFilter": "Klicken Sie auf Tags, um Elemente zu filtern" + } + }, + "items": { + "empty": { + "noItems": "Keine Paket-Manager-Elemente gefunden", + "withFilters": "Versuchen Sie, Ihre Filter anzupassen", + "noSources": "Versuchen Sie, eine Quelle im Quellen-Tab hinzuzufügen" + }, + "count": "{{count}} Elemente gefunden", + "refresh": { + "button": "Aktualisieren", + "refreshing": "Aktualisiere..." + }, + "card": { + "by": "von {{author}}", + "from": "von {{source}}", + "externalComponents": "Enthält {{count}} externe Komponente", + "externalComponents_plural": "Enthält {{count}} externe Komponenten", + "viewSource": "Ansehen", + "viewOnSource": "Auf {{source}} ansehen" + } + }, + "sources": { + "title": "Paket-Manager-Quellen konfigurieren", + "description": "Fügen Sie Git-Repositories hinzu, die Paket-Manager-Elemente enthalten. Diese Repositories werden beim Durchsuchen des Paket-Managers abgerufen.", + "add": { + "title": "Neue Quelle hinzufügen", + "urlPlaceholder": "Git-Repository-URL (z.B. https://github.com/username/repo)", + "urlFormats": "Unterstützte Formate: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) oder Git-Protokoll (git://github.com/username/repo.git)", + "namePlaceholder": "Anzeigename (optional, max. 20 Zeichen)", + "button": "Quelle hinzufügen" + }, + "current": { + "title": "Aktuelle Quellen", + "count": "{{current}}/{{max}} maximal", + "empty": "Keine Quellen konfiguriert. Fügen Sie eine Quelle hinzu, um zu beginnen.", + "refresh": "Diese Quelle aktualisieren", + "remove": "Quelle entfernen" + }, + "errors": { + "emptyUrl": "URL darf nicht leer sein", + "invalidUrl": "Ungültiges URL-Format", + "nonVisibleChars": "URL enthält unsichtbare Zeichen außer Leerzeichen", + "invalidGitUrl": "URL muss eine gültige Git-Repository-URL sein (z.B. https://github.com/username/repo)", + "duplicateUrl": "Diese URL ist bereits in der Liste (Groß-/Kleinschreibung und Leerzeichen werden ignoriert)", + "nameTooLong": "Name darf maximal 20 Zeichen lang sein", + "nonVisibleCharsName": "Name enthält unsichtbare Zeichen außer Leerzeichen", + "duplicateName": "Dieser Name wird bereits verwendet (Groß-/Kleinschreibung und Leerzeichen werden ignoriert)", + "maxSources": "Maximal {{max}} Quellen erlaubt" + } + } +} diff --git a/webview-ui/src/i18n/locales/en/package-manager.json b/webview-ui/src/i18n/locales/en/package-manager.json new file mode 100644 index 0000000000..6c64859c43 --- /dev/null +++ b/webview-ui/src/i18n/locales/en/package-manager.json @@ -0,0 +1,84 @@ +{ + "title": "Package Manager", + "tabs": { + "browse": "Browse", + "sources": "Sources" + }, + "filters": { + "search": { + "placeholder": "Search package manager items..." + }, + "type": { + "label": "Filter by type:", + "all": "All types", + "mode": "Mode", + "mcp server": "MCP Server", + "prompt": "Prompt", + "package": "Package" + }, + "sort": { + "label": "Sort by:", + "name": "Name", + "author": "Author", + "lastUpdated": "Last Updated" + }, + "tags": { + "label": "Filter by tags:", + "available": "{{count}} available", + "clear": "Clear tags ({{count}})", + "placeholder": "Type to search and select tags...", + "noResults": "No matching tags found", + "selected": "Showing items with any of the selected tags ({{count}} selected)", + "clickToFilter": "Click tags to filter items" + } + }, + "items": { + "empty": { + "noItems": "No package manager items found", + "withFilters": "Try adjusting your filters", + "noSources": "Try adding a source in the Sources tab" + }, + "count": "{{count}} items found", + "refresh": { + "button": "Refresh", + "refreshing": "Refreshing..." + }, + "card": { + "by": "by {{author}}", + "from": "from {{source}}", + "externalComponents": "Contains {{count}} external component", + "externalComponents_plural": "Contains {{count}} external components", + "viewSource": "View", + "viewOnSource": "View on {{source}}" + } + }, + "sources": { + "title": "Configure Package Manager Sources", + "description": "Add Git repositories that contain package manager items. These repositories will be fetched when browsing the package manager.", + "add": { + "title": "Add New Source", + "urlPlaceholder": "Git repository URL (e.g., https://github.com/username/repo)", + "urlFormats": "Supported formats: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git), or Git protocol (git://github.com/username/repo.git)", + "namePlaceholder": "Display name (optional, max 20 chars)", + "button": "Add Source" + }, + "current": { + "title": "Current Sources", + "count": "{{current}}/{{max}} max", + "empty": "No sources configured. Add a source to get started.", + "refresh": "Refresh this source", + "remove": "Remove source" + }, + "errors": { + "emptyUrl": "URL cannot be empty", + "invalidUrl": "Invalid URL format", + "nonVisibleChars": "URL contains non-visible characters other than spaces", + "invalidGitUrl": "URL must be a valid Git repository URL (e.g., https://github.com/username/repo)", + "duplicateUrl": "This URL is already in the list (case and whitespace insensitive match)", + "nameTooLong": "Name must be 20 characters or less", + "nonVisibleCharsName": "Name contains non-visible characters other than spaces", + "duplicateName": "This name is already in use (case and whitespace insensitive match)", + "maxSources": "Maximum of {{max}} sources allowed" + } + } +} diff --git a/webview-ui/src/i18n/locales/es/package-manager.json b/webview-ui/src/i18n/locales/es/package-manager.json new file mode 100644 index 0000000000..8e51e73454 --- /dev/null +++ b/webview-ui/src/i18n/locales/es/package-manager.json @@ -0,0 +1,84 @@ +{ + "title": "Gestor de Paquetes", + "tabs": { + "browse": "Explorar", + "sources": "Fuentes" + }, + "filters": { + "search": { + "placeholder": "Buscar elementos del gestor de paquetes..." + }, + "type": { + "label": "Filtrar por tipo:", + "all": "Todos los tipos", + "mode": "Modo", + "mcp server": "Servidor MCP", + "prompt": "Prompt", + "package": "Paquete" + }, + "sort": { + "label": "Ordenar por:", + "name": "Nombre", + "author": "Autor", + "lastUpdated": "Última actualización" + }, + "tags": { + "label": "Filtrar por etiquetas:", + "available": "{{count}} disponibles", + "clear": "Limpiar etiquetas ({{count}})", + "placeholder": "Escriba para buscar y seleccionar etiquetas...", + "noResults": "No se encontraron etiquetas coincidentes", + "selected": "Mostrando elementos con cualquiera de las etiquetas seleccionadas ({{count}} seleccionadas)", + "clickToFilter": "Haga clic en las etiquetas para filtrar elementos" + } + }, + "items": { + "empty": { + "noItems": "No se encontraron elementos del gestor de paquetes", + "withFilters": "Intente ajustar sus filtros", + "noSources": "Intente agregar una fuente en la pestaña Fuentes" + }, + "count": "{{count}} elementos encontrados", + "refresh": { + "button": "Actualizar", + "refreshing": "Actualizando..." + }, + "card": { + "by": "por {{author}}", + "from": "de {{source}}", + "externalComponents": "Contiene {{count}} componente externo", + "externalComponents_plural": "Contiene {{count}} componentes externos", + "viewSource": "Ver", + "viewOnSource": "Ver en {{source}}" + } + }, + "sources": { + "title": "Configurar Fuentes del Gestor de Paquetes", + "description": "Agregue repositorios Git que contengan elementos del gestor de paquetes. Estos repositorios se recuperarán al explorar el gestor de paquetes.", + "add": { + "title": "Agregar Nueva Fuente", + "urlPlaceholder": "URL del repositorio Git (ej., https://github.com/username/repo)", + "urlFormats": "Formatos admitidos: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git), o protocolo Git (git://github.com/username/repo.git)", + "namePlaceholder": "Nombre para mostrar (opcional, máx. 20 caracteres)", + "button": "Agregar Fuente" + }, + "current": { + "title": "Fuentes Actuales", + "count": "{{current}}/{{max}} máximo", + "empty": "No hay fuentes configuradas. Agregue una fuente para comenzar.", + "refresh": "Actualizar esta fuente", + "remove": "Eliminar fuente" + }, + "errors": { + "emptyUrl": "La URL no puede estar vacía", + "invalidUrl": "Formato de URL inválido", + "nonVisibleChars": "La URL contiene caracteres no visibles además de espacios", + "invalidGitUrl": "La URL debe ser una URL válida de repositorio Git (ej., https://github.com/username/repo)", + "duplicateUrl": "Esta URL ya está en la lista (coincidencia insensible a mayúsculas y espacios)", + "nameTooLong": "El nombre debe tener 20 caracteres o menos", + "nonVisibleCharsName": "El nombre contiene caracteres no visibles además de espacios", + "duplicateName": "Este nombre ya está en uso (coincidencia insensible a mayúsculas y espacios)", + "maxSources": "Máximo de {{max}} fuentes permitidas" + } + } +} diff --git a/webview-ui/src/i18n/locales/fr/package-manager.json b/webview-ui/src/i18n/locales/fr/package-manager.json new file mode 100644 index 0000000000..25138a1730 --- /dev/null +++ b/webview-ui/src/i18n/locales/fr/package-manager.json @@ -0,0 +1,87 @@ +{ + "title": "Gestionnaire de Paquets", + "tabs": { + "browse": "Parcourir", + "sources": "Sources" + }, + "filters": { + "search": { + "placeholder": "Rechercher des éléments du gestionnaire de paquets..." + }, + "type": { + "label": "Filtrer par type :", + "all": "Tous les types", + "mode": "Mode", + "mcp server": "Serveur MCP", + "prompt": "Prompt", + "package": "Paquet" + }, + "sort": { + "label": "Trier par :", + "name": "Nom", + "author": "Auteur", + "lastUpdated": "Dernière mise à jour" + }, + "tags": { + "label": "Filtrer par tags :", + "available": "{{count}} disponible", + "available_plural": "{{count}} disponibles", + "clear": "Effacer les tags ({{count}})", + "placeholder": "Tapez pour rechercher et sélectionner des tags...", + "noResults": "Aucun tag correspondant trouvé", + "selected": "Affichage des éléments avec l'un des tags sélectionnés ({{count}} sélectionné)", + "selected_plural": "Affichage des éléments avec l'un des tags sélectionnés ({{count}} sélectionnés)", + "clickToFilter": "Cliquez sur les tags pour filtrer les éléments" + } + }, + "items": { + "empty": { + "noItems": "Aucun élément trouvé dans le gestionnaire de paquets", + "withFilters": "Essayez d'ajuster vos filtres", + "noSources": "Essayez d'ajouter une source dans l'onglet Sources" + }, + "count": "{{count}} élément trouvé", + "count_plural": "{{count}} éléments trouvés", + "refresh": { + "button": "Actualiser", + "refreshing": "Actualisation..." + }, + "card": { + "by": "par {{author}}", + "from": "de {{source}}", + "externalComponents": "Contient {{count}} composant externe", + "externalComponents_plural": "Contient {{count}} composants externes", + "viewSource": "Voir", + "viewOnSource": "Voir sur {{source}}" + } + }, + "sources": { + "title": "Configurer les Sources du Gestionnaire de Paquets", + "description": "Ajoutez des dépôts Git contenant des éléments du gestionnaire de paquets. Ces dépôts seront récupérés lors de la navigation dans le gestionnaire de paquets.", + "add": { + "title": "Ajouter une Nouvelle Source", + "urlPlaceholder": "URL du dépôt Git (ex. https://github.com/username/repo)", + "urlFormats": "Formats pris en charge : HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git), ou protocole Git (git://github.com/username/repo.git)", + "namePlaceholder": "Nom d'affichage (optionnel, max 20 caractères)", + "button": "Ajouter la Source" + }, + "current": { + "title": "Sources Actuelles", + "count": "{{current}}/{{max}} maximum", + "empty": "Aucune source configurée. Ajoutez une source pour commencer.", + "refresh": "Actualiser cette source", + "remove": "Supprimer la source" + }, + "errors": { + "emptyUrl": "L'URL ne peut pas être vide", + "invalidUrl": "Format d'URL invalide", + "nonVisibleChars": "L'URL contient des caractères non visibles autres que des espaces", + "invalidGitUrl": "L'URL doit être une URL de dépôt Git valide (ex. https://github.com/username/repo)", + "duplicateUrl": "Cette URL est déjà dans la liste (correspondance insensible à la casse et aux espaces)", + "nameTooLong": "Le nom doit faire 20 caractères ou moins", + "nonVisibleCharsName": "Le nom contient des caractères non visibles autres que des espaces", + "duplicateName": "Ce nom est déjà utilisé (correspondance insensible à la casse et aux espaces)", + "maxSources": "Maximum de {{max}} sources autorisées" + } + } +} diff --git a/webview-ui/src/i18n/locales/hi/package-manager.json b/webview-ui/src/i18n/locales/hi/package-manager.json new file mode 100644 index 0000000000..cc5ad0e50a --- /dev/null +++ b/webview-ui/src/i18n/locales/hi/package-manager.json @@ -0,0 +1,85 @@ +{ + "title": "पैकेज प्रबंधक", + "tabs": { + "browse": "ब्राउज़", + "sources": "स्रोत" + }, + "filters": { + "search": { + "placeholder": "पैकेज प्रबंधक आइटम खोजें..." + }, + "type": { + "label": "प्रकार से फ़िल्टर करें:", + "all": "सभी प्रकार", + "mode": "मोड", + "mcp server": "एमसीपी सर्वर", + "prompt": "प्रॉम्प्ट", + "package": "पैकेज" + }, + "sort": { + "label": "इसके अनुसार क्रमबद्ध करें:", + "name": "नाम", + "author": "लेखक", + "lastUpdated": "अंतिम अपडेट" + }, + "tags": { + "label": "टैग से फ़िल्टर करें:", + "available": "{{count}} उपलब्ध", + "clear": "टैग साफ़ करें ({{count}})", + "placeholder": "टैग खोजने और चुनने के लिए टाइप करें...", + "noResults": "कोई मिलान टैग नहीं मिला", + "selected": "चयनित टैग में से किसी एक वाले आइटम दिखा रहा है ({{count}} चयनित)", + "clickToFilter": "आइटम फ़िल्टर करने के लिए टैग पर क्लिक करें" + } + }, + "items": { + "empty": { + "noItems": "कोई पैकेज प्रबंधक आइटम नहीं मिला", + "withFilters": "फ़िल्टर समायोजित करने का प्रयास करें", + "noSources": "स्रोत टैब में एक स्रोत जोड़ने का प्रयास करें" + }, + "count": "{{count}} आइटम मिला", + "count_plural": "{{count}} आइटम मिले", + "refresh": { + "button": "रीफ्रेश", + "refreshing": "रीफ्रेश हो रहा है..." + }, + "card": { + "by": "लेखक: {{author}}", + "from": "स्रोत: {{source}}", + "externalComponents": "{{count}} बाहरी कंपोनेंट शामिल है", + "externalComponents_plural": "{{count}} बाहरी कंपोनेंट शामिल हैं", + "viewSource": "देखें", + "viewOnSource": "{{source}} पर देखें" + } + }, + "sources": { + "title": "पैकेज प्रबंधक स्रोत कॉन्फ़िगर करें", + "description": "पैकेज प्रबंधक आइटम वाले Git रिपॉजिटरी जोड़ें। पैकेज प्रबंधक ब्राउज़ करते समय इन रिपॉजिटरी को प्राप्त किया जाएगा।", + "add": { + "title": "नया स्रोत जोड़ें", + "urlPlaceholder": "Git रिपॉजिटरी URL (उदा. https://github.com/username/repo)", + "urlFormats": "समर्थित प्रारूप: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git), या Git प्रोटोकॉल (git://github.com/username/repo.git)", + "namePlaceholder": "प्रदर्शन नाम (वैकल्पिक, अधिकतम 20 वर्ण)", + "button": "स्रोत जोड़ें" + }, + "current": { + "title": "वर्तमान स्रोत", + "count": "{{current}}/{{max}} अधिकतम", + "empty": "कोई स्रोत कॉन्फ़िगर नहीं किया गया। शुरू करने के लिए एक स्रोत जोड़ें।", + "refresh": "यह स्रोत रीफ्रेश करें", + "remove": "स्रोत हटाएं" + }, + "errors": { + "emptyUrl": "URL खाली नहीं हो सकता", + "invalidUrl": "अमान्य URL प्रारूप", + "nonVisibleChars": "URL में स्पेस के अलावा अदृश्य वर्ण हैं", + "invalidGitUrl": "URL एक वैध Git रिपॉजिटरी URL होना चाहिए (उदा. https://github.com/username/repo)", + "duplicateUrl": "यह URL पहले से सूची में है (केस और स्पेस असंवेदनशील मिलान)", + "nameTooLong": "नाम 20 वर्णों से अधिक नहीं हो सकता", + "nonVisibleCharsName": "नाम में स्पेस के अलावा अदृश्य वर्ण हैं", + "duplicateName": "यह नाम पहले से उपयोग में है (केस और स्पेस असंवेदनशील मिलान)", + "maxSources": "अधिकतम {{max}} स्रोत की अनुमति है" + } + } +} diff --git a/webview-ui/src/i18n/locales/it/package-manager.json b/webview-ui/src/i18n/locales/it/package-manager.json new file mode 100644 index 0000000000..836ea47a02 --- /dev/null +++ b/webview-ui/src/i18n/locales/it/package-manager.json @@ -0,0 +1,87 @@ +{ + "title": "Gestore Pacchetti", + "tabs": { + "browse": "Sfoglia", + "sources": "Sorgenti" + }, + "filters": { + "search": { + "placeholder": "Cerca elementi del gestore pacchetti..." + }, + "type": { + "label": "Filtra per tipo:", + "all": "Tutti i tipi", + "mode": "Modalità", + "mcp server": "Server MCP", + "prompt": "Prompt", + "package": "Pacchetto" + }, + "sort": { + "label": "Ordina per:", + "name": "Nome", + "author": "Autore", + "lastUpdated": "Ultimo aggiornamento" + }, + "tags": { + "label": "Filtra per tag:", + "available": "{{count}} disponibile", + "available_plural": "{{count}} disponibili", + "clear": "Cancella tag ({{count}})", + "placeholder": "Digita per cercare e selezionare i tag...", + "noResults": "Nessun tag corrispondente trovato", + "selected": "Visualizzazione elementi con uno dei tag selezionati ({{count}} selezionato)", + "selected_plural": "Visualizzazione elementi con uno dei tag selezionati ({{count}} selezionati)", + "clickToFilter": "Clicca sui tag per filtrare gli elementi" + } + }, + "items": { + "empty": { + "noItems": "Nessun elemento del gestore pacchetti trovato", + "withFilters": "Prova a modificare i filtri", + "noSources": "Prova ad aggiungere una sorgente nella scheda Sorgenti" + }, + "count": "{{count}} elemento trovato", + "count_plural": "{{count}} elementi trovati", + "refresh": { + "button": "Aggiorna", + "refreshing": "Aggiornamento in corso..." + }, + "card": { + "by": "di {{author}}", + "from": "da {{source}}", + "externalComponents": "Contiene {{count}} componente esterno", + "externalComponents_plural": "Contiene {{count}} componenti esterni", + "viewSource": "Visualizza", + "viewOnSource": "Visualizza su {{source}}" + } + }, + "sources": { + "title": "Configura Sorgenti del Gestore Pacchetti", + "description": "Aggiungi repository Git che contengono elementi del gestore pacchetti. Questi repository verranno recuperati durante la navigazione del gestore pacchetti.", + "add": { + "title": "Aggiungi Nuova Sorgente", + "urlPlaceholder": "URL del repository Git (es. https://github.com/username/repo)", + "urlFormats": "Formati supportati: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) o protocollo Git (git://github.com/username/repo.git)", + "namePlaceholder": "Nome visualizzato (opzionale, max 20 caratteri)", + "button": "Aggiungi Sorgente" + }, + "current": { + "title": "Sorgenti Attuali", + "count": "{{current}}/{{max}} massimo", + "empty": "Nessuna sorgente configurata. Aggiungi una sorgente per iniziare.", + "refresh": "Aggiorna questa sorgente", + "remove": "Rimuovi sorgente" + }, + "errors": { + "emptyUrl": "L'URL non può essere vuoto", + "invalidUrl": "Formato URL non valido", + "nonVisibleChars": "L'URL contiene caratteri non visibili oltre agli spazi", + "invalidGitUrl": "L'URL deve essere un URL di repository Git valido (es. https://github.com/username/repo)", + "duplicateUrl": "Questo URL è già presente nell'elenco (corrispondenza senza distinzione tra maiuscole/minuscole e spazi)", + "nameTooLong": "Il nome deve essere di massimo 20 caratteri", + "nonVisibleCharsName": "Il nome contiene caratteri non visibili oltre agli spazi", + "duplicateName": "Questo nome è già in uso (corrispondenza senza distinzione tra maiuscole/minuscole e spazi)", + "maxSources": "Massimo {{max}} sorgenti consentite" + } + } +} diff --git a/webview-ui/src/i18n/locales/ja/package-manager.json b/webview-ui/src/i18n/locales/ja/package-manager.json new file mode 100644 index 0000000000..feacda8275 --- /dev/null +++ b/webview-ui/src/i18n/locales/ja/package-manager.json @@ -0,0 +1,83 @@ +{ + "title": "パッケージマネージャー", + "tabs": { + "browse": "ブラウズ", + "sources": "ソース" + }, + "filters": { + "search": { + "placeholder": "パッケージマネージャーのアイテムを検索..." + }, + "type": { + "label": "タイプで絞り込み:", + "all": "すべてのタイプ", + "mode": "モード", + "mcp server": "MCPサーバー", + "prompt": "プロンプト", + "package": "パッケージ" + }, + "sort": { + "label": "並び替え:", + "name": "名前", + "author": "作成者", + "lastUpdated": "最終更新" + }, + "tags": { + "label": "タグで絞り込み:", + "available": "{{count}}個利用可能", + "clear": "タグをクリア({{count}}個)", + "placeholder": "タグを検索して選択...", + "noResults": "一致するタグが見つかりません", + "selected": "選択したタグのいずれかを含むアイテムを表示中({{count}}個選択)", + "clickToFilter": "タグをクリックしてアイテムを絞り込む" + } + }, + "items": { + "empty": { + "noItems": "パッケージマネージャーのアイテムが見つかりません", + "withFilters": "フィルターを調整してみてください", + "noSources": "ソースタブでソースを追加してみてください" + }, + "count": "{{count}}個のアイテムが見つかりました", + "refresh": { + "button": "更新", + "refreshing": "更新中..." + }, + "card": { + "by": "作成者:{{author}}", + "from": "ソース:{{source}}", + "externalComponents": "外部コンポーネント{{count}}個を含む", + "viewSource": "表示", + "viewOnSource": "{{source}}で表示" + } + }, + "sources": { + "title": "パッケージマネージャーのソース設定", + "description": "パッケージマネージャーのアイテムを含むGitリポジトリを追加します。これらのリポジトリはパッケージマネージャーの閲覧時に取得されます。", + "add": { + "title": "新規ソースの追加", + "urlPlaceholder": "GitリポジトリのURL(例:https://github.com/username/repo)", + "urlFormats": "対応フォーマット:HTTPS(https://github.com/username/repo)、SSH(git@github.com:username/repo.git)、またはGitプロトコル(git://github.com/username/repo.git)", + "namePlaceholder": "表示名(オプション、最大20文字)", + "button": "ソースを追加" + }, + "current": { + "title": "現在のソース", + "count": "{{current}}/{{max}}個(最大)", + "empty": "ソースが設定されていません。ソースを追加して始めてください。", + "refresh": "このソースを更新", + "remove": "ソースを削除" + }, + "errors": { + "emptyUrl": "URLを入力してください", + "invalidUrl": "URLの形式が無効です", + "nonVisibleChars": "URLに空白以外の不可視文字が含まれています", + "invalidGitUrl": "有効なGitリポジトリのURLを入力してください(例:https://github.com/username/repo)", + "duplicateUrl": "このURLは既にリストに存在します(大文字小文字と空白を区別しない一致)", + "nameTooLong": "名前は20文字以内にしてください", + "nonVisibleCharsName": "名前に空白以外の不可視文字が含まれています", + "duplicateName": "この名前は既に使用されています(大文字小文字と空白を区別しない一致)", + "maxSources": "ソースは最大{{max}}個まで追加できます" + } + } +} diff --git a/webview-ui/src/i18n/locales/ko/package-manager.json b/webview-ui/src/i18n/locales/ko/package-manager.json new file mode 100644 index 0000000000..4cbd275f94 --- /dev/null +++ b/webview-ui/src/i18n/locales/ko/package-manager.json @@ -0,0 +1,83 @@ +{ + "title": "패키지 관리자", + "tabs": { + "browse": "탐색", + "sources": "소스" + }, + "filters": { + "search": { + "placeholder": "패키지 관리자 항목 검색..." + }, + "type": { + "label": "유형별 필터링:", + "all": "모든 유형", + "mode": "모드", + "mcp server": "MCP 서버", + "prompt": "프롬프트", + "package": "패키지" + }, + "sort": { + "label": "정렬 기준:", + "name": "이름", + "author": "작성자", + "lastUpdated": "최근 업데이트" + }, + "tags": { + "label": "태그별 필터링:", + "available": "{{count}}개 사용 가능", + "clear": "태그 지우기 ({{count}}개)", + "placeholder": "태그 검색 및 선택...", + "noResults": "일치하는 태그가 없습니다", + "selected": "선택한 태그 중 하나를 포함하는 항목 표시 ({{count}}개 선택됨)", + "clickToFilter": "태그를 클릭하여 항목 필터링" + } + }, + "items": { + "empty": { + "noItems": "패키지 관리자 항목을 찾을 수 없습니다", + "withFilters": "필터 조건을 조정해 보세요", + "noSources": "소스 탭에서 소스를 추가해 보세요" + }, + "count": "{{count}}개의 항목 발견", + "refresh": { + "button": "새로 고침", + "refreshing": "새로 고치는 중..." + }, + "card": { + "by": "작성자: {{author}}", + "from": "출처: {{source}}", + "externalComponents": "외부 컴포넌트 {{count}}개 포함", + "viewSource": "보기", + "viewOnSource": "{{source}}에서 보기" + } + }, + "sources": { + "title": "패키지 관리자 소스 구성", + "description": "패키지 관리자 항목이 포함된 Git 저장소를 추가합니다. 패키지 관리자를 탐색할 때 이러한 저장소를 가져옵니다.", + "add": { + "title": "새 소스 추가", + "urlPlaceholder": "Git 저장소 URL (예: https://github.com/username/repo)", + "urlFormats": "지원되는 형식: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git), 또는 Git 프로토콜 (git://github.com/username/repo.git)", + "namePlaceholder": "표시 이름 (선택 사항, 최대 20자)", + "button": "소스 추가" + }, + "current": { + "title": "현재 소스", + "count": "{{current}}/{{max}}개 (최대)", + "empty": "구성된 소스가 없습니다. 소스를 추가하여 시작하세요.", + "refresh": "이 소스 새로 고침", + "remove": "소스 제거" + }, + "errors": { + "emptyUrl": "URL을 입력해야 합니다", + "invalidUrl": "잘못된 URL 형식", + "nonVisibleChars": "URL에 공백 이외의 보이지 않는 문자가 포함되어 있습니다", + "invalidGitUrl": "URL은 유효한 Git 저장소 URL이어야 합니다 (예: https://github.com/username/repo)", + "duplicateUrl": "이 URL은 이미 목록에 있습니다 (대소문자 및 공백 구분 없이 일치)", + "nameTooLong": "이름은 20자를 초과할 수 없습니다", + "nonVisibleCharsName": "이름에 공백 이외의 보이지 않는 문자가 포함되어 있습니다", + "duplicateName": "이 이름은 이미 사용 중입니다 (대소문자 및 공백 구분 없이 일치)", + "maxSources": "최대 {{max}}개의 소스만 허용됩니다" + } + } +} diff --git a/webview-ui/src/i18n/locales/pl/package-manager.json b/webview-ui/src/i18n/locales/pl/package-manager.json new file mode 100644 index 0000000000..4a206357d9 --- /dev/null +++ b/webview-ui/src/i18n/locales/pl/package-manager.json @@ -0,0 +1,91 @@ +{ + "title": "Menedżer Pakietów", + "tabs": { + "browse": "Przeglądaj", + "sources": "Źródła" + }, + "filters": { + "search": { + "placeholder": "Szukaj elementów menedżera pakietów..." + }, + "type": { + "label": "Filtruj według typu:", + "all": "Wszystkie typy", + "mode": "Tryb", + "mcp server": "Serwer MCP", + "prompt": "Prompt", + "package": "Pakiet" + }, + "sort": { + "label": "Sortuj według:", + "name": "Nazwa", + "author": "Autor", + "lastUpdated": "Ostatnia aktualizacja" + }, + "tags": { + "label": "Filtruj według tagów:", + "available": "{{count}} dostępny", + "available_2-4": "{{count}} dostępne", + "available_5": "{{count}} dostępnych", + "clear": "Wyczyść tagi ({{count}})", + "placeholder": "Wpisz, aby wyszukać i wybrać tagi...", + "noResults": "Nie znaleziono pasujących tagów", + "selected": "Wyświetlanie elementów z dowolnym z wybranych tagów ({{count}} wybrany)", + "selected_2-4": "Wyświetlanie elementów z dowolnym z wybranych tagów ({{count}} wybrane)", + "selected_5": "Wyświetlanie elementów z dowolnym z wybranych tagów ({{count}} wybranych)", + "clickToFilter": "Kliknij tagi, aby filtrować elementy" + } + }, + "items": { + "empty": { + "noItems": "Nie znaleziono elementów menedżera pakietów", + "withFilters": "Spróbuj dostosować filtry", + "noSources": "Spróbuj dodać źródło w zakładce Źródła" + }, + "count": "Znaleziono {{count}} element", + "count_2-4": "Znaleziono {{count}} elementy", + "count_5": "Znaleziono {{count}} elementów", + "refresh": { + "button": "Odśwież", + "refreshing": "Odświeżanie..." + }, + "card": { + "by": "autor: {{author}}", + "from": "z {{source}}", + "externalComponents": "Zawiera {{count}} komponent zewnętrzny", + "externalComponents_2-4": "Zawiera {{count}} komponenty zewnętrzne", + "externalComponents_5": "Zawiera {{count}} komponentów zewnętrznych", + "viewSource": "Zobacz", + "viewOnSource": "Zobacz na {{source}}" + } + }, + "sources": { + "title": "Konfiguruj Źródła Menedżera Pakietów", + "description": "Dodaj repozytoria Git zawierające elementy menedżera pakietów. Te repozytoria będą pobierane podczas przeglądania menedżera pakietów.", + "add": { + "title": "Dodaj Nowe Źródło", + "urlPlaceholder": "URL repozytorium Git (np. https://github.com/username/repo)", + "urlFormats": "Obsługiwane formaty: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) lub protokół Git (git://github.com/username/repo.git)", + "namePlaceholder": "Nazwa wyświetlana (opcjonalnie, maks. 20 znaków)", + "button": "Dodaj Źródło" + }, + "current": { + "title": "Aktualne Źródła", + "count": "{{current}}/{{max}} maksymalnie", + "empty": "Brak skonfigurowanych źródeł. Dodaj źródło, aby rozpocząć.", + "refresh": "Odśwież to źródło", + "remove": "Usuń źródło" + }, + "errors": { + "emptyUrl": "URL nie może być pusty", + "invalidUrl": "Nieprawidłowy format URL", + "nonVisibleChars": "URL zawiera znaki niewidoczne inne niż spacje", + "invalidGitUrl": "URL musi być prawidłowym URL-em repozytorium Git (np. https://github.com/username/repo)", + "duplicateUrl": "Ten URL już znajduje się na liście (dopasowanie bez rozróżniania wielkości liter i spacji)", + "nameTooLong": "Nazwa nie może przekraczać 20 znaków", + "nonVisibleCharsName": "Nazwa zawiera znaki niewidoczne inne niż spacje", + "duplicateName": "Ta nazwa jest już używana (dopasowanie bez rozróżniania wielkości liter i spacji)", + "maxSources": "Maksymalna liczba źródeł to {{max}}" + } + } +} diff --git a/webview-ui/src/i18n/locales/pt-BR/package-manager.json b/webview-ui/src/i18n/locales/pt-BR/package-manager.json new file mode 100644 index 0000000000..9746b460d4 --- /dev/null +++ b/webview-ui/src/i18n/locales/pt-BR/package-manager.json @@ -0,0 +1,87 @@ +{ + "title": "Gerenciador de Pacotes", + "tabs": { + "browse": "Navegar", + "sources": "Fontes" + }, + "filters": { + "search": { + "placeholder": "Pesquisar itens do gerenciador de pacotes..." + }, + "type": { + "label": "Filtrar por tipo:", + "all": "Todos os tipos", + "mode": "Modo", + "mcp server": "Servidor MCP", + "prompt": "Prompt", + "package": "Pacote" + }, + "sort": { + "label": "Ordenar por:", + "name": "Nome", + "author": "Autor", + "lastUpdated": "Última atualização" + }, + "tags": { + "label": "Filtrar por tags:", + "available": "{{count}} disponível", + "available_plural": "{{count}} disponíveis", + "clear": "Limpar tags ({{count}})", + "placeholder": "Digite para pesquisar e selecionar tags...", + "noResults": "Nenhuma tag correspondente encontrada", + "selected": "Exibindo itens com qualquer uma das tags selecionadas ({{count}} selecionada)", + "selected_plural": "Exibindo itens com qualquer uma das tags selecionadas ({{count}} selecionadas)", + "clickToFilter": "Clique nas tags para filtrar os itens" + } + }, + "items": { + "empty": { + "noItems": "Nenhum item do gerenciador de pacotes encontrado", + "withFilters": "Tente ajustar os filtros", + "noSources": "Tente adicionar uma fonte na aba Fontes" + }, + "count": "{{count}} item encontrado", + "count_plural": "{{count}} itens encontrados", + "refresh": { + "button": "Atualizar", + "refreshing": "Atualizando..." + }, + "card": { + "by": "por {{author}}", + "from": "de {{source}}", + "externalComponents": "Contém {{count}} componente externo", + "externalComponents_plural": "Contém {{count}} componentes externos", + "viewSource": "Visualizar", + "viewOnSource": "Visualizar no {{source}}" + } + }, + "sources": { + "title": "Configurar Fontes do Gerenciador de Pacotes", + "description": "Adicione repositórios Git que contenham itens do gerenciador de pacotes. Estes repositórios serão obtidos ao navegar pelo gerenciador de pacotes.", + "add": { + "title": "Adicionar Nova Fonte", + "urlPlaceholder": "URL do repositório Git (ex: https://github.com/username/repo)", + "urlFormats": "Formatos suportados: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) ou protocolo Git (git://github.com/username/repo.git)", + "namePlaceholder": "Nome de exibição (opcional, máx. 20 caracteres)", + "button": "Adicionar Fonte" + }, + "current": { + "title": "Fontes Atuais", + "count": "{{current}}/{{max}} máximo", + "empty": "Nenhuma fonte configurada. Adicione uma fonte para começar.", + "refresh": "Atualizar esta fonte", + "remove": "Remover fonte" + }, + "errors": { + "emptyUrl": "A URL não pode estar vazia", + "invalidUrl": "Formato de URL inválido", + "nonVisibleChars": "A URL contém caracteres não visíveis além de espaços", + "invalidGitUrl": "A URL deve ser uma URL de repositório Git válida (ex: https://github.com/username/repo)", + "duplicateUrl": "Esta URL já está na lista (correspondência sem distinção entre maiúsculas/minúsculas e espaços)", + "nameTooLong": "O nome deve ter no máximo 20 caracteres", + "nonVisibleCharsName": "O nome contém caracteres não visíveis além de espaços", + "duplicateName": "Este nome já está em uso (correspondência sem distinção entre maiúsculas/minúsculas e espaços)", + "maxSources": "Máximo de {{max}} fontes permitidas" + } + } +} diff --git a/webview-ui/src/i18n/locales/tr/package-manager.json b/webview-ui/src/i18n/locales/tr/package-manager.json new file mode 100644 index 0000000000..e7a850bd73 --- /dev/null +++ b/webview-ui/src/i18n/locales/tr/package-manager.json @@ -0,0 +1,83 @@ +{ + "title": "Paket Yöneticisi", + "tabs": { + "browse": "Gözat", + "sources": "Kaynaklar" + }, + "filters": { + "search": { + "placeholder": "Paket yöneticisi öğelerini ara..." + }, + "type": { + "label": "Türe göre filtrele:", + "all": "Tüm türler", + "mode": "Mod", + "mcp server": "MCP Sunucusu", + "prompt": "Komut", + "package": "Paket" + }, + "sort": { + "label": "Sıralama ölçütü:", + "name": "Ad", + "author": "Yazar", + "lastUpdated": "Son güncelleme" + }, + "tags": { + "label": "Etiketlere göre filtrele:", + "available": "{{count}} etiket mevcut", + "clear": "Etiketleri temizle ({{count}})", + "placeholder": "Etiket aramak ve seçmek için yazın...", + "noResults": "Eşleşen etiket bulunamadı", + "selected": "Seçili etiketlerden herhangi birini içeren öğeler gösteriliyor ({{count}} seçili)", + "clickToFilter": "Öğeleri filtrelemek için etiketlere tıklayın" + } + }, + "items": { + "empty": { + "noItems": "Paket yöneticisi öğesi bulunamadı", + "withFilters": "Filtreleri ayarlamayı deneyin", + "noSources": "Kaynaklar sekmesinde bir kaynak eklemeyi deneyin" + }, + "count": "{{count}} öğe bulundu", + "refresh": { + "button": "Yenile", + "refreshing": "Yenileniyor..." + }, + "card": { + "by": "yazar: {{author}}", + "from": "kaynak: {{source}}", + "externalComponents": "{{count}} harici bileşen içeriyor", + "viewSource": "Görüntüle", + "viewOnSource": "{{source}} üzerinde görüntüle" + } + }, + "sources": { + "title": "Paket Yöneticisi Kaynaklarını Yapılandır", + "description": "Paket yöneticisi öğeleri içeren Git depolarını ekleyin. Bu depolar, paket yöneticisinde gezinirken alınacaktır.", + "add": { + "title": "Yeni Kaynak Ekle", + "urlPlaceholder": "Git deposu URL'si (örn. https://github.com/username/repo)", + "urlFormats": "Desteklenen biçimler: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) veya Git protokolü (git://github.com/username/repo.git)", + "namePlaceholder": "Görünen ad (isteğe bağlı, en fazla 20 karakter)", + "button": "Kaynak Ekle" + }, + "current": { + "title": "Mevcut Kaynaklar", + "count": "{{current}}/{{max}} en fazla", + "empty": "Yapılandırılmış kaynak yok. Başlamak için bir kaynak ekleyin.", + "refresh": "Bu kaynağı yenile", + "remove": "Kaynağı kaldır" + }, + "errors": { + "emptyUrl": "URL boş olamaz", + "invalidUrl": "Geçersiz URL biçimi", + "nonVisibleChars": "URL boşluk dışında görünmez karakterler içeriyor", + "invalidGitUrl": "URL geçerli bir Git deposu URL'si olmalıdır (örn. https://github.com/username/repo)", + "duplicateUrl": "Bu URL zaten listede mevcut (büyük/küçük harf ve boşluk duyarsız eşleşme)", + "nameTooLong": "Ad en fazla 20 karakter olmalıdır", + "nonVisibleCharsName": "Ad boşluk dışında görünmez karakterler içeriyor", + "duplicateName": "Bu ad zaten kullanımda (büyük/küçük harf ve boşluk duyarsız eşleşme)", + "maxSources": "En fazla {{max}} kaynak eklenebilir" + } + } +} diff --git a/webview-ui/src/i18n/locales/vi/package-manager.json b/webview-ui/src/i18n/locales/vi/package-manager.json new file mode 100644 index 0000000000..0e95658bc1 --- /dev/null +++ b/webview-ui/src/i18n/locales/vi/package-manager.json @@ -0,0 +1,83 @@ +{ + "title": "Trình Quản Lý Gói", + "tabs": { + "browse": "Duyệt", + "sources": "Nguồn" + }, + "filters": { + "search": { + "placeholder": "Tìm kiếm các mục trong trình quản lý gói..." + }, + "type": { + "label": "Lọc theo loại:", + "all": "Tất cả các loại", + "mode": "Chế độ", + "mcp server": "Máy chủ MCP", + "prompt": "Lời nhắc", + "package": "Gói" + }, + "sort": { + "label": "Sắp xếp theo:", + "name": "Tên", + "author": "Tác giả", + "lastUpdated": "Cập nhật lần cuối" + }, + "tags": { + "label": "Lọc theo thẻ:", + "available": "{{count}} thẻ có sẵn", + "clear": "Xóa thẻ ({{count}})", + "placeholder": "Gõ để tìm kiếm và chọn thẻ...", + "noResults": "Không tìm thấy thẻ phù hợp", + "selected": "Hiển thị các mục có bất kỳ thẻ đã chọn nào (đã chọn {{count}} thẻ)", + "clickToFilter": "Nhấp vào thẻ để lọc các mục" + } + }, + "items": { + "empty": { + "noItems": "Không tìm thấy mục nào trong trình quản lý gói", + "withFilters": "Thử điều chỉnh bộ lọc", + "noSources": "Thử thêm một nguồn trong tab Nguồn" + }, + "count": "Tìm thấy {{count}} mục", + "refresh": { + "button": "Làm mới", + "refreshing": "Đang làm mới..." + }, + "card": { + "by": "bởi {{author}}", + "from": "từ {{source}}", + "externalComponents": "Chứa {{count}} thành phần bên ngoài", + "viewSource": "Xem", + "viewOnSource": "Xem trên {{source}}" + } + }, + "sources": { + "title": "Cấu Hình Nguồn Trình Quản Lý Gói", + "description": "Thêm kho lưu trữ Git chứa các mục của trình quản lý gói. Các kho lưu trữ này sẽ được tải khi duyệt trình quản lý gói.", + "add": { + "title": "Thêm Nguồn Mới", + "urlPlaceholder": "URL kho lưu trữ Git (ví dụ: https://github.com/username/repo)", + "urlFormats": "Định dạng được hỗ trợ: HTTPS (https://github.com/username/repo), SSH (git@github.com:username/repo.git) hoặc giao thức Git (git://github.com/username/repo.git)", + "namePlaceholder": "Tên hiển thị (tùy chọn, tối đa 20 ký tự)", + "button": "Thêm Nguồn" + }, + "current": { + "title": "Nguồn Hiện Tại", + "count": "{{current}}/{{max}} tối đa", + "empty": "Chưa có nguồn nào được cấu hình. Thêm một nguồn để bắt đầu.", + "refresh": "Làm mới nguồn này", + "remove": "Xóa nguồn" + }, + "errors": { + "emptyUrl": "URL không được để trống", + "invalidUrl": "Định dạng URL không hợp lệ", + "nonVisibleChars": "URL chứa ký tự không nhìn thấy ngoài khoảng trắng", + "invalidGitUrl": "URL phải là URL kho lưu trữ Git hợp lệ (ví dụ: https://github.com/username/repo)", + "duplicateUrl": "URL này đã có trong danh sách (khớp không phân biệt chữ hoa/thường và khoảng trắng)", + "nameTooLong": "Tên không được vượt quá 20 ký tự", + "nonVisibleCharsName": "Tên chứa ký tự không nhìn thấy ngoài khoảng trắng", + "duplicateName": "Tên này đã được sử dụng (khớp không phân biệt chữ hoa/thường và khoảng trắng)", + "maxSources": "Chỉ cho phép tối đa {{max}} nguồn" + } + } +} diff --git a/webview-ui/src/i18n/locales/zh-CN/package-manager.json b/webview-ui/src/i18n/locales/zh-CN/package-manager.json new file mode 100644 index 0000000000..238c8dd420 --- /dev/null +++ b/webview-ui/src/i18n/locales/zh-CN/package-manager.json @@ -0,0 +1,83 @@ +{ + "title": "包管理器", + "tabs": { + "browse": "浏览", + "sources": "源" + }, + "filters": { + "search": { + "placeholder": "搜索包管理器项目..." + }, + "type": { + "label": "按类型筛选:", + "all": "所有类型", + "mode": "模式", + "mcp server": "MCP服务器", + "prompt": "提示", + "package": "包" + }, + "sort": { + "label": "排序方式:", + "name": "名称", + "author": "作者", + "lastUpdated": "最后更新" + }, + "tags": { + "label": "按标签筛选:", + "available": "可用{{count}}个", + "clear": "清除标签({{count}}个)", + "placeholder": "输入以搜索和选择标签...", + "noResults": "未找到匹配的标签", + "selected": "显示包含任一所选标签的项目(已选择{{count}}个)", + "clickToFilter": "点击标签以筛选项目" + } + }, + "items": { + "empty": { + "noItems": "未找到包管理器项目", + "withFilters": "请尝试调整筛选条件", + "noSources": "请尝试在源标签页中添加源" + }, + "count": "找到{{count}}个项目", + "refresh": { + "button": "刷新", + "refreshing": "刷新中..." + }, + "card": { + "by": "作者:{{author}}", + "from": "来源:{{source}}", + "externalComponents": "包含{{count}}个外部组件", + "viewSource": "查看", + "viewOnSource": "在{{source}}上查看" + } + }, + "sources": { + "title": "配置包管理器源", + "description": "添加包含包管理器项目的Git仓库。浏览包管理器时将获取这些仓库。", + "add": { + "title": "添加新源", + "urlPlaceholder": "Git仓库URL(例如:https://github.com/username/repo)", + "urlFormats": "支持的格式:HTTPS(https://github.com/username/repo)、SSH(git@github.com:username/repo.git)或Git协议(git://github.com/username/repo.git)", + "namePlaceholder": "显示名称(可选,最多20个字符)", + "button": "添加源" + }, + "current": { + "title": "当前源", + "count": "{{current}}/{{max}}个(最多)", + "empty": "未配置源。添加源以开始使用。", + "refresh": "刷新此源", + "remove": "删除源" + }, + "errors": { + "emptyUrl": "URL不能为空", + "invalidUrl": "无效的URL格式", + "nonVisibleChars": "URL包含空格以外的不可见字符", + "invalidGitUrl": "URL必须是有效的Git仓库URL(例如:https://github.com/username/repo)", + "duplicateUrl": "此URL已在列表中(不区分大小写和空格的匹配)", + "nameTooLong": "名称不能超过20个字符", + "nonVisibleCharsName": "名称包含空格以外的不可见字符", + "duplicateName": "此名称已被使用(不区分大小写和空格的匹配)", + "maxSources": "最多允许{{max}}个源" + } + } +} diff --git a/webview-ui/src/i18n/locales/zh-TW/package-manager.json b/webview-ui/src/i18n/locales/zh-TW/package-manager.json new file mode 100644 index 0000000000..6709aae4a4 --- /dev/null +++ b/webview-ui/src/i18n/locales/zh-TW/package-manager.json @@ -0,0 +1,83 @@ +{ + "title": "套件管理器", + "tabs": { + "browse": "瀏覽", + "sources": "來源" + }, + "filters": { + "search": { + "placeholder": "搜尋套件管理器項目..." + }, + "type": { + "label": "按類型篩選:", + "all": "所有類型", + "mode": "模式", + "mcp server": "MCP伺服器", + "prompt": "提示", + "package": "套件" + }, + "sort": { + "label": "排序方式:", + "name": "名稱", + "author": "作者", + "lastUpdated": "最後更新" + }, + "tags": { + "label": "按標籤篩選:", + "available": "可用{{count}}個", + "clear": "清除標籤({{count}}個)", + "placeholder": "輸入以搜尋和選擇標籤...", + "noResults": "未找到符合的標籤", + "selected": "顯示包含任一所選標籤的項目(已選擇{{count}}個)", + "clickToFilter": "點擊標籤以篩選項目" + } + }, + "items": { + "empty": { + "noItems": "未找到套件管理器項目", + "withFilters": "請嘗試調整篩選條件", + "noSources": "請嘗試在來源分頁中新增來源" + }, + "count": "找到{{count}}個項目", + "refresh": { + "button": "重新整理", + "refreshing": "重新整理中..." + }, + "card": { + "by": "作者:{{author}}", + "from": "來源:{{source}}", + "externalComponents": "包含{{count}}個外部元件", + "viewSource": "檢視", + "viewOnSource": "在{{source}}上檢視" + } + }, + "sources": { + "title": "設定套件管理器來源", + "description": "新增包含套件管理器項目的Git儲存庫。瀏覽套件管理器時將取得這些儲存庫。", + "add": { + "title": "新增來源", + "urlPlaceholder": "Git儲存庫URL(例如:https://github.com/username/repo)", + "urlFormats": "支援的格式:HTTPS(https://github.com/username/repo)、SSH(git@github.com:username/repo.git)或Git協定(git://github.com/username/repo.git)", + "namePlaceholder": "顯示名稱(選填,最多20個字元)", + "button": "新增來源" + }, + "current": { + "title": "目前來源", + "count": "{{current}}/{{max}}個(最多)", + "empty": "未設定來源。新增來源以開始使用。", + "refresh": "重新整理此來源", + "remove": "移除來源" + }, + "errors": { + "emptyUrl": "URL不能為空", + "invalidUrl": "無效的URL格式", + "nonVisibleChars": "URL包含空格以外的不可見字元", + "invalidGitUrl": "URL必須是有效的Git儲存庫URL(例如:https://github.com/username/repo)", + "duplicateUrl": "此URL已在清單中(不區分大小寫和空格的匹配)", + "nameTooLong": "名稱不能超過20個字元", + "nonVisibleCharsName": "名稱包含空格以外的不可見字元", + "duplicateName": "此名稱已被使用(不區分大小寫和空格的匹配)", + "maxSources": "最多允許{{max}}個來源" + } + } +} diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index 5d240d8fd2..93ba4f2904 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -1,13 +1,13 @@ import { mentionRegex } from "../../../src/shared/context-mentions" import { Fzf } from "fzf" import { ModeConfig } from "../../../src/shared/modes" -import * as path from "path" export interface SearchResult { path: string type: "file" | "folder" label?: string } + export function insertMention( text: string, position: number, @@ -231,11 +231,13 @@ export function getContextMenuOptions( // Convert search results to queryItems format const searchResultItems = dynamicSearchResults.map((result) => { const formattedPath = result.path.startsWith("/") ? result.path : `/${result.path}` + const pathParts = formattedPath.split("/") + const fileName = pathParts[pathParts.length - 1] return { type: result.type === "folder" ? ContextMenuOptionType.Folder : ContextMenuOptionType.File, value: formattedPath, - label: result.label || path.basename(result.path), + label: result.label || fileName, description: formattedPath, } })