# | Nickel to Nickel Migration Helper # | Automates pattern detection and application # | Follows: .claude/kcl_to_nickel_migration_framework.md # | Author: Migration Framework # | Date: 2025-12-15 # ============================================================ # Pattern Detection # ============================================================ # Detect if Nickel file uses schema inheritance pattern export def "detect-inheritance" [decl_file: path] -> bool { let content = open $decl_file | into string ($content | str contains "schema ") and ($content | str contains "(") } # Detect if Nickel file exports global instances export def "detect-exports" [decl_file: path] -> list { let content = open $decl_file | into string $content | lines | filter { |line| ($line | str contains ": ") and not ($line | str contains "schema") } | filter { |line| ($line | str contains " = ") } | map { |line| $line | str trim } } # Detect if Nickel file only defines schemas (no exports) export def "is-schema-only" [decl_file: path] -> bool { let exports = (detect-exports $decl_file) ($exports | length) == 0 } # Get migration template type for Nickel file export def "get-template-type" [decl_file: path] -> string { let has_inheritance = (detect-inheritance $decl_file) let is_empty_export = (is-schema-only $decl_file) let exports = (detect-exports $decl_file) let export_count = ($exports | length) if $is_empty_export { "template-1-schema-only" } else if $has_inheritance { "template-4-inheritance" } else if $export_count == 1 { "template-2-single-instance" } else if $export_count > 1 { "template-5-multiple-schemas" } else { "template-3-complex-nesting" } } # ============================================================ # Value Conversion # ============================================================ # Convert Nickel boolean to Nickel export def "convert-boolean" [value: string] -> string { match ($value | str trim) { "True" => "true", "False" => "false", "true" => "true", "false" => "false", _ => $value, } } # Convert Nickel None to Nickel null export def "convert-none" [value: string] -> string { match ($value | str trim) { "None" => "null", "null" => "null", _ => $value, } } # Convert Nickel value to Nickel value export def "convert-value" [decl_value: string] -> string { let trimmed = ($decl_value | str trim) let bool_converted = (convert-boolean $trimmed) (convert-none $bool_converted) } # ============================================================ # JSON Equivalence Validation # ============================================================ # Export Nickel file to JSON for comparison export def "nickel-to-json" [decl_file: path] { if not ($decl_file | path exists) { error make {msg: $"Nickel file not found: ($decl_file)"} } nickel export $decl_file --format json 2>&1 } # Export Nickel file to JSON for comparison export def "nickel-to-json" [nickel_file: path] { if not ($nickel_file | path exists) { error make {msg: $"Nickel file not found: ($nickel_file)"} } nickel export $nickel_file 2>&1 | from json | to json } # Compare Nickel and Nickel JSON outputs for equivalence export def "compare-equivalence" [decl_file: path, nickel_file: path] -> bool { let source_json = (nickel-to-json $decl_file | from json) let nickel_json = (nickel-to-json $nickel_file | from json) $source_json == $nickel_json } # Show detailed comparison between Nickel and Nickel export def "show-comparison" [decl_file: path, nickel_file: path] { print $"Comparing: ($decl_file) ⇄ ($nickel_file)\n" let source_json = (nickel-to-json $decl_file) let nickel_json = (nickel-to-json $nickel_file) print "=== Source Output (JSON) ===" print $source_json print "" print "=== Target Output (JSON) ===" print $nickel_json print "" let equivalent = ($source_json == $nickel_json) if $equivalent { print "✅ Outputs are EQUIVALENT" } else { print "❌ Outputs DIFFER" print "\nDifferences:" diff <(print $source_json | jq -S .) <(print $nickel_json | jq -S .) } } # ============================================================ # Migration Workflow # ============================================================ # Analyze Nickel file and recommend migration approach export def "analyze-nickel" [decl_file: path] { if not ($decl_file | path exists) { error make {msg: $"File not found: ($decl_file)"} } let template = (get-template-type $decl_file) let has_inheritance = (detect-inheritance $decl_file) let exports = (detect-exports $decl_file) let is_empty = (is-schema-only $decl_file) print $"File: ($decl_file)" print $"Template Type: ($template)" print $"Has Schema Inheritance: ($has_inheritance)" print $"Is Schema-Only (no exports): ($is_empty)" print $"Exported Instances: ($exports | length)" if ($exports | length) > 0 { print "\nExported instances:" $exports | each { |exp| print $" - ($exp)" } } } # Generate skeleton Nickel file from Nickel template export def "generate-nickel-skeleton" [decl_file: path, output_file: path] { let template = (get-template-type $decl_file) let source_name = ($decl_file | path basename | str replace ".ncl" "") let skeleton = match $template { "template-1-schema-only" => { $"# | Schema definitions migrated from ($source_name).ncl\n# | Migrated: 2025-12-15\n\n{{}}" }, "template-2-single-instance" => { let exports = (detect-exports $decl_file) let instance = ($exports | get 0 | str split " " | get 0) $"# | Configuration migrated from ($source_name).ncl\n\n{\n ($instance) = {\n # TODO: Fill in fields\n },\n}" }, _ => { $"# | Migrated from ($source_name).ncl\n# | Template: ($template)\n\n{\n # TODO: Implement\n}" }, } print $skeleton print $"\nTo save: print output to ($output_file)" } # ============================================================ # Batch Migration # ============================================================ # Migrate multiple Nickel files to Nickel using templates export def "batch-migrate" [ source_dir: path, nickel_dir: path, --pattern: string = "*.ncl", --dry-run: bool = false, ] { let source_files = (glob $"($source_dir)/($pattern)") print $"Found ($source_files | length) Nickel files matching pattern: ($pattern)" print "" $source_files | each { |source_file| let relative_path = ($source_file | str replace $"($source_dir)/" "") let nickel_file = $"($nickel_dir)/($relative_path | str replace ".ncl" ".ncl")" print $"[$relative_path]" let template = (get-template-type $source_file) print $" Template: ($template)" if not $dry_run { if ($nickel_file | path exists) { print $" ⚠️ Already exists: ($nickel_file)" } else { print $" → Would migrate to: ($nickel_file)" } } } } # ============================================================ # Validation # ============================================================ # Validate Nickel file syntax export def "validate-nickel" [nickel_file: path] -> bool { # Validate Nickel syntax (no try-catch) let result = (do { nickel export $nickel_file | null } | complete) ($result.exit_code == 0) } # Full migration validation for a file pair export def "validate-migration" [decl_file: path, nickel_file: path] -> record { let source_exists = ($decl_file | path exists) let nickel_exists = ($nickel_file | path exists) let nickel_valid = if $nickel_exists { (validate-nickel $nickel_file) } else { false } let equivalent = if ($source_exists and $nickel_valid) { (compare-equivalence $decl_file $nickel_file) } else { false } { source_exists: $source_exists, nickel_exists: $nickel_exists, nickel_valid: $nickel_valid, outputs_equivalent: $equivalent, status: if $equivalent { "✅ PASS" } else { "❌ FAIL" }, } } # Validation report for all migrated files export def "validation-report" [source_dir: path, nickel_dir: path] { let nickel_files = (glob $"($nickel_dir)/**/*.ncl") print $"Validation Report: ($nickel_files | length) Nickel files\n" let results = $nickel_files | map { |nickel_file| let relative = ($nickel_file | str replace $"($nickel_dir)/" "") let source_file = $"($source_dir)/($relative | str replace ".ncl" ".ncl")" let validation = (validate-migration $source_file $nickel_file) print $"($validation.status) $relative" if not $validation.nickel_valid { print " ⚠️ Nickel syntax error" } if not $validation.outputs_equivalent { print " ⚠️ JSON outputs differ" } $validation } let passed = ($results | where {|r| $r.outputs_equivalent} | length) let total = ($results | length) print $"\nSummary: ($passed)/($total) files PASS equivalence check" }