src/public/Execution/Invoke-AitherCmd.ps1

<#
.SYNOPSIS
    Main CLI for AitherOS interaction.
    
.DESCRIPTION
    Provides a unified command-line interface alias 'aither' to interact with
    AitherOS services, logs, and APIs.
    
.EXAMPLE
    aither status moltbook
    aither logs genesis
    aither call moltbook /search "query"
    aither list
    
.FUNCTIONALITY
    Core AitherOS CLI
#>

function Invoke-AitherCmd {
    [CmdletBinding()]
    [Alias('aither')]
    param(
        [Parameter(Position=0, Mandatory=$false)]
        [string]$Command = "help",

        [Parameter(Position=1, Mandatory=$false)]
        [string]$Target,

        [Parameter(Position=2, Mandatory=$false)]
        [string]$Arg1,

        [Parameter(Position=3, Mandatory=$false)]
        [string]$Arg2,
        
        [Parameter(ValueFromRemainingArguments=$true)]
        $RemainingArgs
    )

    # --- Configuration & Cache ---
    $CACHE_FILE = "$env:TEMP\aither_services_cache.json"
    $PYTHON_PATH_WIN = "d:\AitherOS-Fresh\AitherOS\.venv\Scripts\python.exe"
    $PYTHON_PATH_NIX = "d:\AitherOS-Fresh\AitherOS\.venv\bin\python"
    
    $PythonExe = if (Test-Path $PYTHON_PATH_WIN) { $PYTHON_PATH_WIN } else { $PYTHON_PATH_NIX }
    if (-not (Test-Path $PythonExe)) {
        # Fallback to system python if venv not found
        $PythonExe = "python"
    }

    $ProjectRoot = "d:\AitherOS-Fresh"
    $HelperScript = "$ProjectRoot\AitherZero\library\helpers\get_services_config.py"

    # --- Helper Functions ---
    
    function Get-AitherConfig {
        if (Test-Path $CACHE_FILE) {
            # simple cache check - if file is older than 1 hour, refresh
            $lastWrite = (Get-Item $CACHE_FILE).LastWriteTime
            if ($lastWrite -lt (Get-Date).AddHours(-1)) {
                Remove-Item $CACHE_FILE -ErrorAction SilentlyContinue
            } else {
                return (Get-Content $CACHE_FILE -Raw | ConvertFrom-Json)
            }
        }
        
        Write-Host " [AitherCLI] Refreshing service configuration..." -ForegroundColor DarkGray
        $output = & $PythonExe $HelperScript 2>&1
        if ($LASTEXITCODE -ne 0) {
            Write-Error "Failed to load config: $output"
            return $null
        }
        $output | Out-File -FilePath $CACHE_FILE -Encoding utf8
        return ($output | ConvertFrom-Json)
    }

    function Get-ServicePort {
        param($Config, $ServiceName)
        # Normalize name (case insensitive)
        foreach ($key in $Config.services.PSObject.Properties.Name) {
            if ($key.ToLower() -eq $ServiceName.ToLower() -or "aither$($key.ToLower())" -eq $ServiceName.ToLower()) {
                return $Config.services.$key.port
            }
            # Check aliases
            $aliases = $Config.services.$key.aliases
            if ($aliases) {
                 foreach ($alias in $aliases) {
                    if ($alias.ToLower() -eq $ServiceName.ToLower()) {
                        return $Config.services.$key.port
                    }
                 }
            }
        }
        return $null
    }
    
    function Get-RealServiceName {
        param($Config, $ServiceName)
         foreach ($key in $Config.services.PSObject.Properties.Name) {
            if ($key.ToLower() -eq $ServiceName.ToLower() -or "aither$($key.ToLower())" -eq $ServiceName.ToLower()) {
                return $key
            }
             # Check aliases
            $aliases = $Config.services.$key.aliases
            if ($aliases) {
                 foreach ($alias in $aliases) {
                    if ($alias.ToLower() -eq $ServiceName.ToLower()) {
                        return $key
                    }
                 }
            }
        }
        return $ServiceName # Fallback
    }

    # --- Execution Logic ---

    $Config = Get-AitherConfig
    if (-not $Config) { return }

    switch -Regex ($Command.ToLower()) {
        '^help$' {
            Write-Host "AitherOS CLI (aither)" -ForegroundColor Cyan
            Write-Host "Usage: aither <command> [target] [args]"
            Write-Host ""
            Write-Host "Commands:"
            Write-Host " list / services List all configured services and ports"
            Write-Host " status <svc> Check health of a service"
            Write-Host " logs <svc> Tail logs for a service"
            Write-Host " ports Dump all ports"
            Write-Host " start <svc> Start a service (Docker)"
            Write-Host " stop <svc> Stop a service (Docker)"
            Write-Host " restart <svc> Restart a service (Genesis API preferred)"
            Write-Host " build <svc> Build & restart a service (docker compose --build)"
            Write-Host " rebuild <svc> Full rebuild (no cache) & restart"
            Write-Host " call <svc> <ep> [body] Call a service endpoint (GET or POST)"
            Write-Host " clean-cache Clear the CLI config cache"
        }

        '^clean-cache$' {
            Remove-Item $CACHE_FILE -ErrorAction SilentlyContinue
            Write-Host "Cache cleared." -ForegroundColor Green
        }

        '^(list|services)$' {
            $services = @()
            foreach ($prop in $Config.services.PSObject.Properties) {
                $s = $prop.Value
                $services += [PSCustomObject]@{
                    Service = $prop.Name
                    Port = $s.port
                    Group = $s.group
                    Description = $s.description
                }
            }
            $services | Format-Table -AutoSize
        }

        '^ports$' {
             $ports = @()
             foreach ($prop in $Config.services.PSObject.Properties) {
                $s = $prop.Value
                $ports += [PSCustomObject]@{
                    Service = $prop.Name
                    Port = $s.port
                }
            }
            $ports | Sort-Object Port | Format-Table -AutoSize
        }

        '^status$' {
            if (-not $Target) { Write-Error "Usage: aither status <service>"; return }
            $port = Get-ServicePort $Config $Target
            if (-not $port) { Write-Error "Service '$Target' not found."; return }
            
            $url = "http://localhost:$port/health"
            Write-Host "Checking $Target on $url ..." -ForegroundColor DarkGray
            try {
                $res = Invoke-RestMethod -Uri $url -Method Get -TimeoutSec 3
                $res
            } catch {
                Write-Warning "Service down or unreachable ($_)"
            }
        }

        '^call$' {
            if (-not $Target) { Write-Error "Usage: aither call <service> <endpoint> [json_body]"; return }
            $port = Get-ServicePort $Config $Target
            if (-not $port) { Write-Error "Service '$Target' not found."; return }
            
            # Arg1 is endpoint
            if (-not $Arg1) { $Arg1 = "/" }
            if (-not $Arg1.StartsWith("/")) { $Arg1 = "/$Arg1" }
            
            $url = "http://localhost:$port$Arg1"
            $method = if ($Arg2) { "Post" } else { "Get" }
            
            Write-Host "$method $url" -ForegroundColor DarkGray
            
            try {
                if ($method -eq "Post") {
                    $body = $Arg2
                    # If arg2 looks like a file, read it? No, keep it simple for now, expect JSON string
                    Invoke-RestMethod -Uri $url -Method Post -Body $body -ContentType "application/json"
                } else {
                    Invoke-RestMethod -Uri $url -Method Get
                }
            } catch {
                Write-Error $_
            }
        }

        '^restart$' {
             if (-not $Target) { Write-Error "Usage: aither restart <service>"; return }
             $realName = Get-RealServiceName $Config $Target
             
             # Try Genesis first
             $genesisPort = Get-ServicePort $Config "Genesis"
             $url = "http://localhost:$genesisPort/services/$realName/restart"
             
             Write-Host "Requesting Genesis to restart $realName..." -ForegroundColor Cyan
             try {
                Invoke-RestMethod -Uri $url -Method Post -TimeoutSec 5
                Write-Host "Restart signal sent." -ForegroundColor Green
             } catch {
                Write-Warning "Genesis unreachable. Fallback to Docker restart..."
                # Convert ServiceName to container-name
                # Usually AitherMoltbook -> aither-moltbook
                # or just look at format
                $containerName = "aither-" + $realName.ToLower().Replace("aither", "")
                Write-Host "Docker restart $containerName..."
                docker restart $containerName
             }
        }

        '^stop$' {
             if (-not $Target) { Write-Error "Usage: aither stop <service>"; return }
             $realName = Get-RealServiceName $Config $Target
             $containerName = "aither-" + $realName.ToLower().Replace("aither", "")
             docker stop $containerName
             Write-Host "Stopped $containerName" -ForegroundColor Yellow
        }

        '^start$' {
             if (-not $Target) { Write-Error "Usage: aither start <service>"; return }
             $realName = Get-RealServiceName $Config $Target
             $containerName = "aither-" + $realName.ToLower().Replace("aither", "")
             docker start $containerName
             Write-Host "Started $containerName" -ForegroundColor Green
        }

        '^build$' {
             if (-not $Target) { Write-Error "Usage: aither build <service>"; return }
             $realName = Get-RealServiceName $Config $Target
             $containerName = "aither-" + $realName.ToLower().Replace("aither", "")
             Write-Host "Building & restarting $containerName..." -ForegroundColor Cyan
             $cmdCtx = Get-AitherLiveContext
             $composeFile = Join-Path $ProjectRoot $cmdCtx.ComposeFile
             docker compose -f $composeFile up -d --build $containerName
             if ($LASTEXITCODE -eq 0) {
                 Write-Host "Build complete." -ForegroundColor Green
                 docker ps --filter "name=$containerName" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
             } else {
                 Write-Error "Build failed for $containerName"
             }
        }

        '^rebuild$' {
             if (-not $Target) { Write-Error "Usage: aither rebuild <service>"; return }
             $realName = Get-RealServiceName $Config $Target
             $containerName = "aither-" + $realName.ToLower().Replace("aither", "")
             Write-Host "Full rebuild (no cache) for $containerName..." -ForegroundColor Yellow
             if (-not $cmdCtx) { $cmdCtx = Get-AitherLiveContext }
             $composeFile = Join-Path $ProjectRoot $cmdCtx.ComposeFile
             docker compose -f $composeFile build --no-cache $containerName
             if ($LASTEXITCODE -eq 0) {
                 docker compose -f $composeFile up -d $containerName
                 Write-Host "Rebuild complete." -ForegroundColor Green
                 docker ps --filter "name=$containerName" --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
             } else {
                 Write-Error "Rebuild failed for $containerName"
             }
        }

        '^logs$' {
             if (-not $Target) { Write-Error "Usage: aither logs <service>"; return }
             $realName = Get-RealServiceName $Config $Target
             $containerName = "aither-" + $realName.ToLower().Replace("aither", "")
             
             # If Arg1 is provided, use as tail lines, else default to -f logic?
             # CLI tool usually blocks.
             docker logs -f $containerName
        }

        default {
            Write-Warning "Unknown command '$Command'. Try 'aither help'."
        }
    }
}

# Alias — exported by build.ps1
Set-Alias -Name aither -Value Invoke-AitherCmd -Scope Script