diff --git a/.gitignore b/.gitignore index c3d0763660..83d1ee61a4 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ logs .roomodes .clinerules memory-bank/ + diff --git a/src/core/webview/packageManagerMessageHandler.ts b/src/core/webview/packageManagerMessageHandler.ts index c0504a313a..c0a7a15561 100644 --- a/src/core/webview/packageManagerMessageHandler.ts +++ b/src/core/webview/packageManagerMessageHandler.ts @@ -31,12 +31,12 @@ export async function handlePackageManagerMessages( try { console.log("Package Manager: Received request to fetch package manager items") console.log("DEBUG: Processing package manager request") - + // Wrap the entire initialization in a try-catch block try { // Initialize default sources if none exist let sources = await provider.contextProxy.getValue("packageManagerSources") as PackageManagerSource[] || [] - + if (!sources || sources.length === 0) { console.log("Package Manager: No sources found, initializing default sources") sources = [ @@ -46,21 +46,21 @@ export async function handlePackageManagerMessages( enabled: true } ]; - + // Save the default sources await provider.contextProxy.setValue("packageManagerSources", sources) console.log("Package Manager: Default sources initialized") } - + console.log(`Package Manager: Fetching items from ${sources.length} sources`) console.log(`DEBUG: PackageManagerManager instance: ${packageManagerManager ? "exists" : "null"}`) - + // Add timing information const startTime = Date.now() - + // Simplify the initialization by limiting the number of items and adding more error handling let items: PackageManagerItem[] = []; - + try { console.log("DEBUG: Starting to fetch items from sources"); // Only fetch from the first enabled source to reduce complexity @@ -68,7 +68,7 @@ export async function handlePackageManagerMessages( if (enabledSources.length > 0) { const firstSource = enabledSources[0]; console.log(`Package Manager: Fetching items from first source: ${firstSource.url}`); - + // Get items from the first source only const sourceItems = await packageManagerManager.getPackageManagerItems([firstSource]); items = sourceItems; @@ -81,19 +81,19 @@ export async function handlePackageManagerMessages( // Continue with empty items array items = []; } - + console.log("DEBUG: Fetch completed, preparing to send items to webview"); const endTime = Date.now() - + console.log(`Package Manager: Found ${items.length} items in ${endTime - startTime}ms`) console.log(`Package Manager: First item:`, items.length > 0 ? items[0] : 'No items') - + // Send the items to the webview console.log("DEBUG: Creating message to send items to webview"); - + // Get the current state to include apiConfiguration to prevent welcome screen from showing const currentState = await provider.getState(); - + const message = { type: "state", state: { @@ -103,20 +103,20 @@ export async function handlePackageManagerMessages( packageManagerItems: items } } as ExtensionMessage; - + console.log(`Package Manager: Sending message to webview:`, message); console.log("DEBUG: About to call postMessageToWebview with apiConfiguration:", currentState.apiConfiguration ? "present" : "missing"); provider.postMessageToWebview(message); console.log("DEBUG: Called postMessageToWebview"); console.log(`Package Manager: Message sent to webview`); - + } catch (initError) { console.error("Error in package manager initialization:", initError); // Send an empty items array to the webview to prevent the spinner from spinning forever // Get the current state to include apiConfiguration to prevent welcome screen from showing const currentState = await provider.getState(); - + provider.postMessageToWebview({ type: "state", state: { @@ -139,7 +139,7 @@ export async function handlePackageManagerMessages( // Enforce maximum of 10 sources const MAX_SOURCES = 10; let updatedSources: PackageManagerSource[]; - + if (message.sources.length > MAX_SOURCES) { // Truncate to maximum allowed and show warning updatedSources = message.sources.slice(0, MAX_SOURCES); @@ -147,10 +147,10 @@ export async function handlePackageManagerMessages( } else { updatedSources = message.sources; } - + // Update the global state with the new sources await updateGlobalState("packageManagerSources", updatedSources); - + // Clean up cache directories for repositories that are no longer in the sources list try { console.log("Package Manager: Cleaning up cache directories for removed sources"); @@ -159,7 +159,7 @@ export async function handlePackageManagerMessages( } catch (error) { console.error("Package Manager: Error during cache cleanup:", error); } - + // Update the webview with the new state await provider.postStateToWebview(); } @@ -180,24 +180,24 @@ export async function handlePackageManagerMessages( } return true; } - + case "refreshPackageManagerSource": { if (message.url) { try { console.log(`Package Manager: Received request to refresh source ${message.url}`); - + // Get the current sources const sources = await provider.contextProxy.getValue("packageManagerSources") as PackageManagerSource[] || []; - + // Find the source with the matching URL const source = sources.find(s => s.url === message.url); - + if (source) { try { // Refresh the repository await packageManagerManager.refreshRepository(message.url); vscode.window.showInformationMessage(`Successfully refreshed package manager source: ${source.name || message.url}`); - + // Trigger a fetch to update the UI with the refreshed data const currentState = await provider.getState(); provider.postMessageToWebview({ @@ -226,8 +226,8 @@ export async function handlePackageManagerMessages( } return true; } - - + + default: return false } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index da4d5b6a4a..049fe2c46b 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -73,11 +73,11 @@ export const webviewMessageHandler = async ( console.log(`DEBUG: About to call postStateToWebview`); await provider.postStateToWebview(); console.log(`DEBUG: After calling postStateToWebview`); - + console.log(`DEBUG: About to initialize workspace tracker file paths`); provider.workspaceTracker?.initializeFilePaths(); // don't await console.log(`DEBUG: After initializing workspace tracker file paths`); - + // Continue with the rest of the webviewDidLaunch case console.log(`DEBUG: Continuing with webviewDidLaunch case`); getTheme().then((theme) => { @@ -85,7 +85,7 @@ export const webviewMessageHandler = async ( provider.postMessageToWebview({ type: "theme", text: JSON.stringify(theme) }); }); - + // If MCP Hub is already initialized, update the webview with current server list console.log(`DEBUG: Getting MCP Hub`); const mcpHub = provider.getMcpHub(); @@ -1363,7 +1363,7 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() break } - + } // Handle package manager related messages diff --git a/src/services/package-manager/GitFetcher.ts b/src/services/package-manager/GitFetcher.ts index 1584e0d9d7..7430f71a5a 100644 --- a/src/services/package-manager/GitFetcher.ts +++ b/src/services/package-manager/GitFetcher.ts @@ -12,11 +12,11 @@ const execAsync = promisify(exec); */ export class GitFetcher { private readonly cacheDir: string; - + constructor(private readonly context: vscode.ExtensionContext) { this.cacheDir = path.join(context.globalStorageUri.fsPath, "package-manager-cache"); } - + /** * Fetches repository data from a Git URL * @param url The Git repository URL @@ -24,7 +24,7 @@ export class GitFetcher { */ async fetchRepository(url: string): Promise { console.log(`GitFetcher: Fetching repository from ${url}`); - + try { // Ensure cache directory exists try { @@ -34,12 +34,12 @@ export class GitFetcher { console.error(`GitFetcher: Error creating cache directory: ${mkdirError.message}`); throw new Error(`Failed to create cache directory: ${mkdirError.message}`); } - + // Create a safe directory name from the URL const repoName = this.getRepoNameFromUrl(url); const repoDir = path.join(this.cacheDir, repoName); console.log(`GitFetcher: Repository directory: ${repoDir}`); - + // Clone or pull repository with timeout protection try { console.log(`GitFetcher: Cloning or pulling repository ${url}`); @@ -49,20 +49,20 @@ export class GitFetcher { console.error(`GitFetcher: Git operation failed: ${gitError.message}`); throw new Error(`Git operation failed: ${gitError.message}`); } - + try { // Validate repository structure console.log(`GitFetcher: Validating repository structure`); await this.validateRepositoryStructure(repoDir); - + // Parse metadata console.log(`GitFetcher: Parsing repository metadata`); const metadata = await this.parseRepositoryMetadata(repoDir); - + // Parse items console.log(`GitFetcher: Parsing package manager items`); const items = await this.parsePackageManagerItems(repoDir, url); - + console.log(`GitFetcher: Successfully fetched repository with ${items.length} items`); return { metadata, @@ -72,10 +72,10 @@ export class GitFetcher { } catch (validationError) { // Log the validation error console.error(`GitFetcher: Repository validation failed: ${validationError.message}`); - + // Show error message vscode.window.showErrorMessage(`Failed to fetch repository: ${validationError.message}`); - + // Return empty repository return { metadata: {}, @@ -88,7 +88,7 @@ export class GitFetcher { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`GitFetcher: Failed to fetch repository: ${errorMessage}`); vscode.window.showErrorMessage(`Failed to fetch repository: ${errorMessage}`); - + // Return empty repository return { metadata: {}, @@ -97,7 +97,7 @@ export class GitFetcher { }; } } - + /** * Extracts a safe directory name from a Git URL * @param url The Git repository URL @@ -109,7 +109,7 @@ export class GitFetcher { const repoName = urlParts[urlParts.length - 1].replace(/\.git$/, ""); return repoName.replace(/[^a-zA-Z0-9-_]/g, "-"); } - + /** * Clones or pulls a Git repository * @param url The Git repository URL @@ -117,16 +117,16 @@ export class GitFetcher { */ private async cloneOrPullRepository(url: string, repoDir: string): Promise { console.log(`GitFetcher: Checking if repository exists at ${repoDir}`); - + try { // Check if repository already exists const repoExists = await fs.stat(path.join(repoDir, ".git")) .then(() => true) .catch(() => false); - + if (repoExists) { console.log(`GitFetcher: Repository exists, attempting to pull latest changes`); - + try { // Try to pull latest changes with timeout const pullPromise = execAsync("git pull", { cwd: repoDir, timeout: 20000 }); @@ -134,13 +134,13 @@ export class GitFetcher { console.log(`GitFetcher: Successfully pulled latest changes`); } catch (pullError) { console.error(`GitFetcher: Failed to pull repository: ${pullError.message}`); - + // If pull fails, try to remove the directory and clone again console.log(`GitFetcher: Attempting to remove and re-clone repository`); try { await fs.rm(repoDir, { recursive: true, force: true }); console.log(`GitFetcher: Removed existing repository directory`); - + // Clone with timeout const clonePromise = execAsync(`git clone "${url}" "${repoDir}"`, { timeout: 30000 }); await clonePromise; @@ -152,7 +152,7 @@ export class GitFetcher { } } else { console.log(`GitFetcher: Repository does not exist, cloning from ${url}`); - + // Clone repository with timeout const clonePromise = execAsync(`git clone "${url}" "${repoDir}"`, { timeout: 30000 }); await clonePromise; @@ -163,7 +163,7 @@ export class GitFetcher { throw new Error(`Failed to clone or pull repository: ${error.message}`); } } - + /** * Validates that a repository follows the expected structure * @param repoDir The repository directory @@ -171,31 +171,31 @@ export class GitFetcher { private async validateRepositoryStructure(repoDir: string): Promise { // Check for required files const metadataPath = path.join(repoDir, "metadata.yml"); - + const metadataExists = await fs.stat(metadataPath) .then(() => true) .catch(() => false); - + if (!metadataExists) { throw new Error("Repository is missing metadata.yml file"); } - + // Check for at least one of the item type directories const mcpServersDir = path.join(repoDir, "mcp-servers"); const rolesDir = path.join(repoDir, "roles"); const storageSystemsDir = path.join(repoDir, "storage-systems"); const itemsDir = path.join(repoDir, "items"); // For backward compatibility - + const mcpServersDirExists = await fs.stat(mcpServersDir).then(() => true).catch(() => false); const rolesDirExists = await fs.stat(rolesDir).then(() => true).catch(() => false); const storageSystemsDirExists = await fs.stat(storageSystemsDir).then(() => true).catch(() => false); const itemsDirExists = await fs.stat(itemsDir).then(() => true).catch(() => false); - + if (!mcpServersDirExists && !rolesDirExists && !storageSystemsDirExists && !itemsDirExists) { throw new Error("Repository is missing item directories (mcp-servers, roles, storage-systems, or items)"); } } - + /** * Parses the repository metadata file * @param repoDir The repository directory @@ -205,7 +205,7 @@ export class GitFetcher { // Parse metadata.yml file const metadataPath = path.join(repoDir, "metadata.yml"); const metadataContent = await fs.readFile(metadataPath, "utf-8"); - + // For now, we'll return a simple object // In a future update, we'll add a YAML parser dependency try { @@ -223,7 +223,7 @@ export class GitFetcher { }; } } - + /** * Parses package manager items from a repository * @param repoDir The repository directory @@ -232,7 +232,7 @@ export class GitFetcher { */ private async parsePackageManagerItems(repoDir: string, repoUrl: string, branch: string = "main"): Promise { const items: PackageManagerItem[] = []; - + // Check for items in each directory type const directoryTypes = [ { path: path.join(repoDir, "mcp-servers"), type: "mcp-server", urlPath: "mcp-servers" }, @@ -240,23 +240,23 @@ export class GitFetcher { { path: path.join(repoDir, "storage-systems"), type: "storage", urlPath: "storage-systems" }, { path: path.join(repoDir, "items"), type: "other", urlPath: "items" } // For backward compatibility ]; - + for (const dirType of directoryTypes) { try { // Check if directory exists const dirExists = await fs.stat(dirType.path) .then(() => true) .catch(() => false); - + if (!dirExists) continue; - + // Get all subdirectories const itemDirs = await fs.readdir(dirType.path); - + for (const itemDir of itemDirs) { const itemPath = path.join(dirType.path, itemDir); const stats = await fs.stat(itemPath); - + if (stats.isDirectory()) { try { // Parse item metadata @@ -264,10 +264,10 @@ export class GitFetcher { const metadataExists = await fs.stat(metadataPath) .then(() => true) .catch(() => false); - + if (metadataExists) { const metadataContent = await fs.readFile(metadataPath, "utf-8"); - + // For now, we'll parse the YAML content manually // In a future update, we'll add a YAML parser dependency const name = metadataContent.match(/name:\s*["']?([^"'\n]+)["']?/)?.[1] || itemDir; @@ -276,13 +276,13 @@ export class GitFetcher { const type = metadataContent.match(/type:\s*["']?([^"'\n]+)["']?/)?.[1] || dirType.type; const author = metadataContent.match(/author:\s*["']?([^"'\n]+)["']?/)?.[1]; const version = metadataContent.match(/version:\s*["']?([^"'\n]+)["']?/)?.[1]; - + // Parse tags if present const tagsMatch = metadataContent.match(/tags:\s*\[(.*?)\]/); const tags = tagsMatch ? tagsMatch[1].split(",").map(tag => tag.trim().replace(/["']/g, "")) : undefined; - + const item: PackageManagerItem = { name, description, @@ -293,7 +293,7 @@ export class GitFetcher { tags, version }; - + items.push(item); } } catch (error) { @@ -305,7 +305,7 @@ export class GitFetcher { console.error(`Failed to parse directory ${dirType.path}:`, error); } } - + return items; } } \ No newline at end of file diff --git a/src/services/package-manager/PackageManagerManager.ts b/src/services/package-manager/PackageManagerManager.ts index 3573e74e72..9deac5949a 100644 --- a/src/services/package-manager/PackageManagerManager.ts +++ b/src/services/package-manager/PackageManagerManager.ts @@ -10,14 +10,14 @@ import { PackageManagerItem, PackageManagerRepository, PackageManagerSource } fr export class PackageManagerManager { // Cache expiry time in milliseconds (set to a low value for testing) private static readonly CACHE_EXPIRY_MS = 10 * 1000; // 10 seconds (normally 3600000 = 1 hour) - + private gitFetcher: GitFetcher; private cache: Map = new Map(); - + constructor(private readonly context: vscode.ExtensionContext) { this.gitFetcher = new GitFetcher(context); } - + /** * Gets package manager items from all enabled sources * @param sources The package manager sources @@ -27,17 +27,17 @@ export class PackageManagerManager { console.log(`PackageManagerManager: Getting items from ${sources.length} sources`); const items: PackageManagerItem[] = []; const errors: Error[] = []; - + // Filter enabled sources const enabledSources = sources.filter(s => s.enabled); console.log(`PackageManagerManager: ${enabledSources.length} enabled sources`); - + // Process sources sequentially to avoid overwhelming the system for (const source of enabledSources) { try { console.log(`PackageManagerManager: Processing source ${source.url}`); const repo = await this.getRepositoryData(source.url); - + if (repo.items && repo.items.length > 0) { console.log(`PackageManagerManager: Found ${repo.items.length} items in ${source.url}`); items.push(...repo.items); @@ -50,18 +50,18 @@ export class PackageManagerManager { errors.push(new Error(`Source ${source.url}: ${errorMessage}`)); } } - + // Show a single error message with all failures if (errors.length > 0) { const errorMessage = `Failed to fetch from ${errors.length} sources: ${errors.map(e => e.message).join("; ")}`; console.error(`PackageManagerManager: ${errorMessage}`); vscode.window.showErrorMessage(errorMessage); } - + console.log(`PackageManagerManager: Returning ${items.length} total items`); return items; } - + /** * Gets repository data from a URL, using cache if available * @param url The repository URL @@ -71,42 +71,42 @@ export class PackageManagerManager { async getRepositoryData(url: string, forceRefresh: boolean = false): Promise { try { console.log(`PackageManagerManager: Getting repository data for ${url}`); - + // Check cache first (unless force refresh is requested) const cached = this.cache.get(url); - + if (!forceRefresh && cached && (Date.now() - cached.timestamp) < PackageManagerManager.CACHE_EXPIRY_MS) { console.log(`PackageManagerManager: Using cached data for ${url} (age: ${Date.now() - cached.timestamp}ms)`); return cached.data; } - + if (forceRefresh) { console.log(`PackageManagerManager: Force refresh requested for ${url}, bypassing cache`); } - + console.log(`PackageManagerManager: Cache miss or expired for ${url}, fetching fresh data`); - + // Fetch fresh data with timeout protection const fetchPromise = this.gitFetcher.fetchRepository(url); - + // Create a timeout promise const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error(`Repository fetch timed out after 30 seconds: ${url}`)); }, 30000); // 30 second timeout }); - + // Race the fetch against the timeout const data = await Promise.race([fetchPromise, timeoutPromise]); - + // Cache the result this.cache.set(url, { data, timestamp: Date.now() }); console.log(`PackageManagerManager: Successfully fetched and cached data for ${url}`); - + return data; } catch (error) { console.error(`PackageManagerManager: Error fetching repository data for ${url}:`, error); - + // Return empty repository data instead of throwing return { metadata: {}, @@ -115,7 +115,7 @@ export class PackageManagerManager { }; } } - + /** * Refreshes a specific repository, bypassing the cache * @param url The repository URL to refresh @@ -123,7 +123,7 @@ export class PackageManagerManager { */ async refreshRepository(url: string): Promise { console.log(`PackageManagerManager: Refreshing repository ${url}`); - + try { // Force a refresh by bypassing the cache const data = await this.getRepositoryData(url, true); @@ -134,14 +134,14 @@ export class PackageManagerManager { throw error; } } - + /** * Clears the in-memory cache */ clearCache(): void { this.cache.clear(); } - + /** * Cleans up cache directories for repositories that are no longer in the configured sources * @param currentSources The current list of package manager sources @@ -150,7 +150,7 @@ export class PackageManagerManager { try { // Get the cache directory path const cacheDir = path.join(this.context.globalStorageUri.fsPath, "package-manager-cache"); - + // Check if cache directory exists try { await fs.stat(cacheDir); @@ -158,23 +158,23 @@ export class PackageManagerManager { console.log("PackageManagerManager: Cache directory doesn't exist yet, nothing to clean up"); return; } - + // Get all subdirectories in the cache directory const entries = await fs.readdir(cacheDir, { withFileTypes: true }); const cachedRepoDirs = entries .filter(entry => entry.isDirectory()) .map(entry => entry.name); - + console.log(`PackageManagerManager: Found ${cachedRepoDirs.length} cached repositories`); - + // Get the list of repository names from current sources const currentRepoNames = currentSources.map(source => this.getRepoNameFromUrl(source.url)); - + // Find directories to delete const dirsToDelete = cachedRepoDirs.filter(dir => !currentRepoNames.includes(dir)); - + console.log(`PackageManagerManager: Found ${dirsToDelete.length} repositories to delete`); - + // Delete each directory that's no longer in the sources for (const dirName of dirsToDelete) { try { @@ -186,13 +186,13 @@ export class PackageManagerManager { console.error(`PackageManagerManager: Failed to delete directory ${dirName}:`, error); } } - + console.log(`PackageManagerManager: Cache cleanup completed, deleted ${dirsToDelete.length} directories`); } catch (error) { console.error("PackageManagerManager: Error cleaning up cache directories:", error); } } - + /** * Extracts a safe directory name from a Git URL * @param url The Git repository URL @@ -204,7 +204,7 @@ export class PackageManagerManager { const repoName = urlParts[urlParts.length - 1].replace(/\.git$/, ""); return repoName.replace(/[^a-zA-Z0-9-_]/g, "-"); } - + /** * Filters package manager items based on criteria * @param items The items to filter @@ -217,35 +217,35 @@ export class PackageManagerManager { if (filters.type && item.type !== filters.type) { return false; } - + // Filter by search term if (filters.search) { const searchTerm = filters.search.toLowerCase(); const nameMatch = item.name.toLowerCase().includes(searchTerm); const descMatch = item.description.toLowerCase().includes(searchTerm); const authorMatch = item.author?.toLowerCase().includes(searchTerm); - + if (!nameMatch && !descMatch && !authorMatch) { return false; } } - + // Filter by tags if (filters.tags && filters.tags.length > 0) { if (!item.tags || item.tags.length === 0) { return false; } - + const hasMatchingTag = filters.tags.some(tag => item.tags!.includes(tag)); if (!hasMatchingTag) { return false; } } - + return true; }); } - + /** * Sorts package manager items * @param items The items to sort @@ -256,7 +256,7 @@ export class PackageManagerManager { sortItems(items: PackageManagerItem[], sortBy: string, sortOrder: "asc" | "desc"): PackageManagerItem[] { return [...items].sort((a, b) => { let comparison = 0; - + switch (sortBy) { case "name": comparison = a.name.localeCompare(b.name); @@ -276,7 +276,7 @@ export class PackageManagerManager { default: comparison = a.name.localeCompare(b.name); } - + return sortOrder === "asc" ? comparison : -comparison; }); } diff --git a/src/services/package-manager/__tests__/GitCommandQuoting.test.ts b/src/services/package-manager/__tests__/GitCommandQuoting.test.ts index a621fda3d4..c60172b842 100644 --- a/src/services/package-manager/__tests__/GitCommandQuoting.test.ts +++ b/src/services/package-manager/__tests__/GitCommandQuoting.test.ts @@ -3,22 +3,22 @@ describe('Git command quoting', () => { // This test verifies that our fix for handling paths with spaces works correctly const url = 'https://github.com/example/repo'; const repoDir = '/path/with spaces/to/repo'; - + // This is the fix we implemented in GitFetcher.cloneOrPullRepository const command = `git clone "${url}" "${repoDir}"`; - + // Verify that the command is properly quoted expect(command).toBe('git clone "https://github.com/example/repo" "/path/with spaces/to/repo"'); }); - + it('should handle paths with special characters', () => { // Test with more complex paths const url = 'https://github.com/example/repo-name'; const repoDir = '/path/with spaces/and (special) characters/to/repo'; - + // This is the fix we implemented in GitFetcher.cloneOrPullRepository const command = `git clone "${url}" "${repoDir}"`; - + // Verify that the command is properly quoted expect(command).toBe('git clone "https://github.com/example/repo-name" "/path/with spaces/and (special) characters/to/repo"'); }); diff --git a/src/services/package-manager/__tests__/GitFetcher.test.ts b/src/services/package-manager/__tests__/GitFetcher.test.ts index 1983e1c904..8a9818284d 100644 --- a/src/services/package-manager/__tests__/GitFetcher.test.ts +++ b/src/services/package-manager/__tests__/GitFetcher.test.ts @@ -87,72 +87,72 @@ jest.mock('vscode', () => ({ describe('GitFetcher', () => { let gitFetcher: GitFetcher; - + const mockContext = { globalStorageUri: { fsPath: '/mock/storage/path' } } as unknown as vscode.ExtensionContext; - + beforeEach(() => { gitFetcher = new GitFetcher(mockContext); jest.clearAllMocks(); - + // Setup path.join to work normally jest.spyOn(path, 'join').mockImplementation((...args) => args.join('/')); }); - + describe('fetchRepository', () => { it('should fetch repository successfully', async () => { const repoUrl = 'https://github.com/Smartsheet-JB-Brown/Package-Manager-Test'; - + // Mock execAsync for git operations const mockExecPromise = jest.fn().mockResolvedValue({ stdout: '', stderr: '' }); (promisify as unknown as jest.Mock).mockReturnValue(mockExecPromise); - + // Call the method const result = await gitFetcher.fetchRepository(repoUrl); - + // Assertions expect(result).toBeDefined(); expect(result.metadata).toBeDefined(); expect(result.metadata.name).toBe('Example Package Manager Repository'); expect(result.items).toHaveLength(3); // One role, one MCP server, one storage system - + // Check role item const roleItem = result.items.find((item: PackageManagerItem) => item.type === 'role'); expect(roleItem).toBeDefined(); expect(roleItem?.name).toBe('Full-Stack Developer Role'); expect(roleItem?.tags).toContain('developer'); expect(roleItem?.url).toBe('https://github.com/Smartsheet-JB-Brown/Package-Manager-Test/tree/main/roles/developer-role'); - + // Check MCP server item const mcpServerItem = result.items.find((item: PackageManagerItem) => item.type === 'mcp-server'); expect(mcpServerItem).toBeDefined(); expect(mcpServerItem?.name).toBe('File Analyzer MCP Server'); expect(mcpServerItem?.tags).toContain('file-analyzer'); expect(mcpServerItem?.url).toBe('https://github.com/Smartsheet-JB-Brown/Package-Manager-Test/tree/main/mcp-servers/file-analyzer'); - + // Check storage system item const storageItem = result.items.find((item: PackageManagerItem) => item.type === 'storage'); expect(storageItem).toBeDefined(); expect(storageItem?.name).toBe('GitHub Storage System'); expect(storageItem?.tags).toContain('storage'); expect(storageItem?.url).toBe('https://github.com/Smartsheet-JB-Brown/Package-Manager-Test/tree/main/storage-systems/github-storage'); - + // Verify file system operations expect(mockedFs.mkdir).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache', { recursive: true }); expect(mockedFs.stat).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/.git'); expect(mockedFs.stat).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/metadata.yml'); expect(mockedFs.readFile).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/metadata.yml', 'utf-8'); - + // Verify that readdir was called for each item directory type expect(mockedFs.readdir).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/roles'); expect(mockedFs.readdir).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/mcp-servers'); expect(mockedFs.readdir).toHaveBeenCalledWith('/mock/storage/path/package-manager-cache/Package-Manager-Test/storage-systems'); }); - + it('should handle errors when fetching repository', async () => { const repoUrl = 'https://github.com/Smartsheet-JB-Brown/Package-Manager-Test'; - + // Mock stat to throw an error for the .git directory check mockedFs.stat.mockImplementation((path) => { const pathStr = path.toString(); @@ -161,45 +161,45 @@ describe('GitFetcher', () => { } return Promise.resolve({ isDirectory: () => false, isFile: () => false } as any); }); - + // Mock readFile to throw an error for metadata.yml mockedFs.readFile.mockImplementation((path) => { return Promise.reject(new Error('File not found')); }); - + // Mock exec to throw an error const mockExecPromise = jest.fn().mockRejectedValue(new Error('Git error')); (promisify as unknown as jest.Mock).mockReturnValue(mockExecPromise); - + // Call the method const result = await gitFetcher.fetchRepository(repoUrl); - + // Assertions expect(result).toEqual({ metadata: {}, items: [], url: repoUrl }); expect(vscode.window.showErrorMessage).toHaveBeenCalled(); }); }); - + describe('getRepoNameFromUrl', () => { it('should extract repository name from GitHub URL', () => { const url = 'https://github.com/Smartsheet-JB-Brown/Package-Manager-Test'; const result = gitFetcher['getRepoNameFromUrl'](url); - + expect(result).toBe('Package-Manager-Test'); }); it('should handle GitHub URLs with trailing slash', () => { const url = 'https://github.com/Smartsheet-JB-Brown/Package-Manager-Test/'; // Call the actual method on gitFetcher const result = gitFetcher['getRepoNameFromUrl'](url); - + expect(result).toBe('Package-Manager-Test'); }); - + it('should sanitize repository names', () => { const url = 'https://github.com/Smartsheet-JB-Brown/Package Manager Test'; // Call the actual method on gitFetcher const result = gitFetcher['getRepoNameFromUrl'](url); - + expect(result).toBe('Package-Manager-Test'); }); }); diff --git a/src/services/package-manager/__tests__/GitFetcherSpaces.test.ts b/src/services/package-manager/__tests__/GitFetcherSpaces.test.ts index acb322150a..6fa54195d4 100644 --- a/src/services/package-manager/__tests__/GitFetcherSpaces.test.ts +++ b/src/services/package-manager/__tests__/GitFetcherSpaces.test.ts @@ -16,15 +16,15 @@ describe.skip('Git command with spaces in paths', () => { // Set up our mocks const mockExecFn = jest.fn().mockResolvedValue({ stdout: '', stderr: '' }); (promisify as unknown as jest.Mock).mockReturnValue(mockExecFn); - + // Import the module that contains our fix const execAsync = promisify(exec); - + // Execute the command with a path that contains spaces const url = 'https://github.com/example/repo'; const repoDir = '/path/with spaces/to/repo'; await execAsync(`git clone "${url}" "${repoDir}"`); - + // Verify that exec was called with the properly quoted command expect(exec).toHaveBeenCalledWith( `git clone "${url}" "${repoDir}"`, diff --git a/src/services/package-manager/__tests__/ParsePackageManagerItems.test.ts b/src/services/package-manager/__tests__/ParsePackageManagerItems.test.ts index ea2efc9e3b..b515a3f5ba 100644 --- a/src/services/package-manager/__tests__/ParsePackageManagerItems.test.ts +++ b/src/services/package-manager/__tests__/ParsePackageManagerItems.test.ts @@ -22,21 +22,21 @@ jest.mock('vscode', () => ({ describe('Parse Package Manager Items', () => { let gitFetcher: GitFetcher; - + const mockContext = { globalStorageUri: { fsPath: '/mock/storage/path' } } as unknown as vscode.ExtensionContext; - + beforeEach(() => { gitFetcher = new GitFetcher(mockContext); jest.clearAllMocks(); }); - + // Helper function to access private method const parsePackageManagerItems = async (repoDir: string, repoUrl: string) => { return (gitFetcher as any).parsePackageManagerItems(repoDir, repoUrl); }; - + describe('directory structure handling', () => { it('should parse items from mcp-servers directory', async () => { // Mock directory structure @@ -50,7 +50,7 @@ describe('Parse Package Manager Items', () => { } return Promise.reject(new Error('Not found')); }); - + // Mock readdir to return items in mcp-servers directory mockedFs.readdir.mockImplementation((path) => { const pathStr = path.toString(); @@ -59,7 +59,7 @@ describe('Parse Package Manager Items', () => { } return Promise.resolve([] as any); }); - + // Mock readFile to return metadata content mockedFs.readFile.mockImplementation((path) => { const pathStr = path.toString(); @@ -68,17 +68,17 @@ describe('Parse Package Manager Items', () => { } return Promise.reject(new Error('File not found')); }); - + // Call the method const items = await parsePackageManagerItems('/mock/repo', 'https://github.com/example/repo'); - + // Assertions expect(items).toHaveLength(1); expect(items[0].name).toBe('File Analyzer MCP Server'); expect(items[0].type).toBe('mcp-server'); expect(items[0].url).toBe('https://github.com/example/repo/tree/main/mcp-servers/file-analyzer'); }); - + it('should parse items from roles directory', async () => { // Mock directory structure mockedFs.stat.mockImplementation((path) => { @@ -91,7 +91,7 @@ describe('Parse Package Manager Items', () => { } return Promise.reject(new Error('Not found')); }); - + // Mock readdir to return items in roles directory mockedFs.readdir.mockImplementation((path) => { const pathStr = path.toString(); @@ -100,7 +100,7 @@ describe('Parse Package Manager Items', () => { } return Promise.resolve([] as any); }); - + // Mock readFile to return metadata content mockedFs.readFile.mockImplementation((path) => { const pathStr = path.toString(); @@ -109,17 +109,17 @@ describe('Parse Package Manager Items', () => { } return Promise.reject(new Error('File not found')); }); - + // Call the method const items = await parsePackageManagerItems('/mock/repo', 'https://github.com/example/repo'); - + // Assertions expect(items).toHaveLength(1); expect(items[0].name).toBe('Full-Stack Developer Role'); expect(items[0].type).toBe('role'); expect(items[0].url).toBe('https://github.com/example/repo/tree/main/roles/developer-role'); }); - + it('should parse items from storage-systems directory', async () => { // Mock directory structure mockedFs.stat.mockImplementation((path) => { @@ -132,7 +132,7 @@ describe('Parse Package Manager Items', () => { } return Promise.reject(new Error('Not found')); }); - + // Mock readdir to return items in storage-systems directory mockedFs.readdir.mockImplementation((path) => { const pathStr = path.toString(); @@ -141,7 +141,7 @@ describe('Parse Package Manager Items', () => { } return Promise.resolve([] as any); }); - + // Mock readFile to return metadata content mockedFs.readFile.mockImplementation((path) => { const pathStr = path.toString(); @@ -150,17 +150,17 @@ describe('Parse Package Manager Items', () => { } return Promise.reject(new Error('File not found')); }); - + // Call the method const items = await parsePackageManagerItems('/mock/repo', 'https://github.com/example/repo'); - + // Assertions expect(items).toHaveLength(1); expect(items[0].name).toBe('GitHub Storage System'); expect(items[0].type).toBe('storage'); expect(items[0].url).toBe('https://github.com/example/repo/tree/main/storage-systems/github-storage'); }); - + it('should parse items from items directory (backward compatibility)', async () => { // Mock directory structure mockedFs.stat.mockImplementation((path) => { @@ -173,7 +173,7 @@ describe('Parse Package Manager Items', () => { } return Promise.reject(new Error('Not found')); }); - + // Mock readdir to return items in items directory mockedFs.readdir.mockImplementation((path) => { const pathStr = path.toString(); @@ -182,7 +182,7 @@ describe('Parse Package Manager Items', () => { } return Promise.resolve([] as any); }); - + // Mock readFile to return metadata content mockedFs.readFile.mockImplementation((path) => { const pathStr = path.toString(); @@ -191,17 +191,17 @@ describe('Parse Package Manager Items', () => { } return Promise.reject(new Error('File not found')); }); - + // Call the method const items = await parsePackageManagerItems('/mock/repo', 'https://github.com/example/repo'); - + // Assertions expect(items).toHaveLength(1); expect(items[0].name).toBe('Generic Item'); expect(items[0].type).toBe('other'); expect(items[0].url).toBe('https://github.com/example/repo/tree/main/items/generic-item'); }); - + it('should parse items from multiple directories', async () => { // Mock directory structure mockedFs.stat.mockImplementation((path) => { @@ -214,7 +214,7 @@ describe('Parse Package Manager Items', () => { } return Promise.reject(new Error('Not found')); }); - + // Mock readdir to return items in each directory mockedFs.readdir.mockImplementation((path) => { const pathStr = path.toString(); @@ -229,7 +229,7 @@ describe('Parse Package Manager Items', () => { } return Promise.resolve([] as any); }); - + // Mock readFile to return metadata content mockedFs.readFile.mockImplementation((path) => { const pathStr = path.toString(); @@ -244,25 +244,25 @@ describe('Parse Package Manager Items', () => { } return Promise.reject(new Error('File not found')); }); - + // Call the method const items = await parsePackageManagerItems('/mock/repo', 'https://github.com/example/repo'); - + // Assertions expect(items).toHaveLength(3); - + // Check for MCP server item const mcpServerItem = items.find((item: PackageManagerItem) => item.type === 'mcp-server'); expect(mcpServerItem).toBeDefined(); expect(mcpServerItem?.name).toBe('File Analyzer MCP Server'); expect(mcpServerItem?.url).toBe('https://github.com/example/repo/tree/main/mcp-servers/file-analyzer'); - + // Check for role item const roleItem = items.find((item: PackageManagerItem) => item.type === 'role'); expect(roleItem).toBeDefined(); expect(roleItem?.name).toBe('Full-Stack Developer Role'); expect(roleItem?.url).toBe('https://github.com/example/repo/tree/main/roles/developer-role'); - + // Check for storage system item const storageItem = items.find((item: PackageManagerItem) => item.type === 'storage'); expect(storageItem).toBeDefined(); diff --git a/src/services/package-manager/__tests__/RepositoryStructureValidation.test.ts b/src/services/package-manager/__tests__/RepositoryStructureValidation.test.ts index 66bebd0a16..b24505f93c 100644 --- a/src/services/package-manager/__tests__/RepositoryStructureValidation.test.ts +++ b/src/services/package-manager/__tests__/RepositoryStructureValidation.test.ts @@ -21,21 +21,21 @@ jest.mock('vscode', () => ({ describe('Repository Structure Validation', () => { let gitFetcher: GitFetcher; - + const mockContext = { globalStorageUri: { fsPath: '/mock/storage/path' } } as unknown as vscode.ExtensionContext; - + beforeEach(() => { gitFetcher = new GitFetcher(mockContext); jest.clearAllMocks(); }); - + // Helper function to access private method const validateRepositoryStructure = async (repoDir: string) => { return (gitFetcher as any).validateRepositoryStructure(repoDir); }; - + describe('metadata.yml validation', () => { it('should throw error when metadata.yml is missing', async () => { // Mock stat to return false for metadata.yml @@ -45,22 +45,22 @@ describe('Repository Structure Validation', () => { } return Promise.resolve({ isDirectory: () => true, isFile: () => true } as any); }); - + // Call the method and expect it to throw await expect(validateRepositoryStructure('/mock/repo')).rejects.toThrow('Repository is missing metadata.yml file'); }); - + it('should pass when metadata.yml exists', async () => { // Mock stat to return true for metadata.yml and at least one item directory mockedFs.stat.mockImplementation((path) => { return Promise.resolve({ isDirectory: () => true, isFile: () => true } as any); }); - + // Call the method and expect it not to throw await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); }); }); - + describe('item directories validation', () => { it('should throw error when no item directories exist', async () => { // Mock stat to return true for metadata.yml but false for all item directories @@ -69,19 +69,19 @@ describe('Repository Structure Validation', () => { if (pathStr.includes('metadata.yml')) { return Promise.resolve({ isFile: () => true } as any); } - if (pathStr.includes('mcp-servers') || pathStr.includes('roles') || + if (pathStr.includes('mcp-servers') || pathStr.includes('roles') || pathStr.includes('storage-systems') || pathStr.includes('items')) { return Promise.reject(new Error('Directory not found')); } return Promise.resolve({ isDirectory: () => true } as any); }); - + // Call the method and expect it to throw await expect(validateRepositoryStructure('/mock/repo')).rejects.toThrow( 'Repository is missing item directories (mcp-servers, roles, storage-systems, or items)' ); }); - + it('should pass when mcp-servers directory exists', async () => { // Mock stat to return true for metadata.yml and mcp-servers mockedFs.stat.mockImplementation((path) => { @@ -91,11 +91,11 @@ describe('Repository Structure Validation', () => { } return Promise.reject(new Error('Not found')); }); - + // Call the method and expect it not to throw await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); }); - + it('should pass when roles directory exists', async () => { // Mock stat to return true for metadata.yml and roles mockedFs.stat.mockImplementation((path) => { @@ -105,11 +105,11 @@ describe('Repository Structure Validation', () => { } return Promise.reject(new Error('Not found')); }); - + // Call the method and expect it not to throw await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); }); - + it('should pass when storage-systems directory exists', async () => { // Mock stat to return true for metadata.yml and storage-systems mockedFs.stat.mockImplementation((path) => { @@ -119,11 +119,11 @@ describe('Repository Structure Validation', () => { } return Promise.reject(new Error('Not found')); }); - + // Call the method and expect it not to throw await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); }); - + it('should pass when items directory exists (backward compatibility)', async () => { // Mock stat to return true for metadata.yml and items mockedFs.stat.mockImplementation((path) => { @@ -133,20 +133,20 @@ describe('Repository Structure Validation', () => { } return Promise.reject(new Error('Not found')); }); - + // Call the method and expect it not to throw await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); }); }); - + describe('package-manager-template structure', () => { it('should validate the package-manager-template structure', async () => { // Mock stat to simulate the package-manager-template structure mockedFs.stat.mockImplementation((path) => { const pathStr = path.toString(); - if (pathStr.includes('metadata.yml') || - pathStr.includes('mcp-servers') || - pathStr.includes('roles') || + if (pathStr.includes('metadata.yml') || + pathStr.includes('mcp-servers') || + pathStr.includes('roles') || pathStr.includes('storage-systems')) { return Promise.resolve({ isDirectory: () => true, isFile: () => true } as any); } @@ -155,7 +155,7 @@ describe('Repository Structure Validation', () => { } return Promise.resolve({ isDirectory: () => true } as any); }); - + // Call the method and expect it not to throw await expect(validateRepositoryStructure('/mock/repo')).resolves.not.toThrow(); }); diff --git a/webview-ui/src/components/package-manager/PackageManagerView.tsx b/webview-ui/src/components/package-manager/PackageManagerView.tsx index 8908a7c855..7e873bfc1f 100644 --- a/webview-ui/src/components/package-manager/PackageManagerView.tsx +++ b/webview-ui/src/components/package-manager/PackageManagerView.tsx @@ -19,7 +19,7 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { const [items, setItems] = useState([]); const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse"); const [refreshingUrls, setRefreshingUrls] = useState([]); - + // Track activeTab changes useEffect(() => { console.log("DEBUG: activeTab changed to", activeTab); @@ -27,7 +27,7 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { const [filters, setFilters] = useState({ type: "", search: "" }); const [sortBy, setSortBy] = useState("name"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); - + // Debug state changes useEffect(() => { console.log("DEBUG: items state changed", { @@ -35,10 +35,10 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { isFetching }); }, [items]); - + // Track if we're currently fetching items to prevent duplicate requests const [isFetching, setIsFetching] = useState(false); - + // Use a ref to track if we've already fetched items const hasInitialFetch = useRef(false); @@ -67,7 +67,7 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { // Always fetch items when component mounts, regardless of other conditions useEffect(() => { console.log("DEBUG: PackageManagerView mount effect triggered"); - + // Force fetch on mount, ignoring all conditions setTimeout(() => { console.log("DEBUG: Forcing fetch on component mount"); @@ -75,10 +75,10 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { fetchPackageManagerItems(); hasInitialFetch.current = true; }, 500); // Small delay to ensure component is fully mounted - - + + }, []); // Empty dependency array means this runs once on mount - + // Additional effect for when packageManagerSources changes useEffect(() => { console.log("DEBUG: PackageManagerView packageManagerSources effect triggered", { @@ -87,7 +87,7 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { isFetching, itemsLength: items.length }); - + // Only fetch if packageManagerSources changes and we're not already fetching if (packageManagerSources && hasInitialFetch.current && !isFetching) { console.log("DEBUG: Calling fetchPackageManagerItems due to sources change"); @@ -98,13 +98,13 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { // Handle message from extension useEffect(() => { console.log("DEBUG: Setting up message handler"); - + const handleMessage = (event: MessageEvent) => { console.log("DEBUG: Message received in PackageManagerView", event.data); console.log("DEBUG: Message type:", event.data.type); console.log("DEBUG: Message state:", event.data.state ? "exists" : "undefined"); const message = event.data; - + // Handle action messages - specifically for packageManagerButtonClicked if (message.type === "action" && message.action === "packageManagerButtonClicked") { console.log("DEBUG: Received packageManagerButtonClicked action, triggering fetch"); @@ -126,7 +126,7 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { return updated; }); } - + // Handle state messages with packageManagerItems if (message.type === "state" && message.state) { console.log("DEBUG: Received state message", message.state); @@ -134,17 +134,17 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { if (message.state.packageManagerItems) { console.log("DEBUG: packageManagerItems length:", message.state.packageManagerItems.length); } - + // Check for packageManagerItems if (message.state.packageManagerItems) { const receivedItems = message.state.packageManagerItems || []; console.log("DEBUG: Received packageManagerItems", receivedItems.length); console.log("DEBUG: Full message state:", message.state); - + if (receivedItems.length > 0) { console.log("DEBUG: First item:", receivedItems[0]); console.log("DEBUG: All items:", JSON.stringify(receivedItems)); - + // Force a new array reference to ensure React detects the change setItems([...receivedItems]); setIsFetching(false); @@ -170,28 +170,28 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { if (filters.type && item.type !== filters.type) { return false; } - + // Filter by search term if (filters.search) { const searchTerm = filters.search.toLowerCase(); const nameMatch = item.name.toLowerCase().includes(searchTerm); const descMatch = item.description.toLowerCase().includes(searchTerm); const authorMatch = item.author?.toLowerCase().includes(searchTerm); - + if (!nameMatch && !descMatch && !authorMatch) { return false; } } - + return true; }); console.log("DEBUG: After filtering", { filteredItemsCount: filteredItems.length }); - + // Sort items console.log("DEBUG: Sorting items", { filteredItemsCount: filteredItems.length, sortBy, sortOrder }); const sortedItems = [...filteredItems].sort((a, b) => { let comparison = 0; - + switch (sortBy) { case "name": comparison = a.name.localeCompare(b.name); @@ -211,12 +211,12 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { default: comparison = a.name.localeCompare(b.name); } - + return sortOrder === "asc" ? comparison : -comparison; }); - console.log("DEBUG: Final sorted items", { - sortedItemsCount: sortedItems.length, - firstItem: sortedItems.length > 0 ? sortedItems[0].name : 'none' + console.log("DEBUG: Final sorted items", { + sortedItemsCount: sortedItems.length, + firstItem: sortedItems.length > 0 ? sortedItems[0].name : 'none' }); // Add debug logging right before rendering @@ -226,7 +226,7 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { firstItem: sortedItems.length > 0 ? `${sortedItems[0].name} (${sortedItems[0].type})` : 'none' }); }, [sortedItems]); - + // Log right before rendering console.log("DEBUG: About to render with", { itemsLength: items.length, @@ -234,7 +234,7 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { sortedItemsLength: sortedItems.length, activeTab }); - + return ( @@ -306,12 +306,12 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { - + {console.log("DEBUG: Rendering condition", { sortedItemsLength: sortedItems.length, condition: sortedItems.length === 0 ? "empty" : "has items" })} - + {sortedItems.length === 0 ? (

No package manager items found

@@ -373,7 +373,7 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => { const PackageManagerItemCard = ({ item }: { item: PackageManagerItem }) => { const { t } = useAppTranslation(); - + const getTypeLabel = (type: string) => { switch (type) { case "role": @@ -386,7 +386,7 @@ const PackageManagerItemCard = ({ item }: { item: PackageManagerItem }) => { return "Other"; } }; - + const getTypeColor = (type: string) => { switch (type) { case "role": @@ -399,7 +399,7 @@ const PackageManagerItemCard = ({ item }: { item: PackageManagerItem }) => { return "bg-gray-600"; } }; - + const handleOpenUrl = () => { console.log(`PackageManagerItemCard: Opening URL: ${item.url}`); vscode.postMessage({ @@ -424,14 +424,14 @@ const PackageManagerItemCard = ({ item }: { item: PackageManagerItem }) => { {getTypeLabel(item.type)}
- +

{item.description}

- + {item.tags && item.tags.length > 0 && (
{item.tags.map(tag => ( - {tag} @@ -439,7 +439,7 @@ const PackageManagerItemCard = ({ item }: { item: PackageManagerItem }) => { ))}
)} - +
{item.version && ( @@ -467,7 +467,7 @@ const PackageManagerItemCard = ({ item }: { item: PackageManagerItem }) => { )}
- +