microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
docs/621-ai-artifacts

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/extension/Package-Extension.ps1

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