mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-07 02:58:15 +00:00
locale fixes for metadata
This commit is contained in:
parent
71d30b835b
commit
af6c27bdff
17 changed files with 2449 additions and 2662 deletions
|
|
@ -34,19 +34,21 @@ The Package Manager components interact through a well-defined message flow:
|
|||
### Core Interaction Patterns
|
||||
|
||||
1. **Data Loading**:
|
||||
- MetadataScanner loads package data from repositories
|
||||
- PackageManagerManager stores and manages this data
|
||||
- UI requests data through the message handler
|
||||
|
||||
- MetadataScanner loads package data from repositories
|
||||
- PackageManagerManager stores and manages this data
|
||||
- UI requests data through the message handler
|
||||
|
||||
2. **Filtering and Search**:
|
||||
- UI sends filter/search criteria to the backend
|
||||
- PackageManagerManager applies filters to the data
|
||||
- Filtered results are returned to the UI
|
||||
|
||||
- UI sends filter/search criteria to the backend
|
||||
- PackageManagerManager applies filters to the data
|
||||
- Filtered results are returned to the UI
|
||||
|
||||
3. **Source Management**:
|
||||
- UI sends source management commands
|
||||
- PackageManagerManager updates source configurations
|
||||
- MetadataScanner reloads data from updated sources
|
||||
- UI sends source management commands
|
||||
- PackageManagerManager updates source configurations
|
||||
- MetadataScanner reloads data from updated sources
|
||||
|
||||
## Data Flow Diagram
|
||||
|
||||
|
|
@ -235,48 +237,53 @@ classDiagram
|
|||
### Backend Components
|
||||
|
||||
1. **MetadataScanner**
|
||||
- Scans directories and repositories for package metadata
|
||||
- Parses YAML metadata files
|
||||
- Builds component hierarchies
|
||||
- Handles file system and Git operations
|
||||
|
||||
- Scans directories and repositories for package metadata
|
||||
- Parses YAML metadata files
|
||||
- Builds component hierarchies
|
||||
- Handles file system and Git operations
|
||||
|
||||
2. **PackageManagerManager**
|
||||
- Stores and manages package items
|
||||
- Applies filters and search criteria
|
||||
- Manages package sources
|
||||
- Handles package operations
|
||||
|
||||
- Stores and manages package items
|
||||
- Applies filters and search criteria
|
||||
- Manages package sources
|
||||
- Handles package operations
|
||||
|
||||
3. **packageManagerMessageHandler**
|
||||
- Routes messages between UI and backend
|
||||
- Processes commands from the UI
|
||||
- Returns data and status updates to the UI
|
||||
- Handles error conditions
|
||||
- Routes messages between UI and backend
|
||||
- Processes commands from the UI
|
||||
- Returns data and status updates to the UI
|
||||
- Handles error conditions
|
||||
|
||||
### Frontend Components
|
||||
|
||||
1. **PackageManagerView**
|
||||
- Main container component
|
||||
- Manages overall UI state
|
||||
- Handles tab navigation
|
||||
- Displays filter controls
|
||||
|
||||
- Main container component
|
||||
- Manages overall UI state
|
||||
- Handles tab navigation
|
||||
- Displays filter controls
|
||||
|
||||
2. **PackageManagerItemCard**
|
||||
- Displays individual package information
|
||||
- Handles tag interactions
|
||||
- Manages expandable details section
|
||||
- Provides action buttons
|
||||
|
||||
- Displays individual package information
|
||||
- Handles tag interactions
|
||||
- Manages expandable details section
|
||||
- Provides action buttons
|
||||
|
||||
3. **ExpandableSection**
|
||||
- Provides collapsible UI sections
|
||||
- Manages expand/collapse state
|
||||
- Handles animations
|
||||
- Displays section headers and badges
|
||||
|
||||
- Provides collapsible UI sections
|
||||
- Manages expand/collapse state
|
||||
- Handles animations
|
||||
- Displays section headers and badges
|
||||
|
||||
4. **TypeGroup**
|
||||
- Groups and displays components by type
|
||||
- Formats item lists
|
||||
- Highlights search matches
|
||||
- Provides consistent styling
|
||||
- Groups and displays components by type
|
||||
- Formats item lists
|
||||
- Highlights search matches
|
||||
- Provides consistent styling
|
||||
|
||||
## Data Flow Patterns
|
||||
|
||||
|
|
@ -285,104 +292,114 @@ classDiagram
|
|||
The Package Manager uses a message-based architecture for communication between the frontend and backend:
|
||||
|
||||
1. **Message Structure**:
|
||||
```typescript
|
||||
{
|
||||
type: string; // The message type (e.g., "search", "filter", "addSource")
|
||||
payload: any; // The message data
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: string // The message type (e.g., "search", "filter", "addSource")
|
||||
payload: any // The message data
|
||||
}
|
||||
```
|
||||
|
||||
2. **Common Message Types**:
|
||||
- `search`: Apply a search term filter
|
||||
- `filter`: Apply type or tag filters
|
||||
- `addSource`: Add a new package source
|
||||
- `removeSource`: Remove a package source
|
||||
- `refreshSources`: Reload data from sources
|
||||
|
||||
- `search`: Apply a search term filter
|
||||
- `filter`: Apply type or tag filters
|
||||
- `addSource`: Add a new package source
|
||||
- `removeSource`: Remove a package source
|
||||
- `refreshSources`: Reload data from sources
|
||||
|
||||
3. **Response Structure**:
|
||||
```typescript
|
||||
{
|
||||
type: string; // The response type
|
||||
data: any; // The response data
|
||||
error?: string; // Optional error message
|
||||
}
|
||||
```
|
||||
```typescript
|
||||
{
|
||||
type: string; // The response type
|
||||
data: any; // The response data
|
||||
error?: string; // Optional error message
|
||||
}
|
||||
```
|
||||
|
||||
### State Management
|
||||
|
||||
The Package Manager maintains state in several places:
|
||||
|
||||
1. **Backend State**:
|
||||
- Current items in the PackageManagerManager
|
||||
- Source configurations
|
||||
- Cached metadata
|
||||
|
||||
- Current items in the PackageManagerManager
|
||||
- Source configurations
|
||||
- Cached metadata
|
||||
|
||||
2. **Frontend State**:
|
||||
- Current filters and search terms
|
||||
- UI state (active tab, expanded sections)
|
||||
- Display preferences
|
||||
|
||||
- Current filters and search terms
|
||||
- UI state (active tab, expanded sections)
|
||||
- Display preferences
|
||||
|
||||
3. **Persistent State**:
|
||||
- Source configurations stored in extension settings
|
||||
- User preferences
|
||||
- Source configurations stored in extension settings
|
||||
- User preferences
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
The Package Manager architecture addresses several performance challenges:
|
||||
|
||||
1. **Lazy Loading**:
|
||||
- Metadata is loaded on demand
|
||||
- Repositories are scanned only when needed
|
||||
- UI components render incrementally
|
||||
|
||||
- Metadata is loaded on demand
|
||||
- Repositories are scanned only when needed
|
||||
- UI components render incrementally
|
||||
|
||||
2. **Efficient Filtering**:
|
||||
- Filtering happens on the backend to reduce data transfer
|
||||
- Search algorithms optimize for common patterns
|
||||
- Results are cached when possible
|
||||
|
||||
- Filtering happens on the backend to reduce data transfer
|
||||
- Search algorithms optimize for common patterns
|
||||
- Results are cached when possible
|
||||
|
||||
3. **Responsive UI**:
|
||||
- Asynchronous operations prevent UI blocking
|
||||
- Animations provide feedback during loading
|
||||
- Pagination limits the number of items displayed at once
|
||||
- Asynchronous operations prevent UI blocking
|
||||
- Animations provide feedback during loading
|
||||
- Pagination limits the number of items displayed at once
|
||||
|
||||
## Error Handling
|
||||
|
||||
The architecture includes robust error handling:
|
||||
|
||||
1. **Source Errors**:
|
||||
- Invalid repositories are marked with error states
|
||||
- Users are notified of access issues
|
||||
- The system continues to function with other sources
|
||||
|
||||
- Invalid repositories are marked with error states
|
||||
- Users are notified of access issues
|
||||
- The system continues to function with other sources
|
||||
|
||||
2. **Parsing Errors**:
|
||||
- Malformed metadata is gracefully handled
|
||||
- Partial results are displayed when possible
|
||||
- Error details are logged for debugging
|
||||
|
||||
- Malformed metadata is gracefully handled
|
||||
- Partial results are displayed when possible
|
||||
- Error details are logged for debugging
|
||||
|
||||
3. **Network Errors**:
|
||||
- Timeouts and retries for network operations
|
||||
- Offline mode with cached data
|
||||
- Clear error messages for user troubleshooting
|
||||
- Timeouts and retries for network operations
|
||||
- Offline mode with cached data
|
||||
- Clear error messages for user troubleshooting
|
||||
|
||||
## Extensibility Points
|
||||
|
||||
The Package Manager architecture is designed for extensibility:
|
||||
|
||||
1. **New Component Types**:
|
||||
- The system can be extended to support new component types
|
||||
- Type-specific rendering can be added to the UI
|
||||
- Backend processing adapts to new types
|
||||
|
||||
- The system can be extended to support new component types
|
||||
- Type-specific rendering can be added to the UI
|
||||
- Backend processing adapts to new types
|
||||
|
||||
2. **Additional Filters**:
|
||||
- New filter types can be added to the system
|
||||
- Filter logic can be extended in the PackageManagerManager
|
||||
- UI can be updated to display new filter controls
|
||||
|
||||
- New filter types can be added to the system
|
||||
- Filter logic can be extended in the PackageManagerManager
|
||||
- UI can be updated to display new filter controls
|
||||
|
||||
3. **Custom Sources**:
|
||||
- The source system supports various repository types
|
||||
- Custom source providers can be implemented
|
||||
- Authentication mechanisms can be extended
|
||||
- The source system supports various repository types
|
||||
- Custom source providers can be implemented
|
||||
- Authentication mechanisms can be extended
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Adding Custom Package Sources](../user-guide/06-adding-custom-sources.md) | **Next**: [Core Components](./02-core-components.md)
|
||||
**Previous**: [Adding Custom Package Sources](../user-guide/06-adding-custom-sources.md) | **Next**: [Core Components](./02-core-components.md)
|
||||
|
|
|
|||
|
|
@ -18,42 +18,42 @@ The MetadataScanner is responsible for reading and parsing package metadata from
|
|||
|
||||
```typescript
|
||||
class MetadataScanner {
|
||||
/**
|
||||
* Scans a directory for package metadata
|
||||
* @param directoryPath Path to the directory to scan
|
||||
* @param baseUrl Base URL for the repository (for remote sources)
|
||||
* @returns Array of package items
|
||||
*/
|
||||
public async scanDirectory(directoryPath: string, baseUrl?: string): Promise<PackageManagerItem[]> {
|
||||
// Implementation details
|
||||
}
|
||||
/**
|
||||
* Scans a directory for package metadata
|
||||
* @param directoryPath Path to the directory to scan
|
||||
* @param baseUrl Base URL for the repository (for remote sources)
|
||||
* @returns Array of package items
|
||||
*/
|
||||
public async scanDirectory(directoryPath: string, baseUrl?: string): Promise<PackageManagerItem[]> {
|
||||
// Implementation details
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans a Git repository for package metadata
|
||||
* @param repoUrl URL of the Git repository
|
||||
* @returns Array of package items
|
||||
*/
|
||||
public async scanRepository(repoUrl: string): Promise<PackageManagerItem[]> {
|
||||
// Implementation details
|
||||
}
|
||||
/**
|
||||
* Scans a Git repository for package metadata
|
||||
* @param repoUrl URL of the Git repository
|
||||
* @returns Array of package items
|
||||
*/
|
||||
public async scanRepository(repoUrl: string): Promise<PackageManagerItem[]> {
|
||||
// Implementation details
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a YAML metadata file
|
||||
* @param filePath Path to the metadata file
|
||||
* @returns Parsed metadata object
|
||||
*/
|
||||
private async parseMetadataFile(filePath: string): Promise<any> {
|
||||
// Implementation details
|
||||
}
|
||||
/**
|
||||
* Parses a YAML metadata file
|
||||
* @param filePath Path to the metadata file
|
||||
* @returns Parsed metadata object
|
||||
*/
|
||||
private async parseMetadataFile(filePath: string): Promise<any> {
|
||||
// Implementation details
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a component hierarchy from flat items
|
||||
* @param items Array of items to organize
|
||||
* @returns Hierarchical structure of items
|
||||
*/
|
||||
private buildComponentHierarchy(items: any[]): PackageManagerItem[] {
|
||||
// Implementation details
|
||||
}
|
||||
/**
|
||||
* Builds a component hierarchy from flat items
|
||||
* @param items Array of items to organize
|
||||
* @returns Hierarchical structure of items
|
||||
*/
|
||||
private buildComponentHierarchy(items: any[]): PackageManagerItem[] {
|
||||
// Implementation details
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -67,9 +67,9 @@ The directory scanning algorithm recursively traverses directories looking for m
|
|||
2. Look for `metadata.*.yml` files in the current directory
|
||||
3. Parse found metadata files
|
||||
4. For each subdirectory:
|
||||
- Determine the component type based on directory name
|
||||
- Recursively scan the subdirectory
|
||||
- Associate child components with parent components
|
||||
- Determine the component type based on directory name
|
||||
- Recursively scan the subdirectory
|
||||
- Associate child components with parent components
|
||||
5. Build the component hierarchy
|
||||
|
||||
#### Metadata Parsing
|
||||
|
|
@ -109,67 +109,67 @@ The PackageManagerManager is the central component that manages package items, a
|
|||
|
||||
```typescript
|
||||
class PackageManagerManager {
|
||||
private currentItems: PackageManagerItem[] = [];
|
||||
private sources: PackageManagerSource[] = [];
|
||||
private currentItems: PackageManagerItem[] = []
|
||||
private sources: PackageManagerSource[] = []
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param context VS Code extension context
|
||||
*/
|
||||
constructor(private context: vscode.ExtensionContext) {
|
||||
// Initialize from stored state
|
||||
}
|
||||
/**
|
||||
* Constructor
|
||||
* @param context VS Code extension context
|
||||
*/
|
||||
constructor(private context: vscode.ExtensionContext) {
|
||||
// Initialize from stored state
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all items
|
||||
* @returns Array of all package items
|
||||
*/
|
||||
public getItems(): PackageManagerItem[] {
|
||||
return this.currentItems;
|
||||
}
|
||||
/**
|
||||
* Get all items
|
||||
* @returns Array of all package items
|
||||
*/
|
||||
public getItems(): PackageManagerItem[] {
|
||||
return this.currentItems
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter items based on criteria
|
||||
* @param filters Filter criteria
|
||||
* @returns Filtered array of items
|
||||
*/
|
||||
public filterItems(filters: { type?: string; search?: string; tags?: string[] }): PackageManagerItem[] {
|
||||
// Implementation details
|
||||
}
|
||||
/**
|
||||
* Filter items based on criteria
|
||||
* @param filters Filter criteria
|
||||
* @returns Filtered array of items
|
||||
*/
|
||||
public filterItems(filters: { type?: string; search?: string; tags?: string[] }): PackageManagerItem[] {
|
||||
// Implementation details
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new package source
|
||||
* @param url Source repository URL
|
||||
* @param name Optional source name
|
||||
* @returns Success status
|
||||
*/
|
||||
public async addSource(url: string, name?: string): Promise<boolean> {
|
||||
// Implementation details
|
||||
}
|
||||
/**
|
||||
* Add a new package source
|
||||
* @param url Source repository URL
|
||||
* @param name Optional source name
|
||||
* @returns Success status
|
||||
*/
|
||||
public async addSource(url: string, name?: string): Promise<boolean> {
|
||||
// Implementation details
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a package source
|
||||
* @param url Source repository URL
|
||||
* @returns Success status
|
||||
*/
|
||||
public removeSource(url: string): boolean {
|
||||
// Implementation details
|
||||
}
|
||||
/**
|
||||
* Remove a package source
|
||||
* @param url Source repository URL
|
||||
* @returns Success status
|
||||
*/
|
||||
public removeSource(url: string): boolean {
|
||||
// Implementation details
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh all sources
|
||||
* @returns Updated items
|
||||
*/
|
||||
public async refreshSources(): Promise<PackageManagerItem[]> {
|
||||
// Implementation details
|
||||
}
|
||||
/**
|
||||
* Refresh all sources
|
||||
* @returns Updated items
|
||||
*/
|
||||
public async refreshSources(): Promise<PackageManagerItem[]> {
|
||||
// Implementation details
|
||||
}
|
||||
|
||||
/**
|
||||
* Save state to persistent storage
|
||||
*/
|
||||
private saveState(): void {
|
||||
// Implementation details
|
||||
}
|
||||
/**
|
||||
* Save state to persistent storage
|
||||
*/
|
||||
private saveState(): void {
|
||||
// Implementation details
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -181,14 +181,14 @@ The filtering algorithm applies multiple criteria to the package items:
|
|||
|
||||
1. Start with the complete set of items
|
||||
2. If a type filter is specified:
|
||||
- Keep only items matching the specified type
|
||||
- Keep only items matching the specified type
|
||||
3. If a search term is specified:
|
||||
- Check item name, description, and author for matches
|
||||
- Check subcomponents for matches
|
||||
- Keep items that match or have matching subcomponents
|
||||
- Add match information to the items
|
||||
- Check item name, description, and author for matches
|
||||
- Check subcomponents for matches
|
||||
- Keep items that match or have matching subcomponents
|
||||
- Add match information to the items
|
||||
4. If tag filters are specified:
|
||||
- Keep only items that have at least one of the specified tags
|
||||
- Keep only items that have at least one of the specified tags
|
||||
5. Return the filtered items with match information
|
||||
|
||||
#### Source Management
|
||||
|
|
@ -196,25 +196,27 @@ The filtering algorithm applies multiple criteria to the package items:
|
|||
The source management process handles adding, removing, and refreshing sources:
|
||||
|
||||
1. For adding a source:
|
||||
- Validate the repository URL
|
||||
- Check if the source already exists
|
||||
- Add the source to the list
|
||||
- Scan the repository for items
|
||||
- Add the items to the current set
|
||||
- Save the updated source list
|
||||
|
||||
- Validate the repository URL
|
||||
- Check if the source already exists
|
||||
- Add the source to the list
|
||||
- Scan the repository for items
|
||||
- Add the items to the current set
|
||||
- Save the updated source list
|
||||
|
||||
2. For removing a source:
|
||||
- Find the source in the list
|
||||
- Remove items from that source
|
||||
- Remove the source from the list
|
||||
- Save the updated source list
|
||||
|
||||
- Find the source in the list
|
||||
- Remove items from that source
|
||||
- Remove the source from the list
|
||||
- Save the updated source list
|
||||
|
||||
3. For refreshing sources:
|
||||
- Clear the current items
|
||||
- For each enabled source:
|
||||
- Scan the repository for items
|
||||
- Add the items to the current set
|
||||
- Return the updated items
|
||||
- Clear the current items
|
||||
- For each enabled source:
|
||||
- Scan the repository for items
|
||||
- Add the items to the current set
|
||||
- Return the updated items
|
||||
|
||||
### State Persistence
|
||||
|
||||
|
|
@ -247,49 +249,46 @@ The packageManagerMessageHandler is responsible for routing messages between the
|
|||
* @param packageManager The package manager instance
|
||||
* @returns Response object
|
||||
*/
|
||||
export async function handlePackageManagerMessages(
|
||||
message: any,
|
||||
packageManager: PackageManagerManager
|
||||
): Promise<any> {
|
||||
switch (message.type) {
|
||||
case "getItems":
|
||||
return {
|
||||
type: "items",
|
||||
data: packageManager.getItems()
|
||||
};
|
||||
export async function handlePackageManagerMessages(message: any, packageManager: PackageManagerManager): Promise<any> {
|
||||
switch (message.type) {
|
||||
case "getItems":
|
||||
return {
|
||||
type: "items",
|
||||
data: packageManager.getItems(),
|
||||
}
|
||||
|
||||
case "search":
|
||||
return {
|
||||
type: "searchResults",
|
||||
data: packageManager.filterItems({
|
||||
search: message.search,
|
||||
type: message.typeFilter,
|
||||
tags: message.tagFilters
|
||||
})
|
||||
};
|
||||
case "search":
|
||||
return {
|
||||
type: "searchResults",
|
||||
data: packageManager.filterItems({
|
||||
search: message.search,
|
||||
type: message.typeFilter,
|
||||
tags: message.tagFilters,
|
||||
}),
|
||||
}
|
||||
|
||||
case "addSource":
|
||||
try {
|
||||
const success = await packageManager.addSource(message.url, message.name);
|
||||
return {
|
||||
type: "sourceAdded",
|
||||
data: { success }
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
type: "error",
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
case "addSource":
|
||||
try {
|
||||
const success = await packageManager.addSource(message.url, message.name)
|
||||
return {
|
||||
type: "sourceAdded",
|
||||
data: { success },
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
type: "error",
|
||||
error: error.message,
|
||||
}
|
||||
}
|
||||
|
||||
// Additional message handlers...
|
||||
// Additional message handlers...
|
||||
|
||||
default:
|
||||
return {
|
||||
type: "error",
|
||||
error: `Unknown message type: ${message.type}`
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {
|
||||
type: "error",
|
||||
error: `Unknown message type: ${message.type}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -300,75 +299,86 @@ The message handler processes several types of messages:
|
|||
#### Input Messages
|
||||
|
||||
1. **getItems**: Request all package items
|
||||
```typescript
|
||||
{ type: "getItems" }
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "getItems"
|
||||
}
|
||||
```
|
||||
|
||||
2. **search**: Apply search and filter criteria
|
||||
```typescript
|
||||
{
|
||||
type: "search",
|
||||
search: "search term",
|
||||
typeFilter: "mode",
|
||||
tagFilters: ["tag1", "tag2"]
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "search",
|
||||
search: "search term",
|
||||
typeFilter: "mode",
|
||||
tagFilters: ["tag1", "tag2"]
|
||||
}
|
||||
```
|
||||
|
||||
3. **addSource**: Add a new package source
|
||||
```typescript
|
||||
{
|
||||
type: "addSource",
|
||||
url: "https://github.com/username/repo.git",
|
||||
name: "Custom Source"
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "addSource",
|
||||
url: "https://github.com/username/repo.git",
|
||||
name: "Custom Source"
|
||||
}
|
||||
```
|
||||
|
||||
4. **removeSource**: Remove a package source
|
||||
```typescript
|
||||
{
|
||||
type: "removeSource",
|
||||
url: "https://github.com/username/repo.git"
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "removeSource",
|
||||
url: "https://github.com/username/repo.git"
|
||||
}
|
||||
```
|
||||
|
||||
5. **refreshSources**: Refresh all sources
|
||||
```typescript
|
||||
{ type: "refreshSources" }
|
||||
```
|
||||
```typescript
|
||||
{
|
||||
type: "refreshSources"
|
||||
}
|
||||
```
|
||||
|
||||
#### Output Messages
|
||||
|
||||
1. **items**: Response with all items
|
||||
```typescript
|
||||
{
|
||||
type: "items",
|
||||
data: [/* package items */]
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "items",
|
||||
data: [/* package items */]
|
||||
}
|
||||
```
|
||||
|
||||
2. **searchResults**: Response with filtered items
|
||||
```typescript
|
||||
{
|
||||
type: "searchResults",
|
||||
data: [/* filtered items */]
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "searchResults",
|
||||
data: [/* filtered items */]
|
||||
}
|
||||
```
|
||||
|
||||
3. **sourceAdded**: Response after adding a source
|
||||
```typescript
|
||||
{
|
||||
type: "sourceAdded",
|
||||
data: { success: true }
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: "sourceAdded",
|
||||
data: { success: true }
|
||||
}
|
||||
```
|
||||
|
||||
4. **error**: Error response
|
||||
```typescript
|
||||
{
|
||||
type: "error",
|
||||
error: "Error message"
|
||||
}
|
||||
```
|
||||
```typescript
|
||||
{
|
||||
type: "error",
|
||||
error: "Error message"
|
||||
}
|
||||
```
|
||||
|
||||
### Asynchronous Processing
|
||||
|
||||
|
|
@ -389,54 +399,45 @@ The main container component that manages the overall UI:
|
|||
|
||||
```tsx
|
||||
const PackageManagerView: React.FC = () => {
|
||||
const [items, setItems] = useState<PackageManagerItem[]>([]);
|
||||
const [filters, setFilters] = useState({ type: "", search: "", tags: [] });
|
||||
const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse");
|
||||
const [items, setItems] = useState<PackageManagerItem[]>([])
|
||||
const [filters, setFilters] = useState({ type: "", search: "", tags: [] })
|
||||
const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse")
|
||||
|
||||
// Implementation details...
|
||||
// Implementation details...
|
||||
|
||||
return (
|
||||
<div className="package-manager-container">
|
||||
<div className="tabs">
|
||||
<button
|
||||
className={activeTab === "browse" ? "active" : ""}
|
||||
onClick={() => setActiveTab("browse")}
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
<button
|
||||
className={activeTab === "sources" ? "active" : ""}
|
||||
onClick={() => setActiveTab("sources")}
|
||||
>
|
||||
Sources
|
||||
</button>
|
||||
</div>
|
||||
return (
|
||||
<div className="package-manager-container">
|
||||
<div className="tabs">
|
||||
<button className={activeTab === "browse" ? "active" : ""} onClick={() => setActiveTab("browse")}>
|
||||
Browse
|
||||
</button>
|
||||
<button className={activeTab === "sources" ? "active" : ""} onClick={() => setActiveTab("sources")}>
|
||||
Sources
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeTab === "browse" ? (
|
||||
<div className="browse-container">
|
||||
<FilterPanel
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
/>
|
||||
<div className="results-area">
|
||||
{items.map(item => (
|
||||
<PackageManagerItemCard
|
||||
key={item.name}
|
||||
item={item}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SourcesPanel />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
{activeTab === "browse" ? (
|
||||
<div className="browse-container">
|
||||
<FilterPanel filters={filters} setFilters={setFilters} />
|
||||
<div className="results-area">
|
||||
{items.map((item) => (
|
||||
<PackageManagerItemCard
|
||||
key={item.name}
|
||||
item={item}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
activeTab={activeTab}
|
||||
setActiveTab={setActiveTab}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<SourcesPanel />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Component Interactions
|
||||
|
|
@ -444,37 +445,41 @@ const PackageManagerView: React.FC = () => {
|
|||
The UI components interact through props and state:
|
||||
|
||||
1. **Parent-Child Communication**:
|
||||
- Parent components pass data and callbacks to children
|
||||
- Children invoke callbacks to notify parents of events
|
||||
|
||||
- Parent components pass data and callbacks to children
|
||||
- Children invoke callbacks to notify parents of events
|
||||
|
||||
2. **State Management**:
|
||||
- Component state for UI-specific state
|
||||
- Shared state for filters and active tab
|
||||
- Backend state accessed through messages
|
||||
|
||||
- Component state for UI-specific state
|
||||
- Shared state for filters and active tab
|
||||
- Backend state accessed through messages
|
||||
|
||||
3. **Event Handling**:
|
||||
- UI events trigger state updates
|
||||
- State updates cause re-renders
|
||||
- Messages are sent to the backend when needed
|
||||
- UI events trigger state updates
|
||||
- State updates cause re-renders
|
||||
- Messages are sent to the backend when needed
|
||||
|
||||
### Accessibility Features
|
||||
|
||||
The UI components include several accessibility features:
|
||||
|
||||
1. **Keyboard Navigation**:
|
||||
- Tab order follows logical flow
|
||||
- Focus indicators are visible
|
||||
- Keyboard shortcuts for common actions
|
||||
|
||||
- Tab order follows logical flow
|
||||
- Focus indicators are visible
|
||||
- Keyboard shortcuts for common actions
|
||||
|
||||
2. **Screen Reader Support**:
|
||||
- ARIA attributes for dynamic content
|
||||
- Semantic HTML structure
|
||||
- Descriptive labels and announcements
|
||||
|
||||
- ARIA attributes for dynamic content
|
||||
- Semantic HTML structure
|
||||
- Descriptive labels and announcements
|
||||
|
||||
3. **Visual Accessibility**:
|
||||
- High contrast mode support
|
||||
- Resizable text
|
||||
- Color schemes that work with color blindness
|
||||
- High contrast mode support
|
||||
- Resizable text
|
||||
- Color schemes that work with color blindness
|
||||
|
||||
## Component Integration
|
||||
|
||||
|
|
@ -509,25 +514,28 @@ The core components work together to provide a complete package management exper
|
|||
The core components include several performance optimizations:
|
||||
|
||||
1. **Lazy Loading**:
|
||||
- Items are loaded on demand
|
||||
- Heavy operations are deferred
|
||||
- Components render incrementally
|
||||
|
||||
- Items are loaded on demand
|
||||
- Heavy operations are deferred
|
||||
- Components render incrementally
|
||||
|
||||
2. **Caching**:
|
||||
- Parsed metadata is cached
|
||||
- Filter results can be cached
|
||||
- Repository data is cached when possible
|
||||
|
||||
- Parsed metadata is cached
|
||||
- Filter results can be cached
|
||||
- Repository data is cached when possible
|
||||
|
||||
3. **Efficient Filtering**:
|
||||
- Filtering happens on the backend
|
||||
- Only necessary data is transferred
|
||||
- Algorithms optimize for common cases
|
||||
|
||||
- Filtering happens on the backend
|
||||
- Only necessary data is transferred
|
||||
- Algorithms optimize for common cases
|
||||
|
||||
4. **UI Optimizations**:
|
||||
- Virtual scrolling for large lists
|
||||
- Debounced search input
|
||||
- Optimized rendering of complex components
|
||||
- Virtual scrolling for large lists
|
||||
- Debounced search input
|
||||
- Optimized rendering of complex components
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [Package Manager Architecture](./01-architecture.md) | **Next**: [Data Structures](./03-data-structures.md)
|
||||
**Previous**: [Package Manager Architecture](./01-architecture.md) | **Next**: [Data Structures](./03-data-structures.md)
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ The Package Manager uses a type system to categorize different kinds of componen
|
|||
/**
|
||||
* Supported component types
|
||||
*/
|
||||
export type ComponentType = "mode" | "prompt" | "package" | "mcp server";
|
||||
export type ComponentType = "mode" | "prompt" | "package" | "mcp server"
|
||||
```
|
||||
|
||||
These types represent the different kinds of components that can be managed by the Package Manager:
|
||||
|
|
@ -35,10 +35,10 @@ The Package Manager uses a set of interfaces to define the structure of metadata
|
|||
* Base metadata interface
|
||||
*/
|
||||
export interface BaseMetadata {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
tags?: string[];
|
||||
name: string
|
||||
description: string
|
||||
version: string
|
||||
tags?: string[]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ This interface represents the metadata for a package source repository. It curre
|
|||
* Component metadata with type
|
||||
*/
|
||||
export interface ComponentMetadata extends BaseMetadata {
|
||||
type: ComponentType;
|
||||
type: ComponentType
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -80,12 +80,12 @@ This interface extends BaseMetadata to include a type field, which specifies the
|
|||
* Package metadata with optional subcomponents
|
||||
*/
|
||||
export interface PackageMetadata extends ComponentMetadata {
|
||||
type: "package";
|
||||
items?: {
|
||||
type: ComponentType;
|
||||
path: string;
|
||||
metadata?: ComponentMetadata;
|
||||
}[];
|
||||
type: "package"
|
||||
items?: {
|
||||
type: ComponentType
|
||||
path: string
|
||||
metadata?: ComponentMetadata
|
||||
}[]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -93,9 +93,9 @@ This interface represents packages that can contain subcomponents:
|
|||
|
||||
- **type**: Always "package" for this interface
|
||||
- **items**: Optional array of subcomponents, each with:
|
||||
- **type**: The subcomponent type
|
||||
- **path**: The file system path to the subcomponent
|
||||
- **metadata**: Optional metadata for the subcomponent
|
||||
- **type**: The subcomponent type
|
||||
- **path**: The file system path to the subcomponent
|
||||
- **metadata**: Optional metadata for the subcomponent
|
||||
|
||||
### SubcomponentMetadata
|
||||
|
||||
|
|
@ -104,10 +104,10 @@ This interface represents packages that can contain subcomponents:
|
|||
* Subcomponent metadata with parent reference
|
||||
*/
|
||||
export interface SubcomponentMetadata extends ComponentMetadata {
|
||||
parentPackage: {
|
||||
name: string;
|
||||
path: string;
|
||||
};
|
||||
parentPackage: {
|
||||
name: string
|
||||
path: string
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -115,8 +115,8 @@ This interface represents components that are part of a parent package:
|
|||
|
||||
- All fields from ComponentMetadata
|
||||
- **parentPackage**: Reference to the parent package
|
||||
- **name**: The name of the parent package
|
||||
- **path**: The file system path to the parent package
|
||||
- **name**: The name of the parent package
|
||||
- **path**: The file system path to the parent package
|
||||
|
||||
## Item Structures
|
||||
|
||||
|
|
@ -129,13 +129,13 @@ The Package Manager uses several interfaces to represent items in the UI:
|
|||
* Information about why an item matched search/filter criteria
|
||||
*/
|
||||
export interface MatchInfo {
|
||||
matched: boolean;
|
||||
matchReason?: {
|
||||
nameMatch?: boolean;
|
||||
descriptionMatch?: boolean;
|
||||
tagMatch?: boolean;
|
||||
hasMatchingSubcomponents?: boolean;
|
||||
};
|
||||
matched: boolean
|
||||
matchReason?: {
|
||||
nameMatch?: boolean
|
||||
descriptionMatch?: boolean
|
||||
tagMatch?: boolean
|
||||
hasMatchingSubcomponents?: boolean
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -143,10 +143,10 @@ This interface provides information about why an item matched search or filter c
|
|||
|
||||
- **matched**: Boolean indicating if the item matched
|
||||
- **matchReason**: Optional object with specific match reasons
|
||||
- **nameMatch**: True if the name matched
|
||||
- **descriptionMatch**: True if the description matched
|
||||
- **tagMatch**: True if a tag matched
|
||||
- **hasMatchingSubcomponents**: True if a subcomponent matched
|
||||
- **nameMatch**: True if the name matched
|
||||
- **descriptionMatch**: True if the description matched
|
||||
- **tagMatch**: True if a tag matched
|
||||
- **hasMatchingSubcomponents**: True if a subcomponent matched
|
||||
|
||||
### PackageManagerItem
|
||||
|
||||
|
|
@ -155,25 +155,25 @@ This interface provides information about why an item matched search or filter c
|
|||
* Represents an individual package manager item
|
||||
*/
|
||||
export interface PackageManagerItem {
|
||||
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;
|
||||
metadata?: ComponentMetadata;
|
||||
lastUpdated?: string;
|
||||
matchInfo?: MatchInfo;
|
||||
}[];
|
||||
matchInfo?: MatchInfo;
|
||||
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
|
||||
metadata?: ComponentMetadata
|
||||
lastUpdated?: string
|
||||
matchInfo?: MatchInfo
|
||||
}[]
|
||||
matchInfo?: MatchInfo
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -200,9 +200,9 @@ This interface represents a complete package manager item as displayed in the UI
|
|||
* Represents a Git repository source for package manager items
|
||||
*/
|
||||
export interface PackageManagerSource {
|
||||
url: string;
|
||||
name?: string;
|
||||
enabled: boolean;
|
||||
url: string
|
||||
name?: string
|
||||
enabled: boolean
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -219,10 +219,10 @@ This interface represents a package source repository:
|
|||
* Represents a repository with its metadata and items
|
||||
*/
|
||||
export interface PackageManagerRepository {
|
||||
metadata: RepositoryMetadata;
|
||||
items: PackageManagerItem[];
|
||||
url: string;
|
||||
error?: string;
|
||||
metadata: RepositoryMetadata
|
||||
items: PackageManagerItem[]
|
||||
url: string
|
||||
error?: string
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -240,8 +240,8 @@ This interface represents a complete repository with its metadata and items:
|
|||
* Utility type for metadata files with locale
|
||||
*/
|
||||
export type LocalizedMetadata<T> = {
|
||||
[locale: string]: T;
|
||||
};
|
||||
[locale: string]: T
|
||||
}
|
||||
```
|
||||
|
||||
This utility type represents metadata that can be localized to different languages:
|
||||
|
|
@ -257,11 +257,11 @@ The Package Manager UI components use several prop interfaces:
|
|||
|
||||
```typescript
|
||||
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">>;
|
||||
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">>
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -277,11 +277,11 @@ This interface defines the props for the PackageManagerItemCard component:
|
|||
|
||||
```typescript
|
||||
interface ExpandableSectionProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
defaultExpanded?: boolean;
|
||||
badge?: string;
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
defaultExpanded?: boolean
|
||||
badge?: string
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -297,15 +297,15 @@ This interface defines the props for the ExpandableSection component:
|
|||
|
||||
```typescript
|
||||
interface TypeGroupProps {
|
||||
type: string;
|
||||
items: Array<{
|
||||
name: string;
|
||||
description?: string;
|
||||
metadata?: any;
|
||||
path?: string;
|
||||
}>;
|
||||
className?: string;
|
||||
searchTerm?: string;
|
||||
type: string
|
||||
items: Array<{
|
||||
name: string
|
||||
description?: string
|
||||
metadata?: any
|
||||
path?: string
|
||||
}>
|
||||
className?: string
|
||||
searchTerm?: string
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -324,15 +324,15 @@ The Package Manager uses a specialized structure for grouping items by type:
|
|||
|
||||
```typescript
|
||||
export interface GroupedItems {
|
||||
[type: string]: {
|
||||
type: string;
|
||||
items: Array<{
|
||||
name: string;
|
||||
description?: string;
|
||||
metadata?: any;
|
||||
path?: string;
|
||||
}>;
|
||||
};
|
||||
[type: string]: {
|
||||
type: string
|
||||
items: Array<{
|
||||
name: string
|
||||
description?: string
|
||||
metadata?: any
|
||||
path?: string
|
||||
}>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -341,10 +341,10 @@ This interface represents items grouped by their type:
|
|||
- **[type: string]**: Keys are component types
|
||||
- **type**: The component type (redundant with the key)
|
||||
- **items**: Array of items of this type
|
||||
- **name**: The item name
|
||||
- **description**: Optional item description
|
||||
- **metadata**: Optional additional metadata
|
||||
- **path**: Optional file system path
|
||||
- **name**: The item name
|
||||
- **description**: Optional item description
|
||||
- **metadata**: Optional additional metadata
|
||||
- **path**: Optional file system path
|
||||
|
||||
## Filter and Sort Structures
|
||||
|
||||
|
|
@ -354,9 +354,9 @@ The Package Manager uses several structures for filtering and sorting:
|
|||
|
||||
```typescript
|
||||
interface Filters {
|
||||
type: string;
|
||||
search: string;
|
||||
tags: string[];
|
||||
type: string
|
||||
search: string
|
||||
tags: string[]
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -370,8 +370,8 @@ This interface represents the filter criteria:
|
|||
|
||||
```typescript
|
||||
interface SortConfig {
|
||||
by: string;
|
||||
order: "asc" | "desc";
|
||||
by: string
|
||||
order: "asc" | "desc"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -521,19 +521,19 @@ The Package Manager includes validation at several levels:
|
|||
|
||||
```typescript
|
||||
function validateMetadata(metadata: any): boolean {
|
||||
// Required fields
|
||||
if (!metadata.name || !metadata.description || !metadata.version) {
|
||||
return false;
|
||||
}
|
||||
// Required fields
|
||||
if (!metadata.name || !metadata.description || !metadata.version) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Type validation for components
|
||||
if (metadata.type && !["mode", "prompt", "package", "mcp server"].includes(metadata.type)) {
|
||||
return false;
|
||||
}
|
||||
// Type validation for components
|
||||
if (metadata.type && !["mode", "prompt", "package", "mcp server"].includes(metadata.type)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Additional validation...
|
||||
// Additional validation...
|
||||
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -541,12 +541,12 @@ function validateMetadata(metadata: any): boolean {
|
|||
|
||||
```typescript
|
||||
function isValidUrl(urlString: string): boolean {
|
||||
try {
|
||||
new URL(urlString);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
new URL(urlString)
|
||||
return true
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -554,13 +554,11 @@ function isValidUrl(urlString: string): boolean {
|
|||
|
||||
```typescript
|
||||
function validateTags(tags: any[]): string[] {
|
||||
if (!Array.isArray(tags)) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(tags)) {
|
||||
return []
|
||||
}
|
||||
|
||||
return tags
|
||||
.filter(tag => typeof tag === "string" && tag.trim().length > 0)
|
||||
.map(tag => tag.trim());
|
||||
return tags.filter((tag) => typeof tag === "string" && tag.trim().length > 0).map((tag) => tag.trim())
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -667,4 +665,4 @@ The Package Manager's data structures are designed for evolution:
|
|||
|
||||
---
|
||||
|
||||
**Previous**: [Core Components](./02-core-components.md) | **Next**: [Search and Filter Implementation](./04-search-and-filter.md)
|
||||
**Previous**: [Core Components](./02-core-components.md) | **Next**: [Search and Filter Implementation](./04-search-and-filter.md)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -24,150 +24,155 @@ Backend unit tests verify the functionality of core services and utilities:
|
|||
|
||||
```typescript
|
||||
describe("MetadataScanner", () => {
|
||||
let scanner: MetadataScanner;
|
||||
let scanner: MetadataScanner
|
||||
|
||||
beforeEach(() => {
|
||||
scanner = new MetadataScanner();
|
||||
});
|
||||
beforeEach(() => {
|
||||
scanner = new MetadataScanner()
|
||||
})
|
||||
|
||||
describe("parseMetadataFile", () => {
|
||||
it("should parse valid YAML metadata", async () => {
|
||||
// Mock file system
|
||||
jest.spyOn(fs, "readFile").mockImplementation((path, options, callback) => {
|
||||
callback(null, Buffer.from(`
|
||||
describe("parseMetadataFile", () => {
|
||||
it("should parse valid YAML metadata", async () => {
|
||||
// Mock file system
|
||||
jest.spyOn(fs, "readFile").mockImplementation((path, options, callback) => {
|
||||
callback(
|
||||
null,
|
||||
Buffer.from(`
|
||||
name: "Test Package"
|
||||
description: "A test package"
|
||||
version: "1.0.0"
|
||||
type: "package"
|
||||
`));
|
||||
});
|
||||
`),
|
||||
)
|
||||
})
|
||||
|
||||
const result = await scanner["parseMetadataFile"]("test/path/metadata.en.yml");
|
||||
const result = await scanner["parseMetadataFile"]("test/path/metadata.en.yml")
|
||||
|
||||
expect(result).toEqual({
|
||||
name: "Test Package",
|
||||
description: "A test package",
|
||||
version: "1.0.0",
|
||||
type: "package"
|
||||
});
|
||||
});
|
||||
expect(result).toEqual({
|
||||
name: "Test Package",
|
||||
description: "A test package",
|
||||
version: "1.0.0",
|
||||
type: "package",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle invalid YAML", async () => {
|
||||
// Mock file system with invalid YAML
|
||||
jest.spyOn(fs, "readFile").mockImplementation((path, options, callback) => {
|
||||
callback(null, Buffer.from(`
|
||||
it("should handle invalid YAML", async () => {
|
||||
// Mock file system with invalid YAML
|
||||
jest.spyOn(fs, "readFile").mockImplementation((path, options, callback) => {
|
||||
callback(
|
||||
null,
|
||||
Buffer.from(`
|
||||
name: "Invalid YAML
|
||||
description: Missing quote
|
||||
`));
|
||||
});
|
||||
`),
|
||||
)
|
||||
})
|
||||
|
||||
await expect(scanner["parseMetadataFile"]("test/path/metadata.en.yml"))
|
||||
.rejects.toThrow();
|
||||
});
|
||||
});
|
||||
await expect(scanner["parseMetadataFile"]("test/path/metadata.en.yml")).rejects.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("scanDirectory", () => {
|
||||
// Tests for directory scanning
|
||||
});
|
||||
});
|
||||
describe("scanDirectory", () => {
|
||||
// Tests for directory scanning
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### PackageManagerManager Tests
|
||||
|
||||
```typescript
|
||||
describe("PackageManagerManager", () => {
|
||||
let manager: PackageManagerManager;
|
||||
let mockContext: vscode.ExtensionContext;
|
||||
let manager: PackageManagerManager
|
||||
let mockContext: vscode.ExtensionContext
|
||||
|
||||
beforeEach(() => {
|
||||
// Create mock context
|
||||
mockContext = {
|
||||
extensionPath: "/test/path",
|
||||
globalStorageUri: { fsPath: "/test/storage" },
|
||||
globalState: {
|
||||
get: jest.fn().mockImplementation((key, defaultValue) => defaultValue),
|
||||
update: jest.fn().mockResolvedValue(undefined)
|
||||
}
|
||||
} as unknown as vscode.ExtensionContext;
|
||||
beforeEach(() => {
|
||||
// Create mock context
|
||||
mockContext = {
|
||||
extensionPath: "/test/path",
|
||||
globalStorageUri: { fsPath: "/test/storage" },
|
||||
globalState: {
|
||||
get: jest.fn().mockImplementation((key, defaultValue) => defaultValue),
|
||||
update: jest.fn().mockResolvedValue(undefined),
|
||||
},
|
||||
} as unknown as vscode.ExtensionContext
|
||||
|
||||
manager = new PackageManagerManager(mockContext);
|
||||
});
|
||||
manager = new PackageManagerManager(mockContext)
|
||||
})
|
||||
|
||||
describe("filterItems", () => {
|
||||
it("should filter by type", () => {
|
||||
// Set up test data
|
||||
manager["currentItems"] = [
|
||||
{ name: "Item 1", type: "mode", description: "Test item 1" },
|
||||
{ name: "Item 2", type: "package", description: "Test item 2" }
|
||||
] as PackageManagerItem[];
|
||||
describe("filterItems", () => {
|
||||
it("should filter by type", () => {
|
||||
// Set up test data
|
||||
manager["currentItems"] = [
|
||||
{ name: "Item 1", type: "mode", description: "Test item 1" },
|
||||
{ name: "Item 2", type: "package", description: "Test item 2" },
|
||||
] as PackageManagerItem[]
|
||||
|
||||
const result = manager.filterItems({ type: "mode" });
|
||||
const result = manager.filterItems({ type: "mode" })
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe("Item 1");
|
||||
});
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].name).toBe("Item 1")
|
||||
})
|
||||
|
||||
it("should filter by search term", () => {
|
||||
// Set up test data
|
||||
manager["currentItems"] = [
|
||||
{ name: "Alpha Item", type: "mode", description: "Test item" },
|
||||
{ name: "Beta Item", type: "package", description: "Another test" }
|
||||
] as PackageManagerItem[];
|
||||
it("should filter by search term", () => {
|
||||
// Set up test data
|
||||
manager["currentItems"] = [
|
||||
{ name: "Alpha Item", type: "mode", description: "Test item" },
|
||||
{ name: "Beta Item", type: "package", description: "Another test" },
|
||||
] as PackageManagerItem[]
|
||||
|
||||
const result = manager.filterItems({ search: "alpha" });
|
||||
const result = manager.filterItems({ search: "alpha" })
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe("Alpha Item");
|
||||
});
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].name).toBe("Alpha Item")
|
||||
})
|
||||
|
||||
// More filter tests...
|
||||
});
|
||||
// More filter tests...
|
||||
})
|
||||
|
||||
describe("addSource", () => {
|
||||
// Tests for adding sources
|
||||
});
|
||||
});
|
||||
describe("addSource", () => {
|
||||
// Tests for adding sources
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### Search Utilities Tests
|
||||
|
||||
```typescript
|
||||
describe("searchUtils", () => {
|
||||
describe("containsSearchTerm", () => {
|
||||
it("should return true for exact matches", () => {
|
||||
expect(containsSearchTerm("hello world", "hello")).toBe(true);
|
||||
});
|
||||
describe("containsSearchTerm", () => {
|
||||
it("should return true for exact matches", () => {
|
||||
expect(containsSearchTerm("hello world", "hello")).toBe(true)
|
||||
})
|
||||
|
||||
it("should be case insensitive", () => {
|
||||
expect(containsSearchTerm("Hello World", "hello")).toBe(true);
|
||||
expect(containsSearchTerm("hello world", "WORLD")).toBe(true);
|
||||
});
|
||||
it("should be case insensitive", () => {
|
||||
expect(containsSearchTerm("Hello World", "hello")).toBe(true)
|
||||
expect(containsSearchTerm("hello world", "WORLD")).toBe(true)
|
||||
})
|
||||
|
||||
it("should handle undefined inputs", () => {
|
||||
expect(containsSearchTerm(undefined, "test")).toBe(false);
|
||||
expect(containsSearchTerm("test", "")).toBe(false);
|
||||
});
|
||||
});
|
||||
it("should handle undefined inputs", () => {
|
||||
expect(containsSearchTerm(undefined, "test")).toBe(false)
|
||||
expect(containsSearchTerm("test", "")).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("itemMatchesSearch", () => {
|
||||
it("should match on name", () => {
|
||||
const item = {
|
||||
name: "Test Item",
|
||||
description: "Description"
|
||||
};
|
||||
describe("itemMatchesSearch", () => {
|
||||
it("should match on name", () => {
|
||||
const item = {
|
||||
name: "Test Item",
|
||||
description: "Description",
|
||||
}
|
||||
|
||||
expect(itemMatchesSearch(item, "test")).toEqual({
|
||||
matched: true,
|
||||
matchReason: {
|
||||
nameMatch: true,
|
||||
descriptionMatch: false
|
||||
}
|
||||
});
|
||||
});
|
||||
expect(itemMatchesSearch(item, "test")).toEqual({
|
||||
matched: true,
|
||||
matchReason: {
|
||||
nameMatch: true,
|
||||
descriptionMatch: false,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// More search matching tests...
|
||||
});
|
||||
});
|
||||
// More search matching tests...
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Frontend Unit Tests
|
||||
|
|
@ -318,87 +323,87 @@ Integration tests verify that different components work together correctly.
|
|||
|
||||
```typescript
|
||||
describe("Package Manager Integration", () => {
|
||||
let manager: PackageManagerManager;
|
||||
let metadataScanner: MetadataScanner;
|
||||
let templateItems: PackageManagerItem[];
|
||||
let manager: PackageManagerManager
|
||||
let metadataScanner: MetadataScanner
|
||||
let templateItems: PackageManagerItem[]
|
||||
|
||||
beforeAll(async () => {
|
||||
// Load real data from template
|
||||
metadataScanner = new MetadataScanner();
|
||||
const templatePath = path.resolve(__dirname, "../../../../package-manager-template");
|
||||
templateItems = await metadataScanner.scanDirectory(templatePath, "https://example.com");
|
||||
});
|
||||
beforeAll(async () => {
|
||||
// Load real data from template
|
||||
metadataScanner = new MetadataScanner()
|
||||
const templatePath = path.resolve(__dirname, "../../../../package-manager-template")
|
||||
templateItems = await metadataScanner.scanDirectory(templatePath, "https://example.com")
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Create a real context-like object
|
||||
const context = {
|
||||
extensionPath: path.resolve(__dirname, "../../../../"),
|
||||
globalStorageUri: { fsPath: path.resolve(__dirname, "../../../../mock/settings/path") },
|
||||
} as vscode.ExtensionContext;
|
||||
beforeEach(() => {
|
||||
// Create a real context-like object
|
||||
const context = {
|
||||
extensionPath: path.resolve(__dirname, "../../../../"),
|
||||
globalStorageUri: { fsPath: path.resolve(__dirname, "../../../../mock/settings/path") },
|
||||
} as vscode.ExtensionContext
|
||||
|
||||
// Create real instances
|
||||
manager = new PackageManagerManager(context);
|
||||
// Create real instances
|
||||
manager = new PackageManagerManager(context)
|
||||
|
||||
// Set up manager with template data
|
||||
manager["currentItems"] = [...templateItems];
|
||||
});
|
||||
// Set up manager with template data
|
||||
manager["currentItems"] = [...templateItems]
|
||||
})
|
||||
|
||||
describe("Message Handler Integration", () => {
|
||||
it("should handle search messages", async () => {
|
||||
const message = {
|
||||
type: "search",
|
||||
search: "data platform",
|
||||
typeFilter: "",
|
||||
tagFilters: []
|
||||
};
|
||||
describe("Message Handler Integration", () => {
|
||||
it("should handle search messages", async () => {
|
||||
const message = {
|
||||
type: "search",
|
||||
search: "data platform",
|
||||
typeFilter: "",
|
||||
tagFilters: [],
|
||||
}
|
||||
|
||||
const result = await handlePackageManagerMessages(message, manager);
|
||||
const result = await handlePackageManagerMessages(message, manager)
|
||||
|
||||
expect(result.type).toBe("searchResults");
|
||||
expect(result.data).toHaveLength(1);
|
||||
expect(result.data[0].name).toContain("Data Platform");
|
||||
});
|
||||
expect(result.type).toBe("searchResults")
|
||||
expect(result.data).toHaveLength(1)
|
||||
expect(result.data[0].name).toContain("Data Platform")
|
||||
})
|
||||
|
||||
it("should handle type filter messages", async () => {
|
||||
const message = {
|
||||
type: "search",
|
||||
search: "",
|
||||
typeFilter: "mode",
|
||||
tagFilters: []
|
||||
};
|
||||
it("should handle type filter messages", async () => {
|
||||
const message = {
|
||||
type: "search",
|
||||
search: "",
|
||||
typeFilter: "mode",
|
||||
tagFilters: [],
|
||||
}
|
||||
|
||||
const result = await handlePackageManagerMessages(message, manager);
|
||||
const result = await handlePackageManagerMessages(message, manager)
|
||||
|
||||
expect(result.type).toBe("searchResults");
|
||||
expect(result.data.every(item => item.type === "mode")).toBe(true);
|
||||
});
|
||||
expect(result.type).toBe("searchResults")
|
||||
expect(result.data.every((item) => item.type === "mode")).toBe(true)
|
||||
})
|
||||
|
||||
// More message handler tests...
|
||||
});
|
||||
// More message handler tests...
|
||||
})
|
||||
|
||||
describe("End-to-End Flow", () => {
|
||||
it("should find items with matching subcomponents", async () => {
|
||||
const message = {
|
||||
type: "search",
|
||||
search: "validator",
|
||||
typeFilter: "",
|
||||
tagFilters: []
|
||||
};
|
||||
describe("End-to-End Flow", () => {
|
||||
it("should find items with matching subcomponents", async () => {
|
||||
const message = {
|
||||
type: "search",
|
||||
search: "validator",
|
||||
typeFilter: "",
|
||||
tagFilters: [],
|
||||
}
|
||||
|
||||
const result = await handlePackageManagerMessages(message, manager);
|
||||
const result = await handlePackageManagerMessages(message, manager)
|
||||
|
||||
expect(result.data.length).toBeGreaterThan(0);
|
||||
expect(result.data.length).toBeGreaterThan(0)
|
||||
|
||||
// Check that subcomponents are marked as matches
|
||||
const hasMatchingSubcomponent = result.data.some(item =>
|
||||
item.items?.some(subItem => subItem.matchInfo?.matched)
|
||||
);
|
||||
expect(hasMatchingSubcomponent).toBe(true);
|
||||
});
|
||||
// Check that subcomponents are marked as matches
|
||||
const hasMatchingSubcomponent = result.data.some((item) =>
|
||||
item.items?.some((subItem) => subItem.matchInfo?.matched),
|
||||
)
|
||||
expect(hasMatchingSubcomponent).toBe(true)
|
||||
})
|
||||
|
||||
// More end-to-end flow tests...
|
||||
});
|
||||
});
|
||||
// More end-to-end flow tests...
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Frontend Integration Tests
|
||||
|
|
@ -490,17 +495,17 @@ Mock data is used for simple unit tests:
|
|||
|
||||
```typescript
|
||||
const mockItems: PackageManagerItem[] = [
|
||||
{
|
||||
name: "Test Package",
|
||||
description: "A test package",
|
||||
type: "package",
|
||||
url: "https://example.com",
|
||||
repoUrl: "https://github.com/example/repo",
|
||||
tags: ["test", "example"],
|
||||
version: "1.0.0"
|
||||
},
|
||||
// More mock items...
|
||||
];
|
||||
{
|
||||
name: "Test Package",
|
||||
description: "A test package",
|
||||
type: "package",
|
||||
url: "https://example.com",
|
||||
repoUrl: "https://github.com/example/repo",
|
||||
tags: ["test", "example"],
|
||||
version: "1.0.0",
|
||||
},
|
||||
// More mock items...
|
||||
]
|
||||
```
|
||||
|
||||
### Test Fixtures
|
||||
|
|
@ -510,48 +515,48 @@ Test fixtures provide more complex data structures:
|
|||
```typescript
|
||||
// fixtures/metadata.ts
|
||||
export const metadataFixtures = {
|
||||
basic: {
|
||||
name: "Basic Package",
|
||||
description: "A basic package for testing",
|
||||
version: "1.0.0",
|
||||
type: "package"
|
||||
},
|
||||
basic: {
|
||||
name: "Basic Package",
|
||||
description: "A basic package for testing",
|
||||
version: "1.0.0",
|
||||
type: "package",
|
||||
},
|
||||
|
||||
withTags: {
|
||||
name: "Tagged Package",
|
||||
description: "A package with tags",
|
||||
version: "1.0.0",
|
||||
type: "package",
|
||||
tags: ["test", "fixture", "example"]
|
||||
},
|
||||
withTags: {
|
||||
name: "Tagged Package",
|
||||
description: "A package with tags",
|
||||
version: "1.0.0",
|
||||
type: "package",
|
||||
tags: ["test", "fixture", "example"],
|
||||
},
|
||||
|
||||
withSubcomponents: {
|
||||
name: "Complex Package",
|
||||
description: "A package with subcomponents",
|
||||
version: "1.0.0",
|
||||
type: "package",
|
||||
items: [
|
||||
{
|
||||
type: "mode",
|
||||
path: "/test/path/mode",
|
||||
metadata: {
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
type: "mode"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "mcp server",
|
||||
path: "/test/path/server",
|
||||
metadata: {
|
||||
name: "Test Server",
|
||||
description: "A test server",
|
||||
type: "mcp server"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
withSubcomponents: {
|
||||
name: "Complex Package",
|
||||
description: "A package with subcomponents",
|
||||
version: "1.0.0",
|
||||
type: "package",
|
||||
items: [
|
||||
{
|
||||
type: "mode",
|
||||
path: "/test/path/mode",
|
||||
metadata: {
|
||||
name: "Test Mode",
|
||||
description: "A test mode",
|
||||
type: "mode",
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "mcp server",
|
||||
path: "/test/path/server",
|
||||
metadata: {
|
||||
name: "Test Server",
|
||||
description: "A test server",
|
||||
type: "mcp server",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Template Data
|
||||
|
|
@ -560,11 +565,11 @@ Real template data is used for integration tests:
|
|||
|
||||
```typescript
|
||||
beforeAll(async () => {
|
||||
// Load real data from template
|
||||
metadataScanner = new MetadataScanner();
|
||||
const templatePath = path.resolve(__dirname, "../../../../package-manager-template");
|
||||
templateItems = await metadataScanner.scanDirectory(templatePath, "https://example.com");
|
||||
});
|
||||
// Load real data from template
|
||||
metadataScanner = new MetadataScanner()
|
||||
const templatePath = path.resolve(__dirname, "../../../../package-manager-template")
|
||||
templateItems = await metadataScanner.scanDirectory(templatePath, "https://example.com")
|
||||
})
|
||||
```
|
||||
|
||||
### Test Data Generators
|
||||
|
|
@ -574,45 +579,43 @@ Generators create varied test data:
|
|||
```typescript
|
||||
// Test data generator
|
||||
function generatePackageItems(count: number): PackageManagerItem[] {
|
||||
const types: ComponentType[] = ["mode", "mcp server", "package", "prompt"];
|
||||
const tags = ["test", "example", "data", "ui", "server", "client"];
|
||||
const types: ComponentType[] = ["mode", "mcp server", "package", "prompt"]
|
||||
const tags = ["test", "example", "data", "ui", "server", "client"]
|
||||
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const type = types[i % types.length];
|
||||
const randomTags = tags
|
||||
.filter(() => Math.random() > 0.5)
|
||||
.slice(0, Math.floor(Math.random() * 4));
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const type = types[i % types.length]
|
||||
const randomTags = tags.filter(() => Math.random() > 0.5).slice(0, Math.floor(Math.random() * 4))
|
||||
|
||||
return {
|
||||
name: `Test ${type} ${i + 1}`,
|
||||
description: `This is a test ${type} for testing purposes`,
|
||||
type,
|
||||
url: `https://example.com/${type}/${i + 1}`,
|
||||
repoUrl: "https://github.com/example/repo",
|
||||
tags: randomTags.length ? randomTags : undefined,
|
||||
version: "1.0.0",
|
||||
lastUpdated: new Date().toISOString(),
|
||||
items: type === "package" ? generateSubcomponents(Math.floor(Math.random() * 5) + 1) : undefined
|
||||
};
|
||||
});
|
||||
return {
|
||||
name: `Test ${type} ${i + 1}`,
|
||||
description: `This is a test ${type} for testing purposes`,
|
||||
type,
|
||||
url: `https://example.com/${type}/${i + 1}`,
|
||||
repoUrl: "https://github.com/example/repo",
|
||||
tags: randomTags.length ? randomTags : undefined,
|
||||
version: "1.0.0",
|
||||
lastUpdated: new Date().toISOString(),
|
||||
items: type === "package" ? generateSubcomponents(Math.floor(Math.random() * 5) + 1) : undefined,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function generateSubcomponents(count: number): PackageManagerItem["items"] {
|
||||
const types: ComponentType[] = ["mode", "mcp server", "prompt"];
|
||||
const types: ComponentType[] = ["mode", "mcp server", "prompt"]
|
||||
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const type = types[i % types.length];
|
||||
return Array.from({ length: count }, (_, i) => {
|
||||
const type = types[i % types.length]
|
||||
|
||||
return {
|
||||
type,
|
||||
path: `/test/path/${type}/${i + 1}`,
|
||||
metadata: {
|
||||
name: `Test ${type} ${i + 1}`,
|
||||
description: `This is a test ${type} subcomponent`,
|
||||
type
|
||||
}
|
||||
};
|
||||
});
|
||||
return {
|
||||
type,
|
||||
path: `/test/path/${type}/${i + 1}`,
|
||||
metadata: {
|
||||
name: `Test ${type} ${i + 1}`,
|
||||
description: `This is a test ${type} subcomponent`,
|
||||
type,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -635,20 +638,20 @@ Tests are organized into logical groups:
|
|||
|
||||
```typescript
|
||||
describe("Package Manager", () => {
|
||||
// Shared setup
|
||||
// Shared setup
|
||||
|
||||
describe("Direct Filtering", () => {
|
||||
// Tests for filtering functionality
|
||||
});
|
||||
describe("Direct Filtering", () => {
|
||||
// Tests for filtering functionality
|
||||
})
|
||||
|
||||
describe("Message Handler Integration", () => {
|
||||
// Tests for message handling
|
||||
});
|
||||
describe("Message Handler Integration", () => {
|
||||
// Tests for message handling
|
||||
})
|
||||
|
||||
describe("Sorting", () => {
|
||||
// Tests for sorting functionality
|
||||
});
|
||||
});
|
||||
describe("Sorting", () => {
|
||||
// Tests for sorting functionality
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Test Coverage
|
||||
|
|
@ -666,24 +669,24 @@ The Package Manager maintains high test coverage:
|
|||
```typescript
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
// ...other config
|
||||
collectCoverage: true,
|
||||
coverageReporters: ["text", "lcov", "html"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 80,
|
||||
functions: 85,
|
||||
lines: 85,
|
||||
statements: 85
|
||||
},
|
||||
"src/services/package-manager/*.ts": {
|
||||
branches: 90,
|
||||
functions: 90,
|
||||
lines: 90,
|
||||
statements: 90
|
||||
}
|
||||
}
|
||||
};
|
||||
// ...other config
|
||||
collectCoverage: true,
|
||||
coverageReporters: ["text", "lcov", "html"],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 80,
|
||||
functions: 85,
|
||||
lines: 85,
|
||||
statements: 85,
|
||||
},
|
||||
"src/services/package-manager/*.ts": {
|
||||
branches: 90,
|
||||
functions: 90,
|
||||
lines: 90,
|
||||
statements: 90,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Critical Path Testing
|
||||
|
|
@ -703,12 +706,12 @@ The Package Manager tests are optimized for performance:
|
|||
```typescript
|
||||
// Fast unit tests with minimal dependencies
|
||||
describe("containsSearchTerm", () => {
|
||||
it("should return true for exact matches", () => {
|
||||
expect(containsSearchTerm("hello world", "hello")).toBe(true);
|
||||
});
|
||||
it("should return true for exact matches", () => {
|
||||
expect(containsSearchTerm("hello world", "hello")).toBe(true)
|
||||
})
|
||||
|
||||
// More tests...
|
||||
});
|
||||
// More tests...
|
||||
})
|
||||
```
|
||||
|
||||
### Optimized Integration Tests
|
||||
|
|
@ -716,19 +719,19 @@ describe("containsSearchTerm", () => {
|
|||
```typescript
|
||||
// Optimized integration tests
|
||||
describe("Package Manager Integration", () => {
|
||||
// Load template data once for all tests
|
||||
beforeAll(async () => {
|
||||
templateItems = await metadataScanner.scanDirectory(templatePath);
|
||||
});
|
||||
// Load template data once for all tests
|
||||
beforeAll(async () => {
|
||||
templateItems = await metadataScanner.scanDirectory(templatePath)
|
||||
})
|
||||
|
||||
// Create fresh manager for each test
|
||||
beforeEach(() => {
|
||||
manager = new PackageManagerManager(mockContext);
|
||||
manager["currentItems"] = [...templateItems];
|
||||
});
|
||||
// Create fresh manager for each test
|
||||
beforeEach(() => {
|
||||
manager = new PackageManagerManager(mockContext)
|
||||
manager["currentItems"] = [...templateItems]
|
||||
})
|
||||
|
||||
// Tests...
|
||||
});
|
||||
// Tests...
|
||||
})
|
||||
```
|
||||
|
||||
### Parallel Test Execution
|
||||
|
|
@ -736,10 +739,10 @@ describe("Package Manager Integration", () => {
|
|||
```typescript
|
||||
// jest.config.js
|
||||
module.exports = {
|
||||
// ...other config
|
||||
maxWorkers: "50%", // Use 50% of available cores
|
||||
maxConcurrency: 5 // Run up to 5 tests concurrently
|
||||
};
|
||||
// ...other config
|
||||
maxWorkers: "50%", // Use 50% of available cores
|
||||
maxConcurrency: 5, // Run up to 5 tests concurrently
|
||||
}
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
|
@ -753,33 +756,33 @@ The Package Manager tests are integrated into the CI/CD pipeline:
|
|||
name: Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: "16"
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
- name: Run tests
|
||||
run: npm test
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
file: ./coverage/lcov.info
|
||||
```
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
|
@ -787,17 +790,14 @@ jobs:
|
|||
```json
|
||||
// package.json
|
||||
{
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"jest --findRelatedTests"
|
||||
]
|
||||
}
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": ["eslint --fix", "jest --findRelatedTests"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -810,17 +810,17 @@ The Package Manager includes tools for debugging tests:
|
|||
```typescript
|
||||
// Debug logging in tests
|
||||
describe("Complex integration test", () => {
|
||||
it("should handle complex search", async () => {
|
||||
// Enable debug logging for this test
|
||||
const originalDebug = process.env.DEBUG;
|
||||
process.env.DEBUG = "package-manager:*";
|
||||
it("should handle complex search", async () => {
|
||||
// Enable debug logging for this test
|
||||
const originalDebug = process.env.DEBUG
|
||||
process.env.DEBUG = "package-manager:*"
|
||||
|
||||
// Test logic...
|
||||
// Test logic...
|
||||
|
||||
// Restore debug setting
|
||||
process.env.DEBUG = originalDebug;
|
||||
});
|
||||
});
|
||||
// Restore debug setting
|
||||
process.env.DEBUG = originalDebug
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Visual Debugging
|
||||
|
|
@ -860,26 +860,26 @@ The Package Manager tests include comprehensive documentation:
|
|||
* - Matching in subcomponents
|
||||
*/
|
||||
describe("Search functionality", () => {
|
||||
// Tests...
|
||||
});
|
||||
// Tests...
|
||||
})
|
||||
```
|
||||
|
||||
### Test Scenarios
|
||||
|
||||
```typescript
|
||||
describe("Package filtering", () => {
|
||||
/**
|
||||
* Scenario: User filters by type and search term
|
||||
* Given: A list of packages of different types
|
||||
* When: The user selects a type filter and enters a search term
|
||||
* Then: Only packages of the selected type containing the search term should be shown
|
||||
*/
|
||||
it("should combine type and search filters", () => {
|
||||
// Test implementation...
|
||||
});
|
||||
});
|
||||
/**
|
||||
* Scenario: User filters by type and search term
|
||||
* Given: A list of packages of different types
|
||||
* When: The user selects a type filter and enters a search term
|
||||
* Then: Only packages of the selected type containing the search term should be shown
|
||||
*/
|
||||
it("should combine type and search filters", () => {
|
||||
// Test implementation...
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Previous**: [UI Component Design](./05-ui-components.md) | **Next**: [Extending the Package Manager](./07-extending.md)
|
||||
**Previous**: [UI Component Design](./05-ui-components.md) | **Next**: [Extending the Package Manager](./07-extending.md)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,371 +1,3 @@
|
|||
# Package Manager Localization Improvements
|
||||
|
||||
## Issue Identified
|
||||
|
||||
The current implementation of the Package Manager only uses English metadata (`metadata.en.yml`) for all functionality, regardless of the user's locale. While the system loads metadata files for other locales, it doesn't actually use them. The correct behavior should be:
|
||||
|
||||
1. Use the locale-specific version for each package item if it is present
|
||||
2. Fall back to the English version if the locale-specific version is not available
|
||||
3. Skip the item if neither the locale-specific nor the English version is available
|
||||
|
||||
## Implementation Changes Needed
|
||||
|
||||
### 1. Add User Locale Detection
|
||||
|
||||
```typescript
|
||||
// Add to src/services/package-manager/types.ts
|
||||
export interface LocalizationOptions {
|
||||
userLocale: string;
|
||||
fallbackLocale: string;
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// Add to src/services/package-manager/utils.ts
|
||||
export function getUserLocale(): string {
|
||||
// Get from VS Code API or system locale
|
||||
const vscodeLocale = vscode.env.language;
|
||||
// Extract just the language part (e.g., "en-US" -> "en")
|
||||
return vscodeLocale.split('-')[0].toLowerCase();
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Modify MetadataScanner to Use Locale Preference
|
||||
|
||||
```typescript
|
||||
// Update MetadataScanner constructor
|
||||
constructor(git?: SimpleGit, private localizationOptions?: LocalizationOptions) {
|
||||
this.git = git;
|
||||
this.localizationOptions = localizationOptions || {
|
||||
userLocale: getUserLocale(),
|
||||
fallbackLocale: 'en'
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Update Component Creation Logic
|
||||
|
||||
```typescript
|
||||
// Update scanDirectory method in MetadataScanner.ts
|
||||
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);
|
||||
|
||||
// Skip if no metadata found at all
|
||||
if (!metadata) continue;
|
||||
|
||||
// Get localized metadata with fallback
|
||||
const localizedMetadata = this.getLocalizedMetadata(metadata);
|
||||
if (!localizedMetadata) continue;
|
||||
|
||||
const item = await this.createPackageManagerItem(localizedMetadata, componentDir, repoUrl, sourceName);
|
||||
if (item) {
|
||||
// Process package subcomponents with the same localization logic
|
||||
// ...rest of the method
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error scanning directory ${rootDir}:`, error);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Add Localization Selection Helper
|
||||
|
||||
```typescript
|
||||
// Add to MetadataScanner.ts
|
||||
private getLocalizedMetadata(metadata: LocalizedMetadata<ComponentMetadata>): ComponentMetadata | null {
|
||||
const { userLocale, fallbackLocale } = this.localizationOptions;
|
||||
|
||||
// First try user's locale
|
||||
if (metadata[userLocale]) {
|
||||
return metadata[userLocale];
|
||||
}
|
||||
|
||||
// Fall back to English
|
||||
if (metadata[fallbackLocale]) {
|
||||
return metadata[fallbackLocale];
|
||||
}
|
||||
|
||||
// No suitable metadata found
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Update Subcomponent Processing
|
||||
|
||||
```typescript
|
||||
// Update the subcomponent processing in scanDirectory
|
||||
if (this.isPackageMetadata(localizedMetadata)) {
|
||||
// Load metadata for items listed in package metadata
|
||||
if (localizedMetadata.items) {
|
||||
const subcomponents = await Promise.all(
|
||||
localizedMetadata.items.map(async (subItem) => {
|
||||
const subPath = path.join(componentDir, subItem.path);
|
||||
const subMetadata = await this.loadComponentMetadata(subPath);
|
||||
|
||||
// Skip if no metadata found
|
||||
if (!subMetadata) return null;
|
||||
|
||||
// Get localized metadata with fallback
|
||||
const localizedSubMetadata = this.getLocalizedMetadata(subMetadata);
|
||||
if (!localizedSubMetadata) return null;
|
||||
|
||||
return {
|
||||
type: subItem.type,
|
||||
path: subItem.path,
|
||||
metadata: localizedSubMetadata,
|
||||
lastUpdated: await this.getLastModifiedDate(subPath),
|
||||
};
|
||||
}),
|
||||
);
|
||||
item.items = subcomponents.filter((sub): sub is NonNullable<typeof sub> => sub !== null);
|
||||
}
|
||||
|
||||
// Also scan directory for unlisted subcomponents with localization support
|
||||
await this.scanPackageSubcomponents(componentDir, item);
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Update scanPackageSubcomponents Method
|
||||
|
||||
```typescript
|
||||
// Update scanPackageSubcomponents in MetadataScanner.ts
|
||||
private async scanPackageSubcomponents(
|
||||
packageDir: string,
|
||||
packageItem: PackageManagerItem,
|
||||
parentPath: string = "",
|
||||
): Promise<void> {
|
||||
const entries = await fs.readdir(packageDir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
|
||||
const subPath = path.join(packageDir, entry.name);
|
||||
const relativePath = parentPath ? path.join(parentPath, entry.name) : entry.name;
|
||||
|
||||
// Try to load metadata directly
|
||||
const subMetadata = await this.loadComponentMetadata(subPath);
|
||||
|
||||
if (subMetadata) {
|
||||
const isListed = packageItem.items?.some((i) => i.path === relativePath);
|
||||
|
||||
if (!isListed) {
|
||||
// Get localized metadata with fallback
|
||||
const localizedSubMetadata = this.getLocalizedMetadata(subMetadata);
|
||||
if (localizedSubMetadata) {
|
||||
const subItem = {
|
||||
type: localizedSubMetadata.type,
|
||||
path: relativePath,
|
||||
metadata: localizedSubMetadata,
|
||||
lastUpdated: await this.getLastModifiedDate(subPath),
|
||||
};
|
||||
packageItem.items = packageItem.items || [];
|
||||
packageItem.items.push(subItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively scan this directory
|
||||
await this.scanPackageSubcomponents(subPath, packageItem, relativePath);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Update PackageManagerManager to Pass Locale
|
||||
|
||||
```typescript
|
||||
// Update PackageManagerManager.ts
|
||||
constructor(private readonly context: vscode.ExtensionContext) {
|
||||
const userLocale = getUserLocale();
|
||||
this.gitFetcher = new GitFetcher(context, { userLocale, fallbackLocale: 'en' });
|
||||
}
|
||||
```
|
||||
|
||||
## Test Cases
|
||||
|
||||
### Unit Tests
|
||||
|
||||
1. **Test Locale Fallback Logic**
|
||||
|
||||
```typescript
|
||||
describe('Localization Fallback', () => {
|
||||
let metadataScanner: MetadataScanner;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock fs and other dependencies
|
||||
});
|
||||
|
||||
test('should use user locale when available', async () => {
|
||||
// Setup mock metadata with both user locale and English
|
||||
const mockMetadata = {
|
||||
'en': { name: 'English Name', description: 'English Description' },
|
||||
'fr': { name: 'Nom Français', description: 'Description Française' }
|
||||
};
|
||||
|
||||
// Initialize with French locale
|
||||
metadataScanner = new MetadataScanner(null, { userLocale: 'fr', fallbackLocale: 'en' });
|
||||
|
||||
// Call the getLocalizedMetadata method
|
||||
const result = metadataScanner['getLocalizedMetadata'](mockMetadata);
|
||||
|
||||
// Expect French metadata to be used
|
||||
expect(result.name).toBe('Nom Français');
|
||||
expect(result.description).toBe('Description Française');
|
||||
});
|
||||
|
||||
test('should fall back to English when user locale not available', async () => {
|
||||
// Setup mock metadata with only English
|
||||
const mockMetadata = {
|
||||
'en': { name: 'English Name', description: 'English Description' }
|
||||
};
|
||||
|
||||
// Initialize with French locale
|
||||
metadataScanner = new MetadataScanner(null, { userLocale: 'fr', fallbackLocale: 'en' });
|
||||
|
||||
// Call the getLocalizedMetadata method
|
||||
const result = metadataScanner['getLocalizedMetadata'](mockMetadata);
|
||||
|
||||
// Expect English metadata to be used as fallback
|
||||
expect(result.name).toBe('English Name');
|
||||
expect(result.description).toBe('English Description');
|
||||
});
|
||||
|
||||
test('should return null when neither user locale nor English available', async () => {
|
||||
// Setup mock metadata with neither user locale nor English
|
||||
const mockMetadata = {
|
||||
'de': { name: 'Deutscher Name', description: 'Deutsche Beschreibung' }
|
||||
};
|
||||
|
||||
// Initialize with French locale
|
||||
metadataScanner = new MetadataScanner(null, { userLocale: 'fr', fallbackLocale: 'en' });
|
||||
|
||||
// Call the getLocalizedMetadata method
|
||||
const result = metadataScanner['getLocalizedMetadata'](mockMetadata);
|
||||
|
||||
// Expect null result
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
2. **Test Component Loading with Localization**
|
||||
|
||||
```typescript
|
||||
describe('Component Loading with Localization', () => {
|
||||
let metadataScanner: MetadataScanner;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock fs and other dependencies
|
||||
});
|
||||
|
||||
test('should load components with user locale preference', async () => {
|
||||
// Setup mock directory structure with multiple locales
|
||||
mockFs.readdir.mockImplementation((dir, options) => {
|
||||
if (dir === '/test/repo') {
|
||||
return Promise.resolve([
|
||||
{ name: 'component1', isDirectory: () => true },
|
||||
{ name: 'component2', isDirectory: () => true }
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
// Mock loadComponentMetadata to return different locales
|
||||
jest.spyOn(MetadataScanner.prototype, 'loadComponentMetadata').mockImplementation((dir) => {
|
||||
if (dir === '/test/repo/component1') {
|
||||
return Promise.resolve({
|
||||
'en': { name: 'Component 1 EN', description: 'Description EN', type: 'mode' },
|
||||
'fr': { name: 'Component 1 FR', description: 'Description FR', type: 'mode' }
|
||||
});
|
||||
} else if (dir === '/test/repo/component2') {
|
||||
return Promise.resolve({
|
||||
'en': { name: 'Component 2 EN', description: 'Description EN', type: 'mcp server' }
|
||||
});
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
// Initialize with French locale
|
||||
metadataScanner = new MetadataScanner(null, { userLocale: 'fr', fallbackLocale: 'en' });
|
||||
|
||||
// Scan directory
|
||||
const items = await metadataScanner.scanDirectory('/test/repo', 'https://example.com');
|
||||
|
||||
// Expect French for component1, English for component2
|
||||
expect(items.length).toBe(2);
|
||||
expect(items[0].name).toBe('Component 1 FR');
|
||||
expect(items[1].name).toBe('Component 2 EN');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
3. **Test Subcomponent Processing with Localization**
|
||||
|
||||
```typescript
|
||||
describe('Subcomponent Processing with Localization', () => {
|
||||
// Similar tests for subcomponents
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
|
||||
1. **Test End-to-End Localization Flow**
|
||||
|
||||
```typescript
|
||||
describe('End-to-End Localization', () => {
|
||||
test('should display components in user locale with fallback', async () => {
|
||||
// Setup test repository with multiple locales
|
||||
// Initialize PackageManagerManager with specific locale
|
||||
// Verify that components are displayed in the correct locale
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
2. **Test with Real Package Repository**
|
||||
|
||||
```typescript
|
||||
describe('Real Package Repository with Localization', () => {
|
||||
test('should handle real-world package repository with multiple locales', async () => {
|
||||
// Use a real package repository with multiple locales
|
||||
// Verify correct locale selection and fallback
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## UI Changes
|
||||
|
||||
1. **Add Locale Selector in UI (Optional Enhancement)**
|
||||
|
||||
```typescript
|
||||
// Add to webview-ui/src/components/package-manager/PackageManagerView.tsx
|
||||
const [currentLocale, setCurrentLocale] = useState(getUserLocale());
|
||||
|
||||
// Add locale selector dropdown
|
||||
<Select
|
||||
value={currentLocale}
|
||||
onChange={(e) => {
|
||||
setCurrentLocale(e.target.value);
|
||||
// Trigger refresh with new locale
|
||||
}}
|
||||
>
|
||||
<option value="en">English</option>
|
||||
<option value="fr">Français</option>
|
||||
<option value="es">Español</option>
|
||||
{/* Add more languages as needed */}
|
||||
</Select>
|
||||
```
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
Update the documentation to reflect the correct localization behavior:
|
||||
|
|
@ -380,18 +12,10 @@ You can provide metadata in multiple languages by using locale-specific files:
|
|||
- `metadata.fr.yml` - French metadata
|
||||
|
||||
**Important Notes on Localization:**
|
||||
|
||||
- Only files with the pattern `metadata.{locale}.yml` are supported
|
||||
- The Package Manager will display metadata in the user's locale if available
|
||||
- If the user's locale is not available, it will fall back to English
|
||||
- The English locale (`metadata.en.yml`) is required as a fallback
|
||||
- Files without a locale code (e.g., just `metadata.yml`) are not supported
|
||||
```
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
1. Add localization options and user locale detection
|
||||
2. Modify MetadataScanner to use locale preference with fallback
|
||||
3. Update component creation logic to handle localization
|
||||
4. Add tests to verify localization behavior
|
||||
5. Update documentation to reflect the correct behavior
|
||||
6. (Optional) Add UI controls for locale selection
|
||||
|
|
@ -34,20 +34,23 @@ Components within packages are grouped by their type to make them easier to find
|
|||
### Common Component Types
|
||||
|
||||
1. **Modes**
|
||||
- AI assistant personalities with specialized capabilities
|
||||
- Examples: Code Mode, Architect Mode, Debug Mode
|
||||
|
||||
- AI assistant personalities with specialized capabilities
|
||||
- Examples: Code Mode, Architect Mode, Debug Mode
|
||||
|
||||
2. **MCP Servers**
|
||||
- Model Context Protocol servers that provide additional functionality
|
||||
- Examples: File Analyzer, Data Validator, Image Generator
|
||||
|
||||
- Model Context Protocol servers that provide additional functionality
|
||||
- Examples: File Analyzer, Data Validator, Image Generator
|
||||
|
||||
3. **Prompts**
|
||||
- Pre-configured instructions for specific tasks
|
||||
- Examples: Code Review, Documentation Generator, Test Case Creator
|
||||
|
||||
- Pre-configured instructions for specific tasks
|
||||
- Examples: Code Review, Documentation Generator, Test Case Creator
|
||||
|
||||
4. **Packages**
|
||||
- Nested collections of related components
|
||||
- Can contain any of the other component types
|
||||
- Nested collections of related components
|
||||
- Can contain any of the other component types
|
||||
|
||||
### Type Presentation
|
||||
|
||||
|
|
@ -136,4 +139,4 @@ If you search for "validator":
|
|||
|
||||
---
|
||||
|
||||
**Previous**: [Searching and Filtering](./03-searching-and-filtering.md) | **Next**: [Adding Packages](./05-adding-packages.md)
|
||||
**Previous**: [Searching and Filtering](./03-searching-and-filtering.md) | **Next**: [Adding Packages](./05-adding-packages.md)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import * as yaml from "js-yaml"
|
|||
import simpleGit, { SimpleGit } from "simple-git"
|
||||
import { MetadataScanner } from "./MetadataScanner"
|
||||
import { validateAnyMetadata } from "./schemas"
|
||||
import { PackageManagerItem, PackageManagerRepository, RepositoryMetadata } from "./types"
|
||||
import { LocalizationOptions, PackageManagerItem, PackageManagerRepository, RepositoryMetadata } from "./types"
|
||||
import { getUserLocale } from "./utils"
|
||||
|
||||
/**
|
||||
* Handles fetching and caching package manager repositories
|
||||
|
|
@ -14,10 +15,15 @@ export class GitFetcher {
|
|||
private readonly cacheDir: string
|
||||
private metadataScanner: MetadataScanner
|
||||
private git?: SimpleGit
|
||||
private localizationOptions: LocalizationOptions
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
constructor(context: vscode.ExtensionContext, localizationOptions?: LocalizationOptions) {
|
||||
this.cacheDir = path.join(context.globalStorageUri.fsPath, "package-manager-cache")
|
||||
this.metadataScanner = new MetadataScanner()
|
||||
this.localizationOptions = localizationOptions || {
|
||||
userLocale: getUserLocale(),
|
||||
fallbackLocale: "en",
|
||||
}
|
||||
this.metadataScanner = new MetadataScanner(undefined, this.localizationOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -27,7 +33,7 @@ export class GitFetcher {
|
|||
private initGit(repoDir: string): void {
|
||||
this.git = simpleGit(repoDir)
|
||||
// Update MetadataScanner with new git instance
|
||||
this.metadataScanner = new MetadataScanner(this.git)
|
||||
this.metadataScanner = new MetadataScanner(this.git, this.localizationOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,16 +4,29 @@ import * as vscode from "vscode"
|
|||
import * as yaml from "js-yaml"
|
||||
import { SimpleGit } from "simple-git"
|
||||
import { validateAnyMetadata } from "./schemas"
|
||||
import { ComponentMetadata, ComponentType, LocalizedMetadata, PackageManagerItem, PackageMetadata } from "./types"
|
||||
import {
|
||||
ComponentMetadata,
|
||||
ComponentType,
|
||||
LocalizationOptions,
|
||||
LocalizedMetadata,
|
||||
PackageManagerItem,
|
||||
PackageMetadata,
|
||||
} from "./types"
|
||||
import { getUserLocale } from "./utils"
|
||||
|
||||
/**
|
||||
* Handles component discovery and metadata loading
|
||||
*/
|
||||
export class MetadataScanner {
|
||||
private readonly git?: SimpleGit
|
||||
private localizationOptions: LocalizationOptions
|
||||
|
||||
constructor(git?: SimpleGit) {
|
||||
constructor(git?: SimpleGit, localizationOptions?: LocalizationOptions) {
|
||||
this.git = git
|
||||
this.localizationOptions = localizationOptions || {
|
||||
userLocale: getUserLocale(),
|
||||
fallbackLocale: "en",
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -35,44 +48,54 @@ export class MetadataScanner {
|
|||
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) {
|
||||
// If this is a package, scan for subcomponents
|
||||
if (this.isPackageMetadata(metadata["en"])) {
|
||||
// Load metadata for items listed in package metadata
|
||||
if (metadata["en"].items) {
|
||||
const subcomponents = await Promise.all(
|
||||
metadata["en"].items.map(async (subItem) => {
|
||||
const subPath = path.join(componentDir, subItem.path)
|
||||
const subMetadata = await this.loadComponentMetadata(subPath)
|
||||
if (subMetadata?.["en"]) {
|
||||
return {
|
||||
type: subItem.type,
|
||||
path: subItem.path,
|
||||
metadata: subMetadata["en"],
|
||||
lastUpdated: await this.getLastModifiedDate(subPath),
|
||||
}
|
||||
}
|
||||
return null
|
||||
}),
|
||||
)
|
||||
item.items = subcomponents.filter((sub): sub is NonNullable<typeof sub> => sub !== null)
|
||||
}
|
||||
// Skip if no metadata found at all
|
||||
if (!metadata) continue
|
||||
|
||||
// Also scan directory for unlisted subcomponents
|
||||
await this.scanPackageSubcomponents(componentDir, item)
|
||||
}
|
||||
items.push(item)
|
||||
// Skip recursion if this is a package directory
|
||||
if (this.isPackageMetadata(metadata["en"])) {
|
||||
continue
|
||||
// Get localized metadata with fallback
|
||||
const localizedMetadata = this.getLocalizedMetadata(metadata)
|
||||
if (!localizedMetadata) continue
|
||||
|
||||
const item = await this.createPackageManagerItem(localizedMetadata, componentDir, repoUrl, sourceName)
|
||||
if (item) {
|
||||
// If this is a package, scan for subcomponents
|
||||
if (this.isPackageMetadata(localizedMetadata)) {
|
||||
// Load metadata for items listed in package metadata
|
||||
if (localizedMetadata.items) {
|
||||
const subcomponents = await Promise.all(
|
||||
localizedMetadata.items.map(async (subItem) => {
|
||||
const subPath = path.join(componentDir, subItem.path)
|
||||
const subMetadata = await this.loadComponentMetadata(subPath)
|
||||
|
||||
// Skip if no metadata found
|
||||
if (!subMetadata) return null
|
||||
|
||||
// Get localized metadata with fallback
|
||||
const localizedSubMetadata = this.getLocalizedMetadata(subMetadata)
|
||||
if (!localizedSubMetadata) return null
|
||||
|
||||
return {
|
||||
type: subItem.type,
|
||||
path: subItem.path,
|
||||
metadata: localizedSubMetadata,
|
||||
lastUpdated: await this.getLastModifiedDate(subPath),
|
||||
}
|
||||
}),
|
||||
)
|
||||
item.items = subcomponents.filter((sub): sub is NonNullable<typeof sub> => sub !== null)
|
||||
}
|
||||
|
||||
// Also scan directory for unlisted subcomponents
|
||||
await this.scanPackageSubcomponents(componentDir, item)
|
||||
}
|
||||
items.push(item)
|
||||
// Skip recursion if this is a package directory
|
||||
if (this.isPackageMetadata(localizedMetadata)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively scan subdirectories only if not in a package
|
||||
if (!metadata?.["en"] || !this.isPackageMetadata(metadata["en"])) {
|
||||
if (!metadata || !this.isPackageMetadata(localizedMetadata)) {
|
||||
const subItems = await this.scanDirectory(componentDir, repoUrl, sourceName)
|
||||
items.push(...subItems)
|
||||
}
|
||||
|
|
@ -84,6 +107,28 @@ export class MetadataScanner {
|
|||
return items
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets localized metadata with fallback
|
||||
* @param metadata The localized metadata object
|
||||
* @returns The metadata in the user's locale or fallback locale, or null if neither is available
|
||||
*/
|
||||
private getLocalizedMetadata(metadata: LocalizedMetadata<ComponentMetadata>): ComponentMetadata | null {
|
||||
const { userLocale, fallbackLocale } = this.localizationOptions
|
||||
|
||||
// First try user's locale
|
||||
if (metadata[userLocale]) {
|
||||
return metadata[userLocale]
|
||||
}
|
||||
|
||||
// Fall back to fallbackLocale (typically English)
|
||||
if (metadata[fallbackLocale]) {
|
||||
return metadata[fallbackLocale]
|
||||
}
|
||||
|
||||
// No suitable metadata found
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads metadata for a component
|
||||
* @param componentDir The component directory
|
||||
|
|
@ -226,22 +271,27 @@ export class MetadataScanner {
|
|||
|
||||
// Try to load metadata directly
|
||||
const subMetadata = await this.loadComponentMetadata(subPath)
|
||||
console.log(`Metadata for ${entry.name}:`, subMetadata?.["en"])
|
||||
|
||||
if (subMetadata?.["en"]) {
|
||||
const isListed = packageItem.items?.some((i) => i.path === relativePath)
|
||||
console.log(`${entry.name} is ${isListed ? "already listed" : "not listed"}`)
|
||||
if (subMetadata) {
|
||||
// Get localized metadata with fallback
|
||||
const localizedSubMetadata = this.getLocalizedMetadata(subMetadata)
|
||||
if (localizedSubMetadata) {
|
||||
console.log(`Metadata for ${entry.name}:`, localizedSubMetadata)
|
||||
|
||||
if (!isListed) {
|
||||
const subItem = {
|
||||
type: subMetadata["en"].type,
|
||||
path: relativePath,
|
||||
metadata: subMetadata["en"],
|
||||
lastUpdated: await this.getLastModifiedDate(subPath),
|
||||
const isListed = packageItem.items?.some((i) => i.path === relativePath)
|
||||
console.log(`${entry.name} is ${isListed ? "already listed" : "not listed"}`)
|
||||
|
||||
if (!isListed) {
|
||||
const subItem = {
|
||||
type: localizedSubMetadata.type,
|
||||
path: relativePath,
|
||||
metadata: localizedSubMetadata,
|
||||
lastUpdated: await this.getLastModifiedDate(subPath),
|
||||
}
|
||||
packageItem.items = packageItem.items || []
|
||||
packageItem.items.push(subItem)
|
||||
console.log(`Added ${entry.name} to items`)
|
||||
}
|
||||
packageItem.items = packageItem.items || []
|
||||
packageItem.items.push(subItem)
|
||||
console.log(`Added ${entry.name} to items`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import {
|
|||
PackageManagerSource,
|
||||
ComponentType,
|
||||
ComponentMetadata,
|
||||
LocalizationOptions,
|
||||
} from "./types"
|
||||
import { getUserLocale } from "./utils"
|
||||
|
||||
/**
|
||||
* Service for managing package manager data
|
||||
|
|
@ -23,7 +25,11 @@ export class PackageManagerManager {
|
|||
private cache: Map<string, { data: PackageManagerRepository; timestamp: number }> = new Map()
|
||||
|
||||
constructor(private readonly context: vscode.ExtensionContext) {
|
||||
this.gitFetcher = new GitFetcher(context)
|
||||
const localizationOptions: LocalizationOptions = {
|
||||
userLocale: getUserLocale(),
|
||||
fallbackLocale: "en",
|
||||
}
|
||||
this.gitFetcher = new GitFetcher(context, localizationOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -430,16 +436,6 @@ export class PackageManagerManager {
|
|||
this.clearCache()
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to check if an item matches the given filters
|
||||
*/
|
||||
/**
|
||||
* Helper method to check if an item matches the given filters
|
||||
*/
|
||||
/**
|
||||
* Helper method to check if an item matches the given filters
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper method to get the sort value for an item
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
import { MetadataScanner } from "../MetadataScanner"
|
||||
import { ComponentMetadata, LocalizationOptions, LocalizedMetadata } from "../types"
|
||||
|
||||
describe("getLocalizedMetadata", () => {
|
||||
let metadataScanner: MetadataScanner
|
||||
|
||||
beforeEach(() => {
|
||||
// Initialize with French locale
|
||||
const localizationOptions: LocalizationOptions = {
|
||||
userLocale: "fr",
|
||||
fallbackLocale: "en",
|
||||
}
|
||||
metadataScanner = new MetadataScanner(undefined, localizationOptions)
|
||||
})
|
||||
|
||||
test("should use user locale when available", () => {
|
||||
// Create mock metadata with both user locale and English
|
||||
const metadata: LocalizedMetadata<ComponentMetadata> = {
|
||||
en: {
|
||||
name: "English Name",
|
||||
description: "English Description",
|
||||
version: "1.0.0",
|
||||
type: "mode",
|
||||
},
|
||||
fr: {
|
||||
name: "Nom Français",
|
||||
description: "Description Française",
|
||||
version: "1.0.0",
|
||||
type: "mode",
|
||||
},
|
||||
}
|
||||
|
||||
// Call getLocalizedMetadata
|
||||
const result = (metadataScanner as any).getLocalizedMetadata(metadata)
|
||||
|
||||
// Expect French metadata to be used
|
||||
expect(result).toBeDefined()
|
||||
expect(result.name).toBe("Nom Français")
|
||||
expect(result.description).toBe("Description Française")
|
||||
})
|
||||
|
||||
test("should fall back to English when user locale not available", () => {
|
||||
// Create mock metadata with only English
|
||||
const metadata: LocalizedMetadata<ComponentMetadata> = {
|
||||
en: {
|
||||
name: "English Name",
|
||||
description: "English Description",
|
||||
version: "1.0.0",
|
||||
type: "mode",
|
||||
},
|
||||
}
|
||||
|
||||
// Call getLocalizedMetadata
|
||||
const result = (metadataScanner as any).getLocalizedMetadata(metadata)
|
||||
|
||||
// Expect English metadata to be used as fallback
|
||||
expect(result).toBeDefined()
|
||||
expect(result.name).toBe("English Name")
|
||||
expect(result.description).toBe("English Description")
|
||||
})
|
||||
|
||||
test("should return null when neither user locale nor fallback locale is available", () => {
|
||||
// Create mock metadata with neither user locale nor English
|
||||
const metadata: LocalizedMetadata<ComponentMetadata> = {
|
||||
de: {
|
||||
name: "Deutscher Name",
|
||||
description: "Deutsche Beschreibung",
|
||||
version: "1.0.0",
|
||||
type: "mode",
|
||||
},
|
||||
}
|
||||
|
||||
// Call getLocalizedMetadata
|
||||
const result = (metadataScanner as any).getLocalizedMetadata(metadata)
|
||||
|
||||
// Expect null result
|
||||
expect(result).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
mockFs.readdir.mockImplementation((dir, options) => {
|
||||
console.log("Mock readdir called with:", dir)
|
||||
const result = [
|
||||
{ name: "metadata.en.yml", isFile: () => true, isDirectory: () => false },
|
||||
{ name: "metadata.fr.yml", isFile: () => true, isDirectory: () => false },
|
||||
] as any
|
||||
console.log("Mock readdir returning:", result)
|
||||
return Promise.resolve(result)
|
||||
})
|
||||
|
|
@ -5,8 +5,7 @@
|
|||
/**
|
||||
* Default package manager repository URL
|
||||
*/
|
||||
export const DEFAULT_PACKAGE_MANAGER_REPO_URL =
|
||||
"https://github.com/RooVetGit/Roo-Code/tree/main/package-manager-template"
|
||||
export const DEFAULT_PACKAGE_MANAGER_REPO_URL = "https://github.com/RooVetGit/Roo-Code-Packages"
|
||||
|
||||
/**
|
||||
* Default package manager repository name
|
||||
|
|
|
|||
|
|
@ -110,3 +110,11 @@ export interface PackageManagerRepository {
|
|||
export type LocalizedMetadata<T> = {
|
||||
[locale: string]: T
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for localization handling
|
||||
*/
|
||||
export interface LocalizationOptions {
|
||||
userLocale: string
|
||||
fallbackLocale: string
|
||||
}
|
||||
|
|
|
|||
13
src/services/package-manager/utils.ts
Normal file
13
src/services/package-manager/utils.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import * as vscode from "vscode"
|
||||
|
||||
/**
|
||||
* Gets the user's locale from VS Code environment
|
||||
* @returns The user's locale code (e.g., 'en', 'fr')
|
||||
*/
|
||||
export function getUserLocale(): string {
|
||||
// Get from VS Code API
|
||||
const vscodeLocale = vscode.env.language
|
||||
|
||||
// Extract just the language part (e.g., "en-US" -> "en")
|
||||
return vscodeLocale.split("-")[0].toLowerCase()
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue