#!/usr/bin/env nu # Service Dependency Resolution # Handles dependency graph analysis and startup ordering use manager.nu [load-service-registry get-service-definition] # Resolve service dependencies export def resolve-dependencies [ service_name: string ] -> list { let service_def = (get-service-definition $service_name) if ($service_def.dependencies | is-empty) { return [] } # Recursively resolve dependencies let mut all_deps = [] for dep in $service_def.dependencies { # Add the dependency itself if $dep not-in $all_deps { $all_deps = ($all_deps | append $dep) } # Recursively add its dependencies let sub_deps = (resolve-dependencies $dep) for sub_dep in $sub_deps { if $sub_dep not-in $all_deps { $all_deps = ($all_deps | append $sub_dep) } } } $all_deps } # Get dependency tree export def get-dependency-tree [ service_name: string ] -> record { let service_def = (get-service-definition $service_name) if ($service_def.dependencies | is-empty) { return { service: $service_name dependencies: [] } } let deps = ( $service_def.dependencies | each { |dep| get-dependency-tree $dep } ) { service: $service_name dependencies: $deps } } # Topological sort for dependency ordering def topological-sort [ services: list dep_map: record ] -> list { let mut sorted = [] let mut visited = {} let mut visiting = {} # Recursive DFS def visit [ node: string dep_map: record visited: record visiting: record sorted: list ] -> record { if $node in ($visiting | columns) { error make { msg: "Circular dependency detected" label: { text: $"Cycle involving ($node)" span: (metadata $node).span } } } if $node in ($visited | columns) { return { visited: $visited, visiting: $visiting, sorted: $sorted } } let mut visiting = ($visiting | insert $node true) let mut sorted = $sorted let mut visited = $visited let deps = if $node in ($dep_map | columns) { $dep_map | get $node } else { [] } for dep in $deps { let result = (visit $dep $dep_map $visited $visiting $sorted) $visited = $result.visited $visiting = $result.visiting $sorted = $result.sorted } $visiting = ($visiting | reject $node) $visited = ($visited | insert $node true) $sorted = ($sorted | append $node) { visited: $visited, visiting: $visiting, sorted: $sorted } } # Visit all nodes let mut state = { visited: {}, visiting: {}, sorted: [] } for service in $services { if $service not-in ($state.visited | columns) { $state = (visit $service $dep_map $state.visited $state.visiting $state.sorted) } } $state.sorted } # Start services in dependency order export def start-services-with-deps [ service_names: list ] -> record { # Build dependency map let registry = (load-service-registry) let dep_map = ( $registry | transpose name config | reduce -f {} { |row, acc| $acc | insert $row.name $row.config.dependencies } ) # Get all services to start (including dependencies) let mut all_services = [] for service in $service_names { if $service not-in $all_services { $all_services = ($all_services | append $service) } let deps = (resolve-dependencies $service) for dep in $deps { if $dep not-in $all_services { $all_services = ($all_services | append $dep) } } } # Get startup order let startup_order = (topological-sort $all_services $dep_map) print $"Starting services in order: ($startup_order | str join ' -> ')" # Start services let mut started = [] let mut failed = [] for service in $startup_order { use manager.nu is-service-running start-service if (is-service-running $service) { print $"✅ ($service) already running" $started = ($started | append $service) continue } print $"Starting ($service)..." try { let result = (start-service $service) if $result { $started = ($started | append $service) print $"✅ ($service) started" } else { $failed = ($failed | append $service) print $"❌ Failed to start ($service)" break } } catch { $failed = ($failed | append $service) print $"❌ Error starting ($service)" break } } { success: ($failed | is-empty) startup_order: $startup_order started: $started failed: $failed message: (if ($failed | is-empty) { $"Successfully started ($started | length) services" } else { $"Failed at service: ($failed | get 0)" }) } } # Validate dependency graph (detect cycles) export def validate-dependency-graph [] -> record { let registry = (load-service-registry) let dep_map = ( $registry | transpose name config | reduce -f {} { |row, acc| $acc | insert $row.name $row.config.dependencies } ) let services = ($registry | columns) # Try to topologically sort - will error if cycle detected try { let sorted = (topological-sort $services $dep_map) { valid: true has_cycles: false message: "Dependency graph is valid (no cycles)" startup_order: $sorted } } catch { |err| { valid: false has_cycles: true message: $"Dependency graph has cycles: ($err.msg)" error: $err } } } # Get startup order export def get-startup-order [ service_names: list ] -> list { let registry = (load-service-registry) let dep_map = ( $registry | transpose name config | reduce -f {} { |row, acc| $acc | insert $row.name $row.config.dependencies } ) # Get all services to include (with dependencies) let mut all_services = [] for service in $service_names { if $service not-in $all_services { $all_services = ($all_services | append $service) } let deps = (resolve-dependencies $service) for dep in $deps { if $dep not-in $all_services { $all_services = ($all_services | append $dep) } } } # Also sort by start_order field let services_with_order = ( $all_services | each { |name| let service_def = (get-service-definition $name) { name: $name start_order: $service_def.startup.start_order } } | sort-by start_order | get name ) # Perform topological sort try { topological-sort $services_with_order $dep_map } catch { # Fallback to simple order if cycle detected print "⚠️ Warning: Circular dependency detected, using start_order fallback" $services_with_order } } # Get reverse dependencies (which services depend on this one) export def get-reverse-dependencies [ service_name: string ] -> list { let registry = (load-service-registry) $registry | transpose name config | where { |row| $service_name in $row.config.dependencies } | get name } # Get dependency graph visualization export def visualize-dependency-graph [] -> string { let registry = (load-service-registry) let mut output = ["# Service Dependency Graph", ""] for service in ($registry | columns | sort) { let service_def = (get-service-definition $service) $output = ($output | append $"## ($service)") $output = ($output | append $"- Type: ($service_def.type)") $output = ($output | append $"- Category: ($service_def.category)") if not ($service_def.dependencies | is-empty) { $output = ($output | append "- Dependencies:") for dep in $service_def.dependencies { $output = ($output | append $" - ($dep)") } } let reverse_deps = (get-reverse-dependencies $service) if not ($reverse_deps | is-empty) { $output = ($output | append "- Required by:") for dep in $reverse_deps { $output = ($output | append $" - ($dep)") } } if not ($service_def.conflicts | is-empty) { $output = ($output | append "- Conflicts:") for conflict in $service_def.conflicts { $output = ($output | append $" - ($conflict)") } } $output = ($output | append "") } $output | str join "\n" } # Check if service can be stopped safely export def can-stop-service [ service_name: string ] -> record { use manager.nu is-service-running let reverse_deps = (get-reverse-dependencies $service_name) # Check which dependent services are running let running_deps = ( $reverse_deps | where { |dep| is-service-running $dep } ) { service: $service_name can_stop: ($running_deps | is-empty) dependent_services: $reverse_deps running_dependents: $running_deps message: (if ($running_deps | is-empty) { "Service can be stopped safely" } else { $"Cannot stop: ($running_deps | length) dependent services are running" }) } }