#!/usr/bin/env nu # Run schema migrations for KOGRAL # # Usage: nu kogral-migrate.nu [--target ] [--dry-run] def main [ --target: string = "latest" # Target migration version --dry-run # Show what would be migrated without making changes --kogral-dir: string = ".kogral" # KOGRAL directory ] { print $"(ansi green_bold)KOGRAL Migration(ansi reset)" print $"Target version: ($target)" print $"KOGRAL Directory: ($kogral_dir)" if $dry_run { print $"(ansi yellow)DRY RUN MODE - No changes will be made(ansi reset)" } # Check if .kogral directory exists if not ($kogral_dir | path exists) { print $"(ansi red)Error: KOGRAL directory not found: ($kogral_dir)(ansi reset)" exit 1 } # Load current version from config let config_path = $"($kogral_dir)/config.toml" if not ($config_path | path exists) { print $"(ansi red)Error: Config file not found: ($config_path)(ansi reset)" exit 1 } let config = open $config_path | from toml let current_version = $config.graph.version print $"\n(ansi cyan_bold)Current schema version:(ansi reset) ($current_version)" # Define available migrations let migrations = [ { version: "1.0.0", description: "Initial schema" }, { version: "1.1.0", description: "Add metadata field to nodes" }, { version: "1.2.0", description: "Add embedding support" }, ] print $"\n(ansi cyan_bold)Available migrations:(ansi reset)" for migration in $migrations { let indicator = if $migration.version == $current_version { $"(ansi green)✓ [CURRENT](ansi reset)" } else { " " } print $"($indicator) ($migration.version) - ($migration.description)" } # Determine migrations to run let target_version = if $target == "latest" { $migrations | last | get version } else { $target } print $"\n(ansi cyan_bold)Target version:(ansi reset) ($target_version)" if $current_version == $target_version { print $"\n(ansi green)Already at target version. No migrations needed.(ansi reset)" exit 0 } # Find migrations to apply let to_apply = $migrations | where version > $current_version and version <= $target_version if ($to_apply | length) == 0 { print $"\n(ansi yellow)No migrations to apply(ansi reset)" exit 0 } print $"\n(ansi cyan_bold)Migrations to apply:(ansi reset)" for migration in $to_apply { print $" → ($migration.version): ($migration.description)" } if $dry_run { print $"\n(ansi yellow)[DRY RUN] Would apply ($to_apply | length) migration(s)(ansi reset)" exit 0 } # Apply migrations print $"\n(ansi cyan_bold)Applying migrations...(ansi reset)" mut final_version = $current_version for migration in $to_apply { print $"\n(ansi blue)Migrating to ($migration.version)...(ansi reset)" apply_migration $migration $kogral_dir $final_version = $migration.version } # Phase: Update version in config print $"\n(ansi cyan_bold)Updating config version...(ansi reset)" update_config_version $config_path $final_version print $"\n(ansi green_bold)✓ Migration completed(ansi reset)" print $"Schema version: ($current_version) → ($final_version)" } def apply_migration [migration: record, kogral_dir: string] { match $migration.version { "1.0.0" => { print " ✓ Initial schema (no action needed)" }, "1.1.0" => { # Phase 1: Add metadata field to existing nodes print " Adding metadata field support..." add_metadata_field $kogral_dir print " ✓ Metadata field added" }, "1.2.0" => { # Phase 2: Add embedding support print " Adding embedding support..." add_embedding_support $kogral_dir print " ✓ Embedding support added" }, _ => { print $" (ansi yellow)Unknown migration version: ($migration.version)(ansi reset)" } } } def update_config_version [config_path: string, new_version: string] { # Phase 1: Read current config let config = open $config_path | from toml # Phase 2: Update version let updated = $config | insert "graph.version" $new_version # Phase 3: Convert back to TOML and save $updated | to toml | save --force $config_path print " ✓ Config version updated" } def add_metadata_field [kogral_dir: string] { # Phase 1: Find all markdown files let all_files = find_all_markdown_files $kogral_dir # Phase 2: Process each file mut updated = 0 for file in $all_files { let content = open $file let lines = $content | lines # Phase 3: Check if metadata field exists let has_metadata_field = $lines | any { |l| $l =~ '^(metadata|---):' } if not $has_metadata_field { # Phase 4: Add empty metadata field before closing --- let updated_content = insert_metadata_field $content $updated_content | save --force $file $updated = $updated + 1 } } print $" Updated ($updated) files" } def add_embedding_support [kogral_dir: string] { # Phase 1: Find all markdown files let all_files = find_all_markdown_files $kogral_dir # Phase 2: Note that embeddings will be generated on next reindex print $" Embedding vectors will be generated on next reindex" print $" Run 'kogral-reindex.nu' after migration to populate embeddings" } def find_all_markdown_files [kogral_dir: string] { # Phase 1: Collect from all node type directories mut all_files = [] for dir_type in ["notes" "decisions" "guidelines" "patterns" "journal"] { let dir_path = $"($kogral_dir)/($dir_type)" if ($dir_path | path exists) { let files = glob $"($dir_path)/**/*.md" $all_files = ($all_files | append $files) } } $all_files } def insert_metadata_field [content: string] { let lines = $content | lines # Phase 1: Find the closing --- of frontmatter mut closing_idx = 0 mut found_opening = false for idx in (0..<($lines | length)) { if $idx == 0 and ($lines | get $idx | str trim) == "---" { $found_opening = true continue } if $found_opening and ($lines | get $idx | str trim) == "---" { $closing_idx = $idx break } } # Phase 2: Insert metadata field before closing --- if $found_opening and $closing_idx > 0 { let before = $lines | take $closing_idx let after = $lines | skip $closing_idx let updated = $before | append ["metadata: {}"] | append $after $updated | str join "\n" } else { # No frontmatter, return as-is $content } }