microsoft/hve-core
Publicmirrored fromhttps://github.com/microsoft/hve-coreAvailable
scripts/extension/Package-Extension.ps1
1127lines ยท modecode
| 1 | #!/usr/bin/env pwsh |
| 2 | # Copyright (c) Microsoft Corporation. |
| 3 | # SPDX-License-Identifier: MIT |
| 4 | #Requires -Version 7.0 |
| 5 | |
| 6 | <# |
| 7 | .SYNOPSIS |
| 8 | Packages the HVE Core VS Code extension. |
| 9 | |
| 10 | .DESCRIPTION |
| 11 | This script packages the VS Code extension into a .vsix file. |
| 12 | It uses the version from package.json or a specified version. |
| 13 | Optionally adds a dev patch number for pre-release builds. |
| 14 | Supports VS Code Marketplace pre-release channel with -PreRelease switch. |
| 15 | |
| 16 | .PARAMETER Version |
| 17 | Optional. The version to use for the package. |
| 18 | If not specified, uses the version from package.json. |
| 19 | |
| 20 | .PARAMETER DevPatchNumber |
| 21 | Optional. Dev patch number to append (e.g., "123" creates "1.0.0-dev.123"). |
| 22 | |
| 23 | .PARAMETER ChangelogPath |
| 24 | Optional. Path to a changelog file to include in the package. |
| 25 | |
| 26 | .PARAMETER PreRelease |
| 27 | Optional. When specified, packages the extension for VS Code Marketplace pre-release channel. |
| 28 | Uses vsce --pre-release flag which marks the extension for the pre-release track. |
| 29 | |
| 30 | .PARAMETER Collection |
| 31 | Optional. Path to a collection manifest file (YAML or JSON). When specified, only |
| 32 | collection-filtered artifacts are copied and the output filename uses the |
| 33 | collection ID. |
| 34 | |
| 35 | .PARAMETER DryRun |
| 36 | Optional. Validates packaging orchestration without invoking vsce. |
| 37 | |
| 38 | .EXAMPLE |
| 39 | ./Package-Extension.ps1 |
| 40 | # Packages using version from package.json |
| 41 | |
| 42 | .EXAMPLE |
| 43 | ./Package-Extension.ps1 -Version "2.0.0" |
| 44 | # Packages with specific version |
| 45 | |
| 46 | .EXAMPLE |
| 47 | ./Package-Extension.ps1 -DevPatchNumber "123" |
| 48 | # Packages with dev version (e.g., 1.0.0-dev.123) |
| 49 | |
| 50 | .EXAMPLE |
| 51 | ./Package-Extension.ps1 -Version "1.1.0" -DevPatchNumber "456" |
| 52 | # Packages with specific dev version (1.1.0-dev.456) |
| 53 | |
| 54 | .EXAMPLE |
| 55 | ./Package-Extension.ps1 -PreRelease |
| 56 | # Packages for VS Code Marketplace pre-release channel |
| 57 | |
| 58 | .EXAMPLE |
| 59 | ./Package-Extension.ps1 -Version "1.1.0" -PreRelease |
| 60 | # Packages with ODD minor version for pre-release channel |
| 61 | |
| 62 | .EXAMPLE |
| 63 | . ./Package-Extension.ps1 |
| 64 | # Dot-source to import functions for testing without executing packaging. |
| 65 | #> |
| 66 | |
| 67 | [CmdletBinding()] |
| 68 | param( |
| 69 | [Parameter(Mandatory = $false)] |
| 70 | [string]$Version = "", |
| 71 | |
| 72 | [Parameter(Mandatory = $false)] |
| 73 | [string]$DevPatchNumber = "", |
| 74 | |
| 75 | [Parameter(Mandatory = $false)] |
| 76 | [string]$ChangelogPath = "", |
| 77 | |
| 78 | [Parameter(Mandatory = $false)] |
| 79 | [switch]$PreRelease, |
| 80 | |
| 81 | [Parameter(Mandatory = $false)] |
| 82 | [string]$Collection = "", |
| 83 | |
| 84 | [Parameter(Mandatory = $false)] |
| 85 | [Alias('dry-run')] |
| 86 | [switch]$DryRun |
| 87 | ) |
| 88 | |
| 89 | $ErrorActionPreference = 'Stop' |
| 90 | |
| 91 | Import-Module (Join-Path $PSScriptRoot "../lib/Modules/CIHelpers.psm1") -Force |
| 92 | Import-Module (Join-Path $PSScriptRoot "../collections/Modules/CollectionHelpers.psm1") -Force |
| 93 | |
| 94 | #region Pure Functions |
| 95 | |
| 96 | function Test-VsceAvailable { |
| 97 | <# |
| 98 | .SYNOPSIS |
| 99 | Checks if vsce or npx is available for packaging. |
| 100 | .OUTPUTS |
| 101 | Hashtable with IsAvailable, CommandType ('vsce', 'npx', or $null), and Command path. |
| 102 | #> |
| 103 | [CmdletBinding()] |
| 104 | [OutputType([hashtable])] |
| 105 | param() |
| 106 | |
| 107 | $vsceCmd = Get-Command vsce -ErrorAction SilentlyContinue |
| 108 | if ($vsceCmd) { |
| 109 | return @{ |
| 110 | IsAvailable = $true |
| 111 | CommandType = 'vsce' |
| 112 | Command = $vsceCmd.Source |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | $npxCmd = Get-Command npx -ErrorAction SilentlyContinue |
| 117 | if ($npxCmd) { |
| 118 | return @{ |
| 119 | IsAvailable = $true |
| 120 | CommandType = 'npx' |
| 121 | Command = $npxCmd.Source |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | return @{ |
| 126 | IsAvailable = $false |
| 127 | CommandType = $null |
| 128 | Command = $null |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | function Test-ExtensionManifestValid { |
| 133 | <# |
| 134 | .SYNOPSIS |
| 135 | Validates an extension manifest (package.json content) for required fields and format. |
| 136 | .PARAMETER ManifestContent |
| 137 | The parsed package.json content as a PSObject. |
| 138 | .OUTPUTS |
| 139 | Hashtable with IsValid boolean and Errors array. |
| 140 | #> |
| 141 | [CmdletBinding()] |
| 142 | [OutputType([hashtable])] |
| 143 | param( |
| 144 | [Parameter(Mandatory = $true)] |
| 145 | [PSObject]$ManifestContent |
| 146 | ) |
| 147 | |
| 148 | $errors = @() |
| 149 | |
| 150 | # Check required fields |
| 151 | if (-not $ManifestContent.PSObject.Properties['name']) { |
| 152 | $errors += "Missing required 'name' field" |
| 153 | } |
| 154 | |
| 155 | if (-not $ManifestContent.PSObject.Properties['version']) { |
| 156 | $errors += "Missing required 'version' field" |
| 157 | } elseif ($ManifestContent.version -notmatch '^\d+\.\d+\.\d+') { |
| 158 | $errors += "Invalid version format: '$($ManifestContent.version)'. Expected semantic version (e.g., 1.0.0)" |
| 159 | } |
| 160 | |
| 161 | if (-not $ManifestContent.PSObject.Properties['publisher']) { |
| 162 | $errors += "Missing required 'publisher' field" |
| 163 | } |
| 164 | |
| 165 | if (-not $ManifestContent.PSObject.Properties['engines']) { |
| 166 | $errors += "Missing required 'engines' field" |
| 167 | } elseif (-not $ManifestContent.engines.PSObject.Properties['vscode']) { |
| 168 | $errors += "Missing required 'engines.vscode' field" |
| 169 | } |
| 170 | |
| 171 | return @{ |
| 172 | IsValid = ($errors.Count -eq 0) |
| 173 | Errors = $errors |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | function Get-VscePackageCommand { |
| 178 | <# |
| 179 | .SYNOPSIS |
| 180 | Builds the vsce package command arguments without executing. |
| 181 | .PARAMETER CommandType |
| 182 | The type of command to use ('vsce' or 'npx'). |
| 183 | .PARAMETER PreRelease |
| 184 | Whether to include the --pre-release flag. |
| 185 | .OUTPUTS |
| 186 | Hashtable with Executable and Arguments array. |
| 187 | #> |
| 188 | [CmdletBinding()] |
| 189 | [OutputType([hashtable])] |
| 190 | param( |
| 191 | [Parameter(Mandatory = $true)] |
| 192 | [ValidateSet('vsce', 'npx')] |
| 193 | [string]$CommandType, |
| 194 | |
| 195 | [Parameter(Mandatory = $false)] |
| 196 | [switch]$PreRelease |
| 197 | ) |
| 198 | |
| 199 | $vsceArgs = @('package', '--no-dependencies') |
| 200 | if ($PreRelease) { |
| 201 | $vsceArgs += '--pre-release' |
| 202 | } |
| 203 | |
| 204 | if ($CommandType -eq 'npx') { |
| 205 | # --yes auto-confirms npx package installation for non-interactive CI environments |
| 206 | return @{ |
| 207 | Executable = 'npx' |
| 208 | Arguments = @('--yes', '@vscode/vsce@3.7.1') + $vsceArgs |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | return @{ |
| 213 | Executable = 'vsce' |
| 214 | Arguments = $vsceArgs |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | function New-PackagingResult { |
| 219 | <# |
| 220 | .SYNOPSIS |
| 221 | Creates a standardized packaging result object. |
| 222 | .PARAMETER Success |
| 223 | Whether the packaging operation succeeded. |
| 224 | .PARAMETER OutputPath |
| 225 | Path to the generated .vsix file (if successful). |
| 226 | .PARAMETER Version |
| 227 | The package version used. |
| 228 | .PARAMETER ErrorMessage |
| 229 | Error message if the operation failed. |
| 230 | .OUTPUTS |
| 231 | Hashtable with Success, OutputPath, Version, and ErrorMessage. |
| 232 | #> |
| 233 | [CmdletBinding()] |
| 234 | [OutputType([hashtable])] |
| 235 | param( |
| 236 | [Parameter(Mandatory = $true)] |
| 237 | [bool]$Success, |
| 238 | |
| 239 | [Parameter(Mandatory = $false)] |
| 240 | [string]$OutputPath = "", |
| 241 | |
| 242 | [Parameter(Mandatory = $false)] |
| 243 | [string]$Version = "", |
| 244 | |
| 245 | [Parameter(Mandatory = $false)] |
| 246 | [string]$ErrorMessage = "" |
| 247 | ) |
| 248 | |
| 249 | return @{ |
| 250 | Success = $Success |
| 251 | OutputPath = $OutputPath |
| 252 | Version = $Version |
| 253 | ErrorMessage = $ErrorMessage |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | function Get-CollectionReadmePath { |
| 258 | <# |
| 259 | .SYNOPSIS |
| 260 | Resolves the collection-specific README path from a collection manifest. |
| 261 | .DESCRIPTION |
| 262 | Maps a collection manifest to its collection-specific README file. Returns |
| 263 | null when the collection is the flagship package (hve-core) or when no |
| 264 | matching collection README exists on disk. Supports both YAML and JSON |
| 265 | manifest formats. |
| 266 | .PARAMETER CollectionPath |
| 267 | Path to the collection manifest file (YAML or JSON). |
| 268 | .PARAMETER ExtensionDirectory |
| 269 | Path to the extension directory containing README files. |
| 270 | .OUTPUTS |
| 271 | String path to the collection README, or $null if not applicable. |
| 272 | #> |
| 273 | [CmdletBinding()] |
| 274 | [OutputType([string])] |
| 275 | param( |
| 276 | [Parameter(Mandatory = $true)] |
| 277 | [string]$CollectionPath, |
| 278 | |
| 279 | [Parameter(Mandatory = $true)] |
| 280 | [string]$ExtensionDirectory |
| 281 | ) |
| 282 | |
| 283 | $manifest = Get-CollectionManifest -CollectionPath $CollectionPath |
| 284 | $collectionId = $manifest.id |
| 285 | |
| 286 | # Flagship package uses the default README.md |
| 287 | if ($collectionId -eq 'hve-core') { |
| 288 | return $null |
| 289 | } |
| 290 | |
| 291 | $collectionReadmePath = Join-Path $ExtensionDirectory "README.$collectionId.md" |
| 292 | if (Test-Path $collectionReadmePath) { |
| 293 | return $collectionReadmePath |
| 294 | } |
| 295 | |
| 296 | return $null |
| 297 | } |
| 298 | |
| 299 | function Get-ResolvedPackageVersion { |
| 300 | <# |
| 301 | .SYNOPSIS |
| 302 | Resolves the package version from parameters or manifest content. |
| 303 | .PARAMETER SpecifiedVersion |
| 304 | Version specified via parameter (may be empty). |
| 305 | .PARAMETER ManifestVersion |
| 306 | Version from the package.json manifest. |
| 307 | .PARAMETER DevPatchNumber |
| 308 | Optional dev patch number to append. |
| 309 | .OUTPUTS |
| 310 | Hashtable with IsValid, BaseVersion, PackageVersion, and ErrorMessage. |
| 311 | #> |
| 312 | [CmdletBinding()] |
| 313 | [OutputType([hashtable])] |
| 314 | param( |
| 315 | [Parameter(Mandatory = $false)] |
| 316 | [string]$SpecifiedVersion = "", |
| 317 | |
| 318 | [Parameter(Mandatory = $true)] |
| 319 | [string]$ManifestVersion, |
| 320 | |
| 321 | [Parameter(Mandatory = $false)] |
| 322 | [string]$DevPatchNumber = "" |
| 323 | ) |
| 324 | |
| 325 | $baseVersion = "" |
| 326 | |
| 327 | if ($SpecifiedVersion -and $SpecifiedVersion -ne "") { |
| 328 | # Validate specified version format |
| 329 | if ($SpecifiedVersion -notmatch '^\d+\.\d+\.\d+$') { |
| 330 | return @{ |
| 331 | IsValid = $false |
| 332 | BaseVersion = "" |
| 333 | PackageVersion = "" |
| 334 | ErrorMessage = "Invalid version format specified: '$SpecifiedVersion'. Expected semantic version format (e.g., 1.0.0)." |
| 335 | } |
| 336 | } |
| 337 | $baseVersion = $SpecifiedVersion |
| 338 | } else { |
| 339 | # Validate manifest version |
| 340 | if ($ManifestVersion -notmatch '^\d+\.\d+\.\d+') { |
| 341 | return @{ |
| 342 | IsValid = $false |
| 343 | BaseVersion = "" |
| 344 | PackageVersion = "" |
| 345 | ErrorMessage = "Invalid version format in package.json: '$ManifestVersion'. Expected semantic version format (e.g., 1.0.0)." |
| 346 | } |
| 347 | } |
| 348 | # Extract base version |
| 349 | $ManifestVersion -match '^(\d+\.\d+\.\d+)' | Out-Null |
| 350 | $baseVersion = $Matches[1] |
| 351 | } |
| 352 | |
| 353 | # Apply dev patch number if provided |
| 354 | $packageVersion = if ($DevPatchNumber -and $DevPatchNumber -ne "") { |
| 355 | "$baseVersion-dev.$DevPatchNumber" |
| 356 | } else { |
| 357 | $baseVersion |
| 358 | } |
| 359 | |
| 360 | return @{ |
| 361 | IsValid = $true |
| 362 | BaseVersion = $baseVersion |
| 363 | PackageVersion = $packageVersion |
| 364 | ErrorMessage = "" |
| 365 | } |
| 366 | } |
| 367 | |
| 368 | function Test-PackagingInputsValid { |
| 369 | <# |
| 370 | .SYNOPSIS |
| 371 | Validates all required paths for extension packaging. |
| 372 | .DESCRIPTION |
| 373 | Pure function that checks existence of ExtensionDirectory, package.json, |
| 374 | .github directory, and CIHelpers.psm1 module. Returns resolved paths for use |
| 375 | by downstream functions. |
| 376 | .PARAMETER ExtensionDirectory |
| 377 | Absolute path to the extension directory. |
| 378 | .PARAMETER RepoRoot |
| 379 | Absolute path to the repository root. |
| 380 | .OUTPUTS |
| 381 | Hashtable with IsValid, Errors array, and resolved paths. |
| 382 | #> |
| 383 | [CmdletBinding()] |
| 384 | [OutputType([hashtable])] |
| 385 | param( |
| 386 | [Parameter(Mandatory = $true)] |
| 387 | [string]$ExtensionDirectory, |
| 388 | |
| 389 | [Parameter(Mandatory = $true)] |
| 390 | [string]$RepoRoot |
| 391 | ) |
| 392 | |
| 393 | $errors = @() |
| 394 | |
| 395 | if (-not (Test-Path $ExtensionDirectory)) { |
| 396 | $errors += "Extension directory not found: $ExtensionDirectory" |
| 397 | } |
| 398 | |
| 399 | $packageJsonPath = Join-Path $ExtensionDirectory "package.json" |
| 400 | if (-not (Test-Path $packageJsonPath)) { |
| 401 | $errors += "package.json not found: $packageJsonPath" |
| 402 | } |
| 403 | |
| 404 | $githubDir = Join-Path $RepoRoot ".github" |
| 405 | if (-not (Test-Path $githubDir)) { |
| 406 | $errors += ".github directory not found: $githubDir" |
| 407 | } |
| 408 | |
| 409 | $ciHelpersPath = Join-Path $RepoRoot "scripts/lib/Modules/CIHelpers.psm1" |
| 410 | if (-not (Test-Path $ciHelpersPath)) { |
| 411 | $errors += "CIHelpers.psm1 not found: $ciHelpersPath" |
| 412 | } |
| 413 | |
| 414 | return @{ |
| 415 | IsValid = ($errors.Count -eq 0) |
| 416 | Errors = $errors |
| 417 | PackageJsonPath = $packageJsonPath |
| 418 | GitHubDir = $githubDir |
| 419 | CIHelpersPath = $ciHelpersPath |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | function Get-PackagingDirectorySpec { |
| 424 | <# |
| 425 | .SYNOPSIS |
| 426 | Returns specification for directories to copy during packaging. |
| 427 | .DESCRIPTION |
| 428 | Pure function that defines source to destination mappings without performing I/O. |
| 429 | Each spec includes Source, Destination, Required flag, and optional IsFile flag. |
| 430 | .PARAMETER RepoRoot |
| 431 | Absolute path to the repository root. |
| 432 | .PARAMETER ExtensionDirectory |
| 433 | Absolute path to the extension directory. |
| 434 | .OUTPUTS |
| 435 | Array of hashtables with Source, Destination, Required, and IsFile properties. |
| 436 | #> |
| 437 | [CmdletBinding()] |
| 438 | [OutputType([hashtable[]])] |
| 439 | param( |
| 440 | [Parameter(Mandatory = $true)] |
| 441 | [string]$RepoRoot, |
| 442 | |
| 443 | [Parameter(Mandatory = $true)] |
| 444 | [string]$ExtensionDirectory |
| 445 | ) |
| 446 | |
| 447 | return @( |
| 448 | @{ |
| 449 | Source = Join-Path $RepoRoot ".github" |
| 450 | Destination = Join-Path $ExtensionDirectory ".github" |
| 451 | IsFile = $false |
| 452 | }, |
| 453 | @{ |
| 454 | Source = Join-Path $RepoRoot "scripts/lib/Modules/CIHelpers.psm1" |
| 455 | Destination = Join-Path $ExtensionDirectory "scripts/lib/Modules/CIHelpers.psm1" |
| 456 | IsFile = $true |
| 457 | }, |
| 458 | @{ |
| 459 | Source = Join-Path $RepoRoot "docs/templates" |
| 460 | Destination = Join-Path $ExtensionDirectory "docs/templates" |
| 461 | IsFile = $false |
| 462 | } |
| 463 | ) |
| 464 | } |
| 465 | |
| 466 | #endregion Pure Functions |
| 467 | |
| 468 | #region I/O Functions |
| 469 | |
| 470 | function Copy-DirectoryFiltered { |
| 471 | <# |
| 472 | .SYNOPSIS |
| 473 | Copies a directory tree while excluding dev artifact directories. |
| 474 | .DESCRIPTION |
| 475 | Recursive copy that skips directories matching ExcludePatterns. |
| 476 | Prevents copying large non-distributable artifacts (.venv, __pycache__, etc.) |
| 477 | that slow down packaging and crash vsce's secret scanner. |
| 478 | .PARAMETER Source |
| 479 | Source directory path. |
| 480 | .PARAMETER Destination |
| 481 | Destination directory path. |
| 482 | .PARAMETER ExcludePatterns |
| 483 | Directory names to exclude from the copy. |
| 484 | #> |
| 485 | [CmdletBinding()] |
| 486 | param( |
| 487 | [Parameter(Mandatory = $true)] |
| 488 | [string]$Source, |
| 489 | |
| 490 | [Parameter(Mandatory = $true)] |
| 491 | [string]$Destination, |
| 492 | |
| 493 | [Parameter(Mandatory = $false)] |
| 494 | [string[]]$ExcludePatterns = @('.venv', '.ruff_cache', '.pytest_cache', '__pycache__', 'node_modules') |
| 495 | ) |
| 496 | |
| 497 | if (-not (Test-Path $Destination)) { |
| 498 | New-Item -Path $Destination -ItemType Directory -Force | Out-Null |
| 499 | } |
| 500 | |
| 501 | # Copy files at current level |
| 502 | Get-ChildItem -Path $Source -File -ErrorAction SilentlyContinue | ForEach-Object { |
| 503 | Copy-Item -Path $_.FullName -Destination (Join-Path $Destination $_.Name) -Force |
| 504 | } |
| 505 | |
| 506 | # Recurse into subdirectories, skipping excluded patterns |
| 507 | Get-ChildItem -Path $Source -Directory -ErrorAction SilentlyContinue | ForEach-Object { |
| 508 | if ($_.Name -notin $ExcludePatterns) { |
| 509 | Copy-DirectoryFiltered -Source $_.FullName -Destination (Join-Path $Destination $_.Name) -ExcludePatterns $ExcludePatterns |
| 510 | } |
| 511 | } |
| 512 | } |
| 513 | |
| 514 | function Copy-CollectionArtifacts { |
| 515 | <# |
| 516 | .SYNOPSIS |
| 517 | Copies only collection-filtered artifacts to the extension directory. |
| 518 | .DESCRIPTION |
| 519 | Reads the prepared package.json to determine which artifacts were selected |
| 520 | by collection filtering, then copies only those files instead of the entire |
| 521 | .github directory. |
| 522 | .PARAMETER RepoRoot |
| 523 | Absolute path to the repository root. |
| 524 | .PARAMETER ExtensionDirectory |
| 525 | Absolute path to the extension directory. |
| 526 | .PARAMETER PrepareResult |
| 527 | Result hashtable from Invoke-PrepareExtension. Reserved for future collection metadata handling. |
| 528 | #> |
| 529 | [CmdletBinding()] |
| 530 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'PrepareResult', Justification = 'Reserved for future collection metadata handling')] |
| 531 | param( |
| 532 | [Parameter(Mandatory = $true)] |
| 533 | [string]$RepoRoot, |
| 534 | |
| 535 | [Parameter(Mandatory = $true)] |
| 536 | [string]$ExtensionDirectory, |
| 537 | |
| 538 | [Parameter(Mandatory = $true)] |
| 539 | [hashtable]$PrepareResult |
| 540 | ) |
| 541 | |
| 542 | $preparedPkgJson = Get-Content -Path (Join-Path $ExtensionDirectory "package.json") -Raw | ConvertFrom-Json |
| 543 | |
| 544 | # Copy filtered agents |
| 545 | if ($preparedPkgJson.contributes.chatAgents) { |
| 546 | foreach ($agent in $preparedPkgJson.contributes.chatAgents) { |
| 547 | $srcPath = Join-Path $RepoRoot ($agent.path -replace '^\.[\\/]', '') |
| 548 | if (-not (Test-Path $srcPath)) { |
| 549 | Write-Warning "Skipping missing collection artifact: $srcPath (referenced by contributes.chatAgents in package.json)" |
| 550 | continue |
| 551 | } |
| 552 | $destPath = Join-Path $ExtensionDirectory ($agent.path -replace '^\.[\\/]', '') |
| 553 | $destDir = Split-Path $destPath -Parent |
| 554 | New-Item -Path $destDir -ItemType Directory -Force | Out-Null |
| 555 | Copy-Item -Path $srcPath -Destination $destPath -Force |
| 556 | } |
| 557 | } |
| 558 | |
| 559 | # Copy filtered prompts |
| 560 | if ($preparedPkgJson.contributes.chatPromptFiles) { |
| 561 | foreach ($prompt in $preparedPkgJson.contributes.chatPromptFiles) { |
| 562 | $srcPath = Join-Path $RepoRoot ($prompt.path -replace '^\.[\\/]', '') |
| 563 | if (-not (Test-Path $srcPath)) { |
| 564 | Write-Warning "Skipping missing collection artifact: $srcPath (referenced by contributes.chatPromptFiles in package.json)" |
| 565 | continue |
| 566 | } |
| 567 | $destPath = Join-Path $ExtensionDirectory ($prompt.path -replace '^\.[\\/]', '') |
| 568 | $destDir = Split-Path $destPath -Parent |
| 569 | New-Item -Path $destDir -ItemType Directory -Force | Out-Null |
| 570 | Copy-Item -Path $srcPath -Destination $destPath -Force |
| 571 | } |
| 572 | } |
| 573 | |
| 574 | # Copy filtered instructions |
| 575 | if ($preparedPkgJson.contributes.chatInstructions) { |
| 576 | foreach ($instr in $preparedPkgJson.contributes.chatInstructions) { |
| 577 | $srcPath = Join-Path $RepoRoot ($instr.path -replace '^\.[\\/]', '') |
| 578 | if (-not (Test-Path $srcPath)) { |
| 579 | Write-Warning "Skipping missing collection artifact: $srcPath (referenced by contributes.chatInstructions in package.json)" |
| 580 | continue |
| 581 | } |
| 582 | $destPath = Join-Path $ExtensionDirectory ($instr.path -replace '^\.[\\/]', '') |
| 583 | $destDir = Split-Path $destPath -Parent |
| 584 | New-Item -Path $destDir -ItemType Directory -Force | Out-Null |
| 585 | Copy-Item -Path $srcPath -Destination $destPath -Force |
| 586 | } |
| 587 | } |
| 588 | |
| 589 | # Copy filtered skills |
| 590 | if ($preparedPkgJson.contributes.chatSkills) { |
| 591 | foreach ($skill in $preparedPkgJson.contributes.chatSkills) { |
| 592 | $srcPath = Join-Path $RepoRoot ($skill.path -replace '^\.[\\/]', '') |
| 593 | if (-not (Test-Path $srcPath)) { |
| 594 | Write-Warning "Skipping missing collection artifact: $srcPath (referenced by contributes.chatSkills in package.json)" |
| 595 | continue |
| 596 | } |
| 597 | # Copy the full skill directory, not just SKILL.md |
| 598 | $srcDir = Split-Path $srcPath -Parent |
| 599 | $destPath = Join-Path $ExtensionDirectory ($skill.path -replace '^\.[\\/]', '') |
| 600 | $destDir = Split-Path $destPath -Parent |
| 601 | Copy-DirectoryFiltered -Source $srcDir -Destination $destDir |
| 602 | |
| 603 | # Remove co-located test directories from packaged skills |
| 604 | Get-ChildItem -Path $destDir -Directory -Filter 'tests' -Recurse -ErrorAction SilentlyContinue | |
| 605 | Remove-Item -Recurse -Force |
| 606 | } |
| 607 | } |
| 608 | } |
| 609 | |
| 610 | function Set-CollectionReadme { |
| 611 | <# |
| 612 | .SYNOPSIS |
| 613 | Swaps or restores the collection-specific README for extension packaging. |
| 614 | .DESCRIPTION |
| 615 | In swap mode, backs up the original README.md and copies the collection |
| 616 | README in its place. In restore mode, copies the backup back and removes it. |
| 617 | .PARAMETER ExtensionDirectory |
| 618 | Path to the extension directory. |
| 619 | .PARAMETER CollectionReadmePath |
| 620 | Path to the collection-specific README file. Required for Swap operation. |
| 621 | .PARAMETER Operation |
| 622 | Either 'Swap' to replace README.md with collection content, or 'Restore' |
| 623 | to revert README.md from backup. |
| 624 | #> |
| 625 | [CmdletBinding()] |
| 626 | param( |
| 627 | [Parameter(Mandatory = $true)] |
| 628 | [string]$ExtensionDirectory, |
| 629 | |
| 630 | [Parameter(Mandatory = $false)] |
| 631 | [string]$CollectionReadmePath = "", |
| 632 | |
| 633 | [Parameter(Mandatory = $true)] |
| 634 | [ValidateSet('Swap', 'Restore')] |
| 635 | [string]$Operation |
| 636 | ) |
| 637 | |
| 638 | $readmePath = Join-Path $ExtensionDirectory "README.md" |
| 639 | $backupPath = Join-Path $ExtensionDirectory "README.md.bak" |
| 640 | |
| 641 | if ($Operation -eq 'Swap') { |
| 642 | if (-not $CollectionReadmePath -or $CollectionReadmePath -eq "") { |
| 643 | Write-Warning "No collection README path provided for swap operation" |
| 644 | return |
| 645 | } |
| 646 | Copy-Item -Path $readmePath -Destination $backupPath -Force |
| 647 | Copy-Item -Path $CollectionReadmePath -Destination $readmePath -Force |
| 648 | Write-Host " Swapped README.md with $(Split-Path $CollectionReadmePath -Leaf)" -ForegroundColor Green |
| 649 | } |
| 650 | elseif ($Operation -eq 'Restore') { |
| 651 | if (Test-Path $backupPath) { |
| 652 | Copy-Item -Path $backupPath -Destination $readmePath -Force |
| 653 | Remove-Item -Path $backupPath -Force |
| 654 | Write-Host " Restored original README.md" -ForegroundColor Green |
| 655 | } |
| 656 | } |
| 657 | } |
| 658 | |
| 659 | function Invoke-VsceCommand { |
| 660 | <# |
| 661 | .SYNOPSIS |
| 662 | Executes vsce package command with platform-appropriate wrapper. |
| 663 | .DESCRIPTION |
| 664 | Abstracts platform-specific execution of vsce/npx commands. On Windows with npx, |
| 665 | uses cmd /c to avoid PowerShell misinterpreting @ in @vscode/vsce as splatting. |
| 666 | The UseWindowsWrapper parameter enables deterministic platform behavior in tests. |
| 667 | .PARAMETER Executable |
| 668 | The executable to run ('vsce' or 'npx'). |
| 669 | .PARAMETER Arguments |
| 670 | Array of arguments to pass to the executable. |
| 671 | .PARAMETER WorkingDirectory |
| 672 | Directory to execute the command in. |
| 673 | .PARAMETER UseWindowsWrapper |
| 674 | When true and Executable is 'npx', uses cmd /c wrapper for Windows compatibility. |
| 675 | .OUTPUTS |
| 676 | Hashtable with Success boolean and ExitCode integer. |
| 677 | #> |
| 678 | [CmdletBinding()] |
| 679 | [OutputType([hashtable])] |
| 680 | param( |
| 681 | [Parameter(Mandatory = $true)] |
| 682 | [string]$Executable, |
| 683 | |
| 684 | [Parameter(Mandatory = $true)] |
| 685 | [string[]]$Arguments, |
| 686 | |
| 687 | [Parameter(Mandatory = $true)] |
| 688 | [string]$WorkingDirectory, |
| 689 | |
| 690 | [Parameter(Mandatory = $false)] |
| 691 | [switch]$UseWindowsWrapper |
| 692 | ) |
| 693 | |
| 694 | Push-Location $WorkingDirectory |
| 695 | try { |
| 696 | $global:LASTEXITCODE = 0 |
| 697 | |
| 698 | if ($UseWindowsWrapper -and $Executable -eq 'npx') { |
| 699 | $cmdArgs = @('/c', 'npx') + $Arguments |
| 700 | & cmd @cmdArgs |
| 701 | } else { |
| 702 | & $Executable @Arguments |
| 703 | } |
| 704 | |
| 705 | return @{ |
| 706 | Success = ($LASTEXITCODE -eq 0) |
| 707 | ExitCode = $LASTEXITCODE |
| 708 | } |
| 709 | } |
| 710 | finally { |
| 711 | Pop-Location |
| 712 | } |
| 713 | } |
| 714 | |
| 715 | function Remove-PackagingArtifacts { |
| 716 | <# |
| 717 | .SYNOPSIS |
| 718 | Removes temporary directories created during packaging. |
| 719 | .DESCRIPTION |
| 720 | Cleans up directories copied to the extension folder during the packaging process. |
| 721 | Silently skips directories that do not exist. |
| 722 | .PARAMETER ExtensionDirectory |
| 723 | Absolute path to the extension directory. |
| 724 | .PARAMETER DirectoryNames |
| 725 | Array of directory names to remove. Defaults to .github, docs, scripts. |
| 726 | #> |
| 727 | [CmdletBinding()] |
| 728 | param( |
| 729 | [Parameter(Mandatory = $true)] |
| 730 | [string]$ExtensionDirectory, |
| 731 | |
| 732 | [Parameter(Mandatory = $false)] |
| 733 | [string[]]$DirectoryNames = @(".github", "docs", "scripts") |
| 734 | ) |
| 735 | |
| 736 | foreach ($dir in $DirectoryNames) { |
| 737 | $dirPath = Join-Path $ExtensionDirectory $dir |
| 738 | if (Test-Path $dirPath) { |
| 739 | Remove-Item -Path $dirPath -Recurse -Force |
| 740 | Write-Host " Removed $dir" -ForegroundColor Gray |
| 741 | } |
| 742 | } |
| 743 | } |
| 744 | |
| 745 | function Restore-PackageJsonVersion { |
| 746 | <# |
| 747 | .SYNOPSIS |
| 748 | Restores original version in package.json after packaging. |
| 749 | .DESCRIPTION |
| 750 | Writes the original version back to package.json if it was temporarily modified |
| 751 | during packaging. Safely handles null inputs by returning early. |
| 752 | .PARAMETER PackageJsonPath |
| 753 | Absolute path to the package.json file. |
| 754 | .PARAMETER PackageJson |
| 755 | The parsed package.json object to modify. |
| 756 | .PARAMETER OriginalVersion |
| 757 | The original version string to restore. |
| 758 | #> |
| 759 | [CmdletBinding()] |
| 760 | param( |
| 761 | [Parameter(Mandatory = $false)] |
| 762 | [AllowNull()] |
| 763 | [string]$PackageJsonPath, |
| 764 | |
| 765 | [Parameter(Mandatory = $false)] |
| 766 | [AllowNull()] |
| 767 | [PSObject]$PackageJson, |
| 768 | |
| 769 | [Parameter(Mandatory = $false)] |
| 770 | [AllowNull()] |
| 771 | [string]$OriginalVersion |
| 772 | ) |
| 773 | |
| 774 | # Handle null coercion: PowerShell converts $null to empty string for [string] params |
| 775 | if ([string]::IsNullOrEmpty($OriginalVersion) -or $null -eq $PackageJson -or [string]::IsNullOrEmpty($PackageJsonPath)) { |
| 776 | return |
| 777 | } |
| 778 | |
| 779 | try { |
| 780 | $PackageJson.version = $OriginalVersion |
| 781 | $PackageJson | ConvertTo-Json -Depth 10 | Set-Content -Path $PackageJsonPath -Encoding UTF8NoBOM |
| 782 | Write-Host " Version restored to: $OriginalVersion" -ForegroundColor Green |
| 783 | } |
| 784 | catch { |
| 785 | Write-Warning "Failed to restore original package.json version to '$OriginalVersion': $($_.Exception.Message)" |
| 786 | } |
| 787 | } |
| 788 | |
| 789 | #endregion I/O Functions |
| 790 | |
| 791 | #region Orchestration Functions |
| 792 | |
| 793 | function Invoke-PackageExtension { |
| 794 | <# |
| 795 | .SYNOPSIS |
| 796 | Orchestrates VS Code extension packaging with full error handling. |
| 797 | .DESCRIPTION |
| 798 | Executes the complete packaging workflow: validates paths, resolves version, |
| 799 | prepares directories, invokes vsce, and handles cleanup. |
| 800 | .PARAMETER ExtensionDirectory |
| 801 | Absolute path to the extension directory containing package.json. |
| 802 | .PARAMETER RepoRoot |
| 803 | Absolute path to the repository root directory. |
| 804 | .PARAMETER Version |
| 805 | Optional explicit version string (e.g., "1.2.3"). |
| 806 | .PARAMETER DevPatchNumber |
| 807 | Optional dev build patch number for pre-release versions. |
| 808 | .PARAMETER ChangelogPath |
| 809 | Optional path to changelog file to include in package. |
| 810 | .PARAMETER PreRelease |
| 811 | Switch to mark the package as a pre-release version. |
| 812 | .PARAMETER Collection |
| 813 | Optional path to a collection manifest file (YAML or JSON). When specified, only |
| 814 | collection-filtered artifacts are copied and the output filename uses the |
| 815 | collection ID. |
| 816 | .PARAMETER DryRun |
| 817 | When specified, validates packaging orchestration without invoking vsce. |
| 818 | .OUTPUTS |
| 819 | Hashtable with Success, OutputPath, Version, and ErrorMessage properties. |
| 820 | #> |
| 821 | [CmdletBinding()] |
| 822 | [OutputType([hashtable])] |
| 823 | param( |
| 824 | [Parameter(Mandatory = $true)] |
| 825 | [ValidateNotNullOrEmpty()] |
| 826 | [string]$ExtensionDirectory, |
| 827 | |
| 828 | [Parameter(Mandatory = $true)] |
| 829 | [ValidateNotNullOrEmpty()] |
| 830 | [string]$RepoRoot, |
| 831 | |
| 832 | [Parameter(Mandatory = $false)] |
| 833 | [string]$Version = "", |
| 834 | |
| 835 | [Parameter(Mandatory = $false)] |
| 836 | [string]$DevPatchNumber = "", |
| 837 | |
| 838 | [Parameter(Mandatory = $false)] |
| 839 | [string]$ChangelogPath = "", |
| 840 | |
| 841 | [Parameter(Mandatory = $false)] |
| 842 | [switch]$PreRelease, |
| 843 | |
| 844 | [Parameter(Mandatory = $false)] |
| 845 | [string]$Collection = "", |
| 846 | |
| 847 | [Parameter(Mandatory = $false)] |
| 848 | [switch]$DryRun |
| 849 | ) |
| 850 | |
| 851 | $dirsToClean = @(".github", "docs", "scripts") |
| 852 | $originalVersion = $null |
| 853 | $packageJson = $null |
| 854 | $PackageJsonPath = $null |
| 855 | $packageVersion = $null |
| 856 | $versionWasModified = $false |
| 857 | |
| 858 | try { |
| 859 | # Validate all inputs using pure function |
| 860 | $inputValidation = Test-PackagingInputsValid -ExtensionDirectory $ExtensionDirectory -RepoRoot $RepoRoot |
| 861 | if (-not $inputValidation.IsValid) { |
| 862 | return New-PackagingResult -Success $false -ErrorMessage ($inputValidation.Errors -join '; ') |
| 863 | } |
| 864 | |
| 865 | $PackageJsonPath = $inputValidation.PackageJsonPath |
| 866 | |
| 867 | Write-Host "๐ฆ HVE Core Extension Packager" -ForegroundColor Cyan |
| 868 | Write-Host "==============================" -ForegroundColor Cyan |
| 869 | Write-Host "" |
| 870 | |
| 871 | # Read and validate package.json |
| 872 | Write-Host "๐ Reading package.json..." -ForegroundColor Yellow |
| 873 | try { |
| 874 | $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json |
| 875 | } |
| 876 | catch { |
| 877 | return New-PackagingResult -Success $false -ErrorMessage "Failed to parse package.json: $($_.Exception.Message)" |
| 878 | } |
| 879 | |
| 880 | $manifestValidation = Test-ExtensionManifestValid -ManifestContent $packageJson |
| 881 | if (-not $manifestValidation.IsValid) { |
| 882 | return New-PackagingResult -Success $false -ErrorMessage "Invalid package.json: $($manifestValidation.Errors -join '; ')" |
| 883 | } |
| 884 | |
| 885 | # Resolve version using pure function |
| 886 | $versionResult = Get-ResolvedPackageVersion ` |
| 887 | -SpecifiedVersion $Version ` |
| 888 | -ManifestVersion $packageJson.version ` |
| 889 | -DevPatchNumber $DevPatchNumber |
| 890 | |
| 891 | if (-not $versionResult.IsValid) { |
| 892 | return New-PackagingResult -Success $false -ErrorMessage $versionResult.ErrorMessage |
| 893 | } |
| 894 | |
| 895 | $packageVersion = $versionResult.PackageVersion |
| 896 | Write-Host " Using version: $packageVersion" -ForegroundColor Green |
| 897 | |
| 898 | # Handle temporary version update for dev builds |
| 899 | $originalVersion = $packageJson.version |
| 900 | |
| 901 | if ($packageVersion -ne $originalVersion) { |
| 902 | Write-Host "" |
| 903 | Write-Host "๐ Temporarily updating package.json version..." -ForegroundColor Yellow |
| 904 | $packageJson.version = $packageVersion |
| 905 | $packageJson | ConvertTo-Json -Depth 10 | Set-Content -Path $PackageJsonPath -Encoding UTF8NoBOM |
| 906 | Write-Host " Version: $originalVersion -> $packageVersion" -ForegroundColor Green |
| 907 | $versionWasModified = $true |
| 908 | } |
| 909 | |
| 910 | # Handle changelog if provided |
| 911 | if ($ChangelogPath -and $ChangelogPath -ne "") { |
| 912 | Write-Host "" |
| 913 | Write-Host "๐ Processing changelog..." -ForegroundColor Yellow |
| 914 | |
| 915 | if (Test-Path $ChangelogPath) { |
| 916 | $changelogDest = Join-Path $ExtensionDirectory "CHANGELOG.md" |
| 917 | Copy-Item -Path $ChangelogPath -Destination $changelogDest -Force |
| 918 | Write-Host " Copied changelog to extension directory" -ForegroundColor Green |
| 919 | } |
| 920 | else { |
| 921 | Write-Warning "Changelog file not found: $ChangelogPath" |
| 922 | } |
| 923 | } |
| 924 | |
| 925 | # Prepare extension directory |
| 926 | Write-Host "" |
| 927 | Write-Host "๐๏ธ Preparing extension directory..." -ForegroundColor Yellow |
| 928 | |
| 929 | # Clean any existing copied directories |
| 930 | foreach ($dir in $dirsToClean) { |
| 931 | $dirPath = Join-Path $ExtensionDirectory $dir |
| 932 | if (Test-Path $dirPath) { |
| 933 | Remove-Item -Path $dirPath -Recurse -Force |
| 934 | Write-Host " Cleaned existing $dir directory" -ForegroundColor Gray |
| 935 | } |
| 936 | } |
| 937 | |
| 938 | # Get and execute copy specifications |
| 939 | $copySpecs = Get-PackagingDirectorySpec -RepoRoot $RepoRoot -ExtensionDirectory $ExtensionDirectory |
| 940 | |
| 941 | if ($Collection -and $Collection -ne "") { |
| 942 | # Collection mode: copy only filtered artifacts for .github content |
| 943 | Write-Host " Using collection-filtered artifact copy..." -ForegroundColor Gray |
| 944 | |
| 945 | # Copy non-.github specs normally |
| 946 | foreach ($spec in $copySpecs) { |
| 947 | if ($spec.Source -like "*/.github*" -or $spec.Source -like "*\.github*") { |
| 948 | continue |
| 949 | } |
| 950 | $specName = Split-Path $spec.Source -Leaf |
| 951 | Write-Host " Copying $specName..." -ForegroundColor Gray |
| 952 | |
| 953 | if ($spec.IsFile) { |
| 954 | $parentDir = Split-Path $spec.Destination -Parent |
| 955 | New-Item -Path $parentDir -ItemType Directory -Force | Out-Null |
| 956 | Copy-Item -Path $spec.Source -Destination $spec.Destination -Force |
| 957 | } else { |
| 958 | $parentDir = Split-Path $spec.Destination -Parent |
| 959 | if (-not (Test-Path $parentDir)) { |
| 960 | New-Item -Path $parentDir -ItemType Directory -Force | Out-Null |
| 961 | } |
| 962 | Copy-Item -Path $spec.Source -Destination $spec.Destination -Recurse -Force |
| 963 | } |
| 964 | } |
| 965 | |
| 966 | # Copy collection-specific artifacts |
| 967 | Copy-CollectionArtifacts -RepoRoot $RepoRoot -ExtensionDirectory $ExtensionDirectory -PrepareResult @{} |
| 968 | } else { |
| 969 | # Full mode: copy everything, filtering out dev artifacts during copy |
| 970 | foreach ($spec in $copySpecs) { |
| 971 | $specName = Split-Path $spec.Source -Leaf |
| 972 | Write-Host " Copying $specName..." -ForegroundColor Gray |
| 973 | |
| 974 | if ($spec.IsFile) { |
| 975 | $parentDir = Split-Path $spec.Destination -Parent |
| 976 | New-Item -Path $parentDir -ItemType Directory -Force | Out-Null |
| 977 | Copy-Item -Path $spec.Source -Destination $spec.Destination -Force |
| 978 | } else { |
| 979 | Copy-DirectoryFiltered -Source $spec.Source -Destination $spec.Destination |
| 980 | } |
| 981 | } |
| 982 | } |
| 983 | |
| 984 | # Remove test directories from copied skills (not excluded by Copy-DirectoryFiltered) |
| 985 | $skillsDir = Join-Path $ExtensionDirectory ".github" "skills" |
| 986 | if (Test-Path $skillsDir) { |
| 987 | Get-ChildItem -Path $skillsDir -Directory -Filter 'tests' -Recurse -ErrorAction SilentlyContinue | |
| 988 | Remove-Item -Recurse -Force |
| 989 | } |
| 990 | |
| 991 | Write-Host " โ
Extension directory prepared" -ForegroundColor Green |
| 992 | |
| 993 | # Swap collection README if collection specifies one |
| 994 | if ($Collection -and $Collection -ne "") { |
| 995 | $collectionReadmePath = Get-CollectionReadmePath -CollectionPath $Collection -ExtensionDirectory $ExtensionDirectory |
| 996 | if ($collectionReadmePath) { |
| 997 | Write-Host "" |
| 998 | Write-Host "๐ Applying collection README..." -ForegroundColor Yellow |
| 999 | Set-CollectionReadme -ExtensionDirectory $ExtensionDirectory -CollectionReadmePath $collectionReadmePath -Operation Swap |
| 1000 | } |
| 1001 | } |
| 1002 | |
| 1003 | if ($DryRun) { |
| 1004 | Write-Host "" |
| 1005 | Write-Host "๐งช Dry-run complete: packaging orchestration validated without VSIX creation." -ForegroundColor Yellow |
| 1006 | return New-PackagingResult -Success $true -Version $packageVersion |
| 1007 | } |
| 1008 | |
| 1009 | # Check vsce availability using pure function |
| 1010 | $vsceAvailability = Test-VsceAvailable |
| 1011 | if (-not $vsceAvailability.IsAvailable) { |
| 1012 | return New-PackagingResult -Success $false -ErrorMessage "Neither vsce nor npx found. Please install @vscode/vsce globally or ensure npm is available." |
| 1013 | } |
| 1014 | |
| 1015 | # Build vsce command using pure function |
| 1016 | $vsceCommand = Get-VscePackageCommand -CommandType $vsceAvailability.CommandType -PreRelease:$PreRelease |
| 1017 | |
| 1018 | # Package extension |
| 1019 | Write-Host "" |
| 1020 | Write-Host "๐ฆ Packaging extension..." -ForegroundColor Yellow |
| 1021 | |
| 1022 | if ($PreRelease) { |
| 1023 | Write-Host " Mode: Pre-release channel" -ForegroundColor Magenta |
| 1024 | } |
| 1025 | |
| 1026 | Write-Host " Using $($vsceAvailability.CommandType)..." -ForegroundColor Gray |
| 1027 | |
| 1028 | # Execute vsce command using I/O function |
| 1029 | $useWindowsWrapper = ($IsWindows -or $env:OS -eq 'Windows_NT') -and ($vsceCommand.Executable -eq 'npx') |
| 1030 | $vsceResult = Invoke-VsceCommand ` |
| 1031 | -Executable $vsceCommand.Executable ` |
| 1032 | -Arguments $vsceCommand.Arguments ` |
| 1033 | -WorkingDirectory $ExtensionDirectory ` |
| 1034 | -UseWindowsWrapper:$useWindowsWrapper |
| 1035 | |
| 1036 | if (-not $vsceResult.Success) { |
| 1037 | return New-PackagingResult -Success $false -ErrorMessage "vsce package command failed with exit code $($vsceResult.ExitCode)" |
| 1038 | } |
| 1039 | |
| 1040 | # Find the generated vsix file |
| 1041 | $vsixFile = Get-ChildItem -Path $ExtensionDirectory -Filter "*.vsix" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 |
| 1042 | |
| 1043 | if (-not $vsixFile) { |
| 1044 | return New-PackagingResult -Success $false -ErrorMessage "No .vsix file found after packaging" |
| 1045 | } |
| 1046 | |
| 1047 | Write-Host "" |
| 1048 | Write-Host "โ
Extension packaged successfully!" -ForegroundColor Green |
| 1049 | Write-Host " File: $($vsixFile.Name)" -ForegroundColor Cyan |
| 1050 | Write-Host " Size: $([math]::Round($vsixFile.Length / 1KB, 2)) KB" -ForegroundColor Cyan |
| 1051 | Write-Host " Version: $packageVersion" -ForegroundColor Cyan |
| 1052 | |
| 1053 | # Output for CI/CD consumption |
| 1054 | Set-CIOutput -Name 'version' -Value $packageVersion |
| 1055 | Set-CIOutput -Name 'vsix-file' -Value $vsixFile.Name |
| 1056 | Set-CIOutput -Name 'pre-release' -Value $PreRelease.IsPresent |
| 1057 | |
| 1058 | Write-Host "" |
| 1059 | Write-Host "๐ Done!" -ForegroundColor Green |
| 1060 | Write-Host "" |
| 1061 | |
| 1062 | return New-PackagingResult -Success $true -OutputPath $vsixFile.FullName -Version $packageVersion |
| 1063 | } |
| 1064 | catch { |
| 1065 | return New-PackagingResult -Success $false -ErrorMessage $_.Exception.Message |
| 1066 | } |
| 1067 | finally { |
| 1068 | # Restore canonical package.json from collection template backup |
| 1069 | $backupPath = Join-Path $ExtensionDirectory "package.json.bak" |
| 1070 | if (Test-Path $backupPath) { |
| 1071 | Copy-Item -Path $backupPath -Destination $PackageJsonPath -Force |
| 1072 | Remove-Item -Path $backupPath -Force |
| 1073 | Write-Host " Restored canonical package.json from backup" -ForegroundColor Green |
| 1074 | |
| 1075 | # Re-read restored package.json for downstream restore steps |
| 1076 | $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json |
| 1077 | } |
| 1078 | |
| 1079 | # Restore collection README if it was swapped |
| 1080 | Set-CollectionReadme -ExtensionDirectory $ExtensionDirectory -Operation Restore |
| 1081 | |
| 1082 | # Cleanup copied directories using I/O function |
| 1083 | Write-Host "" |
| 1084 | Write-Host "๐งน Cleaning up..." -ForegroundColor Yellow |
| 1085 | Remove-PackagingArtifacts -ExtensionDirectory $ExtensionDirectory -DirectoryNames $dirsToClean |
| 1086 | |
| 1087 | # Restore original version if it was changed using I/O function |
| 1088 | if ($versionWasModified) { |
| 1089 | Write-Host "" |
| 1090 | Write-Host "๐ Restoring original package.json version..." -ForegroundColor Yellow |
| 1091 | Restore-PackageJsonVersion -PackageJsonPath $PackageJsonPath -PackageJson $packageJson -OriginalVersion $originalVersion |
| 1092 | } |
| 1093 | } |
| 1094 | } |
| 1095 | |
| 1096 | #endregion Orchestration Functions |
| 1097 | |
| 1098 | #region Main Execution |
| 1099 | if ($MyInvocation.InvocationName -ne '.') { |
| 1100 | try { |
| 1101 | $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path |
| 1102 | $RepoRoot = (Get-Item "$ScriptDir/../..").FullName |
| 1103 | $ExtensionDir = Join-Path $RepoRoot "extension" |
| 1104 | |
| 1105 | $result = Invoke-PackageExtension ` |
| 1106 | -ExtensionDirectory $ExtensionDir ` |
| 1107 | -RepoRoot $RepoRoot ` |
| 1108 | -Version $Version ` |
| 1109 | -DevPatchNumber $DevPatchNumber ` |
| 1110 | -ChangelogPath $ChangelogPath ` |
| 1111 | -PreRelease:$PreRelease ` |
| 1112 | -Collection $Collection ` |
| 1113 | -DryRun:$DryRun |
| 1114 | |
| 1115 | if (-not $result.Success) { |
| 1116 | Write-Error -ErrorAction Continue $result.ErrorMessage |
| 1117 | exit 1 |
| 1118 | } |
| 1119 | exit 0 |
| 1120 | } |
| 1121 | catch { |
| 1122 | Write-Error -ErrorAction Continue "Package-Extension failed: $($_.Exception.Message)" |
| 1123 | Write-CIAnnotation -Message $_.Exception.Message -Level Error |
| 1124 | exit 1 |
| 1125 | } |
| 1126 | } |
| 1127 | #endregion Main Execution |