#!/usr/bin/env nu # Extension Creation Tool (Updated for Modern Nushell & Grouped Structure) # Creates new extensions from templates with variable substitution use ../core/nulib/taskservs/discover.nu * # Main extension creation command def main [ type: string, # Extension type: taskserv, provider, cluster name: string, # Extension name (kebab-case) --template: string = "basic", # Template to use --author: string = "Unknown", # Author name --description: string = "", # Extension description --category: string = "", # Category for taskservs (required for taskservs) --port: int = 8080, # Default port for services --output: string = "provisioning/extensions" # Output directory ] { print $"🚀 Creating ($type) extension: ($name)" # Validate extension type if $type not-in ["taskserv", "provider", "cluster"] { error make { msg: "Extension type must be one of: taskserv, provider, cluster" } } # Validate name format if ($name | str contains " ") or ($name | str contains "_") or ($name != ($name | str downcase)) { error make { msg: "Extension name must be in kebab-case format (e.g., my-service)" } } # For taskservs, require category if $type == "taskserv" and ($category | is-empty) { error make { msg: "Category is required for taskservs. Use --category " } } # Create extension from template let extension_info = create_extension_from_template $type $name $template $author $description $category $port $output print $"✅ Extension created successfully:" print $"📁 Location: ($extension_info.path)" print $"📄 Files created: ($extension_info.files | length)" print "" print "🔄 Next steps:" print $" 1. cd ($extension_info.path)" print " 2. Customize the configuration in the main .k file" print " 3. Update README.md with specific details" if $type == "taskserv" { print " 4. Test discovery:" print $" nu -c \"use provisioning/core/nulib/taskservs/discover.nu *; get-taskserv-info ($name)\"" print " 5. Test layer resolution:" print $" nu -c \"use provisioning/workspace/tools/layer-utils.nu *; test_layer_resolution ($name) wuji upcloud\"" } } # Create extension from template def create_extension_from_template [ type: string, name: string, template: string, author: string, description: string, category: string, port: int, output_dir: string ]: nothing -> record { # Determine template path based on type let template_base = "provisioning/templates/extensions" let template_path = match $type { "taskserv" => ($template_base | path join "taskservs" "basic-taskserv") "provider" => ($template_base | path join "providers" "basic-provider") "cluster" => ($template_base | path join "clusters" "basic-cluster") } # Create extension path (with category for taskservs) let extension_path = if $type == "taskserv" { ($output_dir | path join "taskservs" $category $name "kcl") } else { ($output_dir | path join ($type + "s") $name "kcl") } # Create output directory print $"📁 Creating directories..." mkdir $extension_path # Also create default directory for taskservs if $type == "taskserv" { let default_path = ([$extension_path, "..", "default"] | path join) mkdir $default_path } # Generate variables for templates let variables = generate_template_variables $type $name $author $description $category $port # Check if template exists, if not create basic structure if ($template_path | path exists) { # Copy and process template files let template_files = try { ls $template_path | get name } catch { [] } let created_files = ($template_files | each { |file| process_template_file $file $extension_path $variables }) # Create additional files for taskservs if $type == "taskserv" { create_taskserv_extras $extension_path $variables } { path: ([$extension_path, ".."] | path join), files: $created_files, variables: $variables } } else { # Create basic structure without templates print $"⚠️ Template not found: ($template_path)" print "📄 Creating basic structure..." let created_files = create_basic_structure $type $extension_path $variables # Create additional files for taskservs if $type == "taskserv" { create_taskserv_extras $extension_path $variables } { path: ([$extension_path, ".."] | path join), files: $created_files, variables: $variables } } } # Generate template variables for substitution def generate_template_variables [ type: string, name: string, author: string, description: string, category: string, port: int ]: nothing -> record { let name_pascal = ($name | split row "-" | each { |word| $word | str capitalize } | str join "") let name_snake = ($name | str replace -a "-" "_") let display_name = ($name | split row "-" | each { |word| $word | str capitalize } | str join " ") let final_description = if ($description | is-empty) { $"Deployment and management for ($display_name)" } else { $description } # Set defaults based on type and name let default_port = if $port != 8080 { $port } else { match $name { $name if ($name | str contains "redis") => 6379 $name if ($name | str contains "postgres") => 5432 $name if ($name | str contains "mysql") => 3306 $name if ($name | str contains "http") => 8080 $name if ($name | str contains "api") => 8080 _ => 8080 } } let final_category = if ($category | is-empty) { match $name { $name if ($name | str contains "database" or $name | str contains "db" or $name | str contains "postgres" or $name | str contains "mysql" or $name | str contains "redis") => "databases" $name if ($name | str contains "web" or $name | str contains "http" or $name | str contains "nginx" or $name | str contains "proxy") => "networking" $name if ($name | str contains "monitor" or $name | str contains "metric" or $name | str contains "log") => "infrastructure" $name if ($name | str contains "security" or $name | str contains "auth" or $name | str contains "cert") => "infrastructure" _ => "development" } } else { $category } { # Old template compatibility TASKSERV_NAME: $name, TASKSERV_NAME_PASCAL: $name_pascal, TASKSERV_NAME_SNAKE: $name_snake, TASKSERV_DISPLAY_NAME: $display_name, TASKSERV_DESCRIPTION: $final_description, # New extension variables EXTENSION_TYPE: $type, EXTENSION_NAME: $name, EXTENSION_NAME_PASCAL: $name_pascal, EXTENSION_NAME_SNAKE: $name_snake, EXTENSION_DISPLAY_NAME: $display_name, EXTENSION_DESCRIPTION: $final_description, EXTENSION_CATEGORY: $final_category, DEFAULT_PORT: $default_port, AUTHOR_NAME: $author, LICENSE: "MIT", REPOSITORY_URL: $"https://github.com/($author | str downcase)/($name)-($type)", SERVICE_VERSION: "latest", TAGS: $"\"($final_category)\", \"kubernetes\", \"($name)\"", DOCS_URL: $"https://docs.example.com/($name)", SUPPORT_URL: $"https://github.com/($author | str downcase)/($name)-($type)/issues", DATE: (date now | format date "%Y-%m-%d"), VERSION: "1.0.0" } } # Process a single template file def process_template_file [ file: string, extension_path: string, variables: record ]: nothing -> string { let filename = ($file | path basename) let target_file = process_filename $filename $variables let target_path = ($extension_path | path join $target_file) # Read template content let content = try { open $file } catch { "" } # Process template variables let processed_content = process_template_content $content $variables # Save processed file $processed_content | save $target_path print $" ✓ Created ($target_file)" $target_path } # Process template filename with variables def process_filename [filename: string, variables: record]: nothing -> string { mut processed = $filename # Use fold instead of reduce for modern Nushell for key_value in ($variables | transpose key value) { $processed = ($processed | str replace -a $"{{($key_value.key)}}" $key_value.value) } $processed } # Process template content with variables def process_template_content [content: string, variables: record]: nothing -> string { mut processed = $content # Use fold instead of reduce for modern Nushell for key_value in ($variables | transpose key value) { $processed = ($processed | str replace -a $"{{($key_value.key)}}" ($key_value.value | into string)) } $processed } # Create basic structure without templates def create_basic_structure [ type: string, extension_path: string, variables: record ]: nothing -> list { mut created_files = [] match $type { "taskserv" => { # Create kcl.mod let kcl_mod_content = create_basic_kcl_mod $variables ($kcl_mod_content | save ($extension_path | path join "kcl.mod")) $created_files = ($created_files | append "kcl.mod") # Create main KCL file let main_kcl_content = create_basic_taskserv_kcl $variables ($main_kcl_content | save ($extension_path | path join $"($variables.EXTENSION_NAME).k")) $created_files = ($created_files | append $"($variables.EXTENSION_NAME).k") # Create version file let version_content = create_basic_version_kcl $variables ($version_content | save ($extension_path | path join "version.k")) $created_files = ($created_files | append "version.k") # Create README let readme_content = create_basic_readme $variables ($readme_content | save ([$extension_path, "..", "README.md"] | path join)) $created_files = ($created_files | append "README.md") print $" ✓ Created kcl.mod" print $" ✓ Created ($variables.EXTENSION_NAME).k" print $" ✓ Created version.k" print $" ✓ Created README.md" } "provider" => { # Create basic provider structure let provider_content = create_basic_provider_kcl $variables ($provider_content | save ($extension_path | path join $"($variables.EXTENSION_NAME).k")) $created_files = ($created_files | append $"($variables.EXTENSION_NAME).k") print $" ✓ Created ($variables.EXTENSION_NAME).k" } "cluster" => { # Create basic cluster structure let cluster_content = create_basic_cluster_kcl $variables ($cluster_content | save ($extension_path | path join $"($variables.EXTENSION_NAME).k")) $created_files = ($created_files | append $"($variables.EXTENSION_NAME).k") print $" ✓ Created ($variables.EXTENSION_NAME).k" } } $created_files } # Create extra files for taskservs def create_taskserv_extras [ extension_path: string, variables: record ] { let default_path = ([$extension_path, "..", "default"] | path join) # Create defs.toml let defs_content = create_basic_defs_toml $variables ($defs_content | save ($default_path | path join "defs.toml")) # Create install script let install_content = create_basic_install_script $variables ($install_content | save ($default_path | path join $"install-($variables.EXTENSION_NAME).sh")) chmod +x ($default_path | path join $"install-($variables.EXTENSION_NAME).sh") print $" ✓ Created defs.toml" print $" ✓ Created install-($variables.EXTENSION_NAME).sh" } # Template content generators def create_basic_kcl_mod [variables: record]: nothing -> string { $"[package] name = \"($variables.EXTENSION_NAME)\" version = \"($variables.VERSION)\" description = \"($variables.EXTENSION_DESCRIPTION)\" authors = [\"($variables.AUTHOR_NAME)\"] [dependencies] k8s = { oci = \"oci://ghcr.io/kcl-lang/k8s\", tag = \"1.30\" } " } def create_basic_taskserv_kcl [variables: record]: nothing -> string { $"# ($variables.EXTENSION_DISPLAY_NAME) Taskserv Configuration # ($variables.EXTENSION_DESCRIPTION) schema ($variables.EXTENSION_NAME_PASCAL) { # Service metadata name: str = \"($variables.EXTENSION_NAME)\" version: str = \"latest\" namespace: str = \"default\" # Service configuration replicas: int = 1 port: int = ($variables.DEFAULT_PORT) # Resource requirements resources: { cpu: str = \"100m\" memory: str = \"128Mi\" limits?: { cpu?: str = \"500m\" memory?: str = \"512Mi\" } } = { cpu = \"100m\" memory = \"128Mi\" } # Service specific configuration config?: {str: any} = {} # Health checks health?: { enabled: bool = true path: str = \"/health\" initial_delay: int = 30 period: int = 10 } = { enabled = true path = \"/health\" initial_delay = 30 period = 10 } } # Default configuration ($variables.EXTENSION_NAME_SNAKE)_config: ($variables.EXTENSION_NAME_PASCAL) = ($variables.EXTENSION_NAME_PASCAL) { name = \"($variables.EXTENSION_NAME)\" version = \"latest\" replicas = 1 port = ($variables.DEFAULT_PORT) } " } def create_basic_version_kcl [variables: record]: nothing -> string { $"# Version information for ($variables.EXTENSION_NAME) taskserv schema ($variables.EXTENSION_NAME_PASCAL)Version { current: str = \"($variables.VERSION)\" compatible: [str] = [\"($variables.VERSION)\"] deprecated?: [str] = [] changelog?: {str: str} = {} } ($variables.EXTENSION_NAME_SNAKE)_version: ($variables.EXTENSION_NAME_PASCAL)Version = ($variables.EXTENSION_NAME_PASCAL)Version { current = \"($variables.VERSION)\" changelog = { \"($variables.VERSION)\" = \"Initial release\" } } " } def create_basic_defs_toml [variables: record]: nothing -> string { $"# Default configuration for ($variables.EXTENSION_NAME) # Generated on ($variables.DATE) [service] name = \"($variables.EXTENSION_NAME)\" version = \"latest\" port = ($variables.DEFAULT_PORT) replicas = 1 [deployment] strategy = \"RollingUpdate\" max_unavailable = 1 max_surge = 1 [resources] cpu_request = \"100m\" cpu_limit = \"500m\" memory_request = \"128Mi\" memory_limit = \"512Mi\" [health] enabled = true path = \"/health\" initial_delay_seconds = 30 period_seconds = 10 timeout_seconds = 5 [networking] service_type = \"ClusterIP\" expose_metrics = false metrics_port = 9090 " } def create_basic_install_script [variables: record]: nothing -> string { $"#!/bin/bash set -euo pipefail # ($variables.EXTENSION_DISPLAY_NAME) Installation Script # Generated on ($variables.DATE) echo \"🚀 Installing ($variables.EXTENSION_NAME)...\" # Configuration SERVICE_NAME=\"${SERVICE_NAME:-($variables.EXTENSION_NAME)}\" SERVICE_VERSION=\"${SERVICE_VERSION:-latest}\" NAMESPACE=\"${NAMESPACE:-default}\" REPLICAS=\"${REPLICAS:-1}\" PORT=\"${PORT:-($variables.DEFAULT_PORT)}\" # Validation if [[ -z \"$SERVICE_NAME\" ]]; then echo \"❌ SERVICE_NAME is required\" exit 1 fi echo \"📋 Configuration:\" echo \" Service: $SERVICE_NAME\" echo \" Version: $SERVICE_VERSION\" echo \" Namespace: $NAMESPACE\" echo \" Port: $PORT\" echo \" Replicas: $REPLICAS\" # Create namespace echo \"🏗️ Creating namespace...\" kubectl create namespace \"$NAMESPACE\" --dry-run=client -o yaml | kubectl apply -f - # Apply configuration echo \"🚢 Deploying service...\" cat < string { let readme_template = '# {{EXTENSION_DISPLAY_NAME}} Extension {{EXTENSION_DESCRIPTION}} ## Overview This extension provides {{EXTENSION_TYPE}} capabilities for {{EXTENSION_DISPLAY_NAME}}. ## Configuration ### Basic Usage ```kcl import {{EXTENSION_TYPE}}s.{{EXTENSION_CATEGORY}}.{{EXTENSION_NAME}}.kcl.{{EXTENSION_NAME}} as {{EXTENSION_NAME_SNAKE}} {{EXTENSION_NAME_SNAKE}}_config: {{EXTENSION_NAME_SNAKE}}.{{EXTENSION_NAME_PASCAL}} = {{EXTENSION_NAME_SNAKE}}.{{EXTENSION_NAME_SNAKE}}_config { # Customize configuration version = "1.2.3" port = {{DEFAULT_PORT}} } ``` ## Installation ### Using Provisioning System ```bash # Deploy with provisioning CLI provisioning/core/cli/provisioning {{EXTENSION_TYPE}} create {{EXTENSION_NAME}} --infra my-infra --check ``` ### Direct Installation ```bash # Set environment variables export SERVICE_VERSION="1.0.0" export NAMESPACE="production" # Run installation script ./default/install-{{EXTENSION_NAME}}.sh ``` ## Development ### Testing ```bash # Test discovery (for taskservs) nu -c "use provisioning/core/nulib/taskservs/discover.nu *; get-taskserv-info {{EXTENSION_NAME}}" # Test layer resolution (for taskservs) nu -c "use provisioning/workspace/tools/layer-utils.nu *; test_layer_resolution {{EXTENSION_NAME}} wuji upcloud" ``` ### Validation ```bash # Check KCL syntax kcl check kcl/{{EXTENSION_NAME}}.k # Validate extension structure nu provisioning/tools/create-extension.nu validate . ``` ## License {{LICENSE}} ## Author {{AUTHOR_NAME}} ## Generated Created on {{DATE}} using the Extension Creation Tool. ' process_template_content $readme_template $variables } def create_basic_provider_kcl [variables: record]: nothing -> string { $"# ($variables.EXTENSION_DISPLAY_NAME) Provider Configuration # ($variables.EXTENSION_DESCRIPTION) schema ($variables.EXTENSION_NAME_PASCAL)Provider { # Provider metadata name: str = \"($variables.EXTENSION_NAME)\" version: str = \"latest\" region?: str = \"default\" # Provider-specific configuration config?: {str: any} = {} # Authentication auth?: { type: str = \"default\" credentials?: {str: any} = {} } = {} # Resource limits limits?: { instances?: int = 10 storage?: str = \"100GB\" } = {} } # Default configuration ($variables.EXTENSION_NAME_SNAKE)_provider: ($variables.EXTENSION_NAME_PASCAL)Provider = ($variables.EXTENSION_NAME_PASCAL)Provider { name = \"($variables.EXTENSION_NAME)\" version = \"latest\" } " } def create_basic_cluster_kcl [variables: record]: nothing -> string { $"# ($variables.EXTENSION_DISPLAY_NAME) Cluster Configuration # ($variables.EXTENSION_DESCRIPTION) schema ($variables.EXTENSION_NAME_PASCAL)Cluster { # Cluster metadata name: str = \"($variables.EXTENSION_NAME)\" version: str = \"latest\" namespace: str = \"default\" # Cluster configuration nodes: int = 3 node_type: str = \"standard\" # Cluster components components: [str] = [] # Networking networking?: { cidr?: str = \"10.0.0.0/16\" service_cidr?: str = \"10.1.0.0/16\" } = {} # Storage storage?: { type: str = \"persistent\" size: str = \"10Gi\" } = {} } # Default configuration ($variables.EXTENSION_NAME_SNAKE)_cluster: ($variables.EXTENSION_NAME_PASCAL)Cluster = ($variables.EXTENSION_NAME_PASCAL)Cluster { name = \"($variables.EXTENSION_NAME)\" version = \"latest\" nodes = 3 } " } # List available templates export def "main list-templates" [type?: string]: nothing -> table { let templates_base = "provisioning/templates/extensions" if ($type | is-empty) { # List all templates mut all_templates = [] for ext_type in ["taskservs", "providers", "clusters"] { let type_dir = ($templates_base | path join $ext_type) if ($type_dir | path exists) { let templates = try { ls $type_dir | get name | each { |path| { type: ($ext_type | str replace "s$" ""), name: ($path | path basename) } } } catch { [] } $all_templates = ($all_templates | append $templates) } } $all_templates } else { # List templates for specific type let type_plural = $type + "s" let template_dir = ($templates_base | path join $type_plural) if ($template_dir | path exists) { ls $template_dir | get name | each { |path| { type: $type, name: ($path | path basename) } } } else { [] } } } # Show template information export def "main template-info" [type: string, template: string]: nothing -> record { let template_path = $"provisioning/templates/extensions/($type)s/($template)" if not ($template_path | path exists) { error make { msg: $"Template not found: ($template_path)" } } let template_files = try { ls $template_path | get name | each { |file| $file | path basename } } catch { [] } let readme_path = ($template_path | path join "README.md") let readme_content = if ($readme_path | path exists) { try { open $readme_path | lines | take 10 | str join "\n" } catch { "No README content available" } } else { "No README available" } { type: $type, template: $template, path: $template_path, files: $template_files, readme_preview: $readme_content } } # Interactive extension creation export def "main interactive" []: nothing -> nothing { print "🚀 Interactive Extension Creator" print "" # Get extension type let type = (["taskserv", "provider", "cluster"] | input list "Select extension type:") # Get extension name let name = (input "Enter extension name (kebab-case):") if ($name | is-empty) or ($name | str contains " ") or ($name | str contains "_") { error make { msg: "Extension name must be kebab-case format (e.g., my-service)" } } # Get category for taskservs let category = if $type == "taskserv" { let categories = [ "container-runtime" "databases" "development" "infrastructure" "kubernetes" "networking" "storage" ] print "" print "📂 Available categories:" for i in 0..($categories | length) { let cat = ($categories | get $i) print $" ($i + 1). ($cat)" } let category_choice = input "Select category (1-7):" let category_index = (($category_choice | into int) - 1) if $category_index < 0 or $category_index >= ($categories | length) { error make { msg: "Invalid category selection" } } ($categories | get $category_index) } else { "" } # Get port for taskservs let port = if $type == "taskserv" { let port_input = input "Enter port number (default: 8080):" if ($port_input | is-empty) { 8080 } else { $port_input | into int } } else { 8080 } # Get optional information let author = input "Enter author name:" --default "Developer" let description = input $"Enter description for ($name):" --default "" # Show available templates let available_templates = (main list-templates $type) let template = if ($available_templates | length) > 0 { let template_names = ($available_templates | get name) if ($template_names | length) > 1 { $template_names | input list "Select template:" } else { $template_names | first } } else { "basic" } # Get output directory let output = input "Enter output directory:" --default "provisioning/extensions" # Create the extension print "" print "📋 Creating extension with:" print $" Type: ($type)" print $" Name: ($name)" if $type == "taskserv" { print $" Category: ($category)" print $" Port: ($port)" } print $" Template: ($template)" print $" Author: ($author)" print "" let confirm = input "✅ Proceed with creation? (y/N):" if not ($confirm | str downcase | str starts-with "y") { print "❌ Creation cancelled" return } if $type == "taskserv" { main $type $name --template $template --author $author --description $description --category $category --port $port --output $output } else { main $type $name --template $template --author $author --description $description --output $output } } # Validate extension structure export def "main validate" [extension_path: string]: nothing -> record { print $"🔍 Validating extension: ($extension_path)" let kcl_path = ($extension_path | path join "kcl") if not ($kcl_path | path exists) { error make { msg: $"KCL directory not found: ($kcl_path)" } } # Check required files let required_files = ["kcl.mod"] let optional_files = ["version.k", "dependencies.k", "README.md"] let missing_required = ($required_files | where { |file| not (($kcl_path | path join $file) | path exists) }) let missing_optional = ($optional_files | where { |file| not (($kcl_path | path join $file) | path exists) }) # Check KCL syntax let kcl_files = try { glob ($kcl_path | path join "*.k") | get name } catch { [] } let syntax_errors = ($kcl_files | each { |file| try { ^kcl check $file null } catch { |err| { file: $file, error: $err.msg } } } | compact) let is_valid = ($missing_required | is-empty) and ($syntax_errors | is-empty) { valid: $is_valid, extension_path: $extension_path, missing_required_files: $missing_required, missing_optional_files: $missing_optional, syntax_errors: $syntax_errors, recommendations: ( if not $is_valid { [ "Fix missing required files", "Resolve KCL syntax errors", "Add optional files for better documentation" ] } else { ["Extension structure is valid"] } ) } } # Show help export def "main help" [] { print "🛠️ Extension Creation Tool (Updated)" print "" print "USAGE:" print " nu provisioning/tools/create-extension.nu [options]" print "" print "COMMANDS:" print " create Create new extension" print " interactive Interactive extension creator" print " list-templates [type] List available templates" print " template-info