portail 2.1.0

Unified proxy/gateway: AI Gateway + MCP Gateway + CDN cache
Documentation
# portail/portail.nu — CLI wrappers + fleet ops for Portail
# Usage: use portail.nu

const BIN = "cargo run --release --"
const BENCH_HOST = "bench-node"
const BENCH_IP = "178.105.245.135"
const PORTAIL_SERVICE = "portail-staging"
const HEALTH_URL = "http://localhost:8787/health"
const REMOTE_BINARY = "/opt/portail-staging/target/release/portail"

# ── Server ────────────────────────────────────────────────────────

def "portail serve" [
    --port (-p): int = 8787
    --config (-c): string
] {
    let args = ["serve" $"--port=($port)"]
    let args = if $config != null { $args | merge [$"--config=($config)"] } else { $args }
    ^cargo run --release -- ...$args
}

# ── Status ────────────────────────────────────────────────────────

def "portail status" [] {
    let raw = (^cargo run --release -- status | complete)
    if $raw.exit_code != 0 {
        print $"(ansi red_bold)portail status failed(ansi reset)"
        print $raw.stderr
        return
    }
    $raw.stdout | lines | each { |line|
        let parts = ($line | split row ": ")
        if ($parts | length) >= 2 {
            { key: ($parts | first | str trim), value: ($parts | skip 1 | str join ": " | str trim) }
        } else {
            { key: "output", value: ($line | str trim) }
        }
    } | table --index false
}

# ── Health ────────────────────────────────────────────────────────

def "portail health" [
    --url (-u): string = "http://localhost:8787"
] {
    let result = (http get $"($url)/health" --timeout 3sec | complete)
    if $result.exit_code == 0 {
        $result.stdout | from json | table --index false
    } else {
        print $"(ansi red_bold)Health check failed(ansi reset)"
        print $result.stderr
    }
}

# ── Doctor ────────────────────────────────────────────────────────

def "portail doctor" [] {
    ^cargo run --release -- doctor
}

# ── Events ────────────────────────────────────────────────────────

def "portail events" [
    --tail (-t): int = 50
    --follow (-f)
] {
    if $follow {
        ^cargo run --release -- events --tail $tail --follow
    } else {
        let raw = (^cargo run --release -- events --tail $tail | complete)
        if $raw.exit_code != 0 {
            print $raw.stderr
            return
        }
        $raw.stdout | lines | each { |line|
            if ($line | str trim | is-empty) { null } else {
                { raw: ($line | str trim) }
            }
        } | compact | table --index false
    }
}

# ── Config ────────────────────────────────────────────────────────

def "portail config show" [] {
    ^cargo run --release -- config show
}

def "portail config validate" [] {
    ^cargo run --release -- config validate
}

# ── Hooks ─────────────────────────────────────────────────────────

def "portail hooks list" [] {
    ^cargo run --release -- hooks list
}

def "portail hooks create" [
    name: string
    --event: string = "request"
    --script: string
] {
    ^cargo run --release -- hooks create $name --event $event --script $script
}

def "portail hooks delete" [
    id: string
] {
    ^cargo run --release -- hooks delete $id
}

# ── Cache ─────────────────────────────────────────────────────────

def "portail cache stats" [] {
    ^cargo run --release -- cache stats
}

def "portail cache clear" [] {
    ^cargo run --release -- cache clear
}

# ── Targets ───────────────────────────────────────────────────────

def "portail target list" [] {
    ^cargo run --release -- target list
}

def "portail target add" [
    name: string
    --provider: string = "openai"
    --url: string
    --models: string
] {
    let args = ["target" "add" $name $"--provider=($provider)" $"--url=($url)"]
    let args = if $models != null { $args | merge [$"--models=($models)"] } else { $args }
    ^cargo run --release -- ...$args
}

# ── MCP ───────────────────────────────────────────────────────────

def "portail mcp status" [] {
    ^cargo run --release -- mcp status
}

def "portail mcp restart" [] {
    ^cargo run --release -- mcp restart
}

# ── Drift / Spec / Fuzz ──────────────────────────────────────────

def "portail drift-detect" [] {
    ^cargo run --release -- drift-detect
}

def "portail spec-verify" [] {
    ^cargo run --release -- spec-verify
}

def "portail fuzz-route" [
    --url: string = "http://localhost:8787"
] {
    ^cargo run --release -- fuzz-route --url $url
}

# ── Fleet: probe ──────────────────────────────────────────────────

def "portail probe" [] {
    let service = (env PORTAIL_SERVICE | default $PORTAIL_SERVICE)
    let checks = [
        { label: "service", cmd: $"systemctl is-active ($service)" }
        { label: "health",  cmd: $"curl -sf ($HEALTH_URL)" }
        { label: "disk",    cmd: "df -h / | tail -1" }
        { label: "load",    cmd: "cat /proc/loadavg" }
        { label: "memory",  cmd: "free -h | awk '/^Mem:/{print $3\"/\"$2\" used, \" $4\" free\"}'" }
    ]

    print $"(ansi cyan_bold)── Probing (ansi yellow_bold)($BENCH_HOST)(ansi cyan_bold) ──(ansi reset)\n"

    $checks | par-each { |check|
        let result = (do -i { ssh $BENCH_HOST $check.cmd } | complete)
        let status = if $result.exit_code == 0 { (ansi green_bold) + "ok" } else { (ansi red_bold) + "fail" }
        let output = if $result.stdout != "" { $result.stdout | str trim } else { $result.stderr | str trim }
        { check: $check.label, status: status, output: output }
    } | table --index false
}

# ── Fleet: deploy ─────────────────────────────────────────────────

def "portail deploy" [] {
    let service = (env PORTAIL_SERVICE | default $PORTAIL_SERVICE)

    print $"(ansi cyan_bold)── Building release ──(ansi reset)"
    cargo build --release
    if $env.LAST_EXIT_CODE != 0 {
        print $"(ansi red_bold)Build failed.(ansi reset)"
        return
    }

    let bin = ([$env.PWD, "target", "release", "portail"] | path join)
    if not ($bin | path exists) {
        print $"(ansi red_bold)Binary not found at ($bin)(ansi reset)"
        return
    }

    print $"(ansi cyan_bold)── Rsyncing to ($BENCH_HOST) ──(ansi reset)"
    rsync -azP --chmod=+x $bin $"($BENCH_HOST):($REMOTE_BINARY)"
    if $env.LAST_EXIT_CODE != 0 {
        print $"(ansi red_bold)Rsync failed.(ansi reset)"
        return
    }

    print $"(ansi cyan_bold)── Restarting ($service) ──(ansi reset)"
    ssh $BENCH_HOST $"sudo systemctl restart ($service)"
    if $env.LAST_EXIT_CODE == 0 {
        print $"(ansi green_bold)Deploy complete.(ansi reset)"
    } else {
        print $"(ansi red_bold)Restart failed.(ansi reset)"
    }
}

# ── Fleet: drift ──────────────────────────────────────────────────

def "portail drift" [] {
    let local_version = (open Cargo.toml | get package.version)
    let remote_toml = (do -i {
        ssh $BENCH_HOST $"grep '^version' /opt/portail-staging/Cargo.toml | head -1"
    } | complete | get stdout | str trim)
    let remote_version = ($remote_toml | str replace --all 'version\s*=\s*"' "" | str replace --all '"' "")

    let match = $local_version == $remote_version
    let verdict = if $match {
        $"(ansi green_bold)versions match(ansi reset)"
    } else {
        $"(ansi red_bold)DRIFT(ansi reset)"
    }

    { local: $local_version, remote: $remote_version, verdict: $verdict } | table --index false
}

# ── Fleet: restart ────────────────────────────────────────────────

def "portail restart" [] {
    let service = (env PORTAIL_SERVICE | default $PORTAIL_SERVICE)
    print $"(ansi cyan_bold)── Restarting ($service) on ($BENCH_HOST) ──(ansi reset)"
    ssh $BENCH_HOST $"sudo systemctl restart ($service)"
    if $env.LAST_EXIT_CODE == 0 {
        print $"(ansi green_bold)Service restarted.(ansi reset)"
    } else {
        print $"(ansi red_bold)Restart failed.(ansi reset)"
    }
}

# ── Fleet: logs ───────────────────────────────────────────────────

def "portail logs" [
    --lines (-n): int = 80
] {
    let service = (env PORTAIL_SERVICE | default $PORTAIL_SERVICE)
    ssh $BENCH_HOST $"journalctl -u ($service) -f --no-pager -n ($lines)"
}

# ── Fleet: bench status ──────────────────────────────────────────

def "portail bench" [] {
    let service = (env PORTAIL_SERVICE | default $PORTAIL_SERVICE)
    let cmd = $"echo '--- load ---' && cat /proc/loadavg && echo '--- memory ---' && free -h | head -2 && echo '--- disk ---' && df -h / | head -2 && echo '--- process ---' && systemctl is-active ($service)"
    let result = (do -i { ssh $BENCH_HOST $cmd } | complete)
    if $result.exit_code == 0 {
        print $result.stdout
    } else {
        print $"(ansi red_bold)bench-status failed: ($result.stderr)(ansi reset)"
    }
}