walking skeleton

This commit is contained in:
Smartsheet-JB-Brown 2025-04-10 12:26:12 -07:00
parent cd5b894578
commit 655899673b
12 changed files with 243 additions and 242 deletions

1
.gitignore vendored
View file

@ -37,3 +37,4 @@ logs
.roomodes
.clinerules
memory-bank/

View file

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

View file

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

View file

@ -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<PackageManagerRepository> {
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<void> {
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<void> {
// Check for required files
const metadataPath = path.join(repoDir, "metadata.yml");
const metadataExists = await fs.stat(metadataPath)
.then(() => true)
.catch(() => false);
if (!metadataExists) {
throw new Error("Repository is missing metadata.yml file");
}
// Check for at least one of the item type directories
const mcpServersDir = path.join(repoDir, "mcp-servers");
const rolesDir = path.join(repoDir, "roles");
const storageSystemsDir = path.join(repoDir, "storage-systems");
const itemsDir = path.join(repoDir, "items"); // For backward compatibility
const mcpServersDirExists = await fs.stat(mcpServersDir).then(() => true).catch(() => false);
const rolesDirExists = await fs.stat(rolesDir).then(() => true).catch(() => false);
const storageSystemsDirExists = await fs.stat(storageSystemsDir).then(() => true).catch(() => false);
const itemsDirExists = await fs.stat(itemsDir).then(() => true).catch(() => false);
if (!mcpServersDirExists && !rolesDirExists && !storageSystemsDirExists && !itemsDirExists) {
throw new Error("Repository is missing item directories (mcp-servers, roles, storage-systems, or items)");
}
}
/**
* Parses the repository metadata file
* @param repoDir The repository directory
@ -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<PackageManagerItem[]> {
const items: PackageManagerItem[] = [];
// Check for items in each directory type
const directoryTypes = [
{ path: path.join(repoDir, "mcp-servers"), type: "mcp-server", urlPath: "mcp-servers" },
@ -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;
}
}

View file

@ -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<string, { data: PackageManagerRepository, timestamp: number }> = 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<PackageManagerRepository> {
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<PackageManagerRepository>((_, 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<PackageManagerRepository> {
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;
});
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -19,7 +19,7 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => {
const [items, setItems] = useState<PackageManagerItem[]>([]);
const [activeTab, setActiveTab] = useState<"browse" | "sources">("browse");
const [refreshingUrls, setRefreshingUrls] = useState<string[]>([]);
// Track activeTab changes
useEffect(() => {
console.log("DEBUG: activeTab changed to", activeTab);
@ -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 (
<Tab>
<TabHeader className="flex justify-between items-center">
@ -306,12 +306,12 @@ const PackageManagerView = ({ onDone }: PackageManagerViewProps) => {
</div>
</div>
</div>
{console.log("DEBUG: Rendering condition", {
sortedItemsLength: sortedItems.length,
condition: sortedItems.length === 0 ? "empty" : "has items"
})}
{sortedItems.length === 0 ? (
<div className="flex flex-col items-center justify-center h-64 text-vscode-descriptionForeground">
<p>No package manager items found</p>
@ -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)}
</span>
</div>
<p className="my-2 text-vscode-foreground">{item.description}</p>
{item.tags && item.tags.length > 0 && (
<div className="flex flex-wrap gap-1 my-2">
{item.tags.map(tag => (
<span
key={tag}
<span
key={tag}
className="px-2 py-1 text-xs bg-vscode-badge-background text-vscode-badge-foreground rounded-full"
>
{tag}
@ -439,7 +439,7 @@ const PackageManagerItemCard = ({ item }: { item: PackageManagerItem }) => {
))}
</div>
)}
<div className="flex justify-between items-center mt-4">
<div className="flex items-center gap-4 text-sm text-vscode-descriptionForeground">
{item.version && (
@ -467,7 +467,7 @@ const PackageManagerItemCard = ({ item }: { item: PackageManagerItem }) => {
</span>
)}
</div>
<Button onClick={handleOpenUrl}>
<span className="codicon codicon-link-external mr-2"></span>
View on GitHub
@ -528,7 +528,7 @@ const PackageManagerSourcesConfig = ({
};
onSourcesChange([...sources, newSource]);
// Reset form
setNewSourceUrl("");
setNewSourceName("");
@ -545,11 +545,11 @@ const PackageManagerSourcesConfig = ({
const updatedSources = sources.filter((_, i) => i !== index);
onSourcesChange(updatedSources);
};
const handleRefreshSource = (url: string) => {
// Add URL to refreshing list
setRefreshingUrls(prev => [...prev, url]);
// Send message to refresh this specific source
vscode.postMessage({
type: "refreshPackageManagerSource",
@ -563,7 +563,7 @@ const PackageManagerSourcesConfig = ({
<p className="text-vscode-descriptionForeground mb-4">
Add Git repositories that contain package manager items. These repositories will be fetched when browsing the package manager.
</p>
<div className="mb-6">
<h5 className="text-vscode-foreground mb-2">Add New Source</h5>
<div className="flex flex-col gap-2 mb-2">

View file

@ -197,13 +197,13 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
hasPackageManagerItems: !!newState.packageManagerItems,
packageManagerItemsCount: newState.packageManagerItems?.length || 0
});
setState((prevState) => mergeExtensionState(prevState, newState))
const shouldShowWelcome = !checkExistKey(newState.apiConfiguration);
console.log("DEBUG: Setting showWelcome to", shouldShowWelcome,
"based on apiConfiguration check:", newState.apiConfiguration ? "has config" : "missing config");
setShowWelcome(shouldShowWelcome)
setDidHydrateState(true)
break