mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix linting errors
This commit is contained in:
parent
d323015b25
commit
aa066935ce
4 changed files with 1560 additions and 1639 deletions
|
|
@ -12,248 +12,272 @@ import { GlobalState } from "../../schemas"
|
|||
* Handle package manager-related messages from the webview
|
||||
*/
|
||||
export async function handlePackageManagerMessages(
|
||||
provider: ClineProvider,
|
||||
message: WebviewMessage,
|
||||
packageManagerManager: PackageManagerManager
|
||||
provider: ClineProvider,
|
||||
message: WebviewMessage,
|
||||
packageManagerManager: PackageManagerManager,
|
||||
): Promise<boolean> {
|
||||
// Utility function for updating global state
|
||||
const updateGlobalState = async <K extends keyof GlobalState>(key: K, value: GlobalState[K]) =>
|
||||
await provider.contextProxy.setValue(key, value)
|
||||
// Utility function for updating global state
|
||||
const updateGlobalState = async <K extends keyof GlobalState>(key: K, value: GlobalState[K]) =>
|
||||
await provider.contextProxy.setValue(key, value)
|
||||
|
||||
switch (message.type) {
|
||||
case "webviewDidLaunch": {
|
||||
// For webviewDidLaunch, we don't do anything - package manager items will be loaded by explicit fetchPackageManagerItems
|
||||
console.log("Package Manager: webviewDidLaunch received, but skipping fetch (will be triggered by explicit fetchPackageManagerItems)");
|
||||
return true;
|
||||
}
|
||||
case "fetchPackageManagerItems": {
|
||||
// Check if we need to force refresh using type assertion
|
||||
const forceRefresh = (message as any).forceRefresh === true;
|
||||
console.log(`Package Manager: Fetch requested with forceRefresh=${forceRefresh}`);
|
||||
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 = [DEFAULT_PACKAGE_MANAGER_SOURCE];
|
||||
|
||||
// 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
|
||||
const enabledSources = sources.filter(s => s.enabled);
|
||||
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;
|
||||
console.log("DEBUG: Successfully fetched items:", items.length);
|
||||
} else {
|
||||
console.log("DEBUG: No enabled sources found");
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.error("Failed to fetch package manager items:", fetchError);
|
||||
// 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: {
|
||||
// Include the current apiConfiguration to prevent welcome screen from showing
|
||||
// This is critical because ExtensionStateContext checks apiConfiguration to determine if welcome screen should be shown
|
||||
apiConfiguration: currentState.apiConfiguration,
|
||||
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: {
|
||||
// Include the current apiConfiguration to prevent welcome screen from showing
|
||||
// This is critical because ExtensionStateContext checks apiConfiguration to determine if welcome screen should be shown
|
||||
apiConfiguration: currentState.apiConfiguration,
|
||||
packageManagerItems: []
|
||||
}
|
||||
} as any); // Use type assertion to bypass TypeScript checking
|
||||
vscode.window.showErrorMessage(`Package manager initialization failed: ${initError instanceof Error ? initError.message : String(initError)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch package manager items:", error);
|
||||
vscode.window.showErrorMessage(`Failed to fetch package manager items: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
return true
|
||||
}
|
||||
case "packageManagerSources": {
|
||||
if (message.sources) {
|
||||
// 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);
|
||||
vscode.window.showWarningMessage(`Maximum of ${MAX_SOURCES} package manager sources allowed. Additional sources have been removed.`);
|
||||
} else {
|
||||
updatedSources = message.sources;
|
||||
}
|
||||
|
||||
// Validate sources using the validation utility
|
||||
const validationErrors = validateSources(updatedSources);
|
||||
switch (message.type) {
|
||||
case "webviewDidLaunch": {
|
||||
// For webviewDidLaunch, we don't do anything - package manager items will be loaded by explicit fetchPackageManagerItems
|
||||
console.log(
|
||||
"Package Manager: webviewDidLaunch received, but skipping fetch (will be triggered by explicit fetchPackageManagerItems)",
|
||||
)
|
||||
return true
|
||||
}
|
||||
case "fetchPackageManagerItems": {
|
||||
// Check if we need to force refresh using type assertion
|
||||
const forceRefresh = (message as any).forceRefresh === true
|
||||
console.log(`Package Manager: Fetch requested with forceRefresh=${forceRefresh}`)
|
||||
try {
|
||||
console.log("Package Manager: Received request to fetch package manager items")
|
||||
console.log("DEBUG: Processing package manager request")
|
||||
|
||||
// Filter out invalid sources
|
||||
if (validationErrors.length > 0) {
|
||||
console.log("Package Manager: Validation errors found in sources", validationErrors);
|
||||
// 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[]) ||
|
||||
[]
|
||||
|
||||
// Create a map of invalid indices
|
||||
const invalidIndices = new Set<number>();
|
||||
validationErrors.forEach(error => {
|
||||
// Extract index from error message (Source #X: ...)
|
||||
const match = error.message.match(/Source #(\d+):/);
|
||||
if (match && match[1]) {
|
||||
const index = parseInt(match[1], 10) - 1; // Convert to 0-based index
|
||||
if (index >= 0 && index < updatedSources.length) {
|
||||
invalidIndices.add(index);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!sources || sources.length === 0) {
|
||||
console.log("Package Manager: No sources found, initializing default sources")
|
||||
sources = [DEFAULT_PACKAGE_MANAGER_SOURCE]
|
||||
|
||||
// Filter out invalid sources
|
||||
updatedSources = updatedSources.filter((_, index) => !invalidIndices.has(index));
|
||||
// Save the default sources
|
||||
await provider.contextProxy.setValue("packageManagerSources", sources)
|
||||
console.log("Package Manager: Default sources initialized")
|
||||
}
|
||||
|
||||
// Show validation errors
|
||||
const errorMessage = `Package manager sources validation failed:\n${validationErrors.map(e => e.message).join('\n')}`;
|
||||
console.error(errorMessage);
|
||||
vscode.window.showErrorMessage(errorMessage);
|
||||
}
|
||||
console.log(`Package Manager: Fetching items from ${sources.length} sources`)
|
||||
console.log(`DEBUG: PackageManagerManager instance: ${packageManagerManager ? "exists" : "null"}`)
|
||||
|
||||
// Update the global state with the validated 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");
|
||||
await packageManagerManager.cleanupCacheDirectories(updatedSources);
|
||||
console.log("Package Manager: Cache cleanup completed");
|
||||
} catch (error) {
|
||||
console.error("Package Manager: Error during cache cleanup:", error);
|
||||
}
|
||||
|
||||
// Update the webview with the new state
|
||||
await provider.postStateToWebview();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case "openExternal": {
|
||||
if (message.url) {
|
||||
console.log(`Package Manager: Opening external URL: ${message.url}`);
|
||||
try {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url));
|
||||
console.log(`Package Manager: Successfully opened URL: ${message.url}`);
|
||||
} catch (error) {
|
||||
console.error(`Package Manager: Failed to open URL: ${error instanceof Error ? error.message : String(error)}`);
|
||||
vscode.window.showErrorMessage(`Failed to open URL: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
} else {
|
||||
console.error("Package Manager: openExternal called without a URL");
|
||||
}
|
||||
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 with the source name
|
||||
await packageManagerManager.refreshRepository(message.url, source.name);
|
||||
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({
|
||||
type: "state",
|
||||
state: {
|
||||
apiConfiguration: currentState.apiConfiguration,
|
||||
packageManagerItems: await packageManagerManager.getPackageManagerItems(sources.filter(s => s.enabled))
|
||||
}
|
||||
} as ExtensionMessage);
|
||||
} finally {
|
||||
// Always notify the webview that the refresh is complete, even if it failed
|
||||
console.log(`Package Manager: Sending repositoryRefreshComplete message for ${message.url}`);
|
||||
provider.postMessageToWebview({
|
||||
type: "repositoryRefreshComplete",
|
||||
url: message.url
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.error(`Package Manager: Source URL not found: ${message.url}`);
|
||||
vscode.window.showErrorMessage(`Source URL not found: ${message.url}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Package Manager: Failed to refresh source: ${error instanceof Error ? error.message : String(error)}`);
|
||||
vscode.window.showErrorMessage(`Failed to refresh source: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
// 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
|
||||
const enabledSources = sources.filter((s) => s.enabled)
|
||||
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
|
||||
console.log("DEBUG: Successfully fetched items:", items.length)
|
||||
} else {
|
||||
console.log("DEBUG: No enabled sources found")
|
||||
}
|
||||
} catch (fetchError) {
|
||||
console.error("Failed to fetch package manager items:", fetchError)
|
||||
// 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: {
|
||||
// Include the current apiConfiguration to prevent welcome screen from showing
|
||||
// This is critical because ExtensionStateContext checks apiConfiguration to determine if welcome screen should be shown
|
||||
apiConfiguration: currentState.apiConfiguration,
|
||||
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: {
|
||||
// Include the current apiConfiguration to prevent welcome screen from showing
|
||||
// This is critical because ExtensionStateContext checks apiConfiguration to determine if welcome screen should be shown
|
||||
apiConfiguration: currentState.apiConfiguration,
|
||||
packageManagerItems: [],
|
||||
},
|
||||
} as any) // Use type assertion to bypass TypeScript checking
|
||||
vscode.window.showErrorMessage(
|
||||
`Package manager initialization failed: ${initError instanceof Error ? initError.message : String(initError)}`,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch package manager items:", error)
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to fetch package manager items: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
case "packageManagerSources": {
|
||||
if (message.sources) {
|
||||
// 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)
|
||||
vscode.window.showWarningMessage(
|
||||
`Maximum of ${MAX_SOURCES} package manager sources allowed. Additional sources have been removed.`,
|
||||
)
|
||||
} else {
|
||||
updatedSources = message.sources
|
||||
}
|
||||
|
||||
// Validate sources using the validation utility
|
||||
const validationErrors = validateSources(updatedSources)
|
||||
|
||||
// Filter out invalid sources
|
||||
if (validationErrors.length > 0) {
|
||||
console.log("Package Manager: Validation errors found in sources", validationErrors)
|
||||
|
||||
// Create a map of invalid indices
|
||||
const invalidIndices = new Set<number>()
|
||||
validationErrors.forEach((error) => {
|
||||
// Extract index from error message (Source #X: ...)
|
||||
const match = error.message.match(/Source #(\d+):/)
|
||||
if (match && match[1]) {
|
||||
const index = parseInt(match[1], 10) - 1 // Convert to 0-based index
|
||||
if (index >= 0 && index < updatedSources.length) {
|
||||
invalidIndices.add(index)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Filter out invalid sources
|
||||
updatedSources = updatedSources.filter((_, index) => !invalidIndices.has(index))
|
||||
|
||||
// Show validation errors
|
||||
const errorMessage = `Package manager sources validation failed:\n${validationErrors.map((e) => e.message).join("\n")}`
|
||||
console.error(errorMessage)
|
||||
vscode.window.showErrorMessage(errorMessage)
|
||||
}
|
||||
|
||||
// Update the global state with the validated 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")
|
||||
await packageManagerManager.cleanupCacheDirectories(updatedSources)
|
||||
console.log("Package Manager: Cache cleanup completed")
|
||||
} catch (error) {
|
||||
console.error("Package Manager: Error during cache cleanup:", error)
|
||||
}
|
||||
|
||||
// Update the webview with the new state
|
||||
await provider.postStateToWebview()
|
||||
}
|
||||
return true
|
||||
}
|
||||
case "openExternal": {
|
||||
if (message.url) {
|
||||
console.log(`Package Manager: Opening external URL: ${message.url}`)
|
||||
try {
|
||||
vscode.env.openExternal(vscode.Uri.parse(message.url))
|
||||
console.log(`Package Manager: Successfully opened URL: ${message.url}`)
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Package Manager: Failed to open URL: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to open URL: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
console.error("Package Manager: openExternal called without a URL")
|
||||
}
|
||||
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 with the source name
|
||||
await packageManagerManager.refreshRepository(message.url, source.name)
|
||||
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({
|
||||
type: "state",
|
||||
state: {
|
||||
apiConfiguration: currentState.apiConfiguration,
|
||||
packageManagerItems: await packageManagerManager.getPackageManagerItems(
|
||||
sources.filter((s) => s.enabled),
|
||||
),
|
||||
},
|
||||
} as ExtensionMessage)
|
||||
} finally {
|
||||
// Always notify the webview that the refresh is complete, even if it failed
|
||||
console.log(`Package Manager: Sending repositoryRefreshComplete message for ${message.url}`)
|
||||
provider.postMessageToWebview({
|
||||
type: "repositoryRefreshComplete",
|
||||
url: message.url,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
console.error(`Package Manager: Source URL not found: ${message.url}`)
|
||||
vscode.window.showErrorMessage(`Source URL not found: ${message.url}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Package Manager: Failed to refresh source: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to refresh source: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,286 +1,287 @@
|
|||
import * as vscode from "vscode";
|
||||
import * as path from "path";
|
||||
import * as fs from "fs/promises";
|
||||
import { GitFetcher } from "./GitFetcher";
|
||||
import { PackageManagerItem, PackageManagerRepository, PackageManagerSource } from "./types";
|
||||
import * as vscode from "vscode"
|
||||
import * as path from "path"
|
||||
import * as fs from "fs/promises"
|
||||
import { GitFetcher } from "./GitFetcher"
|
||||
import { PackageManagerItem, PackageManagerRepository, PackageManagerSource } from "./types"
|
||||
|
||||
/**
|
||||
* Service for managing package manager data
|
||||
*/
|
||||
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
|
||||
* @returns An array of PackageManagerItem objects
|
||||
*/
|
||||
async getPackageManagerItems(sources: PackageManagerSource[]): Promise<PackageManagerItem[]> {
|
||||
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}`);
|
||||
// Pass the source name to getRepositoryData
|
||||
const repo = await this.getRepositoryData(source.url, false, source.name);
|
||||
|
||||
if (repo.items && repo.items.length > 0) {
|
||||
console.log(`PackageManagerManager: Found ${repo.items.length} items in ${source.url}`);
|
||||
items.push(...repo.items);
|
||||
} else {
|
||||
console.log(`PackageManagerManager: No items found in ${source.url}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`PackageManagerManager: Failed to fetch data from ${source.url}:`, error);
|
||||
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
|
||||
* @param forceRefresh Whether to bypass the cache and force a refresh
|
||||
* @param sourceName The name of the source
|
||||
* @returns A PackageManagerRepository object
|
||||
*/
|
||||
async getRepositoryData(url: string, forceRefresh: boolean = false, sourceName?: string): 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, sourceName);
|
||||
|
||||
// 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: {},
|
||||
items: [],
|
||||
url
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes a specific repository, bypassing the cache
|
||||
* @param url The repository URL to refresh
|
||||
* @param sourceName Optional name of the source
|
||||
* @returns The refreshed repository data
|
||||
*/
|
||||
async refreshRepository(url: string, sourceName?: string): Promise<PackageManagerRepository> {
|
||||
console.log(`PackageManagerManager: Refreshing repository ${url}`);
|
||||
|
||||
try {
|
||||
// Force a refresh by bypassing the cache
|
||||
const data = await this.getRepositoryData(url, true, sourceName);
|
||||
console.log(`PackageManagerManager: Repository ${url} refreshed successfully`);
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error(`PackageManagerManager: Failed to refresh repository ${url}:`, error);
|
||||
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
|
||||
*/
|
||||
async cleanupCacheDirectories(currentSources: PackageManagerSource[]): Promise<void> {
|
||||
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);
|
||||
} catch (error) {
|
||||
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 {
|
||||
const dirPath = path.join(cacheDir, dirName);
|
||||
console.log(`PackageManagerManager: Deleting cache directory ${dirPath}`);
|
||||
await fs.rm(dirPath, { recursive: true, force: true });
|
||||
console.log(`PackageManagerManager: Successfully deleted ${dirPath}`);
|
||||
} catch (error) {
|
||||
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
|
||||
* @returns A sanitized directory name
|
||||
*/
|
||||
private getRepoNameFromUrl(url: string): string {
|
||||
// Extract repo name from URL and sanitize it
|
||||
const urlParts = url.split("/").filter(part => part !== "");
|
||||
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
|
||||
* @param filters The filter criteria
|
||||
* @returns Filtered items
|
||||
*/
|
||||
filterItems(items: PackageManagerItem[], filters: { type?: string, search?: string, tags?: string[] }): PackageManagerItem[] {
|
||||
return items.filter(item => {
|
||||
// Filter by type
|
||||
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
|
||||
* @param sortBy The field to sort by
|
||||
* @param sortOrder The sort order
|
||||
* @returns Sorted items
|
||||
*/
|
||||
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);
|
||||
break;
|
||||
case "author":
|
||||
comparison = (a.author || "").localeCompare(b.author || "");
|
||||
break;
|
||||
case "lastUpdated":
|
||||
comparison = (a.lastUpdated || "").localeCompare(b.lastUpdated || "");
|
||||
break;
|
||||
case "stars":
|
||||
comparison = (a.stars || 0) - (b.stars || 0);
|
||||
break;
|
||||
case "downloads":
|
||||
comparison = (a.downloads || 0) - (b.downloads || 0);
|
||||
break;
|
||||
default:
|
||||
comparison = a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
return sortOrder === "asc" ? comparison : -comparison;
|
||||
});
|
||||
}
|
||||
}
|
||||
// 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
|
||||
* @returns An array of PackageManagerItem objects
|
||||
*/
|
||||
async getPackageManagerItems(sources: PackageManagerSource[]): Promise<PackageManagerItem[]> {
|
||||
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}`)
|
||||
// Pass the source name to getRepositoryData
|
||||
const repo = await this.getRepositoryData(source.url, false, source.name)
|
||||
|
||||
if (repo.items && repo.items.length > 0) {
|
||||
console.log(`PackageManagerManager: Found ${repo.items.length} items in ${source.url}`)
|
||||
items.push(...repo.items)
|
||||
} else {
|
||||
console.log(`PackageManagerManager: No items found in ${source.url}`)
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
console.error(`PackageManagerManager: Failed to fetch data from ${source.url}:`, error)
|
||||
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
|
||||
* @param forceRefresh Whether to bypass the cache and force a refresh
|
||||
* @param sourceName The name of the source
|
||||
* @returns A PackageManagerRepository object
|
||||
*/
|
||||
async getRepositoryData(
|
||||
url: string,
|
||||
forceRefresh: boolean = false,
|
||||
sourceName?: string,
|
||||
): 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, sourceName)
|
||||
|
||||
// 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: {},
|
||||
items: [],
|
||||
url,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes a specific repository, bypassing the cache
|
||||
* @param url The repository URL to refresh
|
||||
* @param sourceName Optional name of the source
|
||||
* @returns The refreshed repository data
|
||||
*/
|
||||
async refreshRepository(url: string, sourceName?: string): Promise<PackageManagerRepository> {
|
||||
console.log(`PackageManagerManager: Refreshing repository ${url}`)
|
||||
|
||||
try {
|
||||
// Force a refresh by bypassing the cache
|
||||
const data = await this.getRepositoryData(url, true, sourceName)
|
||||
console.log(`PackageManagerManager: Repository ${url} refreshed successfully`)
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error(`PackageManagerManager: Failed to refresh repository ${url}:`, error)
|
||||
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
|
||||
*/
|
||||
async cleanupCacheDirectories(currentSources: PackageManagerSource[]): Promise<void> {
|
||||
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)
|
||||
} catch (error) {
|
||||
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 {
|
||||
const dirPath = path.join(cacheDir, dirName)
|
||||
console.log(`PackageManagerManager: Deleting cache directory ${dirPath}`)
|
||||
await fs.rm(dirPath, { recursive: true, force: true })
|
||||
console.log(`PackageManagerManager: Successfully deleted ${dirPath}`)
|
||||
} catch (error) {
|
||||
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
|
||||
* @returns A sanitized directory name
|
||||
*/
|
||||
private getRepoNameFromUrl(url: string): string {
|
||||
// Extract repo name from URL and sanitize it
|
||||
const urlParts = url.split("/").filter((part) => part !== "")
|
||||
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
|
||||
* @param filters The filter criteria
|
||||
* @returns Filtered items
|
||||
*/
|
||||
filterItems(
|
||||
items: PackageManagerItem[],
|
||||
filters: { type?: string; search?: string; tags?: string[] },
|
||||
): PackageManagerItem[] {
|
||||
return items.filter((item) => {
|
||||
// Filter by type
|
||||
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
|
||||
* @param sortBy The field to sort by
|
||||
* @param sortOrder The sort order
|
||||
* @returns Sorted items
|
||||
*/
|
||||
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)
|
||||
break
|
||||
case "author":
|
||||
comparison = (a.author || "").localeCompare(b.author || "")
|
||||
break
|
||||
case "lastUpdated":
|
||||
comparison = (a.lastUpdated || "").localeCompare(b.lastUpdated || "")
|
||||
break
|
||||
default:
|
||||
comparison = a.name.localeCompare(b.name)
|
||||
}
|
||||
|
||||
return sortOrder === "asc" ? comparison : -comparison
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
/**
|
||||
* Validation utilities for package manager sources
|
||||
*/
|
||||
import { PackageManagerSource } from "./types";
|
||||
import { PackageManagerSource } from "./types"
|
||||
|
||||
/**
|
||||
* Error type for package manager source validation
|
||||
*/
|
||||
export interface ValidationError {
|
||||
field: string;
|
||||
message: string;
|
||||
field: string
|
||||
message: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -22,72 +22,74 @@ export interface ValidationError {
|
|||
* @returns True if the URL is a valid Git repository URL, false otherwise
|
||||
*/
|
||||
export function isValidGitRepositoryUrl(url: string): boolean {
|
||||
// Trim the URL to remove any leading/trailing whitespace
|
||||
const trimmedUrl = url.trim();
|
||||
// Trim the URL to remove any leading/trailing whitespace
|
||||
const trimmedUrl = url.trim()
|
||||
|
||||
// HTTPS pattern (GitHub, GitLab, Bitbucket, etc.)
|
||||
// Examples:
|
||||
// - https://github.com/username/repo
|
||||
// - https://github.com/username/repo.git
|
||||
// - https://gitlab.com/username/repo
|
||||
// - https://bitbucket.org/username/repo
|
||||
const httpsPattern = /^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org|dev\.azure\.com)\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(\/.+)*(\.git)?$/;
|
||||
// HTTPS pattern (GitHub, GitLab, Bitbucket, etc.)
|
||||
// Examples:
|
||||
// - https://github.com/username/repo
|
||||
// - https://github.com/username/repo.git
|
||||
// - https://gitlab.com/username/repo
|
||||
// - https://bitbucket.org/username/repo
|
||||
const httpsPattern =
|
||||
/^https?:\/\/(github\.com|gitlab\.com|bitbucket\.org|dev\.azure\.com)\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(\/.+)*(\.git)?$/
|
||||
|
||||
// SSH pattern
|
||||
// Examples:
|
||||
// - git@github.com:username/repo.git
|
||||
// - git@gitlab.com:username/repo.git
|
||||
const sshPattern = /^git@(github\.com|gitlab\.com|bitbucket\.org):([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)(\.git)?$/;
|
||||
// SSH pattern
|
||||
// Examples:
|
||||
// - git@github.com:username/repo.git
|
||||
// - git@gitlab.com:username/repo.git
|
||||
const sshPattern = /^git@(github\.com|gitlab\.com|bitbucket\.org):([a-zA-Z0-9_.-]+)\/([a-zA-Z0-9_.-]+)(\.git)?$/
|
||||
|
||||
// Git protocol pattern
|
||||
// Examples:
|
||||
// - git://github.com/username/repo.git
|
||||
const gitProtocolPattern = /^git:\/\/(github\.com|gitlab\.com|bitbucket\.org)\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(\.git)?$/;
|
||||
// Git protocol pattern
|
||||
// Examples:
|
||||
// - git://github.com/username/repo.git
|
||||
const gitProtocolPattern =
|
||||
/^git:\/\/(github\.com|gitlab\.com|bitbucket\.org)\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+(\.git)?$/
|
||||
|
||||
return httpsPattern.test(trimmedUrl) || sshPattern.test(trimmedUrl) || gitProtocolPattern.test(trimmedUrl);
|
||||
return httpsPattern.test(trimmedUrl) || sshPattern.test(trimmedUrl) || gitProtocolPattern.test(trimmedUrl)
|
||||
}
|
||||
|
||||
export function validateSourceUrl(url: string): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
const errors: ValidationError[] = []
|
||||
|
||||
// Check if URL is empty
|
||||
if (!url) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "URL cannot be empty"
|
||||
});
|
||||
return errors; // Return early if URL is empty
|
||||
}
|
||||
// Check if URL is empty
|
||||
if (!url) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "URL cannot be empty",
|
||||
})
|
||||
return errors // Return early if URL is empty
|
||||
}
|
||||
|
||||
// Check if URL is valid format
|
||||
try {
|
||||
new URL(url);
|
||||
} catch (e) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "Invalid URL format"
|
||||
});
|
||||
return errors; // Return early if URL is not valid
|
||||
}
|
||||
// Check if URL is valid format
|
||||
try {
|
||||
new URL(url)
|
||||
} catch (e) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "Invalid URL format",
|
||||
})
|
||||
return errors // Return early if URL is not valid
|
||||
}
|
||||
|
||||
// Check for non-visible characters (except spaces)
|
||||
const nonVisibleCharRegex = /[^\S ]/;
|
||||
if (nonVisibleCharRegex.test(url)) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "URL contains non-visible characters other than spaces"
|
||||
});
|
||||
}
|
||||
// Check for non-visible characters (except spaces)
|
||||
const nonVisibleCharRegex = /[^\S ]/
|
||||
if (nonVisibleCharRegex.test(url)) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "URL contains non-visible characters other than spaces",
|
||||
})
|
||||
}
|
||||
|
||||
// Check if URL is a valid Git repository URL
|
||||
if (!isValidGitRepositoryUrl(url)) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "URL must be a valid Git repository URL (e.g., https://github.com/username/repo)"
|
||||
});
|
||||
}
|
||||
// Check if URL is a valid Git repository URL
|
||||
if (!isValidGitRepositoryUrl(url)) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: "URL must be a valid Git repository URL (e.g., https://github.com/username/repo)",
|
||||
})
|
||||
}
|
||||
|
||||
return errors;
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -96,31 +98,31 @@ export function validateSourceUrl(url: string): ValidationError[] {
|
|||
* @returns An array of validation errors, empty if valid
|
||||
*/
|
||||
export function validateSourceName(name?: string): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
const errors: ValidationError[] = []
|
||||
|
||||
// Skip validation if name is not provided
|
||||
if (!name) {
|
||||
return errors;
|
||||
}
|
||||
// Skip validation if name is not provided
|
||||
if (!name) {
|
||||
return errors
|
||||
}
|
||||
|
||||
// Check name length
|
||||
if (name.length > 20) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: "Name must be 20 characters or less"
|
||||
});
|
||||
}
|
||||
// Check name length
|
||||
if (name.length > 20) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: "Name must be 20 characters or less",
|
||||
})
|
||||
}
|
||||
|
||||
// Check for non-visible characters (except spaces)
|
||||
const nonVisibleCharRegex = /[^\S ]/;
|
||||
if (nonVisibleCharRegex.test(name)) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: "Name contains non-visible characters other than spaces"
|
||||
});
|
||||
}
|
||||
// Check for non-visible characters (except spaces)
|
||||
const nonVisibleCharRegex = /[^\S ]/
|
||||
if (nonVisibleCharRegex.test(name)) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: "Name contains non-visible characters other than spaces",
|
||||
})
|
||||
}
|
||||
|
||||
return errors;
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -130,83 +132,81 @@ export function validateSourceName(name?: string): ValidationError[] {
|
|||
* @returns An array of validation errors, empty if valid
|
||||
*/
|
||||
export function validateSourceDuplicates(
|
||||
sources: PackageManagerSource[],
|
||||
newSource?: PackageManagerSource
|
||||
sources: PackageManagerSource[],
|
||||
newSource?: PackageManagerSource,
|
||||
): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
const normalizedUrls: { url: string; index: number }[] = [];
|
||||
const normalizedNames: { name: string; index: number }[] = [];
|
||||
const errors: ValidationError[] = []
|
||||
const normalizedUrls: { url: string; index: number }[] = []
|
||||
const normalizedNames: { name: string; index: number }[] = []
|
||||
|
||||
// Process existing sources
|
||||
sources.forEach((source, index) => {
|
||||
// Normalize URL (case and whitespace insensitive)
|
||||
const normalizedUrl = source.url.toLowerCase().replace(/\s+/g, '');
|
||||
normalizedUrls.push({ url: normalizedUrl, index });
|
||||
// Process existing sources
|
||||
sources.forEach((source, index) => {
|
||||
// Normalize URL (case and whitespace insensitive)
|
||||
const normalizedUrl = source.url.toLowerCase().replace(/\s+/g, "")
|
||||
normalizedUrls.push({ url: normalizedUrl, index })
|
||||
|
||||
// Normalize name if it exists (case and whitespace insensitive)
|
||||
if (source.name) {
|
||||
const normalizedName = source.name.toLowerCase().replace(/\s+/g, '');
|
||||
normalizedNames.push({ name: normalizedName, index });
|
||||
}
|
||||
});
|
||||
// Normalize name if it exists (case and whitespace insensitive)
|
||||
if (source.name) {
|
||||
const normalizedName = source.name.toLowerCase().replace(/\s+/g, "")
|
||||
normalizedNames.push({ name: normalizedName, index })
|
||||
}
|
||||
})
|
||||
|
||||
// Check for duplicates within the existing sources
|
||||
normalizedUrls.forEach((item, index) => {
|
||||
const duplicates = normalizedUrls.filter(
|
||||
(other, otherIndex) => other.url === item.url && otherIndex !== index
|
||||
);
|
||||
// Check for duplicates within the existing sources
|
||||
normalizedUrls.forEach((item, index) => {
|
||||
const duplicates = normalizedUrls.filter((other, otherIndex) => other.url === item.url && otherIndex !== index)
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: `Source #${item.index + 1} has a duplicate URL with Source #${duplicates[0].index + 1} (case and whitespace insensitive match)`
|
||||
});
|
||||
}
|
||||
});
|
||||
if (duplicates.length > 0) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: `Source #${item.index + 1} has a duplicate URL with Source #${duplicates[0].index + 1} (case and whitespace insensitive match)`,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
normalizedNames.forEach((item, index) => {
|
||||
const duplicates = normalizedNames.filter(
|
||||
(other, otherIndex) => other.name === item.name && otherIndex !== index
|
||||
);
|
||||
normalizedNames.forEach((item, index) => {
|
||||
const duplicates = normalizedNames.filter(
|
||||
(other, otherIndex) => other.name === item.name && otherIndex !== index,
|
||||
)
|
||||
|
||||
if (duplicates.length > 0) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: `Source #${item.index + 1} has a duplicate name with Source #${duplicates[0].index + 1} (case and whitespace insensitive match)`
|
||||
});
|
||||
}
|
||||
});
|
||||
if (duplicates.length > 0) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: `Source #${item.index + 1} has a duplicate name with Source #${duplicates[0].index + 1} (case and whitespace insensitive match)`,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Check new source against existing sources if provided
|
||||
if (newSource) {
|
||||
// Validate URL
|
||||
if (newSource.url) {
|
||||
const normalizedNewUrl = newSource.url.toLowerCase().replace(/\s+/g, '');
|
||||
const duplicateUrl = normalizedUrls.find(item => item.url === normalizedNewUrl);
|
||||
// Check new source against existing sources if provided
|
||||
if (newSource) {
|
||||
// Validate URL
|
||||
if (newSource.url) {
|
||||
const normalizedNewUrl = newSource.url.toLowerCase().replace(/\s+/g, "")
|
||||
const duplicateUrl = normalizedUrls.find((item) => item.url === normalizedNewUrl)
|
||||
|
||||
if (duplicateUrl) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: `URL is a duplicate of Source #${duplicateUrl.index + 1} (case and whitespace insensitive match)`
|
||||
});
|
||||
}
|
||||
}
|
||||
if (duplicateUrl) {
|
||||
errors.push({
|
||||
field: "url",
|
||||
message: `URL is a duplicate of Source #${duplicateUrl.index + 1} (case and whitespace insensitive match)`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Validate name
|
||||
if (newSource.name) {
|
||||
const normalizedNewName = newSource.name.toLowerCase().replace(/\s+/g, '');
|
||||
const duplicateName = normalizedNames.find(item => item.name === normalizedNewName);
|
||||
// Validate name
|
||||
if (newSource.name) {
|
||||
const normalizedNewName = newSource.name.toLowerCase().replace(/\s+/g, "")
|
||||
const duplicateName = normalizedNames.find((item) => item.name === normalizedNewName)
|
||||
|
||||
if (duplicateName) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: `Name is a duplicate of Source #${duplicateName.index + 1} (case and whitespace insensitive match)`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (duplicateName) {
|
||||
errors.push({
|
||||
field: "name",
|
||||
message: `Name is a duplicate of Source #${duplicateName.index + 1} (case and whitespace insensitive match)`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -216,15 +216,15 @@ export function validateSourceDuplicates(
|
|||
* @returns An array of validation errors, empty if valid
|
||||
*/
|
||||
export function validateSource(
|
||||
source: PackageManagerSource,
|
||||
existingSources: PackageManagerSource[] = []
|
||||
source: PackageManagerSource,
|
||||
existingSources: PackageManagerSource[] = [],
|
||||
): ValidationError[] {
|
||||
// Combine all validation errors
|
||||
return [
|
||||
...validateSourceUrl(source.url),
|
||||
...validateSourceName(source.name),
|
||||
...validateSourceDuplicates(existingSources, source)
|
||||
];
|
||||
// Combine all validation errors
|
||||
return [
|
||||
...validateSourceUrl(source.url),
|
||||
...validateSourceName(source.name),
|
||||
...validateSourceDuplicates(existingSources, source),
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -233,27 +233,24 @@ export function validateSource(
|
|||
* @returns An array of validation errors, empty if valid
|
||||
*/
|
||||
export function validateSources(sources: PackageManagerSource[]): ValidationError[] {
|
||||
const errors: ValidationError[] = [];
|
||||
const errors: ValidationError[] = []
|
||||
|
||||
// Validate each source individually
|
||||
sources.forEach((source, index) => {
|
||||
const sourceErrors = [
|
||||
...validateSourceUrl(source.url),
|
||||
...validateSourceName(source.name)
|
||||
];
|
||||
// Validate each source individually
|
||||
sources.forEach((source, index) => {
|
||||
const sourceErrors = [...validateSourceUrl(source.url), ...validateSourceName(source.name)]
|
||||
|
||||
// Add index to error messages
|
||||
sourceErrors.forEach(error => {
|
||||
errors.push({
|
||||
field: error.field,
|
||||
message: `Source #${index + 1}: ${error.message}`
|
||||
});
|
||||
});
|
||||
});
|
||||
// Add index to error messages
|
||||
sourceErrors.forEach((error) => {
|
||||
errors.push({
|
||||
field: error.field,
|
||||
message: `Source #${index + 1}: ${error.message}`,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Check for duplicates across all sources
|
||||
const duplicateErrors = validateSourceDuplicates(sources);
|
||||
errors.push(...duplicateErrors);
|
||||
// Check for duplicates across all sources
|
||||
const duplicateErrors = validateSourceDuplicates(sources)
|
||||
errors.push(...duplicateErrors)
|
||||
|
||||
return errors;
|
||||
}
|
||||
return errors
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue