feat(analyzer): add iOS/Swift project detection (#389)

- Add Swift/iOS detection via Package.swift or .xcodeproj
- Detect SwiftUI, UIKit, AppKit frameworks from imports
- Identify Apple frameworks (Combine, MapKit, WidgetKit, etc.)
- Parse SPM dependencies from xcodeproj or Package.swift
- Add mobile/desktop project types with icons and colors
- Display Apple Frameworks and SPM Dependencies in Context UI

This enables Auto-Claude to provide rich context for iOS/macOS
projects, feeding framework and dependency info into Ideation
and Roadmap features.

Signed-off-by: Black Circle Sentinel <mludlow000@icloud.com>
This commit is contained in:
Michael Ludlow
2025-12-28 12:38:51 -05:00
committed by GitHub
parent 7751588e56
commit 68548e33c8
4 changed files with 151 additions and 2 deletions
@@ -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"):
@@ -106,6 +106,34 @@ export function ServiceCard({ name, service }: ServiceCardProps) {
)}
</div>
{/* Apple Frameworks (iOS/Swift) */}
{service.apple_frameworks && service.apple_frameworks.length > 0 && (
<div className="pt-2 border-t border-border">
<p className="text-xs text-muted-foreground mb-1.5">Apple Frameworks</p>
<div className="flex flex-wrap gap-1">
{service.apple_frameworks.map((fw) => (
<Badge key={fw} variant="secondary" className="text-xs">
{fw}
</Badge>
))}
</div>
</div>
)}
{/* SPM Dependencies (iOS/Swift) */}
{service.spm_dependencies && service.spm_dependencies.length > 0 && (
<div className="pt-2 border-t border-border">
<p className="text-xs text-muted-foreground mb-1.5">SPM Dependencies</p>
<div className="flex flex-wrap gap-1">
{service.spm_dependencies.map((dep) => (
<Badge key={dep} variant="outline" className="text-xs font-mono">
{dep}
</Badge>
))}
</div>
</div>
)}
{/* Collapsible Sections */}
<EnvironmentSection environment={service.environment} />
<APIRoutesSection api={service.api} />
@@ -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<string, React.ElementType> = {
scraper: Code,
library: Package,
proxy: GitBranch,
mobile: Smartphone,
desktop: Monitor,
unknown: FileCode
};
@@ -30,6 +34,8 @@ export const serviceTypeColors: Record<string, string> = {
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'
};
+4 -1
View File
@@ -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?: {