Refactor conflict detection system and move to manager extension (#5436)

## Refactor conflict detection system and move to manager extension

### Description

This PR refactors the conflict detection system, moving it from the
global composables to the manager extension folder for better code
organization. Additionally, it improves test type safety and adds
comprehensive test coverage for utility functions.

### Main Changes

#### 📦 Code Organization
- **Moved conflict detection to manager extension** - Relocated all
conflict detection related composables, stores, and utilities from
global scope to `/workbench/extensions/manager/` for better modularity
(https://github.com/Comfy-Org/ComfyUI_frontend/pull/5722)
- **Moved from** `src/composables/useConflictDetection.ts` **to**
`src/workbench/extensions/manager/composables/useConflictDetection.ts`
- Moved related stores and composables to maintain cohesive module
structure

#### ♻️ Refactoring
- **Extracted utility functions** - Split conflict detection logic into
separate utility modules:
  - `conflictUtils.ts` - Conflict consolidation and summary generation
- `systemCompatibility.ts` - OS and accelerator compatibility checking
  - `versionUtil.ts` - Version compatibility checking
- **Removed duplicate state management** - Cleaned up redundant state
and unused functions
- **Improved naming conventions** - Renamed functions for better clarity
- **Removed unused system environment code** - Cleaned up deprecated
code

#### 🔧 Test Improvements
- **Fixed TypeScript errors** in all test files - removed all `any` type
usage
- **Added comprehensive test coverage**:
  - `conflictUtils.test.ts` - 299 lines of tests for conflict utilities
- `systemCompatibility.test.ts` - 270 lines of tests for compatibility
checking
  - `versionUtil.test.ts` - 342 lines of tests for version utilities
- **Updated mock objects** to match actual implementations
- **Aligned with backend changes** - Updated SystemStats structure to
include `pytorch_version`, `embedded_python`,
`required_frontend_version`

#### 🐛 Bug Fixes
- **Fixed OS detection bug** - Resolved issue where 'darwin' was
incorrectly matched as 'Windows' due to containing 'win' substring
- **Fixed import paths** - Updated all import paths after moving to
manager extension
- **Fixed unused exports** - Removed all unused function exports
- **Fixed lint errors** - Resolved all ESLint and Prettier issues

### File Structure Changes

```
Before:
src/
├── composables/
│   └── useConflictDetection.ts (1374 lines)
└── types/

After:
src/
├── utils/
│   ├── conflictUtils.ts (114 lines)
│   ├── systemCompatibility.ts (125 lines)
│   └── versionUtil.ts (enhanced)
└── workbench/extensions/manager/
    ├── composables/
    │   ├── useConflictDetection.ts (758 lines)
    │   └── [other composables]
    └── stores/
        └── conflictDetectionStore.ts
```

### Testing

All tests pass successfully:
-  **155 test files passed**
-  **2209 tests passed**
-  19 skipped (intentionally skipped subgraph-related tests)

### Impact

- **Better code organization** - Manager-specific code is now properly
isolated
- **Improved maintainability** - Smaller, focused utility functions are
easier to test and maintain
- **Enhanced type safety** - No more `any` types in tests
- **Comprehensive test coverage** - All utility functions are thoroughly
tested

### Commits in this PR
1. OS detection bug fix and refactor
2. Remove unused system environment code
3. Improve function naming
4. Refactor conflict detection
5. Remove duplicate state and unused functions
6. Fix unused function exports
7. Move manager features to workbench extension folder
8. Fix import paths
9. Rename systemCompatibility file
10. Improve test type safety
11. Apply ESLint and Prettier fixes

## Screenshots (if applicable)

[screen-capture.webm](https://github.com/user-attachments/assets/b4595604-3761-4d98-ae8e-5693cd0c95bd)


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5436-Manager-refactor-conflict-detect-2686d73d36508186ba06f57dae3656e5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jin Yi
2025-09-26 12:21:05 +09:00
committed by GitHub
parent 78d585eca0
commit 5c1e00ff8e
36 changed files with 1843 additions and 1912 deletions

View File

@@ -22,7 +22,6 @@ import {
type ShowDialogOptions,
useDialogStore
} from '@/stores/dialogStore'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import ManagerProgressDialogContent from '@/workbench/extensions/manager/components/ManagerProgressDialogContent.vue'
import ManagerProgressFooter from '@/workbench/extensions/manager/components/ManagerProgressFooter.vue'
import ManagerProgressHeader from '@/workbench/extensions/manager/components/ManagerProgressHeader.vue'
@@ -31,6 +30,7 @@ import ManagerHeader from '@/workbench/extensions/manager/components/manager/Man
import NodeConflictDialogContent from '@/workbench/extensions/manager/components/manager/NodeConflictDialogContent.vue'
import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue'
import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
export type ConfirmationDialogType =
| 'default'

View File

@@ -1,83 +0,0 @@
import { clean, satisfies } from 'semver'
import type {
ConflictDetail,
ConflictType
} from '@/types/conflictDetectionTypes'
/**
* Cleans a version string by removing common prefixes and normalizing format
* @param version Raw version string (e.g., "v1.2.3", "1.2.3-alpha")
* @returns Cleaned version string or original if cleaning fails
*/
export function cleanVersion(version: string): string {
return clean(version) || version
}
/**
* Checks if a version satisfies a version range
* @param version Current version
* @param range Version range (e.g., ">=1.0.0", "^1.2.0", "1.0.0 - 2.0.0")
* @returns true if version satisfies the range
*/
export function satisfiesVersion(version: string, range: string): boolean {
try {
const cleanedVersion = cleanVersion(version)
return satisfies(cleanedVersion, range)
} catch {
return false
}
}
/**
* Checks version compatibility and returns conflict details.
* Supports all semver ranges including >=, <=, >, <, ~, ^ operators.
* @param type Conflict type (e.g., 'comfyui_version', 'frontend_version')
* @param currentVersion Current version string
* @param supportedVersion Required version range string
* @returns ConflictDetail object if incompatible, null if compatible
*/
export function utilCheckVersionCompatibility(
type: ConflictType,
currentVersion: string,
supportedVersion: string
): ConflictDetail | null {
// If current version is unknown, assume compatible (no conflict)
if (!currentVersion || currentVersion === 'unknown') {
return null
}
// If no version requirement specified, assume compatible (no conflict)
if (!supportedVersion || supportedVersion.trim() === '') {
return null
}
try {
// Clean the current version using semver utilities
const cleanCurrent = cleanVersion(currentVersion)
// Check version compatibility using semver library
const isCompatible = satisfiesVersion(cleanCurrent, supportedVersion)
if (!isCompatible) {
return {
type,
current_value: currentVersion,
required_value: supportedVersion
}
}
return null
} catch (error) {
console.warn(
`[VersionUtil] Failed to parse version requirement: ${supportedVersion}`,
error
)
// On error, assume incompatible to be safe
return {
type,
current_value: currentVersion,
required_value: supportedVersion
}
}
}

View File

@@ -91,7 +91,7 @@ const dialogStore = useDialogStore()
const progressDialogContent = useManagerProgressDialogStore()
const comfyManagerStore = useComfyManagerStore()
const settingStore = useSettingStore()
const { performConflictDetection } = useConflictDetection()
const { runFullConflictAnalysis } = useConflictDetection()
// State management for restart process
const isRestarting = ref<boolean>(false)
@@ -154,8 +154,8 @@ const handleRestart = async () => {
await useWorkflowService().reloadCurrentWorkflow()
// Run conflict detection after restart completion
await performConflictDetection()
// Run conflict detection in background after restart completion
void runFullConflictAnalysis()
} finally {
await settingStore.set(
'Comfy.Toast.DisableReconnectingToast',

View File

@@ -168,12 +168,12 @@ import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ContentDivider from '@/components/common/ContentDivider.vue'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import type {
ConflictDetail,
ConflictDetectionResult
} from '@/types/conflictDetectionTypes'
import { getConflictMessage } from '@/utils/conflictMessageUtil'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
} from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import { getConflictMessage } from '@/workbench/extensions/manager/utils/conflictMessageUtil'
const { showAfterWhatsNew = false, conflictedPackages } = defineProps<{
showAfterWhatsNew?: boolean

View File

@@ -20,7 +20,7 @@ import Message from 'primevue/message'
import { computed, inject } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes'
type PackVersionStatus = components['schemas']['NodeVersionStatus']
type PackStatus = components['schemas']['NodeStatus']

View File

@@ -40,7 +40,9 @@ vi.mock('@/workbench/extensions/manager/stores/comfyManagerStore', () => ({
installedPacks: mockInstalledPacks,
isPackInstalled: (id: string) =>
!!mockInstalledPacks[id as keyof typeof mockInstalledPacks],
isPackEnabled: mockIsPackEnabled
isPackEnabled: mockIsPackEnabled,
getInstalledPackVersion: (id: string) =>
mockInstalledPacks[id as keyof typeof mockInstalledPacks]?.ver
}))
}))

View File

@@ -93,10 +93,10 @@ import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import VerifiedIcon from '@/components/icons/VerifiedIcon.vue'
import { useComfyRegistryService } from '@/services/comfyRegistryService'
import type { components } from '@/types/comfyRegistryTypes'
import { getJoinedConflictMessages } from '@/utils/conflictMessageUtil'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
import { getJoinedConflictMessages } from '@/workbench/extensions/manager/utils/conflictMessageUtil'
type ManagerChannel = ManagerComponents['schemas']['ManagerChannel']
type ManagerDatabaseSource =

View File

@@ -31,10 +31,10 @@ import { t } from '@/i18n'
import { useDialogService } from '@/services/dialogService'
import type { ButtonSize } from '@/types/buttonTypes'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import type { components as ManagerComponents } from '@/workbench/extensions/manager/types/generatedManagerTypes'
type NodePack = components['schemas']['Node']

View File

@@ -65,8 +65,6 @@ import { computed, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
import PackVersionBadge from '@/workbench/extensions/manager/components/manager/PackVersionBadge.vue'
import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue'
@@ -78,6 +76,8 @@ import { useImportFailedDetection } from '@/workbench/extensions/manager/composa
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore'
import { IsInstallingKey } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes'
interface InfoItem {
key: string

View File

@@ -46,13 +46,13 @@ import { computed, inject, ref, watch } from 'vue'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
import PackIcon from '@/workbench/extensions/manager/components/manager/packIcon/PackIcon.vue'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes'
const { nodePacks, hasConflict } = defineProps<{
nodePacks: components['schemas']['Node'][]

View File

@@ -59,8 +59,6 @@ import { computed, onUnmounted, provide, toRef } from 'vue'
import { useComfyRegistryStore } from '@/stores/comfyRegistryStore'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
import PackStatusMessage from '@/workbench/extensions/manager/components/manager/PackStatusMessage.vue'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import PackUninstallButton from '@/workbench/extensions/manager/components/manager/button/PackUninstallButton.vue'
@@ -70,6 +68,8 @@ import PackIconStacked from '@/workbench/extensions/manager/components/manager/p
import { usePacksSelection } from '@/workbench/extensions/manager/composables/nodePack/usePacksSelection'
import { usePacksStatus } from '@/workbench/extensions/manager/composables/nodePack/usePacksStatus'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes'
const { nodePacks } = defineProps<{
nodePacks: components['schemas']['Node'][]

View File

@@ -46,11 +46,11 @@ import Tabs from 'primevue/tabs'
import { computed, inject, ref, watchEffect } from 'vue'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/types/importFailedTypes'
import DescriptionTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue'
import NodesTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/NodesTabPanel.vue'
import WarningTabPanel from '@/workbench/extensions/manager/components/manager/infoPanel/tabs/WarningTabPanel.vue'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import { ImportFailedKey } from '@/workbench/extensions/manager/types/importFailedTypes'
const { nodePack, hasCompatibilityIssues, conflictResult } = defineProps<{
nodePack: components['schemas']['Node']

View File

@@ -29,9 +29,9 @@ import { computed } from 'vue'
import { t } from '@/i18n'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import { getConflictMessage } from '@/utils/conflictMessageUtil'
import { useImportFailedDetection } from '@/workbench/extensions/manager/composables/useImportFailedDetection'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import { getConflictMessage } from '@/workbench/extensions/manager/utils/conflictMessageUtil'
const { nodePack, conflictResult } = defineProps<{
nodePack: components['schemas']['Node']

View File

@@ -26,12 +26,12 @@ import { computed, inject } from 'vue'
import { useI18n } from 'vue-i18n'
import type { components } from '@/types/comfyRegistryTypes'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import PackEnableToggle from '@/workbench/extensions/manager/components/manager/button/PackEnableToggle.vue'
import PackInstallButton from '@/workbench/extensions/manager/components/manager/button/PackInstallButton.vue'
import { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { IsInstallingKey } from '@/workbench/extensions/manager/types/comfyManagerTypes'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
const { nodePack } = defineProps<{
nodePack: components['schemas']['Node']

View File

@@ -20,7 +20,12 @@ export const usePackUpdateStatus = (
)
const isUpdateAvailable = computed(() => {
if (!isInstalled.value || isNightlyPack.value || !latestVersion.value) {
if (
!isInstalled.value ||
isNightlyPack.value ||
!latestVersion.value ||
!installedVersion.value
) {
return false
}
return compare(latestVersion.value, installedVersion.value) > 0

View File

@@ -27,7 +27,7 @@ export const useUpdateAvailableNodes = () => {
const isNightlyPack = !!installedVersion && !valid(installedVersion)
if (isNightlyPack || !latestVersion) {
if (isNightlyPack || !latestVersion || !installedVersion) {
return false
}

View File

@@ -2,9 +2,9 @@ import { type ComputedRef, computed, unref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useDialogService } from '@/services/dialogService'
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'
import { useConflictDetectionStore } from '@/workbench/extensions/manager/stores/conflictDetectionStore'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
/**
* Extracting import failed conflicts from conflict list

View File

@@ -1,7 +1,7 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import type { ConflictDetectionResult } from '@/types/conflictDetectionTypes'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
export const useConflictDetectionStore = defineStore(
'conflictDetection',

View File

@@ -0,0 +1,10 @@
/**
* System compatibility type definitions
* Registry supports exactly these values, null/undefined means compatible with all
*/
// Registry OS
export type RegistryOS = 'Windows' | 'macOS' | 'Linux'
// Registry Accelerator
export type RegistryAccelerator = 'CUDA' | 'ROCm' | 'Metal' | 'CPU'

View File

@@ -5,7 +5,7 @@
* This file extends and uses types from comfyRegistryTypes.ts to maintain consistency
* with the Registry API schema.
*/
import type { components } from './comfyRegistryTypes'
import type { components } from '@/types/comfyRegistryTypes'
// Re-export core types from Registry API
export type Node = components['schemas']['Node']
@@ -18,7 +18,6 @@ export type ConflictType =
| 'comfyui_version' // ComfyUI version mismatch
| 'frontend_version' // Frontend version mismatch
| 'import_failed'
// | 'python_version' // Python version mismatch
| 'os' // Operating system incompatibility
| 'accelerator' // GPU/accelerator incompatibility
| 'banned' // Banned package
@@ -28,7 +27,7 @@ export type ConflictType =
* Node Pack requirements from Registry API
* Extends Node type with additional installation and compatibility metadata
*/
export interface NodePackRequirements extends Node {
export interface NodeRequirements extends Node {
installed_version: string
is_enabled: boolean
is_banned: boolean
@@ -42,23 +41,12 @@ export interface NodePackRequirements extends Node {
*/
export interface SystemEnvironment {
// Version information
comfyui_version: string
frontend_version: string
// python_version: string
comfyui_version?: string
frontend_version?: string
// Platform information
os: string
platform_details: string
architecture: string
os?: string
// GPU/accelerator information
available_accelerators: Node['supported_accelerators']
primary_accelerator: string
gpu_memory_mb?: number
// Runtime information
node_env: 'development' | 'production'
user_agent: string
accelerator?: string
}
/**
@@ -81,27 +69,12 @@ export interface ConflictDetail {
required_value: string
}
/**
* Overall conflict detection summary
*/
export interface ConflictDetectionSummary {
total_packages: number
compatible_packages: number
conflicted_packages: number
banned_packages: number
pending_packages: number
conflicts_by_type_details: Record<ConflictType, string[]>
last_check_timestamp: string
check_duration_ms: number
}
/**
* Response payload from conflict detection API
*/
export interface ConflictDetectionResponse {
success: boolean
error_message?: string
summary: ConflictDetectionSummary
results: ConflictDetectionResult[]
detected_system_environment?: Partial<SystemEnvironment>
}

View File

@@ -1,4 +1,4 @@
import type { ConflictDetail } from '@/types/conflictDetectionTypes'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
/**
* Generates a localized conflict message for a given conflict detail.

View File

@@ -0,0 +1,83 @@
import { groupBy, uniqBy } from 'es-toolkit/compat'
import { normalizePackId } from '@/utils/packUtils'
import type {
ConflictDetail,
ConflictDetectionResult
} from '@/workbench/extensions/manager/types/conflictDetectionTypes'
/**
* Checks for banned package status conflicts.
*/
export function createBannedConflict(
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.
*/
export function createPendingConflict(
isPending?: boolean
): ConflictDetail | null {
if (isPending === true) {
return {
type: 'pending',
current_value: 'installed',
required_value: 'not_pending'
}
}
return null
}
/**
* Groups and deduplicates conflicts by normalized package name.
* Consolidates multiple conflict sources (registry checks, import failures, disabled packages with version suffix)
* into a single UI entry per package.
*
* Example:
* - Input: [{name: "pack@1_0_3", conflicts: [...]}, {name: "pack", conflicts: [...]}]
* - Output: [{name: "pack", conflicts: [...combined unique conflicts...]}]
*
* @param conflicts Array of conflict detection results (may have duplicate packages with version suffixes)
* @returns Array of deduplicated conflict results grouped by normalized package name
*/
export function consolidateConflictsByPackage(
conflicts: ConflictDetectionResult[]
): ConflictDetectionResult[] {
// Group conflicts by normalized package name using es-toolkit
const grouped = groupBy(conflicts, (conflict) =>
normalizePackId(conflict.package_name)
)
// Merge conflicts for each group
return Object.entries(grouped).map(([packageName, packageConflicts]) => {
// Flatten all conflicts from the group
const allConflicts = packageConflicts.flatMap((pc) => pc.conflicts)
// Remove duplicate conflicts using uniqBy
const uniqueConflicts = uniqBy(
allConflicts,
(conflict) =>
`${conflict.type}|${conflict.current_value}|${conflict.required_value}`
)
// Use the first item as base and update with merged data
const baseItem = packageConflicts[0]
return {
...baseItem,
package_name: packageName, // Use normalized name
conflicts: uniqueConflicts,
has_conflict: uniqueConflicts.length > 0,
is_compatible: uniqueConflicts.length === 0
}
})
}

View File

@@ -0,0 +1,125 @@
import { isEmpty, isNil } from 'es-toolkit/compat'
import type {
RegistryAccelerator,
RegistryOS
} from '@/workbench/extensions/manager/types/compatibility.types'
import type { ConflictDetail } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
/**
* Maps system OS string to Registry OS format
* @param systemOS Raw OS string from system stats ('darwin', 'win32', 'linux', etc)
* @returns Registry OS or undefined if unknown
*/
function getRegistryOS(systemOS?: string): RegistryOS | undefined {
if (!systemOS) return undefined
const lower = systemOS.toLowerCase()
// Check darwin first to avoid matching 'win' in 'darwin'
if (lower.includes('darwin') || lower.includes('mac')) return 'macOS'
if (lower.includes('win')) return 'Windows'
if (lower.includes('linux')) return 'Linux'
return undefined
}
/**
* Maps device type to Registry accelerator format
* @param deviceType Raw device type from system stats ('cuda', 'mps', 'rocm', 'cpu', etc)
* @returns Registry accelerator
*/
function getRegistryAccelerator(deviceType?: string): RegistryAccelerator {
if (!deviceType) return 'CPU'
const lower = deviceType.toLowerCase()
if (lower === 'cuda') return 'CUDA'
if (lower === 'mps') return 'Metal'
if (lower === 'rocm') return 'ROCm'
return 'CPU'
}
/**
* Checks OS compatibility
* @param supported Supported OS list from Registry (null/undefined = all OS supported)
* @param current Current system OS
* @returns ConflictDetail if incompatible, null if compatible
*/
export function checkOSCompatibility(
supported?: RegistryOS[] | null,
current?: string
): ConflictDetail | null {
// null/undefined/empty = all OS supported
if (isNil(supported) || isEmpty(supported)) return null
const currentOS = getRegistryOS(current)
if (!currentOS) {
return {
type: 'os',
current_value: 'Unknown',
required_value: supported.join(', ')
}
}
if (!supported.includes(currentOS)) {
return {
type: 'os',
current_value: currentOS,
required_value: supported.join(', ')
}
}
return null
}
/**
* Checks accelerator compatibility
* @param supported Supported accelerators from Registry (null/undefined = all accelerators supported)
* @param current Current device type
* @returns ConflictDetail if incompatible, null if compatible
*/
export function checkAcceleratorCompatibility(
supported?: RegistryAccelerator[] | null,
current?: string
): ConflictDetail | null {
// null/undefined/empty = all accelerator supported
if (isNil(supported) || isEmpty(supported)) return null
const currentAcc = getRegistryAccelerator(current)
if (!supported.includes(currentAcc)) {
return {
type: 'accelerator',
current_value: currentAcc,
required_value: supported.join(', ')
}
}
return null
}
/**
* Normalizes OS values from Registry API
* Handles edge cases like "OS Independent"
* @returns undefined if all OS supported, otherwise filtered valid OS list
*/
export function normalizeOSList(
osValues?: string[] | null
): RegistryOS[] | undefined {
if (isNil(osValues) || isEmpty(osValues)) return undefined
// "OS Independent" means all OS supported
if (osValues.some((os) => os.toLowerCase() === 'os independent')) {
return undefined
}
// Filter to valid Registry OS values only
const validOS: RegistryOS[] = []
osValues.forEach((os) => {
if (os === 'Windows' || os === 'macOS' || os === 'Linux') {
if (!validOS.includes(os)) validOS.push(os)
}
})
return validOS.length > 0 ? validOS : undefined
}

View File

@@ -0,0 +1,73 @@
import { isEmpty, isNil } from 'es-toolkit/compat'
import { clean, satisfies } from 'semver'
import config from '@/config'
import type {
ConflictDetail,
ConflictType
} from '@/workbench/extensions/manager/types/conflictDetectionTypes'
/**
* Cleans a version string by removing common prefixes and normalizing format
* @param version Raw version string (e.g., "v1.2.3", "1.2.3-alpha")
* @returns Cleaned version string or original if cleaning fails
*/
function cleanVersion(version: string): string {
return clean(version) || version
}
/**
* Checks version compatibility and returns conflict details.
* Supports all semver ranges including >=, <=, >, <, ~, ^ operators.
* @param type Conflict type (e.g., 'comfyui_version', 'frontend_version')
* @param currentVersion Current version string
* @param supportedVersion Required version range string
* @returns ConflictDetail object if incompatible, null if compatible
*/
export function checkVersionCompatibility(
type: ConflictType,
currentVersion?: string,
supportedVersion?: string
): ConflictDetail | null {
// If current version is unknown, assume compatible (no conflict)
if (isNil(currentVersion) || isEmpty(currentVersion)) {
return null
}
// If no version requirement specified, assume compatible (no conflict)
if (isNil(supportedVersion) || isEmpty(supportedVersion?.trim())) {
return null
}
// Clean and check version compatibility
const cleanCurrent = cleanVersion(currentVersion)
// Check if version satisfies the range
let isCompatible = false
try {
isCompatible = satisfies(cleanCurrent, supportedVersion)
} catch {
// If semver can't parse it, return conflict
return {
type,
current_value: currentVersion,
required_value: supportedVersion
}
}
if (isCompatible) return null
return {
type,
current_value: currentVersion,
required_value: supportedVersion
}
}
/**
* get frontend version from config.
* @returns frontend version string or undefined
*/
export function getFrontendVersion(): string | undefined {
return config.app_version || import.meta.env.VITE_APP_VERSION || undefined
}