scripts/install-global.ps1

#!/usr/bin/env pwsh
<#
.SYNOPSIS
    Install dotbot globally to ~/dotbot

.DESCRIPTION
    Copies dotbot files to ~/dotbot and adds the CLI to PATH
#>


[CmdletBinding()]
param(
    [switch]$DryRun,
    [string]$SourceDir
)

$ErrorActionPreference = "Stop"

$ScriptDir = $PSScriptRoot
if (-not $SourceDir) {
    $SourceDir = Split-Path -Parent $ScriptDir
}
$BaseDir = Join-Path $HOME "dotbot"
$BinDir = Join-Path $BaseDir "bin"

# Import platform functions
Import-Module (Join-Path $ScriptDir "Platform-Functions.psm1") -Force

Write-Status "Installing dotbot to $BaseDir"

# Check if source and destination are the same
$resolvedSource = (Resolve-Path $SourceDir).Path.TrimEnd('\', '/')
$resolvedBase = if (Test-Path $BaseDir) { (Resolve-Path $BaseDir).Path.TrimEnd('\', '/') } else { $null }

if ($resolvedBase -and ($resolvedSource -eq $resolvedBase)) {
    Write-Success "Already running from target installation directory"
    Write-Success "dotbot is installed at: $BaseDir"
} else {
    if ($DryRun) {
        Write-DotbotWarning "Would copy files from: $SourceDir"
        Write-DotbotWarning "Would copy to: $BaseDir"
    } else {
        # Create base directory
        if (-not (Test-Path $BaseDir)) {
            New-Item -ItemType Directory -Force -Path $BaseDir | Out-Null
        }
        
        # Allowlist: only copy directories and files needed at runtime.
        # Everything else (server, ideas, tests, docs, assets, etc.) stays in the repo.
        $allowedDirs = @("scripts", "workflows", "stacks")
        $allowedFiles = @("version.json", "dotbot.psm1", "dotbot.psd1", "install.ps1", "install-remote.ps1")

        foreach ($dirName in $allowedDirs) {
            $src = Join-Path $SourceDir $dirName
            if (Test-Path $src) {
                $dest = Join-Path $BaseDir $dirName
                if (Test-Path $dest) { Remove-Item -Path $dest -Recurse -Force }
                Copy-Item -Path $src -Destination $dest -Recurse -Force
            }
        }

        foreach ($fileName in $allowedFiles) {
            $src = Join-Path $SourceDir $fileName
            if (Test-Path $src) {
                Copy-Item -Path $src -Destination (Join-Path $BaseDir $fileName) -Force
            }
        }

        # Copy only deployable studio-ui files (server.ps1, module, static/)
        $editorSrc = Join-Path $SourceDir "studio-ui"
        if (Test-Path $editorSrc) {
            $editorDest = Join-Path $BaseDir "studio-ui"
            if (Test-Path $editorDest) { Remove-Item -Path $editorDest -Recurse -Force }
            New-Item -ItemType Directory -Force -Path $editorDest | Out-Null

            # Copy server script and API module
            foreach ($file in @("server.ps1", "StudioAPI.psm1")) {
                $src = Join-Path $editorSrc $file
                if (Test-Path $src) {
                    Copy-Item -Path $src -Destination (Join-Path $editorDest $file) -Force
                }
            }

            # Copy static/ directory (built client assets)
            $staticSrc = Join-Path $editorSrc "static"
            if (Test-Path $staticSrc) {
                Copy-Item -Path $staticSrc -Destination (Join-Path $editorDest "static") -Recurse -Force
            } else {
                Write-DotbotWarning "studio-ui/static/ not found — the editor UI requires built assets. Run 'npm run build' in studio-ui/ first."
            }
        }
        
        Write-Success "Files copied to: $BaseDir"
    }
}

# Create bin directory with dotbot CLI wrapper
if (-not $DryRun) {
    if (-not (Test-Path $BinDir)) {
        New-Item -ItemType Directory -Force -Path $BinDir | Out-Null
    }
    
    # Create dotbot.ps1 CLI wrapper
    $cliScript = Join-Path $BinDir "dotbot.ps1"
$cliContent = @'
#!/usr/bin/env pwsh
# dotbot CLI wrapper
# Reset strict mode — callers (e.g. setup scripts) may set
# Set-StrictMode -Version Latest which breaks intrinsic .Count
Set-StrictMode -Off
$DotbotBase = Join-Path $HOME "dotbot"
$ScriptsDir = Join-Path $DotbotBase "scripts"

# Import common functions
Import-Module (Join-Path $ScriptsDir "Platform-Functions.psm1") -Force

$Command = $args[0]
[array]$SubArgs = if ($args.Count -gt 1) { $args[1..($args.Count-1)] } else { @() }

# Convert CLI args to a hashtable for proper named-parameter splatting.
# Array splatting only does positional binding; hashtable splatting is
# required for named parameters like -Workflow / -Stack.
$SplatArgs = @{}
if ($args.Count -gt 1) {
    $raw = $args[1..($args.Count-1)]
    $i = 0
    while ($i -lt $raw.Count) {
        if ($raw[$i] -match '^--?(.+)$') {
            $name = $Matches[1]
            if (($i + 1) -lt $raw.Count -and $raw[$i + 1] -notmatch '^--?') {
                $SplatArgs[$name] = $raw[$i + 1]
                $i += 2
            } else {
                $SplatArgs[$name] = $true
                $i++
            }
        } else {
            $i++
        }
    }
}

# Read canonical version from version.json
$DotbotVersion = 'unknown'
try {
    $vf = Join-Path $DotbotBase 'version.json'
    if (Test-Path $vf) { $DotbotVersion = (Get-Content $vf -Raw | ConvertFrom-Json).version }
} catch { Write-DotbotCommand "Parse skipped: $_" }
$env:DOTBOT_VERSION = $DotbotVersion

function Show-Help {
    Write-DotbotBanner -Title "D O T B O T v$DotbotVersion" -Subtitle "Autonomous Development System"
    Write-DotbotSection "COMMANDS"
    Write-DotbotLabel " init " "Initialize .bot in current project"
    Write-DotbotLabel " workflow add " "Add a workflow to existing project"
    Write-DotbotLabel " workflow remove " "Remove an installed workflow"
    Write-DotbotLabel " workflow list " "List installed workflows"
    Write-DotbotLabel " run " "Run/rerun a workflow"
    Write-DotbotLabel " resume " "Resume a paused workflow"
    Write-DotbotLabel " list " "List available workflows and stacks"
    Write-DotbotLabel " status " "Show installation status"
    Write-DotbotLabel " registry add " "Add an enterprise extension registry"
    Write-DotbotLabel " registry list " "List registered extension registries"
    Write-DotbotLabel " registry remove " "Remove an extension registry"
    Write-DotbotLabel " update " "Update global installation"
    Write-DotbotLabel " studio " "Launch visual configuration studio"
    Write-DotbotLabel " doctor " "Scan project for health issues"
    Write-DotbotLabel " help " "Show this help message"
    Write-BlankLine
}

function Invoke-Init {
    $initScript = Join-Path $ScriptsDir "init-project.ps1"
    if (Test-Path $initScript) {
        if ($SplatArgs.Count -gt 0) {
            & $initScript @SplatArgs
        } else {
            & $initScript
        }
    } else {
        Write-DotbotError "Init script not found"
    }
}

function Invoke-Status {
    Write-DotbotBanner -Title "D O T B O T v$DotbotVersion" -Subtitle "Status"

    # Check global installation
    Write-DotbotSection "GLOBAL INSTALLATION"
    Write-DotbotLabel " Status: " "✓ Installed" -ValueType Success
    Write-DotbotLabel " Location: " "$DotbotBase"
    Write-BlankLine

    # Check project installation
    $botDir = Join-Path (Get-Location) ".bot"
    Write-DotbotSection "PROJECT INSTALLATION"

    if (Test-Path $botDir) {
        Write-DotbotLabel " Status: " "✓ Enabled" -ValueType Success
        Write-DotbotLabel " Location: " "$botDir"

        # Count components
        $mcpDir = Join-Path $botDir "systems\mcp"
        $uiDir = Join-Path $botDir "systems\ui"
        $promptsDir = Join-Path $botDir "recipes"

        if (Test-Path $mcpDir) {
            Write-DotbotLabel " MCP: " "✓ Available" -ValueType Success
        }
        if (Test-Path $uiDir) {
            Write-DotbotLabel " UI: " "✓ Available (default port 8686)" -ValueType Success
        }
        if (Test-Path $promptsDir) {
            $agentCount = (Get-ChildItem -Path (Join-Path $promptsDir "agents") -Directory -ErrorAction SilentlyContinue).Count
            $skillCount = (Get-ChildItem -Path (Join-Path $promptsDir "skills") -Directory -ErrorAction SilentlyContinue).Count
            Write-DotbotLabel " Agents: " "$agentCount"
            Write-DotbotLabel " Skills: " "$skillCount"
        }
        Write-BlankLine
    } else {
        Write-DotbotLabel " Status: " "✗ Not initialized" -ValueType Error
        Write-BlankLine
        Write-DotbotWarning "Run 'dotbot init' to add dotbot to this project"
        Write-BlankLine
    }
}

function Invoke-List {
    $workflowsDir = Join-Path $DotbotBase "workflows"
    $stacksDir = Join-Path $DotbotBase "stacks"

    Write-DotbotBanner -Title "D O T B O T v$DotbotVersion" -Subtitle "Available Workflows & Stacks"

    # Workflows
    if (Test-Path $workflowsDir) {
        $wfDirs = @(Get-ChildItem -Path $workflowsDir -Directory)
        if ($wfDirs.Count -gt 0) {
            Write-DotbotSection "WORKFLOWS"
            foreach ($d in $wfDirs) {
                $yamlPath = Join-Path $d.FullName "manifest.yaml"
                if (-not (Test-Path $yamlPath)) { $yamlPath = Join-Path $d.FullName "workflow.yaml" }
                $desc = ""
                if (Test-Path $yamlPath) {
                    Get-Content $yamlPath | ForEach-Object {
                        if ($_ -match '^\s*description:\s*(.+)$') { $desc = $Matches[1].Trim() }
                    }
                }
                Write-DotbotLabel " $($d.Name.PadRight(24))" "$desc"
            }
            Write-BlankLine
        }
    }

    # Stacks
    if (Test-Path $stacksDir) {
        $stDirs = @(Get-ChildItem -Path $stacksDir -Directory)
        if ($stDirs.Count -gt 0) {
            Write-DotbotSection "STACKS (composable)"
            foreach ($d in $stDirs) {
                $yamlPath = Join-Path $d.FullName "manifest.yaml"
                $desc = ""; $extends = ""
                if (Test-Path $yamlPath) {
                    Get-Content $yamlPath | ForEach-Object {
                        if ($_ -match '^\s*description:\s*(.+)$') { $desc = $Matches[1].Trim() }
                        if ($_ -match '^\s*extends:\s*(.+)$') { $extends = $Matches[1].Trim() }
                    }
                }
                $label = $d.Name
                if ($extends) { $label += " (extends: $extends)" }
                Write-DotbotLabel " $($label.PadRight(36))" "$desc"
            }
            Write-BlankLine
        }
    }

    Write-DotbotSection "USAGE"
    Write-DotbotCommand "dotbot init --stack dotnet"
    Write-DotbotCommand "dotbot init --workflow kickstart-via-jira --stack dotnet-blazor"
    Write-BlankLine
}

function Invoke-Update {
    Write-BlankLine
    Write-DotbotWarning "To update dotbot:"
    Write-BlankLine
    Write-DotbotCommand "cd ~/dotbot"
    Write-DotbotCommand "git pull"
    Write-DotbotCommand "./install.ps1"
    Write-BlankLine
}

function Invoke-Workflow {
    $wfSubCmd = if ($SubArgs.Count -gt 0) { $SubArgs[0] } else { 'list' }
    $wfName = if ($SubArgs.Count -gt 1) { $SubArgs[1] } else { '' }
    [string[]]$wfExtra = @()
    if ($SubArgs.Count -gt 2) { $wfExtra = @($SubArgs[2..($SubArgs.Count-1)]) }
    $wfScript = switch ($wfSubCmd) {
        'add' { Join-Path $ScriptsDir 'workflow-add.ps1' }
        'remove' { Join-Path $ScriptsDir 'workflow-remove.ps1' }
        'list' { Join-Path $ScriptsDir 'workflow-list.ps1' }
        default { $null }
    }
    if ($wfScript -and (Test-Path $wfScript)) {
        if ($wfExtra.Count -gt 0) { & $wfScript $wfName @wfExtra } else { & $wfScript $wfName }
    } else {
        Write-DotbotWarning "Usage: dotbot workflow [add|remove|list] [name] [--Force]"
    }
}

function Invoke-Registry {
    # Parse: registry add <name> <source> [--branch <branch>] [--force]
    $regSubCmd = if ($SubArgs.Count -gt 0) { $SubArgs[0] } else { '' }
    $regRest = if ($SubArgs.Count -gt 1) { @($SubArgs[1..($SubArgs.Count-1)]) } else { @() }

    $regScript = switch ($regSubCmd) {
        'add' { Join-Path $ScriptsDir 'registry-add.ps1' }
        'remove' { Join-Path $ScriptsDir 'registry-remove.ps1' }
        'list' { Join-Path $ScriptsDir 'registry-list.ps1' }
        'update' { Join-Path $ScriptsDir 'registry-update.ps1' }
        default { $null }
    }

    if ($regScript -and (Test-Path $regScript)) {
        # Separate positional args from named flags
        $regSplat = @{}
        $positional = @()
        $ri = 0
        while ($ri -lt $regRest.Count) {
            if ($regRest[$ri] -match '^--?(.+)$') {
                $pname = $Matches[1]
                if (($ri + 1) -lt $regRest.Count -and $regRest[$ri + 1] -notmatch '^--?') {
                    $regSplat[$pname] = $regRest[$ri + 1]
                    $ri += 2
                } else {
                    $regSplat[$pname] = $true
                    $ri++
                }
            } else {
                $positional += $regRest[$ri]
                $ri++
            }
        }

        # Map positional args to named parameters
        if ($regSubCmd -eq 'add') {
            if ($positional.Count -ge 1) { $regSplat['Name'] = $positional[0] }
            if ($positional.Count -ge 2) { $regSplat['Source'] = $positional[1] }
        } elseif ($regSubCmd -eq 'remove') {
            if ($positional.Count -ge 1) { $regSplat['Name'] = $positional[0] }
        } elseif ($regSubCmd -eq 'update') {
            if ($positional.Count -ge 1) { $regSplat['Name'] = $positional[0] }
        }

        & $regScript @regSplat
    } else {
        Write-DotbotWarning "Usage: dotbot registry [add|list|update|remove] ..."
        Write-DotbotCommand " add <name> <source> [--branch main] [--force]"
        Write-DotbotCommand " list"
        Write-DotbotCommand " update [name] [--force]"
        Write-DotbotCommand " remove <name>"
    }
}

function Invoke-Run {
    $wfName = if ($SplatArgs.Count -gt 0) { $SplatArgs.Values | Select-Object -First 1 } else { '' }
    # Get workflow name from positional args
    $raw = if ($args.Count -gt 1) { $args[1] } else { $wfName }
    $runScript = Join-Path $ScriptsDir 'workflow-run.ps1'
    if ($raw -and (Test-Path $runScript)) {
        & $runScript -WorkflowName $raw
    } else {
        Write-DotbotWarning "Usage: dotbot run <workflow-name>"
    }
}

switch ($Command) {
    "init" { Invoke-Init }
    "workflow" { Invoke-Workflow }
    "registry" { Invoke-Registry }
    "run" { Invoke-Run }
    "resume" {
        Write-BlankLine
        Write-DotbotWarning "'dotbot resume' is not yet supported."
        Write-DotbotWarning "Please use 'dotbot run <workflow-name>' instead."
        Write-BlankLine
    }
    "list" { Invoke-List }
    "profiles" { Invoke-List } # backward compat
    "status" { Invoke-Status }
    "studio" {
        $studioDir = Join-Path $DotbotBase "studio-ui"
        $serverScript = Join-Path $studioDir "server.ps1"
        $portFile = Join-Path $DotbotBase ".studio-port"

        if (-not (Test-Path $serverScript)) {
            Write-BlankLine
            Write-DotbotError "Studio not found."
            Write-DotbotWarning "Run 'dotbot update' to install the studio"
            Write-BlankLine
            break
        }

        # Check if studio is already running
        if (Test-Path $portFile) {
            try {
                $portInfo = Get-Content $portFile -Raw | ConvertFrom-Json
                $existingPort = $portInfo.port
                $existingPid = $portInfo.pid
                # Verify the process is still alive
                $proc = Get-Process -Id $existingPid -ErrorAction SilentlyContinue
                if ($proc -and $proc.ProcessName -match 'pwsh|powershell') {
                    Write-BlankLine
                    Write-Success "Studio already running at http://localhost:$existingPort (PID $existingPid)"
                    Write-Status "Opening browser..."
                    Write-BlankLine
                    Start-Process "http://localhost:$existingPort"
                    break
                }
                # Stale port file — process is gone
                Remove-Item $portFile -Force -ErrorAction SilentlyContinue
            } catch {
                Remove-Item $portFile -Force -ErrorAction SilentlyContinue
            }
        }

        & pwsh -NoProfile -File $serverScript
    }
    "doctor" { & (Join-Path $ScriptsDir 'doctor.ps1') @SplatArgs }
    "update" { Invoke-Update }
    "help" { Show-Help }
    "--help" { Show-Help }
    "-h" { Show-Help }
    $null { Show-Help }
    default {
        Write-BlankLine
        Write-DotbotError "Unknown command: $Command"
        Write-DotbotWarning "Run 'dotbot help' for available commands"
        Write-BlankLine
    }
}
'@

    Set-Content -Path $cliScript -Value $cliContent -Force
    Set-ExecutablePermission -FilePath $cliScript
    Write-Success "Created CLI at: $cliScript"

    # On Unix, create a bash shim so 'dotbot' works without the .ps1 extension
    Initialize-PlatformVariables
    if (-not $IsWindows) {
        $bashShim = Join-Path $BinDir "dotbot"
        $bashShimContent = @'
#!/usr/bin/env bash
# dotbot CLI shim — delegates to the PowerShell wrapper
exec pwsh -NoProfile -File "$(dirname "$0")/dotbot.ps1" "$@"
'@

        Set-Content -Path $bashShim -Value $bashShimContent -Force -NoNewline
        Set-ExecutablePermission -FilePath $bashShim
        Write-Success "Created bash shim at: $bashShim"
    }
}

# Ensure powershell-yaml module is available
if (-not $DryRun) {
    if (-not (Get-Module -ListAvailable powershell-yaml -ErrorAction SilentlyContinue)) {
        Write-Status "Installing powershell-yaml module..."
        Install-Module -Name powershell-yaml -Repository PSGallery -Scope CurrentUser -Force -AllowClobber
        Write-Success "powershell-yaml module installed"
    } else {
        Write-Success "powershell-yaml module already installed"
    }
}

# Add to PATH
if (-not $DryRun) {
    Add-ToPath -Directory $BinDir
}

# Show completion message
Write-BlankLine
Write-Success "Installation Complete!"
Write-Status "Platform: $(Get-PlatformName)"
Write-BlankLine
Write-DotbotSection "NEXT STEPS"
Write-DotbotCommand "1. Restart your terminal"
Write-DotbotCommand "2. Navigate to your project: cd your-project"
Write-DotbotCommand "3. Initialize dotbot: dotbot init"
Write-BlankLine