#!/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 ]: nothing -> list { let service_def = (get-service-definition $service_name) if ($service_def.dependencies | is-empty) { return [] } # Recursively resolve dependencies - collect all unique deps def accumulate-deps [deps: list, all_deps: list]: nothing -> list { if ($deps | is-empty) { return $all_deps } let first = ($deps | get 0) let rest = ($deps | skip 1) let with_first = if $first in $all_deps { $all_deps } else { $all_deps | append $first } let sub_deps = (resolve-dependencies $first) let all_sub_deps = ($sub_deps | append $rest) accumulate-deps $all_sub_deps $with_first } accumulate-deps $service_def.dependencies [] } # Get dependency tree export def get-dependency-tree [ service_name: string ]: nothing -> 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 ]: nothing -> list { # Recursive DFS helper function def visit [ node: string dep_map: record visited: record visiting: record sorted: list ]: nothing -> 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 new_visiting = ($visiting | insert $node true) let deps = if $node in ($dep_map | columns) { $dep_map | get $node } else { [] } # Process dependencies recursively def visit-deps [deps: list, state: record]: nothing -> record { if ($deps | is-empty) { return $state } let first_dep = ($deps | get 0) let rest_deps = ($deps | skip 1) let state_after = (visit $first_dep $dep_map $state.visited $state.visiting $state.sorted) visit-deps $rest_deps $state_after } let result_after_deps = (visit-deps $deps { visited: $visited, visiting: $new_visiting, sorted: $sorted }) let final_visiting = ($result_after_deps.visiting | reject $node) let final_visited = ($result_after_deps.visited | insert $node true) let final_sorted = ($result_after_deps.sorted | append $node) { visited: $final_visited, visiting: $final_visiting, sorted: $final_sorted } } # Visit all nodes recursively starting with empty state def visit-services [services: list, state: record]: nothing -> record { if ($services | is-empty) { return $state } let first_service = ($services | get 0) let rest_services = ($services | skip 1) let state_after = if $first_service not-in ($state.visited | columns) { visit $first_service $dep_map $state.visited $state.visiting $state.sorted } else { $state } visit-services $rest_services $state_after } (visit-services $services { visited: {}, visiting: {}, sorted: [] }) | get sorted } # Start services in dependency order export def start-services-with-deps [ service_names: list ]: nothing -> record { # Build dependency map let registry = (load-service-registry) # Helper to build dep_map from registry entries def build-dep-map [entries: list, acc: record]: nothing -> record { if ($entries | is-empty) { return $acc } let first = ($entries | get 0) let rest = ($entries | skip 1) let new_acc = ($acc | insert $first.name $first.config.dependencies) build-dep-map $rest $new_acc } let dep_map = (build-dep-map ($registry | transpose name config) {}) # Helper to collect all services with their dependencies def collect-services [services: list, all_deps: list]: nothing -> list { if ($services | is-empty) { return $all_deps } let first = ($services | get 0) let rest = ($services | skip 1) let with_first = if $first in $all_deps { $all_deps } else { $all_deps | append $first } let sub_deps = (resolve-dependencies $first) collect-services (($rest | append $sub_deps)) $with_first } let all_services = (collect-services $service_names []) # Get startup order let startup_order = (topological-sort $all_services $dep_map) print $"Starting services in order: ($startup_order | str join ' -> ')" # Helper to start services recursively def start-services [services: list, state: record]: nothing -> record { if ($services | is-empty) { return $state } let first = ($services | get 0) let rest = ($services | skip 1) use manager.nu [is-service-running start-service] let new_state = if (is-service-running $first) { print $"✅ ($first) already running" { started: ($state.started | append $first) failed: $state.failed } } else { print $"Starting [$first]..." let start_result = (do { start-service $first } | complete) if $start_result.exit_code == 0 and ($start_result.stdout == "true" or $start_result.stdout == true) { print $"✅ [$first] started" { started: ($state.started | append $first) failed: $state.failed } } else { print $"❌ Failed to start [$first]: ($start_result.stderr)" { started: $state.started failed: ($state.failed | append $first) } } } start-services $rest $new_state } let result = (start-services $startup_order { started: [], failed: [] }) let started = $result.started let failed = $result.failed { 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 []: nothing -> record { let registry = (load-service-registry) # Helper to build dep_map from registry entries def build-dep-map [entries: list, acc: record]: nothing -> record { if ($entries | is-empty) { return $acc } let first = ($entries | get 0) let rest = ($entries | skip 1) let new_acc = ($acc | insert $first.name $first.config.dependencies) build-dep-map $rest $new_acc } let dep_map = (build-dep-map ($registry | transpose name config) {}) let services = ($registry | columns) # Try to topologically sort - will error if cycle detected let sort_result = (do { topological-sort $services $dep_map } | complete) if $sort_result.exit_code == 0 { { valid: true has_cycles: false message: "Dependency graph is valid (no cycles)" startup_order: ($sort_result.stdout | from json) } } else { { valid: false has_cycles: true message: $"Dependency graph has cycles: ($sort_result.stderr)" error: $sort_result.stderr } } } # Get startup order export def get-startup-order [ service_names: list ]: nothing -> list { let registry = (load-service-registry) # Helper to build dep_map from registry entries def build-dep-map [entries: list, acc: record]: nothing -> record { if ($entries | is-empty) { return $acc } let first = ($entries | get 0) let rest = ($entries | skip 1) let new_acc = ($acc | insert $first.name $first.config.dependencies) build-dep-map $rest $new_acc } let dep_map = (build-dep-map ($registry | transpose name config) {}) # Helper to collect all services with their dependencies def collect-services [services: list, all_deps: list]: nothing -> list { if ($services | is-empty) { return $all_deps } let first = ($services | get 0) let rest = ($services | skip 1) let with_first = if $first in $all_deps { $all_deps } else { $all_deps | append $first } let sub_deps = (resolve-dependencies $first) collect-services (($rest | append $sub_deps)) $with_first } let all_services = (collect-services $service_names []) # 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 let sort_result = (do { topological-sort $services_with_order $dep_map } | complete) if $sort_result.exit_code == 0 { $sort_result.stdout | from json } else { # 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 ]: nothing -> 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 []: nothing -> string { let registry = (load-service-registry) # Helper to format a single service's dependencies def format-service-deps [service: string, lines: list]: nothing -> list { let service_def = (get-service-definition $service) let base_lines = ( $lines | append $"## ($service)" | append $"- Type: ($service_def.type)" | append $"- Category: ($service_def.category)" ) let with_deps = ( if not ($service_def.dependencies | is-empty) { ($base_lines | append "- Dependencies:") | append ( $service_def.dependencies | each { |dep| $" - ($dep)" } ) } else { $base_lines } ) let reverse_deps = (get-reverse-dependencies $service) let with_reverse = ( if not ($reverse_deps | is-empty) { ($with_deps | append "- Required by:") | append ( $reverse_deps | each { |dep| $" - ($dep)" } ) } else { $with_deps } ) let final = ( if not ($service_def.conflicts | is-empty) { ($with_reverse | append "- Conflicts:") | append ( $service_def.conflicts | each { |conflict| $" - ($conflict)" } ) } else { $with_reverse } ) ($final | append "") } # Helper to format all services recursively def format-services [services: list, lines: list]: nothing -> list { if ($services | is-empty) { return $lines } let first = ($services | get 0) let rest = ($services | skip 1) let new_lines = (format-service-deps $first $lines) format-services $rest $new_lines } let output = ( ["# Service Dependency Graph", ""] | append (format-services (($registry | columns | sort)) []) ) $output | str join "\n" } # Check if service can be stopped safely export def can-stop-service [ service_name: string ]: nothing -> 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" }) } }