$ErrorActionPreference = 'Stop' $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $RepoRoot = Split-Path -Parent $ScriptDir $CanonicalHome = if ($env:COPILOT_RESOURCES_HOME) { $env:COPILOT_RESOURCES_HOME } else { Join-Path $HOME '.copilot-resources' } $StateDir = if ($env:COPILOT_RESOURCES_STATE_DIR) { $env:COPILOT_RESOURCES_STATE_DIR } else { Join-Path $HOME '.copilot-resources-state' } $CopilotHome = if ($env:COPILOT_HOME) { $env:COPILOT_HOME } else { Join-Path $HOME '.copilot' } $VscodeUserDir = if ($env:VSCODE_USER_DIR) { $env:VSCODE_USER_DIR } else { Join-Path $env:APPDATA 'Code\User' } $ManagedShellEnv = $null $ProfilePath = $PROFILE.CurrentUserAllHosts $VscodeSettingsFile = Join-Path $VscodeUserDir 'settings.json' function Resolve-Directory { param([string]$Path) (Resolve-Path -LiteralPath $Path).Path } function Ensure-Directory { param([string]$Path) if (-not (Test-Path -LiteralPath $Path)) { New-Item -ItemType Directory -Path $Path -Force | Out-Null } } function Find-NodeExecutable { foreach ($Candidate in @('node', 'node.exe', 'nodejs', 'nodejs.exe')) { $Command = Get-Command $Candidate -ErrorAction SilentlyContinue | Select-Object -First 1 if ($Command) { return $Command.Source } } return $null } function Quote-SingleQuoted { param([string]$Value) "'" + $Value.Replace("'", "''") + "'" } function Ensure-Junction { param( [string]$Target, [string]$Path ) $Parent = Split-Path -Parent $Path Ensure-Directory $Parent if (Test-Path -LiteralPath $Path) { $Existing = Get-Item -LiteralPath $Path -Force if ($Existing.LinkType -eq 'Junction' -or $Existing.LinkType -eq 'SymbolicLink') { $ResolvedTarget = Resolve-Directory $Existing.Target if ($ResolvedTarget -eq (Resolve-Directory $Target)) { return } } if ($Existing.PSIsContainer -and -not (Get-ChildItem -LiteralPath $Path -Force | Select-Object -First 1)) { Remove-Item -LiteralPath $Path -Force } else { throw "Refusing to replace existing path: $Path" } } New-Item -ItemType Junction -Path $Path -Target $Target | Out-Null } function Upsert-ManagedBlock { param( [string]$Path, [string]$BeginMarker, [string]$EndMarker, [string]$Body ) Ensure-Directory (Split-Path -Parent $Path) $Lines = if (Test-Path -LiteralPath $Path) { Get-Content -LiteralPath $Path } else { @() } $Output = New-Object System.Collections.Generic.List[string] $Skip = $false foreach ($Line in $Lines) { if ($Line -eq $BeginMarker) { $Skip = $true continue } if ($Line -eq $EndMarker) { $Skip = $false continue } if (-not $Skip) { $Output.Add($Line) } } if ($Output.Count -gt 0) { $Output.Add('') } $Output.Add($BeginMarker) foreach ($BodyLine in ($Body -split "`r?`n")) { $Output.Add($BodyLine) } $Output.Add($EndMarker) Set-Content -LiteralPath $Path -Value $Output } function Write-ManagedPowerShellEnv { $script:ManagedShellEnv = Join-Path $StateDir 'copilot-cli-env.ps1' Ensure-Directory (Split-Path -Parent $script:ManagedShellEnv) @( '$env:COPILOT_RESOURCES_HOME = ' + (Quote-SingleQuoted $CanonicalHome), '$env:COPILOT_HOME = ' + (Quote-SingleQuoted $CopilotHome), '$env:COPILOT_CUSTOM_INSTRUCTIONS_DIRS = ' + (Quote-SingleQuoted (Join-Path $CanonicalHome 'resources\instructions')) ) | Set-Content -LiteralPath $script:ManagedShellEnv $QuotedEnvPath = Quote-SingleQuoted $script:ManagedShellEnv Upsert-ManagedBlock \ -Path $ProfilePath \ -BeginMarker '# >>> copilot-resources bootstrap >>>' \ -EndMarker '# <<< copilot-resources bootstrap <<<' \ -Body "if (Test-Path -LiteralPath $QuotedEnvPath) {`n . $QuotedEnvPath`n}" } function Merge-VscodeSettings { $NodeExecutable = Find-NodeExecutable if (-not $NodeExecutable) { Write-Warning 'Skipping VS Code settings merge because Node.js is not available.' return } Ensure-Directory (Split-Path -Parent $VscodeSettingsFile) & $NodeExecutable (Join-Path $ScriptDir 'merge-vscode-settings.mjs') --target $VscodeSettingsFile --template (Join-Path $CanonicalHome 'config\vscode\settings.template.jsonc') --set "COPILOT_RESOURCES_HOME=$CanonicalHome" } Ensure-Directory (Split-Path -Parent $CanonicalHome) if (Test-Path -LiteralPath $CanonicalHome) { if ((Resolve-Directory $CanonicalHome) -ne (Resolve-Directory $RepoRoot)) { throw "Canonical path already exists and points elsewhere: $CanonicalHome" } } else { New-Item -ItemType Junction -Path $CanonicalHome -Target $RepoRoot | Out-Null } Ensure-Directory $CopilotHome Ensure-Directory $VscodeUserDir Ensure-Junction -Target (Join-Path $CanonicalHome 'resources\skills') -Path (Join-Path $CopilotHome 'skills') Ensure-Junction -Target (Join-Path $CanonicalHome 'resources\agents') -Path (Join-Path $CopilotHome 'agents') Ensure-Junction -Target (Join-Path $CanonicalHome 'resources\instructions') -Path (Join-Path $CopilotHome 'instructions') Ensure-Junction -Target (Join-Path $CanonicalHome 'resources\hooks') -Path (Join-Path $CopilotHome 'hooks') Ensure-Junction -Target (Join-Path $CanonicalHome 'resources\prompts') -Path (Join-Path $VscodeUserDir 'prompts') Write-ManagedPowerShellEnv Merge-VscodeSettings Ensure-Directory $StateDir @{ canonicalHome = $CanonicalHome repoRoot = $RepoRoot copilotHome = $CopilotHome vscodeUserDir = $VscodeUserDir vscodeSettingsFile = $VscodeSettingsFile shellRcFile = $ProfilePath managedShellEnv = $ManagedShellEnv bootstrapScript = (Join-Path $ScriptDir 'bootstrap.ps1') } | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $StateDir 'install-state.json') Write-Host "Bootstrap complete." Write-Host "Canonical home: $CanonicalHome" Write-Host "Repository root: $RepoRoot" Write-Host "Copilot home: $CopilotHome" Write-Host "VS Code user dir: $VscodeUserDir" Write-Host "Merged managed VS Code settings into: $VscodeSettingsFile" Write-Host "Installed managed Copilot CLI PowerShell environment into: $ManagedShellEnv" Write-Host "Linked PowerShell profile: $ProfilePath" Write-Host "Optional VS Code template: $(Join-Path $CanonicalHome 'config\vscode\settings.template.jsonc')"