diff --git a/frontend-v3-compatibility-plan.md b/frontend-v3-compatibility-plan.md new file mode 100644 index 000000000..ebb5305ce --- /dev/null +++ b/frontend-v3-compatibility-plan.md @@ -0,0 +1,502 @@ +# Frontend V3 Compatibility Layer Implementation Plan (Import-Based API Versioning) + +## Overview + +This document outlines an **import-based API versioning** approach for implementing v3 compatibility in the ComfyUI frontend. This approach provides **type-safe API versioning** through import-based version selection, **dual endpoint data fetching**, and **proxy-based data synchronization**. Extensions choose their API version at import time, ensuring compile-time type safety and backward compatibility. + +## Key Design Principles + +1. **Import-Based Version Selection**: Extensions choose API version through import statements +2. **Type-Safe API Surfaces**: Compile-time type checking for each API version +3. **Dual Endpoint Fetching**: Simultaneous fetching from current and v3 endpoints +4. **Proxy-Based Synchronization**: Bidirectional data sync between API versions +5. **Zero Breaking Changes**: Existing extensions continue to work unchanged +6. **Gradual Migration**: Developers can adopt new versions incrementally + +## Architecture Overview + +### 1. Import-Based API Version Selection + +Extensions import the API version they want to use, getting typed interfaces and guaranteed compatibility: + +```typescript +// Legacy extensions (unchanged) +import { app } from '@/scripts/app' + +// Version-specific imports +import { app } from '@/scripts/app/v1' // v1.x API +import { app } from '@/scripts/app/v1_2' // v1.2 API +import { app } from '@/scripts/app/v2' // v2.x API +import { app } from '@/scripts/app/latest' // Latest/bleeding edge +import { app } from '@/scripts/app' // Defaults to latest + +// Full version-specific imports +import { app, extensionManager, api } from '@/scripts/app/v1_2' + +// Extensions get typed, version-specific interfaces +app.registerExtension({ + name: 'MyExtension', + beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDefV1_2, app: ComfyAppV1_2) { + // nodeData is guaranteed to be in v1.2 format + // app methods are v1.2 compatible + } +}) +``` + +### 2. Dual Endpoint Data Fetching + +The system fetches data from both current and future API endpoints simultaneously: + +```typescript +// Fetch from multiple endpoints +const fetchNodeDefinitions = async () => { + const [currentResponse, v3Response] = await Promise.allSettled([ + api.get('/object_info'), // Current format + api.get('/v3/object_info') // V3 format (when available) + ]) + + // Store all formats + return { + canonical: mergeToCanonical(currentResponse, v3Response), + v1: transformToV1(currentResponse), + v1_2: transformToV1_2(currentResponse), + v3: v3Response || transformToV3(currentResponse) + } +} +``` + +### 3. Proxy-Based Data Synchronization + +Proxies ensure that changes made through any API version stay synchronized: + +```typescript +// Extension modifies node data through v1.2 API +const createV1_2NodeDefProxy = (canonicalNodeDef: ComfyNodeDefLatest) => { + return new Proxy({}, { + get(target, prop) { + // Map v1.2 property access to canonical format + if (prop === 'input') { + return transformLatestToV1_2Input(canonicalNodeDef.inputs) + } + return canonicalNodeDef[mapV1_2PropToLatest(prop)] + }, + + set(target, prop, value) { + // Map v1.2 property changes back to canonical format + if (prop === 'input') { + canonicalNodeDef.inputs = transformV1_2InputToLatest(value) + notifyDataChange(canonicalNodeDef.name, prop, value) + return true + } + canonicalNodeDef[mapV1_2PropToLatest(prop)] = value + return true + } + }) +} +``` + +## Implementation Architecture + +### Phase 1: API Version Infrastructure + +**1.1 Version-Specific Entry Points** +```typescript +// src/scripts/app/index.ts (latest/default) +export * from './latest' + +// src/scripts/app/v1.ts +export { app as default } from './adapters/v1AppAdapter' +export { extensionManager } from './adapters/v1ExtensionAdapter' +export { api } from './adapters/v1ApiAdapter' + +// src/scripts/app/v1_2.ts +export { app as default } from './adapters/v1_2AppAdapter' +export { extensionManager } from './adapters/v1_2ExtensionAdapter' +export { api } from './adapters/v1_2ApiAdapter' +``` + +**1.2 Version-Specific TypeScript Interfaces** +```typescript +// src/types/versions/v1.ts +export interface ComfyNodeDefV1 { + name: string + input?: { + required?: Record + optional?: Record + } + output?: string[] + output_is_list?: boolean[] +} + +// src/types/versions/v1_2.ts +export interface ComfyNodeDefV1_2 extends ComfyNodeDefV1 { + inputs?: ComfyInputSpecV1_2[] + metadata?: NodeMetadataV1_2 +} + +// src/types/versions/v3.ts +export interface ComfyNodeDefV3 { + name: string + schema: JsonSchema + inputs: InputSpecV3[] + outputs: OutputSpecV3[] +} +``` + +### Phase 2: Multi-Version Data Layer + +**2.1 Unified Data Store** +```typescript +// src/stores/nodeDefStore.ts +export const useNodeDefStore = defineStore('nodeDef', () => { + const nodeDefinitions = ref<{ + canonical: Record + v1: Record + v1_2: Record + v3: Record + }>({ + canonical: {}, + v1: {}, + v1_2: {}, + v3: {} + }) + + const fetchNodeDefinitions = async () => { + const [currentData, v3Data] = await Promise.allSettled([ + api.get('/object_info'), + api.get('/v3/object_info') + ]) + + nodeDefinitions.value = transformToAllVersions(currentData, v3Data) + } + + // Version-specific getters with reactivity + const getNodeDefsV1 = computed(() => nodeDefinitions.value.v1) + const getNodeDefsV1_2 = computed(() => nodeDefinitions.value.v1_2) + const getNodeDefsV3 = computed(() => nodeDefinitions.value.v3) + + return { + nodeDefinitions, + fetchNodeDefinitions, + getNodeDefsV1, + getNodeDefsV1_2, + getNodeDefsV3 + } +}) +``` + +**2.2 Data Transformation Pipeline** +```typescript +// src/utils/versionTransforms.ts +export class VersionTransforms { + static transformToAllVersions(currentData: any, v3Data: any) { + const canonical = this.createCanonicalFormat(currentData, v3Data) + + return { + canonical, + v1: this.canonicalToV1(canonical), + v1_2: this.canonicalToV1_2(canonical), + v3: v3Data || this.canonicalToV3(canonical) + } + } + + static canonicalToV1(canonical: ComfyNodeDefLatest): ComfyNodeDefV1 { + return { + name: canonical.name, + input: { + required: canonical.inputs + ?.filter(i => i.required) + .reduce((acc, input) => { + acc[input.name] = input.spec + return acc + }, {} as Record), + optional: canonical.inputs + ?.filter(i => !i.required) + .reduce((acc, input) => { + acc[input.name] = input.spec + return acc + }, {} as Record) + }, + output: canonical.outputs?.map(o => o.type), + output_is_list: canonical.outputs?.map(o => o.is_list) + } + } + + static canonicalToV1_2(canonical: ComfyNodeDefLatest): ComfyNodeDefV1_2 { + return { + ...this.canonicalToV1(canonical), + inputs: canonical.inputs?.map(input => ({ + name: input.name, + type: input.type, + required: input.required, + options: input.options + })) + } + } +} +``` + +### Phase 3: Proxy-Based Synchronization + +**3.1 Bidirectional Data Proxies** +```typescript +// src/utils/versionProxies.ts +export class VersionProxies { + private static canonicalStore = new Map() + private static eventBus = new EventTarget() + + static createV1Proxy(nodeId: string): ComfyNodeDefV1 { + const canonical = this.canonicalStore.get(nodeId) + if (!canonical) throw new Error(`Node ${nodeId} not found`) + + return new Proxy({} as ComfyNodeDefV1, { + get(target, prop: keyof ComfyNodeDefV1) { + return VersionProxies.transformCanonicalToV1Property(canonical, prop) + }, + + set(target, prop: keyof ComfyNodeDefV1, value) { + VersionProxies.transformV1PropertyToCanonical(canonical, prop, value) + VersionProxies.notifyChange(nodeId, prop, value) + return true + } + }) + } + + static createV1_2Proxy(nodeId: string): ComfyNodeDefV1_2 { + const canonical = this.canonicalStore.get(nodeId) + if (!canonical) throw new Error(`Node ${nodeId} not found`) + + return new Proxy({} as ComfyNodeDefV1_2, { + get(target, prop: keyof ComfyNodeDefV1_2) { + return VersionProxies.transformCanonicalToV1_2Property(canonical, prop) + }, + + set(target, prop: keyof ComfyNodeDefV1_2, value) { + VersionProxies.transformV1_2PropertyToCanonical(canonical, prop, value) + VersionProxies.notifyChange(nodeId, prop, value) + return true + } + }) + } + + private static transformCanonicalToV1Property(canonical: ComfyNodeDefLatest, prop: keyof ComfyNodeDefV1) { + switch (prop) { + case 'input': + return { + required: canonical.inputs?.filter(i => i.required).reduce((acc, input) => { + acc[input.name] = input.spec + return acc + }, {} as Record), + optional: canonical.inputs?.filter(i => !i.required).reduce((acc, input) => { + acc[input.name] = input.spec + return acc + }, {} as Record) + } + case 'output': + return canonical.outputs?.map(o => o.type) + case 'output_is_list': + return canonical.outputs?.map(o => o.is_list) + default: + return canonical[prop as keyof ComfyNodeDefLatest] + } + } + + private static transformV1PropertyToCanonical(canonical: ComfyNodeDefLatest, prop: keyof ComfyNodeDefV1, value: any) { + switch (prop) { + case 'input': + canonical.inputs = [ + ...Object.entries(value.required || {}).map(([name, spec]) => ({ + name, + spec, + required: true, + type: this.inferTypeFromSpec(spec) + })), + ...Object.entries(value.optional || {}).map(([name, spec]) => ({ + name, + spec, + required: false, + type: this.inferTypeFromSpec(spec) + })) + ] + break + case 'output': + canonical.outputs = value.map((type: string, index: number) => ({ + type, + is_list: canonical.outputs?.[index]?.is_list || false + })) + break + case 'output_is_list': + canonical.outputs = canonical.outputs?.map((output, index) => ({ + ...output, + is_list: value[index] || false + })) + break + default: + (canonical as any)[prop] = value + } + } + + private static notifyChange(nodeId: string, prop: string, value: any) { + this.eventBus.dispatchEvent(new CustomEvent('nodedef-changed', { + detail: { nodeId, prop, value } + })) + } +} +``` + +### Phase 4: Extension System Integration + +**4.1 Version-Aware Extension Service** +```typescript +// src/services/extensionService.ts +export const useExtensionService = () => { + const extensionsByVersion = new Map() + + const registerExtension = (extension: ComfyExtension, apiVersion: string = 'latest') => { + extension.apiVersion = apiVersion + + if (!extensionsByVersion.has(apiVersion)) { + extensionsByVersion.set(apiVersion, []) + } + extensionsByVersion.get(apiVersion)!.push(extension) + } + + const invokeExtensionsForAllVersions = async (hook: string, canonicalArgs: any[]) => { + const promises = [] + + for (const [version, extensions] of extensionsByVersion) { + const versionPromise = invokeExtensionsForVersion(version, hook, canonicalArgs) + promises.push(versionPromise) + } + + await Promise.all(promises) + } + + const invokeExtensionsForVersion = async (version: string, hook: string, canonicalArgs: any[]) => { + const extensions = extensionsByVersion.get(version) || [] + + for (const extension of extensions) { + if (extension[hook]) { + const transformedArgs = transformArgsForVersion(version, canonicalArgs) + await extension[hook](...transformedArgs) + } + } + } + + const transformArgsForVersion = (version: string, args: any[]) => { + return args.map(arg => { + if (arg && typeof arg === 'object' && arg.name) { + // This is likely a node definition + switch (version) { + case 'v1': + return VersionProxies.createV1Proxy(arg.name) + case 'v1_2': + return VersionProxies.createV1_2Proxy(arg.name) + case 'v3': + return VersionProxies.createV3Proxy(arg.name) + default: + return arg + } + } + return arg + }) + } + + return { + registerExtension, + invokeExtensionsForAllVersions, + invokeExtensionsForVersion + } +} +``` + +**4.2 Version-Specific App Adapters** +```typescript +// src/scripts/app/adapters/v1AppAdapter.ts +export class V1AppAdapter { + constructor(private canonicalApp: ComfyApp) {} + + registerExtension(extension: ComfyExtensionV1) { + const wrappedExtension = { + ...extension, + apiVersion: 'v1', + beforeRegisterNodeDef: (nodeType, nodeData, app) => { + const v1NodeData = VersionProxies.createV1Proxy(nodeData.name) + return extension.beforeRegisterNodeDef?.(nodeType, v1NodeData, this) + }, + nodeCreated: (node, app) => { + return extension.nodeCreated?.(node, this) + } + } + + this.canonicalApp.registerExtension(wrappedExtension) + } + + // Implement other ComfyApp methods with v1 compatibility +} + +// src/scripts/app/adapters/v1_2AppAdapter.ts +export class V1_2AppAdapter { + constructor(private canonicalApp: ComfyApp) {} + + registerExtension(extension: ComfyExtensionV1_2) { + const wrappedExtension = { + ...extension, + apiVersion: 'v1_2', + beforeRegisterNodeDef: (nodeType, nodeData, app) => { + const v1_2NodeData = VersionProxies.createV1_2Proxy(nodeData.name) + return extension.beforeRegisterNodeDef?.(nodeType, v1_2NodeData, this) + } + } + + this.canonicalApp.registerExtension(wrappedExtension) + } +} +``` + +## Migration Strategy + +### Gradual Migration Path + +Extensions can migrate incrementally: + +```typescript +// Phase 1: No changes (works with latest) +import { app } from '@/scripts/app' + +// Phase 2: Explicit version (better compatibility) +import { app } from '@/scripts/app/v1_2' + +// Phase 3: Use newer APIs when ready +import { app } from '@/scripts/app/latest' +``` + +### Development Tools + +```typescript +// Enhanced debugging for version compatibility +ComfyUI.debugExtensions.showVersionMatrix() // Shows which extensions use which API versions +ComfyUI.debugExtensions.testVersionCompatibility() // Tests extension against all API versions +ComfyUI.debugExtensions.validateDataSync() // Validates proxy synchronization +``` + +## Benefits + +1. **Type Safety**: Compile-time type checking for each API version +2. **Zero Breaking Changes**: Existing extensions work unchanged +3. **Bidirectional Sync**: Changes through any API version stay synchronized +4. **Future Proof**: Easy to add new API versions +5. **Performance**: No runtime version detection overhead +6. **Developer Experience**: Clear, typed interfaces for each version + +## Implementation Timeline + +- **Phase 1**: API Version Infrastructure (3-4 days) +- **Phase 2**: Multi-Version Data Layer (2-3 days) +- **Phase 3**: Proxy-Based Synchronization (2-3 days) +- **Phase 4**: Extension System Integration (2-3 days) +- **Phase 5**: Migration Tools & Documentation (1-2 days) + +**Total Estimated Time**: 10-15 days + +This approach provides a solid foundation for API versioning that scales with ComfyUI's growth while maintaining backward compatibility and providing a smooth migration path for extension developers. \ No newline at end of file diff --git a/frontend-v3-compatibility-summary.md b/frontend-v3-compatibility-summary.md new file mode 100644 index 000000000..6ce69dcc2 --- /dev/null +++ b/frontend-v3-compatibility-summary.md @@ -0,0 +1,134 @@ +# ComfyUI Frontend V3 Compatibility - Implementation Summary + +## Core Concept +**Import-based API versioning** with proxy-synchronized data layers. Extensions choose their API version through imports, getting typed interfaces and guaranteed backward compatibility. + +## Architecture Overview + +### 1. Version-Specific Imports +```typescript +// Legacy (unchanged) +import { app } from '@/scripts/app' + +// Version-specific +import { app } from '@/scripts/app/v1' // v1.x API +import { app } from '@/scripts/app/v1_2' // v1.2 API +import { app } from '@/scripts/app/v3' // v3.x API +import { app } from '@/scripts/app/latest' // Latest + +// Typed interfaces per version +app.registerExtension({ + beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDefV1_2, app: ComfyAppV1_2) { + // nodeData guaranteed to be v1.2 format + } +}) +``` + +### 2. Dual Endpoint Data Fetching +```typescript +// Simultaneous fetching from multiple endpoints +const [currentData, v3Data] = await Promise.allSettled([ + api.get('/object_info'), // Current format + api.get('/v3/object_info') // V3 format +]) + +// All versions stored and transformed +nodeDefinitions.value = { + canonical: mergeToCanonical(currentData, v3Data), + v1: transformToV1(currentData), + v1_2: transformToV1_2(currentData), + v3: v3Data || transformToV3(currentData) +} +``` + +### 3. Proxy-Based Synchronization +```typescript +// Bidirectional data sync through proxies +const createV1_2Proxy = (canonical: ComfyNodeDefLatest) => { + return new Proxy({}, { + get(target, prop) { + return transformCanonicalToV1_2(canonical, prop) + }, + set(target, prop, value) { + transformV1_2ToCanonical(canonical, prop, value) + notifyChange(canonical.name, prop, value) + return true + } + }) +} +``` + +## Implementation Structure + +### Phase 1: API Infrastructure (3-4 days) +- **Entry Points**: `src/scripts/app/v1.ts`, `src/scripts/app/v1_2.ts`, etc. +- **Type Definitions**: `src/types/versions/` with version-specific interfaces +- **Adapters**: `src/scripts/app/adapters/` for API compatibility layers + +### Phase 2: Data Layer (2-3 days) +- **Multi-Version Store**: Single store with all format versions +- **Transform Pipeline**: `src/utils/versionTransforms.ts` for format conversion +- **Reactive Getters**: Version-specific computed properties + +### Phase 3: Proxy System (2-3 days) +- **Bidirectional Proxies**: `src/utils/versionProxies.ts` +- **Change Notification**: Event system for data sync +- **Type Safety**: Proper typing for proxy objects + +### Phase 4: Extension Integration (2-3 days) +- **Version-Aware Service**: Extensions grouped by API version +- **Hook Invocation**: Transform args per version before calling +- **App Adapters**: Version-specific app instances + +## Key Benefits + +- **Type Safety**: Compile-time checking for each API version +- **Zero Breaking Changes**: Existing extensions work unchanged +- **Bidirectional Sync**: Changes through any API stay synchronized +- **Performance**: No runtime version detection overhead +- **Future Proof**: Easy to add new API versions +- **Developer Experience**: Clear, typed interfaces + +## Migration Path + +```typescript +// Step 1: No changes (works with latest) +import { app } from '@/scripts/app' + +// Step 2: Explicit version (better compatibility) +import { app } from '@/scripts/app/v1_2' + +// Step 3: Use newer APIs when ready +import { app } from '@/scripts/app/latest' +``` + +## Example Usage + +```typescript +// v1.2 Extension +import { app } from '@/scripts/app/v1_2' + +app.registerExtension({ + name: 'MyExtension', + beforeRegisterNodeDef(nodeType, nodeData: ComfyNodeDefV1_2, app) { + // nodeData.input.required - v1 format + // nodeData.inputs - v1.2 format + if (nodeData.inputs) { + // Use v1.2 features + } else { + // Fallback to v1 format + } + } +}) +``` + +## Implementation Timeline +- **Phase 1**: API Version Infrastructure (3-4 days) +- **Phase 2**: Multi-Version Data Layer (2-3 days) +- **Phase 3**: Proxy-Based Synchronization (2-3 days) +- **Phase 4**: Extension System Integration (2-3 days) +- **Phase 5**: Migration Tools & Documentation (1-2 days) + +**Total**: 10-15 days + +This approach provides type-safe API versioning that scales with ComfyUI's growth while maintaining complete backward compatibility and smooth migration paths for extension developers. \ No newline at end of file diff --git a/src/services/extensionService.ts b/src/services/extensionService.ts index 1fd92b256..3aa4712ed 100644 --- a/src/services/extensionService.ts +++ b/src/services/extensionService.ts @@ -9,6 +9,7 @@ import { useSettingStore } from '@/stores/settingStore' import { useWidgetStore } from '@/stores/widgetStore' import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore' import type { ComfyExtension } from '@/types/comfy' +import { VersionProxies } from '@/utils/versionProxies' export const useExtensionService = () => { const extensionStore = useExtensionStore() @@ -128,10 +129,132 @@ export const useExtensionService = () => { ) } + /** + * Register extension with API version tracking + */ + const registerExtensionWithVersion = ( + extension: ComfyExtension, + apiVersion: string = 'latest' + ) => { + extension.apiVersion = apiVersion + registerExtension(extension) + } + + /** + * Invoke extensions for a specific API version with transformed args + */ + const invokeExtensionsForVersion = async ( + hook: keyof ComfyExtension, + apiVersion: string, + ...args: T + ) => { + const extensions = extensionStore.extensions.filter( + (ext) => + ext.apiVersion === apiVersion || + (!ext.apiVersion && apiVersion === 'latest') + ) + + for (const extension of extensions) { + if (extension[hook] && typeof extension[hook] === 'function') { + try { + const transformedArgs = transformArgsForVersion(apiVersion, args) + await (extension[hook] as (...args: T) => void | Promise)( + ...transformedArgs + ) + } catch (error) { + console.error( + `Error in extension ${extension.name} hook ${String(hook)}:`, + error + ) + } + } + } + } + + /** + * Invoke extensions for all API versions with appropriate data transformation + */ + const invokeExtensionsForAllVersions = async ( + hook: keyof ComfyExtension, + ...args: T + ) => { + const apiVersions = new Set(['latest', 'v1', 'v1_2', 'v3']) + const promises = [] + + for (const version of apiVersions) { + promises.push(invokeExtensionsForVersion(hook, version, ...args)) + } + + await Promise.all(promises) + } + + /** + * Transform arguments for specific API version + */ + const transformArgsForVersion = (version: string, args: any[]) => { + return args.map((arg) => { + // If argument looks like a node definition, create appropriate proxy + if ( + arg && + typeof arg === 'object' && + arg.name && + typeof arg.name === 'string' + ) { + try { + switch (version) { + case 'v1': + return VersionProxies.createV1Proxy(arg.name) + case 'v1_2': + return VersionProxies.createV1_2Proxy(arg.name) + case 'v3': + return VersionProxies.createV3Proxy(arg.name) + default: + return arg + } + } catch (error) { + // If proxy creation fails, return original arg + console.warn(`Failed to create proxy for node ${arg.name}:`, error) + return arg + } + } + return arg + }) + } + + /** + * Get extension compatibility report by API version + */ + const getExtensionVersionReport = () => { + const extensions = extensionStore.extensions + const versionGroups = extensions.reduce( + (acc, ext) => { + const version = ext.apiVersion || 'latest' + if (!acc[version]) acc[version] = [] + acc[version].push(ext) + return acc + }, + {} as Record + ) + + return { + total: extensions.length, + versionGroups, + details: Object.entries(versionGroups).map(([version, exts]) => ({ + version, + count: exts.length, + extensions: exts.map((ext) => ext.name) + })) + } + } + return { loadExtensions, registerExtension, + registerExtensionWithVersion, invokeExtensions, - invokeExtensionsAsync + invokeExtensionsAsync, + invokeExtensionsForVersion, + invokeExtensionsForAllVersions, + getExtensionVersionReport } } diff --git a/src/types/comfy.ts b/src/types/comfy.ts index f78fccaeb..3a8de84c0 100644 --- a/src/types/comfy.ts +++ b/src/types/comfy.ts @@ -47,6 +47,10 @@ export interface ComfyExtension { * The name of the extension */ name: string + /** + * API version this extension is using (set automatically by import) + */ + apiVersion?: string /** * The commands defined by the extension */ diff --git a/src/types/versions/index.ts b/src/types/versions/index.ts new file mode 100644 index 000000000..ca18c0ca6 --- /dev/null +++ b/src/types/versions/index.ts @@ -0,0 +1,4 @@ +// Re-export all version-specific types +export * from './v1' +export * from './v1_2' +export * from './v3' diff --git a/src/types/versions/v1.ts b/src/types/versions/v1.ts new file mode 100644 index 000000000..74cd237bb --- /dev/null +++ b/src/types/versions/v1.ts @@ -0,0 +1,101 @@ +import type { LGraphNode } from '@comfyorg/litegraph' + +/** + * V1 API node definition interface + * This represents the current/legacy node definition format + */ +export interface ComfyNodeDefV1 { + name: string + display_name?: string + description?: string + category?: string + output_node?: boolean + input?: { + required?: Record + optional?: Record + hidden?: Record + } + output?: string[] + output_is_list?: boolean[] + output_name?: string[] + output_tooltips?: string[] + python_module?: string + deprecated?: boolean + experimental?: boolean +} + +/** + * V1 API extension interface + */ +export interface ComfyExtensionV1 { + name: string + apiVersion?: 'v1' + + // Lifecycle hooks + init?(): void | Promise + setup?(): void | Promise + + // Node lifecycle hooks + beforeRegisterNodeDef?( + nodeType: typeof LGraphNode, + nodeData: ComfyNodeDefV1, + app: ComfyAppV1 + ): void + nodeCreated?(node: LGraphNode, app: ComfyAppV1): void + + // Graph hooks + beforeConfigureGraph?(graphData: any, missingNodeTypes: any[]): void + afterConfigureGraph?(missingNodeTypes: any[]): void + + // Canvas hooks + getCustomWidgets?(): Record + + // Menu hooks + addCustomNodeDefs?(defs: ComfyNodeDefV1[]): ComfyNodeDefV1[] + + // Settings + settings?: Array<{ + id: string + name: string + type: string + defaultValue: any + tooltip?: string + }> + + // Commands + commands?: Array<{ + id: string + function: () => void | Promise + }> + + // Keybindings + keybindings?: Array<{ + combo: { key: string; ctrl?: boolean; shift?: boolean; alt?: boolean } + commandId: string + }> +} + +/** + * V1 API ComfyApp interface + * Provides the same interface as the current app + */ +export interface ComfyAppV1 { + registerExtension(extension: ComfyExtensionV1): void + + // Graph management + loadGraphData(graphData: any): Promise + clean(): void + + // Node management + getNodeById(id: number): LGraphNode | null + + // Workflow operations + queuePrompt(number: number, batchCount?: number): Promise + + // Canvas operations + canvas: any + graph: any + + // UI state + ui: any +} diff --git a/src/types/versions/v1_2.ts b/src/types/versions/v1_2.ts new file mode 100644 index 000000000..e7972e243 --- /dev/null +++ b/src/types/versions/v1_2.ts @@ -0,0 +1,111 @@ +import type { ComfyAppV1, ComfyExtensionV1, ComfyNodeDefV1 } from './v1' + +/** + * V1.2 API node definition interface + * Extends V1 with additional structured input information + */ +export interface ComfyNodeDefV1_2 extends ComfyNodeDefV1 { + inputs?: Array<{ + name: string + type: string + required: boolean + options?: any + spec?: any + tooltip?: string + default?: any + }> + + metadata?: { + version?: string + author?: string + description?: string + tags?: string[] + documentation?: string + } + + // Enhanced output information + outputs?: Array<{ + name: string + type: string + is_list: boolean + tooltip?: string + }> +} + +/** + * V1.2 API extension interface + * Extends V1 with additional capabilities + */ +export interface ComfyExtensionV1_2 extends ComfyExtensionV1 { + apiVersion?: 'v1_2' + + // V1.2 specific hooks + beforeRegisterNodeDef?( + nodeType: any, + nodeData: ComfyNodeDefV1_2, + app: ComfyAppV1_2 + ): void + nodeCreated?(node: any, app: ComfyAppV1_2): void + + // Enhanced settings with validation + settings?: Array<{ + id: string + name: string + type: string + defaultValue: any + tooltip?: string + validation?: (value: any) => boolean | string + category?: string + options?: any[] + }> + + // Enhanced commands with descriptions + commands?: Array<{ + id: string + function: () => void | Promise + label?: string + tooltip?: string + category?: string + icon?: string + }> + + // Bottom panel tabs + bottomPanelTabs?: Array<{ + id: string + title: string + icon: string + type: 'vue' | 'custom' + component?: any + tooltip?: string + }> + + // Menu items + menuItems?: Array<{ + path: string[] + commands: string[] + }> +} + +/** + * V1.2 API ComfyApp interface + * Extends V1 with additional functionality + */ +export interface ComfyAppV1_2 extends ComfyAppV1 { + registerExtension(extension: ComfyExtensionV1_2): void + + // Enhanced node operations + createNode(type: string, title?: string, options?: any): any + removeNode(node: any): void + + // Settings operations + getSetting(id: string): any + setSetting(id: string, value: any): void + + // Command operations + executeCommand(id: string): Promise + + // Enhanced UI access + bottomPanel: any + sidebar: any + menu: any +} diff --git a/src/types/versions/v3.ts b/src/types/versions/v3.ts new file mode 100644 index 000000000..cd5b9e101 --- /dev/null +++ b/src/types/versions/v3.ts @@ -0,0 +1,288 @@ +/** + * V3 API node definition interface + * Future schema-based node definition format + */ +export interface ComfyNodeDefV3 { + name: string + display_name?: string + description?: string + category?: string + output_node?: boolean + + // JSON Schema-based definition + schema: { + type: 'object' + properties: Record + required?: string[] + additionalProperties?: boolean + } + + // Structured input/output definitions + inputs: Array<{ + name: string + type: string + required: boolean + schema: any + validation?: any + tooltip?: string + default?: any + ui?: { + widget?: string + options?: any + } + }> + + outputs: Array<{ + name: string + type: string + is_list: boolean + schema?: any + tooltip?: string + }> + + // Enhanced metadata + metadata: { + version: string + author: string + description: string + tags: string[] + documentation?: string + repository?: string + license?: string + dependencies?: Array<{ + name: string + version?: string + optional?: boolean + }> + } + + // Execution information + execution?: { + async?: boolean + gpu_memory?: number + cpu_cores?: number + timeout?: number + retries?: number + } + + python_module?: string + deprecated?: boolean + experimental?: boolean +} + +/** + * V3 API extension interface + * Future extension format with enhanced capabilities + */ +export interface ComfyExtensionV3 { + name: string + apiVersion: 'v3' + + // Required metadata + metadata: { + version: string + author: string + description: string + repository?: string + license?: string + dependencies?: Array<{ + name: string + version?: string + optional?: boolean + }> + } + + // Lifecycle hooks with enhanced data + init?(): void | Promise + setup?(): void | Promise + beforeShutdown?(): void | Promise + + // Node lifecycle hooks + beforeRegisterNodeDef?( + nodeType: any, + nodeData: ComfyNodeDefV3, + app: ComfyAppV3 + ): void | Promise + afterRegisterNodeDef?( + nodeType: any, + nodeData: ComfyNodeDefV3, + app: ComfyAppV3 + ): void | Promise + nodeCreated?(node: any, app: ComfyAppV3): void | Promise + nodeRemoved?(node: any, app: ComfyAppV3): void | Promise + + // Graph hooks + beforeConfigureGraph?( + graphData: any, + missingNodeTypes: any[] + ): void | Promise + afterConfigureGraph?(missingNodeTypes: any[]): void | Promise + graphChanged?(graph: any): void | Promise + + // Enhanced settings with full schema validation + settings?: Array<{ + id: string + name: string + schema: any + defaultValue: any + tooltip?: string + category?: string + validation?: (value: any) => boolean | string + ui?: { + widget?: string + options?: any + } + }> + + // Enhanced commands with full metadata + commands?: Array<{ + id: string + function: () => void | Promise + metadata: { + label: string + tooltip?: string + category?: string + icon?: string + shortcut?: string + } + validation?: () => boolean + }> + + // Keybindings with enhanced options + keybindings?: Array<{ + combo: { + key: string + ctrl?: boolean + shift?: boolean + alt?: boolean + meta?: boolean + } + commandId: string + when?: string // Context condition + priority?: number + }> + + // UI extensions + uiExtensions?: { + bottomPanelTabs?: Array<{ + id: string + title: string + icon: string + component: any + tooltip?: string + when?: string + }> + + contextMenus?: Array<{ + id: string + items: Array<{ + id: string + label: string + commandId: string + when?: string + separator?: boolean + }> + }> + + toolbars?: Array<{ + id: string + location: 'top' | 'bottom' | 'left' | 'right' + items: Array<{ + id: string + type: 'button' | 'separator' | 'dropdown' + commandId?: string + label?: string + icon?: string + tooltip?: string + }> + }> + } + + // API endpoints (for extensions that provide their own APIs) + apiEndpoints?: Array<{ + path: string + method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' + handler: (req: any, res: any) => void | Promise + schema?: any + auth?: boolean + }> +} + +/** + * V3 API ComfyApp interface + * Future app interface with enhanced capabilities + */ +export interface ComfyAppV3 { + registerExtension(extension: ComfyExtensionV3): Promise + + // Enhanced graph management + graph: { + load(data: any): Promise + save(): any + clear(): void + validate(): Promise + getNodes(): any[] + getNodeById(id: string): any | null + addNode(type: string, options?: any): Promise + removeNode(node: any): Promise + connectNodes(source: any, target: any, options?: any): Promise + disconnectNodes(source: any, target: any): Promise + } + + // Enhanced workflow operations + workflow: { + queue(batchCount?: number): Promise + cancel(executionId?: string): Promise + pause(): Promise + resume(): Promise + getStatus(): any + getHistory(): any[] + getQueue(): any[] + } + + // Settings management + settings: { + get(id: string): T + set(id: string, value: any): Promise + getAll(): Record + reset(id?: string): Promise + export(): any + import(data: any): Promise + } + + // Command system + commands: { + execute(id: string, args?: any): Promise + register(command: any): void + unregister(id: string): void + getAll(): any[] + isAvailable(id: string): boolean + } + + // UI management + ui: { + bottomPanel: any + sidebar: any + menu: any + canvas: any + dialogs: any + notifications: any + } + + // Event system + events: { + on(event: string, handler: (data: T) => void): void + off(event: string, handler?: (data: T) => void): void + emit(event: string, data?: T): void + once(event: string, handler: (data: T) => void): void + } + + // API access + api: { + get(path: string, options?: any): Promise + post(path: string, data?: any, options?: any): Promise + put(path: string, data?: any, options?: any): Promise + delete(path: string, options?: any): Promise + patch(path: string, data?: any, options?: any): Promise + } +} diff --git a/src/utils/versionProxies.ts b/src/utils/versionProxies.ts new file mode 100644 index 000000000..2fd58c957 --- /dev/null +++ b/src/utils/versionProxies.ts @@ -0,0 +1,519 @@ +import type { + ComfyNodeDefLatest, + ComfyNodeDefV1, + ComfyNodeDefV1_2, + ComfyNodeDefV3 +} from './versionTransforms' + +/** + * Proxy-based system for synchronizing data between different API versions + */ +export class VersionProxies { + private static canonicalStore = new Map() + private static eventBus = new EventTarget() + + /** + * Register a canonical node definition + */ + static registerCanonicalNode(nodeId: string, nodeData: ComfyNodeDefLatest) { + this.canonicalStore.set(nodeId, nodeData) + } + + /** + * Get canonical node data + */ + static getCanonicalNode(nodeId: string): ComfyNodeDefLatest | undefined { + return this.canonicalStore.get(nodeId) + } + + /** + * Create a V1 proxy for a node + */ + static createV1Proxy(nodeId: string): ComfyNodeDefV1 { + const canonical = this.canonicalStore.get(nodeId) + if (!canonical) { + throw new Error(`Node ${nodeId} not found in canonical store`) + } + + return new Proxy({} as ComfyNodeDefV1, { + get(target, prop: keyof ComfyNodeDefV1) { + return VersionProxies.transformCanonicalToV1Property(canonical, prop) + }, + + set(target, prop: keyof ComfyNodeDefV1, value) { + VersionProxies.transformV1PropertyToCanonical(canonical, prop, value) + VersionProxies.notifyChange(nodeId, prop, value) + return true + }, + + has(target, prop: keyof ComfyNodeDefV1) { + return ( + prop in canonical || + prop === 'input' || + prop === 'output' || + prop === 'output_is_list' + ) + }, + + ownKeys(target) { + return [ + 'name', + 'display_name', + 'description', + 'category', + 'output_node', + 'input', + 'output', + 'output_is_list', + 'python_module' + ] + }, + + getOwnPropertyDescriptor(target, prop) { + return { + enumerable: true, + configurable: true, + value: this.get!(target, prop, target) + } + } + }) + } + + /** + * Create a V1.2 proxy for a node + */ + static createV1_2Proxy(nodeId: string): ComfyNodeDefV1_2 { + const canonical = this.canonicalStore.get(nodeId) + if (!canonical) { + throw new Error(`Node ${nodeId} not found in canonical store`) + } + + return new Proxy({} as ComfyNodeDefV1_2, { + get(target, prop: keyof ComfyNodeDefV1_2) { + return VersionProxies.transformCanonicalToV1_2Property(canonical, prop) + }, + + set(target, prop: keyof ComfyNodeDefV1_2, value) { + VersionProxies.transformV1_2PropertyToCanonical(canonical, prop, value) + VersionProxies.notifyChange(nodeId, prop, value) + return true + }, + + has(target, prop: keyof ComfyNodeDefV1_2) { + return ( + prop in canonical || + prop === 'input' || + prop === 'output' || + prop === 'output_is_list' || + prop === 'inputs' || + prop === 'metadata' + ) + }, + + ownKeys(target) { + return [ + 'name', + 'display_name', + 'description', + 'category', + 'output_node', + 'input', + 'output', + 'output_is_list', + 'inputs', + 'metadata', + 'python_module' + ] + }, + + getOwnPropertyDescriptor(target, prop) { + return { + enumerable: true, + configurable: true, + value: this.get!(target, prop, target) + } + } + }) + } + + /** + * Create a V3 proxy for a node + */ + static createV3Proxy(nodeId: string): ComfyNodeDefV3 { + const canonical = this.canonicalStore.get(nodeId) + if (!canonical) { + throw new Error(`Node ${nodeId} not found in canonical store`) + } + + return new Proxy({} as ComfyNodeDefV3, { + get(target, prop: keyof ComfyNodeDefV3) { + return VersionProxies.transformCanonicalToV3Property(canonical, prop) + }, + + set(target, prop: keyof ComfyNodeDefV3, value) { + VersionProxies.transformV3PropertyToCanonical(canonical, prop, value) + VersionProxies.notifyChange(nodeId, prop, value) + return true + }, + + has(target, prop: keyof ComfyNodeDefV3) { + return ( + prop in canonical || + prop === 'schema' || + prop === 'inputs' || + prop === 'outputs' + ) + }, + + ownKeys(target) { + return [ + 'name', + 'display_name', + 'description', + 'category', + 'output_node', + 'schema', + 'inputs', + 'outputs', + 'python_module' + ] + }, + + getOwnPropertyDescriptor(target, prop) { + return { + enumerable: true, + configurable: true, + value: this.get!(target, prop, target) + } + } + }) + } + + /** + * Transform canonical property to V1 format + */ + private static transformCanonicalToV1Property( + canonical: ComfyNodeDefLatest, + prop: keyof ComfyNodeDefV1 + ): any { + switch (prop) { + case 'input': + return canonical.input + ? { + required: canonical.input.required, + optional: canonical.input.optional + } + : undefined + case 'output': + return canonical.output + case 'output_is_list': + return canonical.output_is_list + case 'name': + case 'display_name': + case 'description': + case 'category': + case 'output_node': + case 'python_module': + return canonical[prop] + default: + return undefined + } + } + + /** + * Transform canonical property to V1.2 format + */ + private static transformCanonicalToV1_2Property( + canonical: ComfyNodeDefLatest, + prop: keyof ComfyNodeDefV1_2 + ): any { + switch (prop) { + case 'inputs': + if (!canonical.input) return undefined + return [ + ...Object.entries(canonical.input.required || {}).map( + ([name, spec]) => ({ + name, + type: this.inferTypeFromSpec(spec), + required: true, + spec, + options: Array.isArray(spec) ? spec : undefined + }) + ), + ...Object.entries(canonical.input.optional || {}).map( + ([name, spec]) => ({ + name, + type: this.inferTypeFromSpec(spec), + required: false, + spec, + options: Array.isArray(spec) ? spec : undefined + }) + ) + ] + case 'metadata': + return { + version: canonical.api_version, + author: 'Unknown', + description: canonical.description + } + default: + return this.transformCanonicalToV1Property( + canonical, + prop as keyof ComfyNodeDefV1 + ) + } + } + + /** + * Transform canonical property to V3 format + */ + private static transformCanonicalToV3Property( + canonical: ComfyNodeDefLatest, + prop: keyof ComfyNodeDefV3 + ): any { + switch (prop) { + case 'schema': { + const requiredInputs = Object.keys(canonical.input?.required || {}) + return { + type: 'object', + properties: { + ...(canonical.input?.required || {}), + ...(canonical.input?.optional || {}) + }, + required: requiredInputs + } + } + case 'inputs': { + if (!canonical.input) return [] + const requiredInputs2 = Object.entries(canonical.input.required || {}) + const optionalInputs = Object.entries(canonical.input.optional || {}) + return [ + ...requiredInputs2.map(([name, spec]) => ({ + name, + type: this.inferTypeFromSpec(spec), + required: true, + schema: spec + })), + ...optionalInputs.map(([name, spec]) => ({ + name, + type: this.inferTypeFromSpec(spec), + required: false, + schema: spec + })) + ] + } + case 'outputs': + return ( + canonical.output?.map((type, index) => ({ + name: `output_${index}`, + type, + is_list: canonical.output_is_list?.[index] || false + })) || [] + ) + case 'name': + case 'display_name': + case 'description': + case 'category': + case 'output_node': + case 'python_module': + return canonical[prop] + default: + return undefined + } + } + + /** + * Transform V1 property changes back to canonical format + */ + private static transformV1PropertyToCanonical( + canonical: ComfyNodeDefLatest, + prop: keyof ComfyNodeDefV1, + value: any + ): void { + switch (prop) { + case 'input': + canonical.input = value + ? { + required: value.required, + optional: value.optional + } + : undefined + break + case 'output': + canonical.output = value + break + case 'output_is_list': + canonical.output_is_list = value + break + case 'name': + case 'display_name': + case 'description': + case 'category': + case 'output_node': + case 'python_module': + ;(canonical as any)[prop] = value + break + } + } + + /** + * Transform V1.2 property changes back to canonical format + */ + private static transformV1_2PropertyToCanonical( + canonical: ComfyNodeDefLatest, + prop: keyof ComfyNodeDefV1_2, + value: any + ): void { + switch (prop) { + case 'inputs': + if (Array.isArray(value)) { + const required: Record = {} + const optional: Record = {} + + value.forEach((input) => { + if (input.required) { + required[input.name] = input.spec + } else { + optional[input.name] = input.spec + } + }) + + canonical.input = { + required: Object.keys(required).length > 0 ? required : undefined, + optional: Object.keys(optional).length > 0 ? optional : undefined + } + } + break + case 'metadata': + if (value && typeof value === 'object') { + canonical.api_version = value.version + canonical.description = value.description || canonical.description + } + break + default: + this.transformV1PropertyToCanonical( + canonical, + prop as keyof ComfyNodeDefV1, + value + ) + } + } + + /** + * Transform V3 property changes back to canonical format + */ + private static transformV3PropertyToCanonical( + canonical: ComfyNodeDefLatest, + prop: keyof ComfyNodeDefV3, + value: any + ): void { + switch (prop) { + case 'schema': + if (value && typeof value === 'object') { + const required: Record = {} + const optional: Record = {} + + Object.entries(value.properties || {}).forEach(([name, spec]) => { + if (value.required?.includes(name)) { + required[name] = spec + } else { + optional[name] = spec + } + }) + + canonical.input = { + required: Object.keys(required).length > 0 ? required : undefined, + optional: Object.keys(optional).length > 0 ? optional : undefined + } + } + break + case 'inputs': + if (Array.isArray(value)) { + const required: Record = {} + const optional: Record = {} + + value.forEach((input) => { + if (input.required) { + required[input.name] = input.schema + } else { + optional[input.name] = input.schema + } + }) + + canonical.input = { + required: Object.keys(required).length > 0 ? required : undefined, + optional: Object.keys(optional).length > 0 ? optional : undefined + } + } + break + case 'outputs': + if (Array.isArray(value)) { + canonical.output = value.map((output) => output.type) + canonical.output_is_list = value.map((output) => output.is_list) + } + break + case 'name': + case 'display_name': + case 'description': + case 'category': + case 'output_node': + case 'python_module': + ;(canonical as any)[prop] = value + break + } + } + + /** + * Notify about data changes + */ + private static notifyChange(nodeId: string, prop: string, value: any): void { + this.eventBus.dispatchEvent( + new CustomEvent('nodedef-changed', { + detail: { nodeId, prop, value } + }) + ) + } + + /** + * Add event listener for data changes + */ + static addEventListener(type: string, listener: EventListener): void { + this.eventBus.addEventListener(type, listener) + } + + /** + * Remove event listener + */ + static removeEventListener(type: string, listener: EventListener): void { + this.eventBus.removeEventListener(type, listener) + } + + /** + * Infer type from input specification + */ + private static inferTypeFromSpec(spec: any): string { + if (Array.isArray(spec)) { + return 'combo' + } + + if (typeof spec === 'object' && spec !== null) { + if (spec.type) { + return spec.type + } + if (spec[0] === 'INT') { + return 'int' + } + if (spec[0] === 'FLOAT') { + return 'float' + } + if (spec[0] === 'STRING') { + return 'string' + } + if (spec[0] === 'BOOLEAN') { + return 'boolean' + } + } + + return 'unknown' + } +} diff --git a/src/utils/versionTransforms.ts b/src/utils/versionTransforms.ts new file mode 100644 index 000000000..b94aaaaee --- /dev/null +++ b/src/utils/versionTransforms.ts @@ -0,0 +1,250 @@ +import type { ComfyNodeDef } from '@/schemas/nodeDefSchema' + +// Type definitions for different API versions +export interface ComfyNodeDefV1 { + name: string + display_name?: string + description?: string + category?: string + output_node?: boolean + input?: { + required?: Record + optional?: Record + } + output?: string[] + output_is_list?: boolean[] + python_module?: string +} + +export interface ComfyNodeDefV1_2 extends ComfyNodeDefV1 { + inputs?: Array<{ + name: string + type: string + required: boolean + options?: any + spec?: any + }> + metadata?: { + version?: string + author?: string + description?: string + } +} + +export interface ComfyNodeDefV3 { + name: string + display_name?: string + description?: string + category?: string + output_node?: boolean + schema: { + type: 'object' + properties: Record + required?: string[] + } + inputs: Array<{ + name: string + type: string + required: boolean + schema: any + }> + outputs: Array<{ + name: string + type: string + is_list: boolean + }> + python_module?: string +} + +// Use current ComfyNodeDef as the canonical format +export type ComfyNodeDefLatest = ComfyNodeDef + +/** + * Transforms node definitions between different API versions + */ +export class VersionTransforms { + /** + * Transform API responses to all supported versions + */ + static transformToAllVersions(currentData: any, v3Data: any = null) { + // Use current data as canonical since it's our primary source + const canonical = currentData || {} + + return { + canonical, + v1: this.canonicalToV1(canonical), + v1_2: this.canonicalToV1_2(canonical), + v3: v3Data || this.canonicalToV3(canonical) + } + } + + /** + * Transform canonical format to v1 format + */ + static canonicalToV1( + canonical: Record + ): Record { + const v1Nodes: Record = {} + + for (const [nodeName, nodeData] of Object.entries(canonical)) { + v1Nodes[nodeName] = { + name: nodeData.name, + display_name: nodeData.display_name, + description: nodeData.description, + category: nodeData.category, + output_node: nodeData.output_node, + python_module: nodeData.python_module, + input: nodeData.input + ? { + required: nodeData.input.required, + optional: nodeData.input.optional + } + : undefined, + output: nodeData.output, + output_is_list: nodeData.output_is_list + } + } + + return v1Nodes + } + + /** + * Transform canonical format to v1.2 format + */ + static canonicalToV1_2( + canonical: Record + ): Record { + const v1_2Nodes: Record = {} + + for (const [nodeName, nodeData] of Object.entries(canonical)) { + const v1Node = this.canonicalToV1({ [nodeName]: nodeData })[nodeName] + + v1_2Nodes[nodeName] = { + ...v1Node, + inputs: nodeData.input + ? [ + ...Object.entries(nodeData.input.required || {}).map( + ([name, spec]) => ({ + name, + type: this.inferTypeFromSpec(spec), + required: true, + spec, + options: Array.isArray(spec) ? spec : undefined + }) + ), + ...Object.entries(nodeData.input.optional || {}).map( + ([name, spec]) => ({ + name, + type: this.inferTypeFromSpec(spec), + required: false, + spec, + options: Array.isArray(spec) ? spec : undefined + }) + ) + ] + : undefined, + metadata: { + version: nodeData.api_version, + author: 'Unknown', + description: nodeData.description + } + } + } + + return v1_2Nodes + } + + /** + * Transform canonical format to v3 format + */ + static canonicalToV3( + canonical: Record + ): Record { + const v3Nodes: Record = {} + + for (const [nodeName, nodeData] of Object.entries(canonical)) { + const requiredInputs = Object.keys(nodeData.input?.required || {}) + const optionalInputs = Object.keys(nodeData.input?.optional || {}) + + v3Nodes[nodeName] = { + name: nodeData.name, + display_name: nodeData.display_name, + description: nodeData.description, + category: nodeData.category, + output_node: nodeData.output_node, + python_module: nodeData.python_module, + schema: { + type: 'object', + properties: { + ...(nodeData.input?.required || {}), + ...(nodeData.input?.optional || {}) + }, + required: requiredInputs + }, + inputs: [ + ...requiredInputs.map((name) => ({ + name, + type: this.inferTypeFromSpec(nodeData.input!.required![name]), + required: true, + schema: nodeData.input!.required![name] + })), + ...optionalInputs.map((name) => ({ + name, + type: this.inferTypeFromSpec(nodeData.input!.optional![name]), + required: false, + schema: nodeData.input!.optional![name] + })) + ], + outputs: + nodeData.output?.map((type, index) => ({ + name: `output_${index}`, + type, + is_list: nodeData.output_is_list?.[index] || false + })) || [] + } + } + + return v3Nodes + } + + /** + * Infer type from input specification + */ + private static inferTypeFromSpec(spec: any): string { + if (Array.isArray(spec)) { + return 'combo' + } + + if (typeof spec === 'object' && spec !== null) { + if (spec.type) { + return spec.type + } + if (spec[0] === 'INT') { + return 'int' + } + if (spec[0] === 'FLOAT') { + return 'float' + } + if (spec[0] === 'STRING') { + return 'string' + } + if (spec[0] === 'BOOLEAN') { + return 'boolean' + } + } + + return 'unknown' + } + + /** + * Merge current and v3 data to create canonical format + */ + private static createCanonicalFormat( + currentData: any, + v3Data: any + ): Record { + // For now, use current data as canonical + // In the future, we might merge v3 data for enhanced information + return currentData || {} + } +}