Initial shared Copilot resources scaffold
This commit is contained in:
182
install/bootstrap.ps1
Normal file
182
install/bootstrap.ps1
Normal file
@@ -0,0 +1,182 @@
|
||||
$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')"
|
||||
Reference in New Issue
Block a user