import { uniqBy } from 'es-toolkit/compat' import { computed, getCurrentInstance, onUnmounted, readonly, ref } from 'vue' import { useInstalledPacks } from '@/composables/nodePack/useInstalledPacks' import { useConflictAcknowledgment } from '@/composables/useConflictAcknowledgment' import config from '@/config' import { useComfyManagerService } from '@/services/comfyManagerService' import { useComfyRegistryService } from '@/services/comfyRegistryService' import { useComfyManagerStore } from '@/stores/comfyManagerStore' import { useConflictDetectionStore } from '@/stores/conflictDetectionStore' import { useSystemStatsStore } from '@/stores/systemStatsStore' import type { SystemStats } from '@/types' import type { components } from '@/types/comfyRegistryTypes' import type { ConflictDetail, ConflictDetectionResponse, ConflictDetectionResult, ConflictDetectionSummary, ConflictType, Node, NodePackRequirements, SystemEnvironment } from '@/types/conflictDetectionTypes' import { cleanVersion, satisfiesVersion, utilCheckVersionCompatibility } from '@/utils/versionUtil' /** * Composable for conflict detection system. * Error-resilient and asynchronous to avoid affecting other components. */ export function useConflictDetection() { const managerStore = useComfyManagerStore() const { startFetchInstalled, installedPacks, installedPacksWithVersions, isReady: installedPacksReady } = useInstalledPacks() const isDetecting = ref(false) const lastDetectionTime = ref(null) const detectionError = ref(null) const systemEnvironment = ref(null) const detectionResults = ref([]) // Store merged conflicts separately for testing const storedMergedConflicts = ref([]) const detectionSummary = ref(null) // Registry API request cancellation const abortController = ref(null) const acknowledgment = useConflictAcknowledgment() const conflictStore = useConflictDetectionStore() const hasConflicts = computed(() => conflictStore.hasConflicts) const conflictedPackages = computed(() => { return conflictStore.conflictedPackages }) const bannedPackages = computed(() => conflictStore.bannedPackages) const securityPendingPackages = computed( () => conflictStore.securityPendingPackages ) /** * Collects current system environment information. * Continues with default values even if errors occur. * @returns Promise that resolves to system environment information */ async function detectSystemEnvironment(): Promise { try { // Get system stats from store (primary source of system information) const systemStatsStore = useSystemStatsStore() if (!systemStatsStore.systemStats) { await systemStatsStore.fetchSystemStats() } // Fetch version information from backend (with error resilience) const [frontendVersion] = await Promise.allSettled([ fetchFrontendVersion() ]) // Extract system information from system stats const systemStats = systemStatsStore.systemStats const comfyuiVersion = systemStats?.system?.comfyui_version || 'unknown' // Use system stats for OS detection (more accurate than browser detection) const systemOS = systemStats?.system?.os || 'unknown' // Extract architecture from system stats device information const architecture = extractArchitectureFromSystemStats(systemStats) // Detect GPU/accelerator information from system stats const acceleratorInfo = extractAcceleratorInfo(systemStats) // Enhanced OS detection using multiple sources const detectedOS = detectOSFromSystemStats(systemOS, systemStats) const environment: SystemEnvironment = { // Version information (use 'unknown' on failure) comfyui_version: comfyuiVersion, frontend_version: frontendVersion.status === 'fulfilled' ? frontendVersion.value : 'unknown', // Platform information (from system stats) os: detectedOS, platform_details: systemOS, architecture: architecture, // GPU/accelerator information available_accelerators: acceleratorInfo.available, primary_accelerator: acceleratorInfo.primary, gpu_memory_mb: acceleratorInfo.memory_mb, // Runtime information node_env: import.meta.env.MODE as 'development' | 'production', user_agent: navigator.userAgent } systemEnvironment.value = environment console.log( '[ConflictDetection] System environment detection completed:', environment ) return environment } catch (error) { console.warn( '[ConflictDetection] Error during system environment detection:', error ) // Try to get frontend version even in fallback mode let frontendVersion = 'unknown' try { frontendVersion = await fetchFrontendVersion() } catch { frontendVersion = 'unknown' } // Provide basic environment information even on error const fallbackEnvironment: SystemEnvironment = { comfyui_version: 'unknown', frontend_version: frontendVersion, os: detectOSFromSystemStats(navigator.platform), platform_details: navigator.platform, architecture: getArchitecture(), available_accelerators: ['CPU'], primary_accelerator: 'CPU', node_env: import.meta.env.MODE as 'development' | 'production', user_agent: navigator.userAgent } systemEnvironment.value = fallbackEnvironment return fallbackEnvironment } } /** * Fetches requirement information for installed packages using Registry Store. * * This function combines local installation data with Registry API compatibility metadata * using the established store layer pattern with caching and batch requests. * * Process * 1. Get locally installed packages * 2. Batch fetch Registry data using store layer * 3. Combine local + Registry data * 4. Extract compatibility requirements * * @returns Promise that resolves to array of node pack requirements */ async function fetchPackageRequirements(): Promise { try { // Step 1: Use installed packs composable instead of direct API calls await startFetchInstalled() // Ensure data is loaded if ( !installedPacksReady.value || !installedPacks.value || installedPacks.value.length === 0 ) { console.warn( '[ConflictDetection] No installed packages available from useInstalledPacks' ) return [] } // Step 2: Get Registry service for bulk API calls const registryService = useComfyRegistryService() // Step 3: Setup abort controller for request cancellation abortController.value = new AbortController() // Step 4: Use bulk API to fetch all version data in a single request const versionDataMap = new Map< string, components['schemas']['NodeVersion'] >() // Prepare bulk request with actual installed versions from Manager API const nodeVersions = installedPacksWithVersions.value.map((pack) => ({ node_id: pack.id, version: pack.version })) if (nodeVersions.length > 0) { try { const bulkResponse = await registryService.getBulkNodeVersions( nodeVersions, abortController.value?.signal ) if (bulkResponse && bulkResponse.node_versions) { // Process bulk response bulkResponse.node_versions.forEach((result) => { if (result.status === 'success' && result.node_version) { versionDataMap.set( result.identifier.node_id, result.node_version ) } else if (result.status === 'error') { console.warn( `[ConflictDetection] Failed to fetch version data for ${result.identifier.node_id}@${result.identifier.version}:`, result.error_message ) } }) } } catch (error) { console.warn( '[ConflictDetection] Failed to fetch bulk version data:', error ) } } // Step 5: Combine local installation data with Registry version data const requirements: NodePackRequirements[] = [] // IMPORTANT: Use installedPacksWithVersions to check ALL installed packages // not just the ones that exist in Registry (installedPacks) for (const installedPack of installedPacksWithVersions.value) { const packageId = installedPack.id const versionData = versionDataMap.get(packageId) const installedVersion = installedPack.version || 'unknown' // Check if package is enabled using store method const isEnabled = managerStore.isPackEnabled(packageId) // Find the pack info from Registry if available const packInfo = installedPacks.value.find((p) => p.id === packageId) if (versionData) { // Combine local installation data with version-specific Registry data const requirement: NodePackRequirements = { // Basic package info id: packageId, name: packInfo?.name || packageId, installed_version: installedVersion, is_enabled: isEnabled, // Version-specific compatibility data supported_comfyui_version: versionData.supported_comfyui_version, supported_comfyui_frontend_version: versionData.supported_comfyui_frontend_version, supported_os: normalizeOSValues(versionData.supported_os), supported_accelerators: versionData.supported_accelerators, // Status information version_status: versionData.status, is_banned: versionData.status === 'NodeVersionStatusBanned', is_pending: versionData.status === 'NodeVersionStatusPending' } requirements.push(requirement) } else { console.warn( `[ConflictDetection] No Registry data found for ${packageId}, using fallback` ) // Create fallback requirement without Registry data const fallbackRequirement: NodePackRequirements = { id: packageId, name: packInfo?.name || packageId, installed_version: installedVersion, is_enabled: isEnabled, is_banned: false, is_pending: false } requirements.push(fallbackRequirement) } } return requirements } catch (error) { console.warn( '[ConflictDetection] Failed to fetch package requirements:', error ) return [] } } /** * Detects conflicts for an individual package using Registry API data. * * @param packageReq Package requirements from Registry * @param sysEnv Current system environment * @returns Conflict detection result for the package */ function detectPackageConflicts( packageReq: NodePackRequirements, sysEnv: SystemEnvironment ): ConflictDetectionResult { const conflicts: ConflictDetail[] = [] // Helper function to check if a value indicates "compatible with all" const isCompatibleWithAll = (value: any): boolean => { if (value === null || value === undefined) return true if (typeof value === 'string' && value.trim() === '') return true if (Array.isArray(value) && value.length === 0) return true return false } // 1. ComfyUI version conflict check if (!isCompatibleWithAll(packageReq.supported_comfyui_version)) { const versionConflict = checkVersionConflict( 'comfyui_version', sysEnv.comfyui_version, packageReq.supported_comfyui_version! ) if (versionConflict) conflicts.push(versionConflict) } // 2. Frontend version conflict check if (!isCompatibleWithAll(packageReq.supported_comfyui_frontend_version)) { const versionConflict = checkVersionConflict( 'frontend_version', sysEnv.frontend_version, packageReq.supported_comfyui_frontend_version! ) if (versionConflict) conflicts.push(versionConflict) } // 3. OS compatibility check if (!isCompatibleWithAll(packageReq.supported_os)) { const osConflict = checkOSConflict(packageReq.supported_os!, sysEnv.os) if (osConflict) conflicts.push(osConflict) } // 4. Accelerator compatibility check if (!isCompatibleWithAll(packageReq.supported_accelerators)) { const acceleratorConflict = checkAcceleratorConflict( packageReq.supported_accelerators!, sysEnv.available_accelerators ) if (acceleratorConflict) conflicts.push(acceleratorConflict) } // 5. Banned package check using shared logic const bannedConflict = checkBannedStatus(packageReq.is_banned) if (bannedConflict) { conflicts.push(bannedConflict) } // 6. Registry data availability check using shared logic const pendingConflict = checkPendingStatus(packageReq.is_pending) if (pendingConflict) { conflicts.push(pendingConflict) } // Generate result const hasConflict = conflicts.length > 0 return { package_id: packageReq.id ?? '', package_name: packageReq.name ?? '', has_conflict: hasConflict, conflicts, is_compatible: !hasConflict } } /** * Fetches Python import failure information from ComfyUI Manager. * Gets installed packages and checks each one for import failures using bulk API. * @returns Promise that resolves to import failure data */ async function fetchImportFailInfo(): Promise> { try { const comfyManagerService = useComfyManagerService() // Use installedPacksWithVersions to match what versions bulk API uses // This ensures both APIs check the same set of packages if ( !installedPacksWithVersions.value || installedPacksWithVersions.value.length === 0 ) { console.warn( '[ConflictDetection] No installed packages available for import failure check' ) return {} } const packageIds = installedPacksWithVersions.value.map((pack) => pack.id) // Use bulk API to get import failure info for all packages at once const bulkResult = await comfyManagerService.getImportFailInfoBulk( { cnr_ids: packageIds }, abortController.value?.signal ) if (bulkResult) { // Filter out null values (packages without import failures) const importFailures: Record = {} Object.entries(bulkResult).forEach(([packageId, failInfo]) => { if (failInfo !== null) { importFailures[packageId] = failInfo console.log( `[ConflictDetection] Import failure found for ${packageId}:`, failInfo ) } }) return importFailures } return {} } catch (error) { console.warn( '[ConflictDetection] Failed to fetch import failure information:', error ) return {} } } /** * Detects runtime conflicts from Python import failures. * @param importFailInfo Import failure data from Manager API * @returns Array of conflict detection results for failed imports */ function detectImportFailConflicts( importFailInfo: Record ): ConflictDetectionResult[] { const results: ConflictDetectionResult[] = [] if (!importFailInfo || typeof importFailInfo !== 'object') { return results } // Process import failures for (const [packageId, failureInfo] of Object.entries(importFailInfo)) { if (failureInfo && typeof failureInfo === 'object') { // Extract error information from Manager API response const errorMsg = failureInfo.msg || 'Unknown import error' const modulePath = failureInfo.path || '' results.push({ package_id: packageId, package_name: packageId, has_conflict: true, conflicts: [ { type: 'import_failed', current_value: 'installed', required_value: failureInfo.msg } ], is_compatible: false }) console.warn( `[ConflictDetection] Python import failure detected for ${packageId}:`, { path: modulePath, error: errorMsg } ) } } return results } /** * Performs complete conflict detection. * @returns Promise that resolves to conflict detection response */ async function performConflictDetection(): Promise { if (isDetecting.value) { console.log('[ConflictDetection] Already detecting, skipping') return { success: false, error_message: 'Already detecting conflicts', summary: detectionSummary.value!, results: detectionResults.value } } isDetecting.value = true detectionError.value = null const startTime = Date.now() try { // 1. Collect system environment information const sysEnv = await detectSystemEnvironment() // 2. Collect package requirement information const packageRequirements = await fetchPackageRequirements() // 3. Detect conflicts for each package (parallel processing) const conflictDetectionTasks = packageRequirements.map( async (packageReq) => { try { return detectPackageConflicts(packageReq, sysEnv) } catch (error) { console.warn( `[ConflictDetection] Failed to detect conflicts for package ${packageReq.name}:`, error ) // Return null for failed packages, will be filtered out return null } } ) const conflictResults = await Promise.allSettled(conflictDetectionTasks) const packageResults: ConflictDetectionResult[] = conflictResults .map((result) => (result.status === 'fulfilled' ? result.value : null)) .filter((result): result is ConflictDetectionResult => result !== null) // 4. Detect Python import failures const importFailInfo = await fetchImportFailInfo() const importFailResults = detectImportFailConflicts(importFailInfo) // 5. Combine all results const allResults = [...packageResults, ...importFailResults] // 6. Generate summary information const summary = generateSummary(allResults, Date.now() - startTime) // 7. Update state detectionResults.value = allResults detectionSummary.value = summary lastDetectionTime.value = new Date().toISOString() console.log('[ConflictDetection] Conflict detection completed:', summary) // Store conflict results for later UI display // Dialog will be shown based on specific events, not on app mount if (allResults.some((result) => result.has_conflict)) { const conflictedResults = allResults.filter( (result) => result.has_conflict ) // Merge conflicts for packages with the same name const mergedConflicts = mergeConflictsByPackageName(conflictedResults) console.log( '[ConflictDetection] Conflicts detected (stored for UI):', mergedConflicts ) // Store merged conflicts in Pinia store for UI usage conflictStore.setConflictedPackages(mergedConflicts) // Also update local state for backward compatibility detectionResults.value.splice( 0, detectionResults.value.length, ...mergedConflicts ) storedMergedConflicts.value = [...mergedConflicts] // Use merged conflicts in response as well const response: ConflictDetectionResponse = { success: true, summary, results: mergedConflicts, detected_system_environment: sysEnv } return response } else { // No conflicts detected, clear the results conflictStore.clearConflicts() detectionResults.value = [] } const response: ConflictDetectionResponse = { success: true, summary, results: allResults, detected_system_environment: sysEnv } return response } catch (error) { console.error( '[ConflictDetection] Error during conflict detection:', error ) detectionError.value = error instanceof Error ? error.message : String(error) return { success: false, error_message: detectionError.value, summary: detectionSummary.value || getEmptySummary(), results: [] } } finally { isDetecting.value = false // Clear abort controller to prevent memory leaks if (abortController.value) { abortController.value = null } } } /** * Error-resilient initialization (called on app mount). * Async function that doesn't block UI setup. * Ensures proper order: installed -> system_stats -> versions bulk -> import_fail_info_bulk */ async function initializeConflictDetection(): Promise { try { // Simply perform conflict detection // The useInstalledPacks will handle fetching installed list if needed await performConflictDetection() } catch (error) { console.warn( '[ConflictDetection] Error during initialization (ignored):', error ) // Errors do not affect other parts of the app } } // Cleanup function for request cancellation function cancelRequests(): void { if (abortController.value) { abortController.value.abort() abortController.value = null } } // Auto-cleanup on component unmount // Only register lifecycle hooks if we're in a Vue component context const instance = getCurrentInstance() if (instance) { onUnmounted(() => { cancelRequests() }) } // Helper functions (implementations at the bottom of the file) /** * Check if conflicts should trigger modal display after "What's New" dismissal */ async function shouldShowConflictModalAfterUpdate(): Promise { console.log( '[ConflictDetection] Checking if conflict modal should show after update...' ) // Ensure conflict detection has run if (detectionResults.value.length === 0) { console.log( '[ConflictDetection] No detection results, running conflict detection...' ) await performConflictDetection() } // Check if this is a version update scenario // In a real scenario, this would check actual version change // For now, we'll assume it's an update if we have conflicts and modal hasn't been dismissed const hasActualConflicts = hasConflicts.value const canShowModal = acknowledgment.shouldShowConflictModal.value console.log('[ConflictDetection] Modal check:', { hasConflicts: hasActualConflicts, canShowModal: canShowModal, conflictedPackagesCount: conflictedPackages.value.length }) return hasActualConflicts && canShowModal } /** * Check compatibility for a node. * Used by components like PackVersionSelectorPopover. */ function checkNodeCompatibility( node: Node | components['schemas']['NodeVersion'] ) { const systemStatsStore = useSystemStatsStore() const systemStats = systemStatsStore.systemStats if (!systemStats) return { hasConflict: false, conflicts: [] } const conflicts: ConflictDetail[] = [] // Check OS compatibility using centralized function // First try latest_version (most accurate), then fallback to top level const supportedOS = ('latest_version' in node ? node.latest_version?.supported_os : null) || node.supported_os if (supportedOS && supportedOS.length > 0) { const currentOS = systemStats.system?.os || 'unknown' const osConflict = checkOSConflict(supportedOS, currentOS) if (osConflict) { conflicts.push(osConflict) } } // Check accelerator compatibility using centralized function // First try latest_version (most accurate), then fallback to top level const supportedAccelerators = ('latest_version' in node ? node.latest_version?.supported_accelerators : null) || node.supported_accelerators if (supportedAccelerators && supportedAccelerators.length > 0) { // Extract available accelerators from system stats const acceleratorInfo = extractAcceleratorInfo(systemStats) const availableAccelerators: Node['supported_accelerators'] = [] acceleratorInfo.available?.forEach((accel) => { if (accel === 'CUDA') availableAccelerators.push('CUDA') if (accel === 'Metal') availableAccelerators.push('Metal') if (accel === 'CPU') availableAccelerators.push('CPU') }) const acceleratorConflict = checkAcceleratorConflict( supportedAccelerators, availableAccelerators ) if (acceleratorConflict) { conflicts.push(acceleratorConflict) } } // Check ComfyUI version compatibility // First try latest_version (most accurate), then fallback to top level const comfyuiVersionRequirement = ('latest_version' in node ? node.latest_version?.supported_comfyui_version : null) || node.supported_comfyui_version if (comfyuiVersionRequirement) { const currentComfyUIVersion = systemStats.system?.comfyui_version if (currentComfyUIVersion && currentComfyUIVersion !== 'unknown') { const versionConflict = utilCheckVersionCompatibility( 'comfyui_version', currentComfyUIVersion, comfyuiVersionRequirement ) if (versionConflict) { conflicts.push(versionConflict) } } } // Check ComfyUI Frontend version compatibility // First try latest_version (most accurate), then fallback to top level const frontendVersionRequirement = ('latest_version' in node ? node.latest_version?.supported_comfyui_frontend_version : null) || node.supported_comfyui_frontend_version if (frontendVersionRequirement) { const currentFrontendVersion = config.app_version if (currentFrontendVersion && currentFrontendVersion !== 'unknown') { const versionConflict = utilCheckVersionCompatibility( 'frontend_version', currentFrontendVersion, frontendVersionRequirement ) if (versionConflict) { conflicts.push(versionConflict) } } } // Check banned package status using shared logic const bannedConflict = checkBannedStatus( node.status === 'NodeStatusBanned' || node.status === 'NodeVersionStatusBanned' ) if (bannedConflict) { conflicts.push(bannedConflict) } // Check pending status using shared logic const pendingConflict = checkPendingStatus( node.status === 'NodeVersionStatusPending' ) if (pendingConflict) { conflicts.push(pendingConflict) } return { hasConflict: conflicts.length > 0, conflicts } } return { // State isDetecting: readonly(isDetecting), lastDetectionTime: readonly(lastDetectionTime), detectionError: readonly(detectionError), systemEnvironment: readonly(systemEnvironment), detectionResults: readonly(detectionResults), detectionSummary: readonly(detectionSummary), // Computed hasConflicts, conflictedPackages, bannedPackages, securityPendingPackages, // Methods performConflictDetection, detectSystemEnvironment, initializeConflictDetection, cancelRequests, shouldShowConflictModalAfterUpdate, // Helper functions for other components checkNodeCompatibility } } // Helper Functions Implementation /** * Merges conflict results for packages with the same name. * Combines all conflicts from different detection sources (registry, python, extension) * into a single result per package name. * @param conflicts Array of conflict detection results * @returns Array of merged conflict detection results */ function mergeConflictsByPackageName( conflicts: ConflictDetectionResult[] ): ConflictDetectionResult[] { const mergedMap = new Map() conflicts.forEach((conflict) => { // Normalize package name by removing version suffix (@1_0_3) for consistent merging const normalizedPackageName = conflict.package_name.includes('@') ? conflict.package_name.substring(0, conflict.package_name.indexOf('@')) : conflict.package_name if (mergedMap.has(normalizedPackageName)) { // Package already exists, merge conflicts const existing = mergedMap.get(normalizedPackageName)! // Combine all conflicts, avoiding duplicates using es-toolkit uniqBy for O(n) performance const allConflicts = [...existing.conflicts, ...conflict.conflicts] const uniqueConflicts = uniqBy( allConflicts, (conflict) => `${conflict.type}|${conflict.current_value}|${conflict.required_value}` ) // Update the existing entry with normalized package name mergedMap.set(normalizedPackageName, { ...existing, package_name: normalizedPackageName, conflicts: uniqueConflicts, has_conflict: uniqueConflicts.length > 0, is_compatible: uniqueConflicts.length === 0 }) } else { // New package, add with normalized package name mergedMap.set(normalizedPackageName, { ...conflict, package_name: normalizedPackageName }) } }) return Array.from(mergedMap.values()) } /** * Fetches frontend version from config. * @returns Promise that resolves to frontend version string */ async function fetchFrontendVersion(): Promise { try { // Get frontend version from vite build-time constant or fallback to config return config.app_version || import.meta.env.VITE_APP_VERSION || 'unknown' } catch { return 'unknown' } } /** * Detects system architecture from user agent. * Note: Browser architecture detection has limitations and may not be 100% accurate. * @returns Architecture string */ function getArchitecture(): string { const ua = navigator.userAgent.toLowerCase() if (ua.includes('arm64') || ua.includes('aarch64')) return 'arm64' if (ua.includes('arm')) return 'arm' if (ua.includes('x86_64') || ua.includes('x64')) return 'x64' if (ua.includes('x86')) return 'x86' return 'unknown' } /** * Normalizes OS values from Registry API to match our SupportedOS type. * Registry Admin guide specifies: Windows, macOS, Linux * @param osValues OS values from Registry API * @returns Normalized OS values */ function normalizeOSValues( osValues: string[] | undefined ): Node['supported_os'] { if (!osValues || osValues.length === 0) { return [] } return osValues.map((os) => { // Map to standard Registry values (case-sensitive) if (os === 'Windows' || os.toLowerCase().includes('win')) { return 'Windows' } if (os === 'macOS' || os.toLowerCase().includes('mac') || os === 'darwin') { return 'macOS' } if (os === 'Linux' || os.toLowerCase().includes('linux')) { return 'Linux' } if (os.toLowerCase() === 'any') { return 'any' } // Return as-is if it matches standard format return os }) } /** * Detects operating system from system stats OS string and additional system information. * @param systemOS OS string from system stats API * @param systemStats Full system stats object for additional context * @returns Operating system type */ function detectOSFromSystemStats( systemOS: string, systemStats?: SystemStats | null ): string { const os = systemOS.toLowerCase() // Handle specific OS strings (return Registry standard format) if (os.includes('darwin') || os.includes('mac')) return 'macOS' if (os.includes('linux')) return 'Linux' if (os.includes('win') || os === 'nt') return 'Windows' // Handle Python's os.name values if (os === 'posix') { // posix could be macOS or Linux, need additional detection // Method 1: Check for MPS device (Metal Performance Shaders = macOS) if (systemStats?.devices) { const hasMpsDevice = systemStats.devices.some( (device) => device.type === 'mps' ) if (hasMpsDevice) { return 'macOS' // Registry standard format } } // Method 2: Check user agent as fallback const userAgent = navigator.userAgent.toLowerCase() if (userAgent.includes('mac')) return 'macOS' if (userAgent.includes('linux')) return 'Linux' // Default to 'any' if we can't determine return 'any' } return 'any' } /** * Extracts architecture information from system stats. * @param systemStats System stats data from API * @returns Architecture string */ function extractArchitectureFromSystemStats( systemStats: SystemStats | null ): string { try { if (systemStats?.devices && systemStats.devices.length > 0) { // Check if we have MPS device (indicates Apple Silicon) const hasMpsDevice = systemStats.devices.some( (device) => device.type === 'mps' ) if (hasMpsDevice) { // MPS is only available on Apple Silicon Macs return 'arm64' } // Check device names for architecture hints (fallback) for (const device of systemStats.devices) { if (!device?.name || typeof device.name !== 'string') { continue } const deviceName = device.name.toLowerCase() // Apple Silicon detection if ( deviceName.includes('apple m1') || deviceName.includes('apple m2') || deviceName.includes('apple m3') || deviceName.includes('apple m4') ) { return 'arm64' } // Intel/AMD detection if ( deviceName.includes('intel') || deviceName.includes('amd') || deviceName.includes('nvidia') || deviceName.includes('geforce') || deviceName.includes('radeon') ) { return 'x64' } } } // Fallback to basic User-Agent detection if system stats don't provide clear info return getArchitecture() } catch (error) { console.warn( '[ConflictDetection] Failed to extract architecture from system stats:', error ) return getArchitecture() } } /** * Extracts accelerator information from system stats. * @param systemStats System stats data from store * @returns Accelerator information object */ function extractAcceleratorInfo(systemStats: SystemStats | null): { available: Node['supported_accelerators'] primary: string memory_mb?: number } { try { if (systemStats?.devices && systemStats.devices.length > 0) { const accelerators = new Set() let primaryDevice: string = 'CPU' let totalMemory = 0 let maxDevicePriority = 0 // Device type priority (higher = better) const getDevicePriority = (type: string): number => { switch (type.toLowerCase()) { case 'cuda': return 5 case 'mps': return 4 case 'rocm': return 3 case 'xpu': return 2 // Intel GPU case 'npu': return 1 // Neural Processing Unit case 'mlu': return 1 // Cambricon MLU case 'cpu': return 0 default: return 0 } } // Process all devices for (const device of systemStats.devices) { const deviceType = device.type.toLowerCase() const priority = getDevicePriority(deviceType) // Map device type to SupportedAccelerator (Registry standard format) let acceleratorType: string = 'CPU' if (deviceType === 'cuda') { acceleratorType = 'CUDA' } else if (deviceType === 'mps') { acceleratorType = 'Metal' // MPS = Metal Performance Shaders } else if (deviceType === 'rocm') { acceleratorType = 'ROCm' } accelerators.add(acceleratorType) // Update primary device if this one has higher priority if (priority > maxDevicePriority) { primaryDevice = acceleratorType maxDevicePriority = priority } // Accumulate memory from all devices if (device.vram_total) { totalMemory += device.vram_total } } accelerators.add('CPU') // CPU is always available return { available: Array.from(accelerators), primary: primaryDevice, memory_mb: totalMemory > 0 ? Math.round(totalMemory / 1024 / 1024) : undefined } } } catch (error) { console.warn( '[ConflictDetection] Failed to extract GPU information:', error ) } // Default values return { available: ['CPU'], primary: 'CPU', memory_mb: undefined } } /** * Unified version conflict check using Registry API version strings. * Uses shared versionUtil functions for consistent version handling. * @param type Type of version being checked * @param currentVersion Current version string * @param supportedVersion Supported version from Registry * @returns Conflict detail if conflict exists, null otherwise */ function checkVersionConflict( type: ConflictType, currentVersion: string, supportedVersion: string ): ConflictDetail | null { // If current version is unknown, assume compatible (no conflict) if (currentVersion === 'unknown') { return null } // If Registry doesn't specify version requirements, assume compatible if (!supportedVersion || supportedVersion.trim() === '') { return null } try { // Clean the current version using shared utility const cleanCurrent = cleanVersion(currentVersion) // Check version compatibility using shared utility const isCompatible = satisfiesVersion(cleanCurrent, supportedVersion) if (!isCompatible) { return { type, current_value: currentVersion, required_value: supportedVersion } } return null } catch (error) { console.warn( `[ConflictDetection] Failed to parse version requirement: ${supportedVersion}`, error ) return { type, current_value: currentVersion, required_value: supportedVersion } } } /** * Checks for OS compatibility conflicts. */ function checkOSConflict( supportedOS: Node['supported_os'], currentOS: string ): ConflictDetail | null { if (supportedOS?.includes('any') || supportedOS?.includes(currentOS)) { return null } return { type: 'os', current_value: currentOS, required_value: supportedOS ? supportedOS?.join(', ') : '' } } /** * Checks for accelerator compatibility conflicts. */ function checkAcceleratorConflict( supportedAccelerators: Node['supported_accelerators'], availableAccelerators: Node['supported_accelerators'] ): ConflictDetail | null { if ( supportedAccelerators?.includes('any') || supportedAccelerators?.some((acc) => availableAccelerators?.includes(acc)) ) { return null } return { type: 'accelerator', current_value: availableAccelerators ? availableAccelerators.join(', ') : '', required_value: supportedAccelerators ? supportedAccelerators.join(', ') : '' } } /** * Checks for banned package status conflicts. */ function checkBannedStatus(isBanned?: boolean): ConflictDetail | null { if (isBanned === true) { return { type: 'banned', current_value: 'installed', required_value: 'not_banned' } } return null } /** * Checks for pending package status conflicts. */ function checkPendingStatus(isPending?: boolean): ConflictDetail | null { if (isPending === true) { return { type: 'pending', current_value: 'installed', required_value: 'not_pending' } } return null } /** * Generates summary of conflict detection results. */ function generateSummary( results: ConflictDetectionResult[], durationMs: number ): ConflictDetectionSummary { const conflictsByType: Record = { comfyui_version: 0, frontend_version: 0, import_failed: 0, os: 0, accelerator: 0, banned: 0, pending: 0 // python_version: 0 } const conflictsByTypeDetails: Record = { comfyui_version: [], frontend_version: [], import_failed: [], os: [], accelerator: [], banned: [], pending: [] // python_version: [], } let bannedCount = 0 let securityPendingCount = 0 results.forEach((result) => { result.conflicts.forEach((conflict) => { conflictsByType[conflict.type]++ if (!conflictsByTypeDetails[conflict.type].includes(result.package_id)) { conflictsByTypeDetails[conflict.type].push(result.package_id) } if (conflict.type === 'banned') bannedCount++ if (conflict.type === 'pending') securityPendingCount++ }) }) return { total_packages: results.length, compatible_packages: results.filter((r) => r.is_compatible).length, conflicted_packages: results.filter((r) => r.has_conflict).length, banned_packages: bannedCount, pending_packages: securityPendingCount, conflicts_by_type_details: conflictsByTypeDetails, last_check_timestamp: new Date().toISOString(), check_duration_ms: durationMs } } /** * Creates an empty summary for error cases. */ function getEmptySummary(): ConflictDetectionSummary { return { total_packages: 0, compatible_packages: 0, conflicted_packages: 0, banned_packages: 0, pending_packages: 0, conflicts_by_type_details: { comfyui_version: [], frontend_version: [], import_failed: [], os: [], accelerator: [], banned: [], pending: [] // python_version: [], }, last_check_timestamp: new Date().toISOString(), check_duration_ms: 0 } }