microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
scripts/extension/Prepare-Extension.ps1
389lines ยท modecode
| 1 | #!/usr/bin/env pwsh |
| 2 | #Requires -Modules PowerShell-Yaml |
| 3 | |
| 4 | <# |
| 5 | .SYNOPSIS |
| 6 | Prepares the HVE Core VS Code extension for packaging. |
| 7 | |
| 8 | .DESCRIPTION |
| 9 | This script prepares the VS Code extension by: |
| 10 | - Auto-discovering chat agents, chatmodes, prompts, and instruction files |
| 11 | - Filtering agents and chatmodes by maturity level based on channel |
| 12 | - Updating package.json with discovered components |
| 13 | - Updating changelog if provided |
| 14 | |
| 15 | The package.json version is not modified. |
| 16 | |
| 17 | .PARAMETER ChangelogPath |
| 18 | Optional. Path to a changelog file to include in the package. |
| 19 | |
| 20 | .PARAMETER Channel |
| 21 | Optional. Release channel controlling which maturity levels are included. |
| 22 | 'Stable' (default): Only includes agents/chatmodes with maturity 'stable'. |
| 23 | 'PreRelease': Includes 'stable', 'preview', and 'experimental' maturity levels. |
| 24 | |
| 25 | .PARAMETER DryRun |
| 26 | Optional. If specified, shows what would be done without making changes. |
| 27 | |
| 28 | .EXAMPLE |
| 29 | ./Prepare-Extension.ps1 |
| 30 | # Prepares stable channel using existing version from package.json |
| 31 | |
| 32 | .EXAMPLE |
| 33 | ./Prepare-Extension.ps1 -Channel PreRelease |
| 34 | # Prepares pre-release channel including experimental agents |
| 35 | |
| 36 | .EXAMPLE |
| 37 | ./Prepare-Extension.ps1 -ChangelogPath "./CHANGELOG.md" |
| 38 | # Prepares with changelog |
| 39 | |
| 40 | .NOTES |
| 41 | Dependencies: PowerShell-Yaml module |
| 42 | #> |
| 43 | |
| 44 | [CmdletBinding()] |
| 45 | param( |
| 46 | [Parameter(Mandatory = $false)] |
| 47 | [string]$ChangelogPath = "", |
| 48 | |
| 49 | [Parameter(Mandatory = $false)] |
| 50 | [ValidateSet('Stable', 'PreRelease')] |
| 51 | [string]$Channel = 'Stable', |
| 52 | |
| 53 | [Parameter(Mandatory = $false)] |
| 54 | [switch]$DryRun |
| 55 | ) |
| 56 | |
| 57 | $ErrorActionPreference = "Stop" |
| 58 | |
| 59 | # Define allowed maturity levels based on channel |
| 60 | $allowedMaturities = if ($Channel -eq 'PreRelease') { |
| 61 | @('stable', 'preview', 'experimental') |
| 62 | } else { |
| 63 | @('stable') |
| 64 | } |
| 65 | |
| 66 | # Helper function to extract frontmatter data from YAML |
| 67 | function Get-FrontmatterData { |
| 68 | param( |
| 69 | [Parameter(Mandatory = $true)] |
| 70 | [string]$FilePath, |
| 71 | |
| 72 | [Parameter(Mandatory = $false)] |
| 73 | [string]$FallbackDescription = "" |
| 74 | ) |
| 75 | |
| 76 | $content = Get-Content -Path $FilePath -Raw |
| 77 | $description = "" |
| 78 | $maturity = "stable" |
| 79 | |
| 80 | # Extract YAML frontmatter and parse with PowerShell-Yaml |
| 81 | if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') { |
| 82 | # Normalize line endings to LF for consistent parsing across platforms |
| 83 | $yamlContent = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n" |
| 84 | try { |
| 85 | $data = ConvertFrom-Yaml -Yaml $yamlContent |
| 86 | if ($data.ContainsKey('description')) { |
| 87 | $description = $data.description |
| 88 | } |
| 89 | if ($data.ContainsKey('maturity')) { |
| 90 | $maturity = $data.maturity |
| 91 | } |
| 92 | } catch { |
| 93 | Write-Warning "Failed to parse YAML frontmatter in $(Split-Path -Leaf $FilePath): $_" |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | # Return hashtable with description and maturity |
| 98 | return @{ |
| 99 | description = if ($description) { $description } else { $FallbackDescription } |
| 100 | maturity = $maturity |
| 101 | } |
| 102 | } |
| 103 | |
| 104 | # Determine script and repo paths |
| 105 | $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path |
| 106 | $RepoRoot = (Get-Item "$ScriptDir/../..").FullName |
| 107 | $ExtensionDir = Join-Path $RepoRoot "extension" |
| 108 | $GitHubDir = Join-Path $RepoRoot ".github" |
| 109 | $PackageJsonPath = Join-Path $ExtensionDir "package.json" |
| 110 | |
| 111 | Write-Host "๐ฆ HVE Core Extension Preparer" -ForegroundColor Cyan |
| 112 | Write-Host "==============================" -ForegroundColor Cyan |
| 113 | Write-Host " Channel: $Channel" -ForegroundColor Cyan |
| 114 | Write-Host "" |
| 115 | |
| 116 | # Verify paths exist |
| 117 | if (-not (Test-Path $ExtensionDir)) { |
| 118 | Write-Error "Extension directory not found: $ExtensionDir" |
| 119 | exit 1 |
| 120 | } |
| 121 | |
| 122 | if (-not (Test-Path $PackageJsonPath)) { |
| 123 | Write-Error "package.json not found: $PackageJsonPath" |
| 124 | exit 1 |
| 125 | } |
| 126 | |
| 127 | if (-not (Test-Path $GitHubDir)) { |
| 128 | Write-Error ".github directory not found: $GitHubDir" |
| 129 | exit 1 |
| 130 | } |
| 131 | |
| 132 | # Read current package.json |
| 133 | Write-Host "๐ Reading package.json..." -ForegroundColor Yellow |
| 134 | try { |
| 135 | $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json |
| 136 | } catch { |
| 137 | Write-Error "Failed to parse package.json: $_`nPlease check $PackageJsonPath for JSON syntax errors." |
| 138 | exit 1 |
| 139 | } |
| 140 | |
| 141 | # Validate package.json has required version field |
| 142 | if (-not $packageJson.PSObject.Properties['version']) { |
| 143 | Write-Error "package.json is missing required 'version' field" |
| 144 | exit 1 |
| 145 | } |
| 146 | |
| 147 | # Use existing version from package.json |
| 148 | $version = $packageJson.version |
| 149 | |
| 150 | # Validate version format |
| 151 | if ($version -notmatch '^\d+\.\d+\.\d+$') { |
| 152 | Write-Error "Invalid version format in package.json: '$version'. Expected semantic version format (e.g., 1.0.0)" |
| 153 | exit 1 |
| 154 | } |
| 155 | |
| 156 | Write-Host " Using version: $version" -ForegroundColor Green |
| 157 | |
| 158 | # Discover chat agents (excluding hve-core-installer which is for manual installation only) |
| 159 | Write-Host "" |
| 160 | Write-Host "๐ Discovering chat agents..." -ForegroundColor Yellow |
| 161 | $agentsDir = Join-Path $GitHubDir "agents" |
| 162 | $chatAgents = @() |
| 163 | |
| 164 | # Agents to exclude from extension packaging |
| 165 | $excludedAgents = @('hve-core-installer') |
| 166 | |
| 167 | if (Test-Path $agentsDir) { |
| 168 | $agentFiles = Get-ChildItem -Path $agentsDir -Filter "*.agent.md" | Sort-Object Name |
| 169 | |
| 170 | foreach ($agentFile in $agentFiles) { |
| 171 | # Extract agent name from filename (e.g., hve-core-installer.agent.md -> hve-core-installer) |
| 172 | $agentName = $agentFile.BaseName -replace '\.agent$', '' |
| 173 | |
| 174 | # Skip excluded agents |
| 175 | if ($excludedAgents -contains $agentName) { |
| 176 | Write-Host " โญ๏ธ $agentName (excluded)" -ForegroundColor DarkGray |
| 177 | continue |
| 178 | } |
| 179 | |
| 180 | # Extract frontmatter data |
| 181 | $frontmatter = Get-FrontmatterData -FilePath $agentFile.FullName -FallbackDescription "AI agent for $agentName" |
| 182 | $description = $frontmatter.description |
| 183 | $maturity = $frontmatter.maturity |
| 184 | |
| 185 | # Filter by maturity based on channel |
| 186 | if ($allowedMaturities -notcontains $maturity) { |
| 187 | Write-Host " โญ๏ธ $agentName (maturity: $maturity, skipped for $Channel)" -ForegroundColor DarkGray |
| 188 | continue |
| 189 | } |
| 190 | |
| 191 | $agent = [PSCustomObject]@{ |
| 192 | name = $agentName |
| 193 | path = "./.github/agents/$($agentFile.Name)" |
| 194 | description = $description |
| 195 | } |
| 196 | |
| 197 | $chatAgents += $agent |
| 198 | Write-Host " โ
$agentName" -ForegroundColor Green |
| 199 | } |
| 200 | } else { |
| 201 | Write-Warning "Agents directory not found: $agentsDir" |
| 202 | } |
| 203 | |
| 204 | # Discover chatmodes |
| 205 | Write-Host "" |
| 206 | Write-Host "๐ Discovering chatmodes..." -ForegroundColor Yellow |
| 207 | $chatmodesDir = Join-Path $GitHubDir "chatmodes" |
| 208 | $chatmodes = @() |
| 209 | |
| 210 | if (Test-Path $chatmodesDir) { |
| 211 | $chatmodeFiles = Get-ChildItem -Path $chatmodesDir -Filter "*.chatmode.md" | Sort-Object Name |
| 212 | |
| 213 | foreach ($chatmodeFile in $chatmodeFiles) { |
| 214 | # Extract chatmode name from filename (e.g., task-planner.chatmode.md -> task-planner) |
| 215 | $chatmodeName = $chatmodeFile.BaseName -replace '\.chatmode$', '' |
| 216 | |
| 217 | # Extract frontmatter data |
| 218 | $displayName = $chatmodeName -replace '-', ' ' |
| 219 | $frontmatter = Get-FrontmatterData -FilePath $chatmodeFile.FullName -FallbackDescription "Chatmode for $displayName" |
| 220 | $description = $frontmatter.description |
| 221 | $maturity = $frontmatter.maturity |
| 222 | |
| 223 | # Filter by maturity based on channel |
| 224 | if ($allowedMaturities -notcontains $maturity) { |
| 225 | Write-Host " โญ๏ธ $chatmodeName (maturity: $maturity, skipped for $Channel)" -ForegroundColor DarkGray |
| 226 | continue |
| 227 | } |
| 228 | |
| 229 | $chatmode = [PSCustomObject]@{ |
| 230 | name = $chatmodeName |
| 231 | path = "./.github/chatmodes/$($chatmodeFile.Name)" |
| 232 | description = $description |
| 233 | } |
| 234 | |
| 235 | $chatmodes += $chatmode |
| 236 | Write-Host " โ
$chatmodeName" -ForegroundColor Green |
| 237 | } |
| 238 | } else { |
| 239 | Write-Warning "Chatmodes directory not found: $chatmodesDir" |
| 240 | } |
| 241 | |
| 242 | # Discover prompts |
| 243 | Write-Host "" |
| 244 | Write-Host "๐ Discovering prompts..." -ForegroundColor Yellow |
| 245 | $promptsDir = Join-Path $GitHubDir "prompts" |
| 246 | $chatPromptFiles = @() |
| 247 | |
| 248 | if (Test-Path $promptsDir) { |
| 249 | $promptFiles = Get-ChildItem -Path $promptsDir -Filter "*.prompt.md" -Recurse | Sort-Object Name |
| 250 | |
| 251 | foreach ($promptFile in $promptFiles) { |
| 252 | # Extract prompt name from filename (e.g., git-commit.prompt.md -> git-commit) |
| 253 | $promptName = $promptFile.BaseName -replace '\.prompt$', '' |
| 254 | |
| 255 | # Extract frontmatter data |
| 256 | $displayName = ($promptName -replace '-', ' ') -replace '(\b\w)', { $_.Groups[1].Value.ToUpper() } |
| 257 | $frontmatter = Get-FrontmatterData -FilePath $promptFile.FullName -FallbackDescription "Prompt for $displayName" |
| 258 | $description = $frontmatter.description |
| 259 | $maturity = $frontmatter.maturity |
| 260 | |
| 261 | # Filter by maturity based on channel |
| 262 | if ($allowedMaturities -notcontains $maturity) { |
| 263 | Write-Host " โญ๏ธ $promptName (maturity: $maturity, skipped for $Channel)" -ForegroundColor DarkGray |
| 264 | continue |
| 265 | } |
| 266 | |
| 267 | # Calculate relative path from .github |
| 268 | $relativePath = [System.IO.Path]::GetRelativePath($GitHubDir, $promptFile.FullName) -replace '\\', '/' |
| 269 | |
| 270 | $prompt = [PSCustomObject]@{ |
| 271 | name = $promptName |
| 272 | path = "./.github/$relativePath" |
| 273 | description = $description |
| 274 | } |
| 275 | |
| 276 | $chatPromptFiles += $prompt |
| 277 | Write-Host " โ
$promptName" -ForegroundColor Green |
| 278 | } |
| 279 | } else { |
| 280 | Write-Warning "Prompts directory not found: $promptsDir" |
| 281 | } |
| 282 | |
| 283 | # Discover instruction files |
| 284 | Write-Host "" |
| 285 | Write-Host "๐ Discovering instruction files..." -ForegroundColor Yellow |
| 286 | $instructionsDir = Join-Path $GitHubDir "instructions" |
| 287 | $chatInstructions = @() |
| 288 | |
| 289 | if (Test-Path $instructionsDir) { |
| 290 | $instructionFiles = Get-ChildItem -Path $instructionsDir -Filter "*.instructions.md" -Recurse | Sort-Object Name |
| 291 | |
| 292 | foreach ($instrFile in $instructionFiles) { |
| 293 | # Extract instruction name from filename (e.g., commit-message.instructions.md -> commit-message-instructions) |
| 294 | $baseName = $instrFile.BaseName -replace '\.instructions$', '' |
| 295 | $instrName = "$baseName-instructions" |
| 296 | |
| 297 | # Extract frontmatter data |
| 298 | $displayName = ($baseName -replace '-', ' ') -replace '(\b\w)', { $_.Groups[1].Value.ToUpper() } |
| 299 | $frontmatter = Get-FrontmatterData -FilePath $instrFile.FullName -FallbackDescription "Instructions for $displayName" |
| 300 | $description = $frontmatter.description |
| 301 | $maturity = $frontmatter.maturity |
| 302 | |
| 303 | # Filter by maturity based on channel |
| 304 | if ($allowedMaturities -notcontains $maturity) { |
| 305 | Write-Host " โญ๏ธ $instrName (maturity: $maturity, skipped for $Channel)" -ForegroundColor DarkGray |
| 306 | continue |
| 307 | } |
| 308 | |
| 309 | # Calculate relative path from .github using cross-platform APIs |
| 310 | $relativePathFromGitHub = [System.IO.Path]::GetRelativePath($GitHubDir, $instrFile.FullName) |
| 311 | $normalizedRelativePath = (Join-Path ".github" $relativePathFromGitHub) -replace '\\', '/' |
| 312 | |
| 313 | $instruction = [PSCustomObject]@{ |
| 314 | name = $instrName |
| 315 | path = "./$normalizedRelativePath" |
| 316 | description = $description |
| 317 | } |
| 318 | |
| 319 | $chatInstructions += $instruction |
| 320 | Write-Host " โ
$instrName" -ForegroundColor Green |
| 321 | } |
| 322 | } else { |
| 323 | Write-Warning "Instructions directory not found: $instructionsDir" |
| 324 | } |
| 325 | |
| 326 | # Update package.json |
| 327 | Write-Host "" |
| 328 | Write-Host "๐ Updating package.json..." -ForegroundColor Yellow |
| 329 | |
| 330 | # Ensure contributes section exists |
| 331 | if (-not $packageJson.contributes) { |
| 332 | $packageJson | Add-Member -NotePropertyName "contributes" -NotePropertyValue ([PSCustomObject]@{}) |
| 333 | } |
| 334 | |
| 335 | # Combine agents and chatmodes into chatAgents (VS Code treats chatmodes as chatAgents) |
| 336 | $allChatAgents = $chatAgents + $chatmodes |
| 337 | |
| 338 | # Update chatAgents |
| 339 | $packageJson.contributes.chatAgents = $allChatAgents |
| 340 | Write-Host " Updated chatAgents: $($allChatAgents.Count) items ($($chatAgents.Count) agents + $($chatmodes.Count) chatmodes)" -ForegroundColor Green |
| 341 | |
| 342 | # Update chatPromptFiles |
| 343 | $packageJson.contributes.chatPromptFiles = $chatPromptFiles |
| 344 | Write-Host " Updated chatPromptFiles: $($chatPromptFiles.Count) prompts" -ForegroundColor Green |
| 345 | |
| 346 | # Update chatInstructions |
| 347 | $packageJson.contributes.chatInstructions = $chatInstructions |
| 348 | Write-Host " Updated chatInstructions: $($chatInstructions.Count) files" -ForegroundColor Green |
| 349 | |
| 350 | if ($DryRun) { |
| 351 | Write-Host "" |
| 352 | Write-Host "๐ DRY RUN - Would write the following package.json:" -ForegroundColor Magenta |
| 353 | Write-Host ($packageJson | ConvertTo-Json -Depth 10) |
| 354 | Write-Host "" |
| 355 | Write-Host "๐ DRY RUN - No changes made" -ForegroundColor Magenta |
| 356 | exit 0 |
| 357 | } |
| 358 | |
| 359 | # Write updated package.json |
| 360 | $packageJson | ConvertTo-Json -Depth 10 | Set-Content -Path $PackageJsonPath -Encoding UTF8NoBOM |
| 361 | Write-Host " Saved package.json" -ForegroundColor Green |
| 362 | |
| 363 | # Handle changelog if provided |
| 364 | if ($ChangelogPath) { |
| 365 | Write-Host "" |
| 366 | Write-Host "๐ Processing changelog..." -ForegroundColor Yellow |
| 367 | |
| 368 | if (Test-Path $ChangelogPath) { |
| 369 | $changelogDest = Join-Path $ExtensionDir "CHANGELOG.md" |
| 370 | Copy-Item -Path $ChangelogPath -Destination $changelogDest -Force |
| 371 | Write-Host " Copied changelog to extension directory" -ForegroundColor Green |
| 372 | } else { |
| 373 | Write-Warning "Changelog file not found: $ChangelogPath" |
| 374 | } |
| 375 | } |
| 376 | |
| 377 | Write-Host "" |
| 378 | Write-Host "๐ Done!" -ForegroundColor Green |
| 379 | Write-Host "" |
| 380 | Write-Host "๐ Summary:" -ForegroundColor Cyan |
| 381 | Write-Host " Version: $version" -ForegroundColor White |
| 382 | Write-Host " Channel: $Channel" -ForegroundColor White |
| 383 | Write-Host " Chat Agents: $($chatAgents.Count)" -ForegroundColor White |
| 384 | Write-Host " Chatmodes: $($chatmodes.Count)" -ForegroundColor White |
| 385 | Write-Host " Prompts: $($chatPromptFiles.Count)" -ForegroundColor White |
| 386 | Write-Host " Instructions: $($chatInstructions.Count)" -ForegroundColor White |
| 387 | Write-Host "" |
| 388 | |
| 389 | exit 0 |
| 390 | |