mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
no type script or linting errors
This commit is contained in:
parent
aa066935ce
commit
6b9b2aa4de
64 changed files with 6385 additions and 1774 deletions
297
docs/package-manager-structure.md
Normal file
297
docs/package-manager-structure.md
Normal 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
1872
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
255
e2e/src/suite/package-manager.test.ts
Normal file
255
e2e/src/suite/package-manager.test.ts
Normal 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
13
package-lock.json
generated
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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"
|
||||
3
package-manager-template/metadata.en.yml
Normal file
3
package-manager-template/metadata.en.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name: "Package Manager Template"
|
||||
description: "A template repository for creating package manager sources"
|
||||
version: "1.0.0"
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
name: "Example Package Manager Repository"
|
||||
description: "A collection of example package manager items for Roo-Code"
|
||||
version: "1.0.0"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"]
|
||||
|
|
@ -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"]
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
name: "データプラットフォームパッケージ"
|
||||
description: "ロール、サーバー、ストレージシステムを含む完全なデータプラットフォームソリューション"
|
||||
type: "package"
|
||||
version: "1.0.0"
|
||||
tags: ["データプラットフォーム", "エンタープライズ", "データエンジニアリング", "完全ソリューション"]
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
154
src/services/package-manager/MetadataScanner.ts
Normal file
154
src/services/package-manager/MetadataScanner.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
140
src/services/package-manager/YamlParser.ts
Normal file
140
src/services/package-manager/YamlParser.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
175
src/services/package-manager/__tests__/MetadataScanner.test.ts
Normal file
175
src/services/package-manager/__tests__/MetadataScanner.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
133
src/services/package-manager/__tests__/schemas.test.ts
Normal file
133
src/services/package-manager/__tests__/schemas.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
})
|
||||
113
src/services/package-manager/schemas.ts
Normal file
113
src/services/package-manager/schemas.ts
Normal 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")
|
||||
}
|
||||
|
|
@ -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
51
test-repo/README.md
Normal 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")
|
||||
4
test-repo/mcp-servers/test-server/metadata.en.yml
Normal file
4
test-repo/mcp-servers/test-server/metadata.en.yml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
name: Test Server
|
||||
description: A minimal test server
|
||||
type: mcp server
|
||||
version: 1.0.0
|
||||
3
test-repo/metadata.en.yml
Normal file
3
test-repo/metadata.en.yml
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
name: Test Repository
|
||||
description: A minimal test repository
|
||||
version: 1.0.0
|
||||
|
|
@ -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
|
||||
|
|
|
|||
87
webview-ui/src/i18n/locales/ca/package-manager.json
Normal file
87
webview-ui/src/i18n/locales/ca/package-manager.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
84
webview-ui/src/i18n/locales/de/package-manager.json
Normal file
84
webview-ui/src/i18n/locales/de/package-manager.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
84
webview-ui/src/i18n/locales/en/package-manager.json
Normal file
84
webview-ui/src/i18n/locales/en/package-manager.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
84
webview-ui/src/i18n/locales/es/package-manager.json
Normal file
84
webview-ui/src/i18n/locales/es/package-manager.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
87
webview-ui/src/i18n/locales/fr/package-manager.json
Normal file
87
webview-ui/src/i18n/locales/fr/package-manager.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
85
webview-ui/src/i18n/locales/hi/package-manager.json
Normal file
85
webview-ui/src/i18n/locales/hi/package-manager.json
Normal 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}} स्रोत की अनुमति है"
|
||||
}
|
||||
}
|
||||
}
|
||||
87
webview-ui/src/i18n/locales/it/package-manager.json
Normal file
87
webview-ui/src/i18n/locales/it/package-manager.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
83
webview-ui/src/i18n/locales/ja/package-manager.json
Normal file
83
webview-ui/src/i18n/locales/ja/package-manager.json
Normal 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": "有効なGitリポジトリのURLを入力してください(例:https://github.com/username/repo)",
|
||||
"duplicateUrl": "このURLは既にリストに存在します(大文字小文字と空白を区別しない一致)",
|
||||
"nameTooLong": "名前は20文字以内にしてください",
|
||||
"nonVisibleCharsName": "名前に空白以外の不可視文字が含まれています",
|
||||
"duplicateName": "この名前は既に使用されています(大文字小文字と空白を区別しない一致)",
|
||||
"maxSources": "ソースは最大{{max}}個まで追加できます"
|
||||
}
|
||||
}
|
||||
}
|
||||
83
webview-ui/src/i18n/locales/ko/package-manager.json
Normal file
83
webview-ui/src/i18n/locales/ko/package-manager.json
Normal 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}}개의 소스만 허용됩니다"
|
||||
}
|
||||
}
|
||||
}
|
||||
91
webview-ui/src/i18n/locales/pl/package-manager.json
Normal file
91
webview-ui/src/i18n/locales/pl/package-manager.json
Normal 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}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
87
webview-ui/src/i18n/locales/pt-BR/package-manager.json
Normal file
87
webview-ui/src/i18n/locales/pt-BR/package-manager.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
83
webview-ui/src/i18n/locales/tr/package-manager.json
Normal file
83
webview-ui/src/i18n/locales/tr/package-manager.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
83
webview-ui/src/i18n/locales/vi/package-manager.json
Normal file
83
webview-ui/src/i18n/locales/vi/package-manager.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
83
webview-ui/src/i18n/locales/zh-CN/package-manager.json
Normal file
83
webview-ui/src/i18n/locales/zh-CN/package-manager.json
Normal 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}}个源"
|
||||
}
|
||||
}
|
||||
}
|
||||
83
webview-ui/src/i18n/locales/zh-TW/package-manager.json
Normal file
83
webview-ui/src/i18n/locales/zh-TW/package-manager.json
Normal 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}}個來源"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue