Files
bw-copilot-resources/install/bootstrap.ps1

183 lines
6.4 KiB
PowerShell

$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')"