diff --git a/apps/backend/analysis/analyzers/framework_analyzer.py b/apps/backend/analysis/analyzers/framework_analyzer.py index 9dcab158..e39b6bf6 100644 --- a/apps/backend/analysis/analyzers/framework_analyzer.py +++ b/apps/backend/analysis/analyzers/framework_analyzer.py @@ -82,6 +82,15 @@ class FrameworkAnalyzer(BaseAnalyzer): content = self._read_file("Gemfile") self._detect_ruby_framework(content) + # Swift/iOS detection + elif self._exists("Package.swift") or any(self.path.glob("*.xcodeproj")): + self.analysis["language"] = "Swift" + if self._exists("Package.swift"): + self.analysis["package_manager"] = "Swift Package Manager" + else: + self.analysis["package_manager"] = "Xcode" + self._detect_swift_framework() + def _detect_python_framework(self, content: str) -> None: """Detect Python framework.""" from .port_detector import PortDetector @@ -290,6 +299,109 @@ class FrameworkAnalyzer(BaseAnalyzer): if "sidekiq" in content.lower(): self.analysis["task_queue"] = "Sidekiq" + def _detect_swift_framework(self) -> None: + """Detect Swift/iOS framework and dependencies.""" + try: + # Scan Swift files for imports, excluding hidden/vendor dirs + swift_files = [] + for swift_file in self.path.rglob("*.swift"): + # Skip hidden directories, node_modules, .worktrees, etc. + if any( + part.startswith(".") or part in ("node_modules", "Pods", "Carthage") + for part in swift_file.parts + ): + continue + swift_files.append(swift_file) + if len(swift_files) >= 50: # Limit for performance + break + + imports = set() + for swift_file in swift_files: + try: + content = swift_file.read_text(encoding="utf-8", errors="ignore") + for line in content.split("\n"): + line = line.strip() + if line.startswith("import "): + module = line.replace("import ", "").split()[0] + imports.add(module) + except Exception: + continue + + # Detect UI framework + if "SwiftUI" in imports: + self.analysis["framework"] = "SwiftUI" + self.analysis["type"] = "mobile" + elif "UIKit" in imports: + self.analysis["framework"] = "UIKit" + self.analysis["type"] = "mobile" + elif "AppKit" in imports: + self.analysis["framework"] = "AppKit" + self.analysis["type"] = "desktop" + + # Detect iOS/Apple frameworks + apple_frameworks = [] + framework_map = { + "Combine": "Combine", + "CoreData": "CoreData", + "MapKit": "MapKit", + "WidgetKit": "WidgetKit", + "CoreLocation": "CoreLocation", + "StoreKit": "StoreKit", + "CloudKit": "CloudKit", + "ActivityKit": "ActivityKit", + "UserNotifications": "UserNotifications", + } + for key, name in framework_map.items(): + if key in imports: + apple_frameworks.append(name) + + if apple_frameworks: + self.analysis["apple_frameworks"] = apple_frameworks + + # Detect SPM dependencies from Package.swift or xcodeproj + dependencies = self._detect_spm_dependencies() + if dependencies: + self.analysis["spm_dependencies"] = dependencies + except Exception: + # Silently fail if Swift detection has issues + pass + + def _detect_spm_dependencies(self) -> list[str]: + """Detect Swift Package Manager dependencies.""" + dependencies = [] + + # Try Package.swift first + if self._exists("Package.swift"): + content = self._read_file("Package.swift") + # Look for .package(url: "...", patterns + import re + + urls = re.findall(r'\.package\s*\([^)]*url:\s*"([^"]+)"', content) + for url in urls: + # Extract package name from URL + name = url.rstrip("/").split("/")[-1].replace(".git", "") + if name: + dependencies.append(name) + + # Also check xcodeproj for XCRemoteSwiftPackageReference + for xcodeproj in self.path.glob("*.xcodeproj"): + pbxproj = xcodeproj / "project.pbxproj" + if pbxproj.exists(): + try: + content = pbxproj.read_text(encoding="utf-8", errors="ignore") + import re + + # Match repositoryURL patterns + urls = re.findall(r'repositoryURL\s*=\s*"([^"]+)"', content) + for url in urls: + name = url.rstrip("/").split("/")[-1].replace(".git", "") + if name and name not in dependencies: + dependencies.append(name) + except Exception: + continue + + return dependencies + def _detect_node_package_manager(self) -> str: """Detect Node.js package manager.""" if self._exists("pnpm-lock.yaml"): diff --git a/apps/frontend/src/renderer/components/context/ServiceCard.tsx b/apps/frontend/src/renderer/components/context/ServiceCard.tsx index 42cbb940..e766f6e6 100644 --- a/apps/frontend/src/renderer/components/context/ServiceCard.tsx +++ b/apps/frontend/src/renderer/components/context/ServiceCard.tsx @@ -106,6 +106,34 @@ export function ServiceCard({ name, service }: ServiceCardProps) { )} + {/* Apple Frameworks (iOS/Swift) */} + {service.apple_frameworks && service.apple_frameworks.length > 0 && ( +
+

Apple Frameworks

+
+ {service.apple_frameworks.map((fw) => ( + + {fw} + + ))} +
+
+ )} + + {/* SPM Dependencies (iOS/Swift) */} + {service.spm_dependencies && service.spm_dependencies.length > 0 && ( +
+

SPM Dependencies

+
+ {service.spm_dependencies.map((dep) => ( + + {dep} + + ))} +
+
+ )} + {/* Collapsible Sections */} diff --git a/apps/frontend/src/renderer/components/context/constants.ts b/apps/frontend/src/renderer/components/context/constants.ts index 04da571b..963afc36 100644 --- a/apps/frontend/src/renderer/components/context/constants.ts +++ b/apps/frontend/src/renderer/components/context/constants.ts @@ -8,7 +8,9 @@ import { FileCode, Lightbulb, FolderTree, - AlertTriangle + AlertTriangle, + Smartphone, + Monitor } from 'lucide-react'; // Service type icon mapping @@ -19,6 +21,8 @@ export const serviceTypeIcons: Record = { scraper: Code, library: Package, proxy: GitBranch, + mobile: Smartphone, + desktop: Monitor, unknown: FileCode }; @@ -30,6 +34,8 @@ export const serviceTypeColors: Record = { scraper: 'bg-green-500/10 text-green-400 border-green-500/30', library: 'bg-gray-500/10 text-gray-400 border-gray-500/30', proxy: 'bg-cyan-500/10 text-cyan-400 border-cyan-500/30', + mobile: 'bg-orange-500/10 text-orange-400 border-orange-500/30', + desktop: 'bg-indigo-500/10 text-indigo-400 border-indigo-500/30', unknown: 'bg-muted text-muted-foreground border-muted' }; diff --git a/apps/frontend/src/shared/types/project.ts b/apps/frontend/src/shared/types/project.ts index d101c9f2..f1858732 100644 --- a/apps/frontend/src/shared/types/project.ts +++ b/apps/frontend/src/shared/types/project.ts @@ -50,7 +50,7 @@ export interface ServiceInfo { path: string; language?: string; framework?: string; - type?: 'backend' | 'frontend' | 'worker' | 'scraper' | 'library' | 'proxy' | 'unknown'; + type?: 'backend' | 'frontend' | 'worker' | 'scraper' | 'library' | 'proxy' | 'mobile' | 'desktop' | 'unknown'; package_manager?: string; default_port?: number; entry_point?: string; @@ -65,6 +65,9 @@ export interface ServiceInfo { styling?: string; state_management?: string; build_tool?: string; + // iOS/Swift specific + apple_frameworks?: string[]; + spm_dependencies?: string[]; dockerfile?: string; consumes?: string[]; environment?: {