mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
walking skeleton
This commit is contained in:
parent
cd5b894578
commit
655899673b
12 changed files with 243 additions and 242 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -37,3 +37,4 @@ logs
|
|||
.roomodes
|
||||
.clinerules
|
||||
memory-bank/
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}"`,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue