locale fixes for metadata

This commit is contained in:
Smartsheet-JB-Brown 2025-04-14 13:33:58 -07:00
parent 71d30b835b
commit af6c27bdff
17 changed files with 2449 additions and 2662 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)
}
/**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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()
}