no type script or linting errors

This commit is contained in:
Smartsheet-JB-Brown 2025-04-12 09:55:08 -07:00
parent aa066935ce
commit 6b9b2aa4de
64 changed files with 6385 additions and 1774 deletions

View file

@ -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]

1872
e2e/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -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",

View file

@ -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<void>
removeSource(url: string): Promise<void>
getSources(): Promise<PackageManagerSource[]>
getItems(): Promise<PackageManagerItem[]>
}
interface WaitForOptions {
timeout?: number
interval?: number
message?: string
}
suite("Package Manager Integration Tests", () => {
let extension: vscode.Extension<RooCodeAPI> | undefined
suiteSetup(async () => {
extension = vscode.extensions.getExtension<RooCodeAPI>("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)
})
})

13
package-lock.json generated
View file

@ -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"
},

View file

@ -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
## 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

View file

@ -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"]

View file

@ -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()

View file

@ -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"]

View file

@ -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"]

View file

@ -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

View file

@ -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"

View file

@ -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"

View file

@ -0,0 +1,3 @@
name: "Package Manager Template"
description: "A template repository for creating package manager sources"
version: "1.0.0"

View file

@ -1,3 +0,0 @@
name: "Example Package Manager Repository"
description: "A collection of example package manager items for Roo-Code"
version: "1.0.0"

View file

@ -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"

View file

@ -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
- Stay updated with the latest technologies and best practices

View file

@ -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"

View file

@ -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()

View file

@ -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"]

View file

@ -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"]

View file

@ -0,0 +1,5 @@
name: "データプラットフォームパッケージ"
description: "ロール、サーバー、ストレージシステムを含む完全なデータプラットフォームソリューション"
type: "package"
version: "1.0.0"
tags: ["データプラットフォーム", "エンタープライズ", "データエンジニアリング", "完全ソリューション"]

View file

@ -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"

View file

@ -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

View file

@ -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"

View file

@ -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<Object>} - 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<any>} - 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<Object>} - 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<Array>} - 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

View file

@ -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",

View file

@ -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

View file

@ -750,7 +750,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> 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<ClineProviderEvents> implements
const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("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,

View file

@ -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}`)

View file

@ -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()

View file

@ -430,6 +430,13 @@ export class McpHub {
config: z.infer<typeof ServerConfigSchema>,
source: "global" | "project" = "global",
): Promise<void> {
// 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<string, any>,
source: "global" | "project" = "global",
): Promise<void> {
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<typeof ServerConfigSchema>
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<typeof ServerConfigSchema>
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<void> {
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<void> {

View file

@ -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<PackageManagerRepository> {
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<string> {
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<void> {
// 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<any> {
// 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<PackageManagerItem[]> {
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<PackageManagerRepository> {
// 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;
}
}
// 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<void> {
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<void> {
// 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<RepositoryMetadata> {
const metadataPath = path.join(repoDir, "metadata.en.yml")
const metadataContent = await fs.readFile(metadataPath, "utf-8")
try {
const parsed = yaml.load(metadataContent) as Record<string, any>
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<PackageManagerItem[]> {
return this.metadataScanner.scanDirectory(repoDir, repoUrl, sourceName)
}
}

View file

@ -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<PackageManagerItem[]> {
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<LocalizedMetadata<ComponentMetadata> | null> {
const metadata: LocalizedMetadata<ComponentMetadata> = {}
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<string, any>
// 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<PackageManagerItem | null> {
// 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<string> {
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)
}
}

View file

@ -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<PackageManagerItem[]> {
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<PackageManagerRepository>((_, 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<void> {
// 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()
}
}

View file

@ -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<T>(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(`<root>${jsonContent}</root>`)
// 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
}
}

View file

@ -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<typeof fs>;
// 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');
});
});
});
describe("GitFetcher", () => {
let gitFetcher: GitFetcher
const mockSimpleGit = simpleGit as jest.MockedFunction<typeof simpleGit>
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",
)
})
})
})

View file

@ -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<typeof fs>
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")
})
})
})

View file

@ -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<typeof fs>;
// 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<typeof fs>
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');
});
});
});
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")
})
})
})

View file

@ -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<typeof fs>;
// 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<typeof fs>
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();
});
});
});
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()
})
})
})

View file

@ -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")
})
})
})

View file

@ -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<T>(data: unknown, schema: z.ZodType<T>): 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<string, unknown>
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")
}

View file

@ -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;
}
metadata: RepositoryMetadata
items: PackageManagerItem[]
url: string
error?: string
}
/**
* Utility type for metadata files with locale
*/
export type LocalizedMetadata<T> = {
[locale: string]: T
}

51
test-repo/README.md Normal file
View file

@ -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")

View file

@ -0,0 +1,4 @@
name: Test Server
description: A minimal test server
type: mcp server
version: 1.0.0

View file

@ -0,0 +1,3 @@
name: Test Repository
description: A minimal test repository
version: 1.0.0

View file

@ -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<React.SetStateAction<{ type: string; search: string; tags: string[] }>>
activeTab: "browse" | "sources"
setActiveTab: React.Dispatch<React.SetStateAction<"browse" | "sources">>
}
const PackageManagerItemCard: React.FC<PackageManagerItemCardProps> = ({
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 (
<div className="border border-vscode-panel-border rounded-md p-4 bg-vscode-panel-background">
<div className="flex justify-between items-start">
<div>
<h3 className="text-lg font-semibold text-vscode-foreground">{item.name}</h3>
{item.author && <p className="text-sm text-vscode-descriptionForeground">{`by ${item.author}`}</p>}
</div>
<span className={`px-2 py-1 text-xs text-white rounded-full ${getTypeColor(item.type)}`}>
{getTypeLabel(item.type)}
</span>
</div>
<p className="my-2 text-vscode-foreground">{item.description}</p>
{item.tags && item.tags.length > 0 && (
<div className="flex flex-wrap gap-1 my-2">
{item.tags.map((tag) => (
<button
key={tag}
className={`px-2 py-1 text-xs rounded-full hover:bg-vscode-button-secondaryBackground ${
filters.tags.includes(tag)
? "bg-vscode-button-background text-vscode-button-foreground"
: "bg-vscode-badge-background text-vscode-badge-foreground"
}`}
onClick={() => {
if (filters.tags.includes(tag)) {
setFilters({
...filters,
tags: filters.tags.filter((t) => t !== tag),
})
} else {
setFilters({
...filters,
tags: [...filters.tags, tag],
})
if (activeTab !== "browse") {
setActiveTab("browse")
}
}
}}
title={filters.tags.includes(tag) ? `Remove tag filter: ${tag}` : `Filter by tag: ${tag}`}>
{tag}
</button>
))}
</div>
)}
<div className="flex justify-between items-center mt-4">
<div className="flex items-center gap-4 text-sm text-vscode-descriptionForeground">
{item.version && (
<span className="flex items-center">
<span className="codicon codicon-tag mr-1"></span>
{item.version}
</span>
)}
{item.lastUpdated && (
<span className="flex items-center">
<span className="codicon codicon-calendar mr-1"></span>
{new Date(item.lastUpdated).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
)}
</div>
<Button onClick={handleOpenUrl}>
<span className="codicon codicon-link-external mr-2"></span>
{item.sourceUrl ? "View" : item.sourceName || "Source"}
</Button>
</div>
</div>
)
}
const PackageManagerView: React.FC<PackageManagerViewProps> = ({ 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<PackageManagerItem[]>([])
const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse")
const [refreshingUrls, setRefreshingUrls] = useState<string[]>([])
// 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<string | null>(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<string>()
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 (
<Tab>
<TabHeader className="flex justify-between items-center">
@ -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">
<option value="">All types</option>
<option value="role">Role</option>
<option value="mcp-server">MCP Server</option>
<option value="storage">Storage</option>
<option value="other">Other</option>
<option value="mode">Mode</option>
<option value="mcp server">MCP Server</option>
<option value="prompt">Prompt</option>
<option value="package">Package</option>
</select>
</div>
@ -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()
}}>
<span
@ -440,19 +440,14 @@ const PackageManagerView = (_props: PackageManagerViewProps) => {
</div>
</div>
{console.log("DEBUG: Rendering condition", {
sortedItemsLength: sortedItems.length,
condition: sortedItems.length === 0 ? "empty" : "has items",
})}
{sortedItems.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-vscode-descriptionForeground">
<p>No package manager items found</p>
<Button
onClick={() => {
isManualRefresh.current = true
setIsFetching(false) // Reset fetching state first
fetchPackageManagerItems() // Use the fetchPackageManagerItems function
setIsFetching(false)
fetchPackageManagerItems()
}}
className="mt-4"
disabled={isFetching}>
@ -470,8 +465,8 @@ const PackageManagerView = (_props: PackageManagerViewProps) => {
<Button
onClick={() => {
isManualRefresh.current = true
setIsFetching(false) // Reset fetching state first
fetchPackageManagerItems() // Use the fetchPackageManagerItems function
setIsFetching(false)
fetchPackageManagerItems()
}}
size="sm"
disabled={isFetching}>
@ -511,200 +506,24 @@ const PackageManagerView = (_props: PackageManagerViewProps) => {
)
}
const PackageManagerItemCard = ({
item,
filters,
setFilters,
activeTab,
setActiveTab,
}: {
item: PackageManagerItem
filters: { type: string; search: string; tags: string[] }
setFilters: React.Dispatch<React.SetStateAction<{ type: string; search: string; tags: string[] }>>
activeTab: "browse" | "sources"
setActiveTab: React.Dispatch<React.SetStateAction<"browse" | "sources">>
}) => {
useAppTranslation() // Keep the hook but don't destructure unused 't'
// Helper function to validate URL
const isValidUrl = (urlString: string): boolean => {
try {
new URL(urlString)
return true
} catch (e) {
return false
}
}
const getTypeLabel = (type: string) => {
switch (type) {
case "role":
return "Role"
case "mcp-server":
return "MCP Server"
case "storage":
return "Storage"
default:
return "Other"
}
}
const getTypeColor = (type: string) => {
switch (type) {
case "role":
return "bg-blue-600"
case "mcp-server":
return "bg-green-600"
case "storage":
return "bg-purple-600"
default:
return "bg-gray-600"
}
}
const handleOpenUrl = () => {
// Use sourceUrl if it exists and is a valid URL, otherwise fall back to url
const urlToOpen = item.sourceUrl && isValidUrl(item.sourceUrl) ? item.sourceUrl : item.url
console.log(`PackageManagerItemCard: Opening URL: ${urlToOpen}`)
vscode.postMessage({
type: "openExternal",
url: urlToOpen,
})
console.log(`PackageManagerItemCard: Sent openExternal message with URL: ${urlToOpen}`)
}
return (
<div className="border border-vscode-panel-border rounded-md p-4 bg-vscode-panel-background">
<div className="flex justify-between items-start">
<div>
<h3 className="text-lg font-semibold text-vscode-foreground">{item.name}</h3>
{item.author && <p className="text-sm text-vscode-descriptionForeground">{`by ${item.author}`}</p>}
</div>
<span className={`px-2 py-1 text-xs text-white rounded-full ${getTypeColor(item.type)}`}>
{getTypeLabel(item.type)}
</span>
</div>
<p className="my-2 text-vscode-foreground">{item.description}</p>
{item.tags && item.tags.length > 0 && (
<div className="flex flex-wrap gap-1 my-2">
{item.tags.map((tag) => (
<button
key={tag}
className={`px-2 py-1 text-xs rounded-full hover:bg-vscode-button-secondaryBackground ${
filters.tags.includes(tag)
? "bg-vscode-button-background text-vscode-button-foreground"
: "bg-vscode-badge-background text-vscode-badge-foreground"
}`}
onClick={(e) => {
e.stopPropagation() // Prevent event bubbling
// Toggle tag selection
if (filters.tags.includes(tag)) {
// Remove tag if already selected
setFilters({
...filters,
tags: filters.tags.filter((t) => t !== tag),
})
} else {
// Add tag if not already selected
setFilters({
...filters,
tags: [...filters.tags, tag],
})
// Switch to browse tab if not already there
if (activeTab !== "browse") {
setActiveTab("browse")
}
}
}}
title={filters.tags.includes(tag) ? `Remove tag filter: ${tag}` : `Filter by tag: ${tag}`}>
{tag}
</button>
))}
</div>
)}
<div className="flex justify-between items-center mt-4">
<div className="flex items-center gap-4 text-sm text-vscode-descriptionForeground">
{item.version && (
<span className="flex items-center">
<span className="codicon codicon-tag mr-1"></span>
{item.version}
</span>
)}
{item.lastUpdated && (
<span className="flex items-center">
<span className="codicon codicon-calendar mr-1"></span>
{new Date(item.lastUpdated).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
)}
</div>
<Button onClick={handleOpenUrl}>
<span className="codicon codicon-link-external mr-2"></span>
{item.sourceUrl ? "View" : `View on ${item.sourceName || "Source"}`}
</Button>
</div>
</div>
)
}
/**
* 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<React.SetStateAction<string[]>>
onSourcesChange: (sources: PackageManagerSource[]) => void
}
const PackageManagerSourcesConfig: React.FC<PackageManagerSourcesConfigProps> = ({
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"
/>
</div>
@ -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

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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}} स्रोत की अनुमति है"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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リポジトリのURLhttps://github.com/username/repo",
"urlFormats": "対応フォーマットHTTPShttps://github.com/username/repo、SSHgit@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}}個まで追加できます"
}
}
}

View file

@ -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}}개의 소스만 허용됩니다"
}
}
}

View file

@ -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}}"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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"
}
}
}

View file

@ -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": "支持的格式HTTPShttps://github.com/username/repo、SSHgit@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}}个源"
}
}
}

View file

@ -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": "支援的格式HTTPShttps://github.com/username/repo、SSHgit@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}}個來源"
}
}
}

View file

@ -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,
}
})