#!/usr/bin/env nu # Enhanced Module Loader CLI # Unified CLI for discovering and loading taskservs, providers, and clusters # Includes template and layer support from enhanced version use ../nulib/taskservs/discover.nu * use ../nulib/taskservs/load.nu * use ../nulib/providers/discover.nu * use ../nulib/providers/load.nu * use ../nulib/clusters/discover.nu * use ../nulib/clusters/load.nu * use ../nulib/lib_provisioning/kcl_module_loader.nu * use ../nulib/lib_provisioning/config/accessor.nu config-get # Main module loader command with enhanced features def main [subcommand?: string] { if ($subcommand | is-empty) { print_enhanced_help return } match $subcommand { "help" => print_enhanced_help "discover" => print_discover_help "load" => print_load_help "list" => print_list_help "unload" => print_unload_help "init" => print_init_help "validate" => print_validate_help "info" => print_info_help "template" => print_template_help "layer" => print_layer_help "override" => print_override_help _ => { print $"Unknown command: ($subcommand)" print_enhanced_help } } } # === DISCOVERY COMMANDS === # Discover available modules export def "main discover" [ type: string, # Module type: taskservs, providers, clusters query?: string, # Search query --format: string = "table", # Output format: table, yaml, json, names --category: string = "", # Filter by category (for taskservs) --group: string = "" # Filter by group (for taskservs) ] { match $type { "taskservs" => { let taskservs = if ($query | is-empty) { discover-taskservs } else { search-taskservs $query } let filtered = if ($category | is-empty) and ($group | is-empty) { $taskservs } else if not ($category | is-empty) { $taskservs | where group == $category } else if not ($group | is-empty) { $taskservs | where group == $group } else { $taskservs } format_output $filtered $format } "providers" => { print "Provider discovery not implemented yet" } "clusters" => { print "Cluster discovery not implemented yet" } _ => { print $"Unknown module type: ($type)" print "Available types: taskservs, providers, clusters" } } } # Sync KCL dependencies for infrastructure workspace export def "main sync-kcl" [ infra: string, # Infrastructure name or path --manifest: string = "providers.manifest.yaml", # Manifest file name --kcl # Show KCL module info after sync ] { # Resolve infrastructure path let infra_path = if ($infra | path exists) { $infra } else { # Try workspace path let workspace_path = $"workspace/infra/($infra)" if ($workspace_path | path exists) { $workspace_path } else { print $"โŒ Infrastructure not found: ($infra)" return } } # Sync KCL dependencies using library function sync-kcl-dependencies $infra_path --manifest $manifest # Show KCL module info if requested if $kcl { print "" print "๐Ÿ“‹ KCL Modules:" let modules_dir = (get-config-value "kcl" "modules_dir") let modules_path = ($infra_path | path join $modules_dir) if ($modules_path | path exists) { ls $modules_path | each {|entry| print $" โ€ข ($entry.name | path basename) โ†’ ($entry.name)" } } } } # === LOAD/UNLOAD COMMANDS === # Load modules into workspace export def "main load" [ type: string, # Module type: taskservs, providers, clusters workspace: string, # Workspace path ...modules: string, # Module names to load --layer: string = "workspace", # Layer to load into: workspace, infra --validate # Validate after loading --force (-f) # Force overwrite existing files ] { if ($modules | is-empty) { print $"No modules specified for loading" return } print $"Loading ($modules | length) ($type) into ($workspace) at layer ($layer)" match $type { "taskservs" | "providers" | "clusters" | "workflows" => { load_extension_to_workspace $type $workspace $modules $layer $force } _ => { print $"Unknown module type: ($type)" } } if $validate { main validate $workspace } } # Enhanced load with template support export def "main load enhanced" [ type: string, # Module type workspace: string, # Workspace path infra: string, # Infrastructure name modules: list, # Module names --layer: string = "workspace", # Target layer --template-base # Use template as base ] { print $"๐Ÿš€ Enhanced loading ($modules | length) ($type) for infra ($infra)" for module in $modules { print $" ๐Ÿ“ฆ Loading ($module)..." # Check if template exists for this module let template_path = $"provisioning/workspace/templates/taskservs/*/($module).k" let has_template = (glob $template_path | length) > 0 if $has_template and $template_base { print $" โœ“ Using template base for ($module)" # Template-based loading would go here } else { print $" โœ“ Direct loading for ($module)" # Direct loading } } print "โœ… Enhanced loading completed" } # Unload module from workspace export def "main unload" [ type: string, # Module type workspace: string, # Workspace path module: string, # Module name to unload --layer: string = "workspace" # Layer to unload from ] { print $"Unloading ($module) from ($workspace) at layer ($layer)" match $type { "taskservs" => { unload_taskserv_from_workspace $workspace $module $layer } "providers" => { print "Provider unloading not implemented yet" } "clusters" => { print "Cluster unloading not implemented yet" } _ => { print $"Unknown module type: ($type)" } } } # === LIST COMMANDS === # List modules in workspace export def "main list" [ type: string, # Module type workspace: string, # Workspace path --layer: string = "all", # Layer to list: workspace, infra, all --format: string = "table" # Output format ] { print $"Listing ($type) in ($workspace) for layer ($layer)" match $type { "taskservs" => { list_workspace_taskservs $workspace $layer $format } "providers" => { print "Provider listing not implemented yet" } "clusters" => { print "Cluster listing not implemented yet" } _ => { print $"Unknown module type: ($type)" } } } # === TEMPLATE COMMANDS === # List available templates export def "main template list" [ --template-type: string = "all", # Template type: taskservs, providers, servers, clusters --format: string = "table" # Output format ] { print $"๐Ÿ“‹ Available templates type: ($template_type)" let template_base = "provisioning/workspace/templates" match $template_type { "taskservs" | "all" => { let taskserv_templates = if (($template_base | path join "taskservs") | path exists) { glob ($template_base | path join "taskservs" "*" "*.k") | each { |path| let category = ($path | path dirname | path basename) let name = ($path | path basename | str replace ".k" "") { type: "taskserv", category: $category, name: $name, path: $path } } } else { [] } format_output $taskserv_templates $format } "providers" => { print "Provider templates not implemented yet" } "servers" => { let server_templates = if (($template_base | path join "servers") | path exists) { ls ($template_base | path join "servers") | get name | each { |path| { type: "server", name: ($path | path basename), path: $path } } } else { [] } format_output $server_templates $format } _ => { print $"Unknown template type: ($template_type)" } } } # Extract template from existing infrastructure export def "main template extract" [ source_infra: string, # Source infrastructure path template_name: string, # Name for the new template --type: string = "taskserv", # Template type --output: string = "provisioning/workspace/templates" # Output directory ] { print $"๐Ÿ“ค Extracting template ($template_name) from ($source_infra)" # Implementation would analyze the source infra and create template print "Template extraction not yet implemented" } # Apply template to infrastructure export def "main template apply" [ template_name: string, # Template to apply target_infra: string, # Target infrastructure --override-file: string = "", # Override file path --dry-run # Show what would be done ] { if $dry_run { print $"๐Ÿ” [DRY RUN] Would apply template ($template_name) to ($target_infra)" } else { print $"๐Ÿ“ฅ Applying template ($template_name) to ($target_infra)" } # Implementation would apply template with overrides print "Template application not yet implemented" } # === LAYER COMMANDS === # Show layer information export def "main layer show" [ workspace: string, # Workspace path --module: string = "", # Specific module to show --type: string = "taskservs" # Module type ] { print $"๐Ÿ“Š Layer information for ($workspace)" if not ($module | is-empty) { # Use existing layer utilities try { nu -c $"use provisioning/workspace/tools/layer-utils.nu *; test_layer_resolution ($module) ($workspace) upcloud" } catch { print $"Could not test layer resolution for ($module)" } } else { print "Showing overall layer structure..." try { nu -c "use provisioning/workspace/tools/layer-utils.nu *; show_layer_stats" } catch { print "Could not show layer statistics" } } } # Test layer resolution export def "main layer test" [ module: string, # Module to test workspace: string, # Workspace/infra name provider: string = "upcloud" # Provider for testing ] { print $"๐Ÿงช Testing layer resolution: ($module) in ($workspace) with ($provider)" try { nu -c $"use provisioning/workspace/tools/layer-utils.nu *; test_layer_resolution ($module) ($workspace) ($provider)" } catch { print $"โŒ Layer resolution test failed for ($module)" } } # === OVERRIDE COMMANDS === # Create configuration override export def "main override create" [ type: string, # Type: taskservs, providers, clusters infra: string, # Infrastructure name module: string, # Module name --from: string = "", # Template to base override on --layer: string = "infra" # Layer for override ] { print $"โš™๏ธ Creating override for ($module) in ($infra) at layer ($layer)" let override_path = match $layer { "infra" => $"workspace/infra/($infra)/overrides/($module).k" "workspace" => $"provisioning/workspace/templates/($type)/($module).k" _ => { print $"Unknown layer: ($layer)" return } } print $"๐Ÿ“ Override will be created at: ($override_path)" if not ($from | is-empty) { print $"๐Ÿ“‹ Based on template: ($from)" } # Create directory if needed mkdir ($override_path | path dirname) # Create basic override file let content = if not ($from | is-empty) { $"# Override for ($module) in ($infra) # Based on template: ($from) import ($type).*.($module).kcl.($module) as base import provisioning.workspace.templates.($type).($from) as template # Infrastructure-specific overrides ($module)_($infra)_override: base.($module | str capitalize) = template.($from)_template { # Add your overrides here # Example: # replicas = 3 # resources.memory = \"1Gi\" } " } else { $"# Override for ($module) in ($infra) import ($type).*.($module).kcl.($module) as base # Infrastructure-specific overrides ($module)_($infra)_override: base.($module | str capitalize) = base.($module)_config { # Add your overrides here # Example: # replicas = 3 # resources.memory = \"1Gi\" } " } $content | save $override_path print $"โœ… Override created: ($override_path)" } # === WORKSPACE MANAGEMENT === # Initialize workspace with modules export def "main init" [ workspace: string, # Workspace path --modules: list = [], # Initial modules to load --template: string = "", # Workspace template --provider: string = "upcloud" # Default provider ] { print $"๐Ÿš€ Initializing workspace: ($workspace)" # Create workspace structure let workspace_dirs = [ $"($workspace)/config" $"($workspace)/taskservs" $"($workspace)/overrides" $"($workspace)/defs" $"($workspace)/clusters" ] for dir in $workspace_dirs { mkdir $dir print $" ๐Ÿ“ Created: ($dir)" } # Create basic configuration let config_content = $"# Workspace configuration for ($workspace) # Provider: ($provider) # Initialized: (date now) provider = "($provider)" workspace = "($workspace)" " $config_content | save $"($workspace)/config/workspace.toml" print $" ๐Ÿ“„ Created: ($workspace)/config/workspace.toml" # Load initial modules if ($modules | length) > 0 { print $"๐Ÿ“ฆ Loading initial modules: (($modules | str join ', '))" main load taskservs $workspace ...$modules } print $"โœ… Workspace ($workspace) initialized successfully" } # Validate workspace integrity export def "main validate" [workspace: string] { print $"๐Ÿ” Validating workspace: ($workspace)" let required_dirs = ["config", "taskservs", "overrides", "defs"] mut validation_errors = [] for dir in $required_dirs { let full_path = ($workspace | path join $dir) if not ($full_path | path exists) { $validation_errors = ($validation_errors | append $"Missing directory: ($full_path)") } } # Check configuration file let config_file = ($workspace | path join "config" "workspace.toml") if not ($config_file | path exists) { $validation_errors = ($validation_errors | append $"Missing configuration: ($config_file)") } # Report results if ($validation_errors | is-empty) { print "โœ… Workspace validation passed" return true } else { print "โŒ Workspace validation failed:" for error in $validation_errors { print $" โ€ข ($error)" } return false } } # Show workspace information export def "main info" [workspace: string] { print $"๐Ÿ“Š Workspace Information: ($workspace)" if not (($workspace | path join "config" "workspace.toml") | path exists) { print "โŒ Workspace not found or not initialized" return } # Show basic info let config = try { open ($workspace | path join "config" "workspace.toml") | from toml } catch { {} } print $" Provider: (($config.provider? | default 'unknown'))" print $" Path: ($workspace)" # Count modules let taskserv_count = try { ls ($workspace | path join "taskservs") | length } catch { 0 } let override_count = try { ls ($workspace | path join "overrides") | length } catch { 0 } print $" Task Services: ($taskserv_count)" print $" Overrides: ($override_count)" # Show recent activity let recent_files = try { ls $workspace | where type == file | sort-by modified | last 3 | get name } catch { [] } if ($recent_files | length) > 0 { print " Recent activity:" for file in $recent_files { print $" โ€ข ($file | path basename)" } } } # === HELPER FUNCTIONS === # Generic extension loading function (taskservs, providers, clusters, workflows) def load_extension_to_workspace [ extension_type: string, # taskservs, providers, clusters, workflows workspace: string, modules: list, layer: string, force: bool = false ] { # Get extension-specific info function based on type let get_info_fn = match $extension_type { "taskservs" => { |name| get-taskserv-info $name } "providers" => { |name| get-provider-info $name } "clusters" => { |name| get-cluster-info $name } _ => { |name| {name: $name, group: "", type: $extension_type} } } # Get source path from config let source_base_path = (config-get $"paths.($extension_type)" | path expand) # Get template base path from config let provisioning_base = (config-get "paths.base" | path expand) let template_base_path = ($provisioning_base | path join "workspace" "templates" $extension_type) for module in $modules { print $" ๐Ÿ“ฆ Loading ($extension_type): ($module)" # Get module info let module_info = try { do $get_info_fn $module } catch { print $" โŒ Module not found: ($module)" continue } print $" โœ“ Found: ($module_info.name) (($module_info.group? | default ""))" # Resolve workspace paths let workspace_abs = ($workspace | path expand) let workspace_root = if ($workspace_abs | str contains "/infra/") { let parts = ($workspace_abs | split row "/infra/") $parts.0 } else { $workspace_abs } # Build source path (handle optional group, "root" means no category) let group_path = ($module_info.group? | default "") let group_path = if ($group_path == "root") { "" } else { $group_path } let source_module_path = if ($group_path | is-not-empty) { $source_base_path | path join $group_path $module } else { $source_base_path | path join $module } # STEP 1: Copy schemas to workspace/.{extension_type} let target_schemas_dir = ($workspace_root | path join $".($extension_type)") let target_module_path = if ($group_path | is-not-empty) { $target_schemas_dir | path join $group_path $module } else { $target_schemas_dir | path join $module } # Config file directory let config_dir = ($workspace_abs | path join $extension_type) let config_file_path = ($config_dir | path join $"($module).k") # Check if already loaded if ($config_file_path | path exists) and ($target_module_path | path exists) { if not $force { print $" โœ… Module already loaded: ($module)" print $" Config: ($config_file_path)" print $" Source: ($target_module_path)" print $" ๐Ÿ’ก Use --force to overwrite existing files" continue } else { print $" ๐Ÿ”„ Overwriting existing module: ($module)" } } # Copy schemas from system extensions to workspace let parent_dir = ($target_module_path | path dirname) mkdir $parent_dir if ($source_module_path | path exists) { print $" ๐Ÿ“ฆ Copying schemas to workspace .($extension_type)..." print $" From: ($source_module_path)" print $" To: ($target_module_path)" if ($target_module_path | path exists) { rm -rf $target_module_path } cp -r $source_module_path $parent_dir print $" โœ“ Schemas copied to workspace .($extension_type)/" # STEP 2a: Update individual module's kcl.mod with correct workspace paths # Calculate relative paths based on categorization depth let provisioning_path = if ($group_path | is-not-empty) { # Categorized: .{ext}/{category}/{module}/kcl/ -> ../../../../.kcl/packages/provisioning "../../../../.kcl/packages/provisioning" } else { # Non-categorized: .{ext}/{module}/kcl/ -> ../../../.kcl/packages/provisioning "../../../.kcl/packages/provisioning" } let parent_path = if ($group_path | is-not-empty) { # Categorized: .{ext}/{category}/{module}/kcl/ -> ../../.. "../../.." } else { # Non-categorized: .{ext}/{module}/kcl/ -> ../.. "../.." } # Update the module's kcl.mod file with workspace-relative paths let module_kcl_mod_path = ($target_module_path | path join "kcl" "kcl.mod") if ($module_kcl_mod_path | path exists) { print $" ๐Ÿ”ง Updating module kcl.mod with workspace paths" let module_kcl_mod_content = $"[package] name = \"($module)\" edition = \"v0.11.3\" version = \"0.0.1\" [dependencies] provisioning = { path = \"($provisioning_path)\", version = \"0.0.1\" } ($extension_type) = { path = \"($parent_path)\", version = \"0.1.0\" } " $module_kcl_mod_content | save -f $module_kcl_mod_path print $" โœ“ Updated kcl.mod: ($module_kcl_mod_path)" } } else { print $" โš ๏ธ Warning: Source not found at ($source_module_path)" } # STEP 2b: Create kcl.mod in workspace/.{extension_type} let extension_kcl_mod = ($target_schemas_dir | path join "kcl.mod") if not ($extension_kcl_mod | path exists) { print $" ๐Ÿ“ฆ Creating kcl.mod for .($extension_type) package" let kcl_mod_content = $"[package] name = \"($extension_type)\" edition = \"v0.11.3\" version = \"0.1.0\" description = \"Workspace-level ($extension_type) schemas\" " $kcl_mod_content | save $extension_kcl_mod } # Ensure config directory exists mkdir $config_dir # STEP 4: Generate config from template let template_path = if ($group_path | is-not-empty) { $template_base_path | path join $group_path $"($module).k" } else { $template_base_path | path join $"($module).k" } # Build import statement with "as {module}" alias let import_stmt = if ($group_path | is-not-empty) { $"import ($extension_type).($group_path).($module).kcl.($module) as ($module)" } else { $"import ($extension_type).($module).kcl.($module) as ($module)" } # Get relative paths for comments let workspace_name = ($workspace_root | path basename) let relative_schema_path = if ($group_path | is-not-empty) { $"($workspace_name)/.($extension_type)/($group_path)/($module)" } else { $"($workspace_name)/.($extension_type)/($module)" } let config_content = if ($template_path | path exists) { print $" ๐Ÿ“„ Using template from: ($template_path)" let template_body = (open $template_path) $"# Configuration for ($module) # Workspace: ($workspace_name) # Schemas from: ($relative_schema_path) ($import_stmt) ($template_body)" } else { $"# Configuration for ($module) # Workspace: ($workspace_name) # Schemas from: ($relative_schema_path) ($import_stmt) # TODO: Configure your ($module) instance # See available schemas at: ($relative_schema_path)/kcl/ " } $config_content | save -f $config_file_path print $" โœ“ Config created: ($config_file_path)" print $" ๐Ÿ“ Edit ($extension_type)/($module).k to configure settings" # STEP 3: Update infra kcl.mod if ($workspace_abs | str contains "/infra/") { let kcl_mod_path = ($workspace_abs | path join "kcl.mod") if ($kcl_mod_path | path exists) { let kcl_mod_content = (open $kcl_mod_path) if not ($kcl_mod_content | str contains $"($extension_type) =") { print $" ๐Ÿ”ง Updating kcl.mod to include ($extension_type) dependency" let new_dependency = $"\n# Workspace-level ($extension_type) \(shared across infras\)\n($extension_type) = { path = \"../../.($extension_type)\" }\n" $"($kcl_mod_content)($new_dependency)" | save -f $kcl_mod_path } } } } } # Unload taskserv from workspace def unload_taskserv_from_workspace [workspace: string, module: string, layer: string] { let target_path = match $layer { "workspace" => ($workspace | path join "taskservs" $"($module).k") "infra" => ($workspace | path join "overrides" $"($module).k") _ => ($workspace | path join "taskservs" $"($module).k") } if ($target_path | path exists) { rm $target_path print $" โœ“ Removed: ($target_path)" } else { print $" โŒ Not found: ($target_path)" } } # List workspace taskservs def list_workspace_taskservs [workspace: string, layer: string, format: string] { let paths = match $layer { "workspace" => [($workspace | path join "taskservs")] "infra" => [($workspace | path join "overrides")] "all" => [($workspace | path join "taskservs"), ($workspace | path join "overrides")] _ => [($workspace | path join "taskservs")] } mut all_taskservs = [] for path in $paths { if ($path | path exists) { let taskservs = ls $path | where type == file | where name =~ '\\.k$' | each { |file| { name: ($file.name | path basename | str replace ".k" "") layer: ($path | path basename) path: $file.name modified: $file.modified } } $all_taskservs = ($all_taskservs | append $taskservs) } } format_output $all_taskservs $format } # Format output based on requested format def format_output [data: any, format: string] { match $format { "json" => ($data | to json) "yaml" => ($data | to yaml) "names" => ($data | get name | str join "\n") "table" | _ => ($data | table) } } # === HELP FUNCTIONS === def print_enhanced_help [] { print "Enhanced Module Loader CLI - Discovery and loading with template support" print "" print "Usage: module-loader [options]" print "" print "CORE COMMANDS:" print " discover [query] [--format ] [--category ] - Discover available modules" print " sync-kcl [--manifest ] [--kcl] - Sync KCL dependencies for infrastructure" print " load [--layer ] - Load modules into workspace" print " list [--layer ] - List loaded modules" print " unload [--layer ] - Unload module from workspace" print "" print "WORKSPACE COMMANDS:" print " init [--modules ] [--template ] - Initialize workspace" print " validate - Validate workspace integrity" print " info - Show workspace information" print "" print "TEMPLATE COMMANDS:" print " template list [--type ] [--format ] - List available templates" print " template extract [--type ] - Extract template from infra" print " template apply