Install-Maestro.ps1
|
#!/usr/bin/env pwsh <# .SYNOPSIS Install MaestroKit (skills, references, agents) into one or more config dirs of a target repo. .DESCRIPTION Copies `skills/`, `references/`, and `agents/` from the Maestro source repo into <Target>/<Location>/ for each location in -Locations. During the copy, every `{{MAESTRO_CONFIG}}` token in `.md` files is replaced with the location string, so skills and agents can reference reference files via a relative path that resolves correctly at runtime. Supported locations: .agents — OpenCode-compatible (default). Full coverage on OpenCode (skills and agents). On Oh My Pi, skills are discovered natively (canonical OMP location) but the agents/ folder is ignored — prefer .omp for OMP. .omp — Oh My Pi native. Full coverage: .omp/skills/ (native provider) and .omp/agents/ (OMP's only project-level subagent root). .claude — Claude Code-compatible. Full coverage on Claude Code; skills also discovered by OpenCode and Oh My Pi (Claude-compatible path). .opencode — OpenCode native. Same coverage as .agents but checked first by OpenCode when present. Pick one location per platform you support, or pass multiple to install in parallel. Re-running overwrites in place, which is the recommended upgrade path. Use -Clean to first wipe stale skill/reference/agent folders (e.g., after a skill has been renamed or removed upstream). .PARAMETER Target Path to the destination repo root. Defaults to the current working directory. .PARAMETER Locations One or more config directories to install into. Defaults to @(".agents"). Accepts: .agents, .omp, .claude, .opencode. Pass multiple to install to all, e.g.: -Locations .agents,.claude,.opencode,.omp .PARAMETER Clean Delete <Target>/<Location>/skills, references, and agents folders before copying. Preserves any other content (opencode.json, user-installed skills, etc.). .PARAMETER Force Skip the confirmation prompt when an existing installation is found and -Clean is not used. Implies overwrite-in-place via Copy-Item -Force. .EXAMPLE .\Install-Maestro.ps1 -Target D:\Personal\my-app .EXAMPLE .\Install-Maestro.ps1 -Target . -Locations .claude,.agents -Clean -Force #> [CmdletBinding()] param( [string]$Target = (Get-Location).Path, [string[]]$Locations = @(".agents"), [ValidateSet('Project', 'User')][string]$Scope = 'Project', [switch]$Clean, [switch]$Force ) $ErrorActionPreference = "Stop" # Non-interactive hosts cannot answer the overwrite prompt — fail closed with guidance. if (-not $Force -and -not [Environment]::UserInteractive) { throw "Non-interactive session detected and an existing installation may need overwriting. Re-run with -Force to overwrite in place." } $ValidLocations = @(".agents", ".omp", ".claude", ".opencode") function Write-Step($msg) { Write-Host "-> $msg" -ForegroundColor Cyan } function Write-Ok($msg) { Write-Host " $msg" -ForegroundColor Green } function Write-Warn($msg) { Write-Host " $msg" -ForegroundColor Yellow } function Install-SingleLocation { param( [string]$SourceRoot, [string]$TargetRoot, [string]$Location, [bool]$CleanFlag, [bool]$ForceFlag ) # Placeholder resolution (see conventions.md): skill texts write # [{{WORKSPACE}}/]{{MAESTRO_CONFIG}}/references/... # Project scope keeps {{WORKSPACE}} runtime-resolved and bakes the harness # folder; User scope bakes an absolute profile root instead. $script:MaestroConfigPath = if ($Scope -eq 'User') { (($EffectiveTarget -replace '\\', '/') + "/" + $Location) } else { ("{{WORKSPACE}}/" + $Location) } $SourceSkills = Join-Path $SourceRoot "skills" $SourceRefs = Join-Path $SourceRoot "references" $SourceAgents = Join-Path $SourceRoot "agents" $VersionFile = Join-Path $SourceRoot "VERSION" $DestDir = Join-Path $TargetRoot $Location $DestSkills = Join-Path $DestDir "skills" $DestRefs = Join-Path $DestDir "references" $DestAgents = Join-Path $DestDir "agents" $DestVersionFile = Join-Path $DestDir "MAESTRO_VERSION" $HasRefs = Test-Path -LiteralPath $SourceRefs -PathType Container $HasAgents = Test-Path -LiteralPath $SourceAgents -PathType Container $Version = $null if (Test-Path -LiteralPath $VersionFile) { $Version = (Get-Content -LiteralPath $VersionFile -TotalCount 1).Trim() } Write-Host "" Write-Host "Installing Maestro -> $DestDir" -ForegroundColor White if ($Version) { Write-Host " version: $Version" -ForegroundColor DarkGray } if (-not (Test-Path -LiteralPath $DestDir)) { Write-Step "Creating $DestDir" New-Item -ItemType Directory -Path $DestDir -Force | Out-Null } if ($CleanFlag) { foreach ($sub in @($DestSkills, $DestRefs, $DestAgents)) { if (Test-Path -LiteralPath $sub) { Write-Step "Cleaning $sub" Remove-Item -LiteralPath $sub -Recurse -Force } } } elseif ((Test-Path -LiteralPath $DestSkills) -or ($HasRefs -and (Test-Path -LiteralPath $DestRefs)) -or ($HasAgents -and (Test-Path -LiteralPath $DestAgents))) { $existingVersion = $null if (Test-Path -LiteralPath $DestVersionFile) { $existingVersion = (Get-Content -LiteralPath $DestVersionFile -TotalCount 1).Trim() } $upgradeNote = if ($existingVersion -and $Version -and $existingVersion -ne $Version) { " Existing install is v$existingVersion; upgrading to v$Version." } elseif ($existingVersion -and $Version -and $existingVersion -eq $Version) { " Existing install is the same version (v$Version); will overwrite in place." } else { "" } if (-not $ForceFlag) { $prompt = @" Existing Maestro installation found at: $DestDir $upgradeNote Re-running will overwrite skill/reference/agent files in place. Any skills or agents you renamed or removed upstream will linger under $Location/. To wipe those first, abort and rerun with -Clean. Continue? "@ $decision = $PSCmdlet.Host.UI.PromptForChoice( "Overwrite existing Maestro installation at $Location?", $prompt, @("&Yes", "&No"), 0) if ($decision -ne 0) { Write-Warn "Skipping $Location. No changes made." return } } else { if ($upgradeNote) { Write-Host $upgradeNote -ForegroundColor DarkGray } } } function Copy-WithSubstitution { param([string]$Src, [string]$Dst) New-Item -ItemType Directory -Path $Dst -Force | Out-Null $items = Get-ChildItem -LiteralPath $Src foreach ($item in $items) { $destItem = Join-Path $Dst $item.Name if ($item.PSIsContainer) { Copy-WithSubstitution -Src $item.FullName -Dst $destItem } else { if ($item.Extension -eq ".md") { $content = [System.IO.File]::ReadAllText($item.FullName, [System.Text.Encoding]::UTF8) $content = $content -replace '(\{\{WORKSPACE\}\}/)?\{\{MAESTRO_CONFIG\}\}', $script:MaestroConfigPath [System.IO.File]::WriteAllText($destItem, $content, (New-Object System.Text.UTF8Encoding $false)) } else { Copy-Item -LiteralPath $item.FullName -Destination $destItem -Force } } } } Write-Step "Copying skills/ -> $Location/skills/" Copy-WithSubstitution -Src $SourceSkills -Dst $DestSkills $skillCount = (Get-ChildItem -LiteralPath $DestSkills -Directory).Count Write-Ok "$skillCount skill(s) installed" if ($HasRefs) { Write-Step "Copying references/ -> $Location/references/" Copy-WithSubstitution -Src $SourceRefs -Dst $DestRefs $refCount = (Get-ChildItem -LiteralPath $DestRefs -File).Count Write-Ok "$refCount reference file(s) installed" } else { Write-Warn "No references/ folder found in source; skipped" } if ($HasAgents) { Write-Step "Copying agents/ -> $Location/agents/" Copy-WithSubstitution -Src $SourceAgents -Dst $DestAgents $agentCount = (Get-ChildItem -LiteralPath $DestAgents -File).Count Write-Ok "$agentCount agent(s) installed" } else { Write-Warn "No agents/ folder found in source; skipped" } if ($Version) { Write-Step "Writing $Location/MAESTRO_VERSION" Set-Content -LiteralPath $DestVersionFile -Value $Version -NoNewline Write-Ok "Recorded version: $Version" $SrcChangelog = Join-Path $SourceRoot "CHANGELOG.md" if (Test-Path -LiteralPath $SrcChangelog) { Copy-Item -LiteralPath $SrcChangelog -Destination (Join-Path $DestDir "MAESTRO_CHANGELOG.md") -Force Write-Ok "Shipped MAESTRO_CHANGELOG.md (migration notes for this version)" } } else { Write-Warn "No VERSION file in source; MAESTRO_VERSION not written" } Write-Ok "Maestro installed to: $DestDir" } # --- main --- if (-not (Test-Path -LiteralPath $Target -PathType Container)) { throw "Target directory does not exist: $Target" } $SourceRoot = $PSScriptRoot $SrcSkills = Join-Path $SourceRoot "skills" if (-not (Test-Path -LiteralPath $SrcSkills -PathType Container)) { throw "Source skills/ not found at: $SrcSkills (run this script from the maestro repo root)" } # Accept both `-.Locations a,b` (interactive: PS binds an array) and # `pwsh -File ... -Locations a,b` (-File mode: binds ONE string) by splitting commas here. $Normalized = foreach ($loc in $Locations) { foreach ($part in $loc.Split(',')) { $trimmed = $part.Trim() if (-not $trimmed) { continue } $first = $trimmed.Substring(0, 1) if ($first -ne '.') { $trimmed = ".$trimmed" } if ($ValidLocations -notcontains $trimmed) { throw "Invalid location '$trimmed'. Valid: $($ValidLocations -join ', ')" } $trimmed } } if (-not $Normalized) { $Normalized = @(".agents") } # Resolve effective install root. Project scope installs into the given repo; # User scope installs into your profile so the bundle is usable everywhere. # An explicit -Target together with -Scope User overrides the profile root # (documented testing hook). $EffectiveTarget = $Target if ($Scope -eq 'User') { if ($PSBoundParameters.ContainsKey('Target')) { $EffectiveTarget = $Target } else { $EffectiveTarget = [Environment]::GetFolderPath('UserProfile') } Write-Host (" scope : user (" + $EffectiveTarget + ")") } Write-Host "Installing Maestro" -ForegroundColor White Write-Host " from : $SourceRoot" -ForegroundColor DarkGray Write-Host " target : $EffectiveTarget" -ForegroundColor DarkGray Write-Host " locations: $($Normalized -join ', ')" -ForegroundColor DarkGray if ($Clean) { Write-Host " mode : clean" -ForegroundColor DarkGray } elseif ($Force) { Write-Host " mode : force-overwrite" -ForegroundColor DarkGray } foreach ($loc in $Normalized) { Install-SingleLocation -SourceRoot $SourceRoot -TargetRoot $EffectiveTarget -Location $loc -CleanFlag ([bool]$Clean) -ForceFlag ([bool]$Force) } Write-Host "" Write-Host "Done." -ForegroundColor Green |