microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/model-selection

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/extension/Prepare-Extension.ps1

1877lines · 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 Prepares the HVE Core VS Code extension for packaging.
9
10.DESCRIPTION
11 This script prepares the VS Code extension by:
12 - Auto-discovering chat agents, prompts, and instruction files
13 - Filtering agents by maturity level based on channel
14 - Updating package.json with discovered components
15 - Updating changelog if provided
16
17 The package.json version is not modified.
18
19.PARAMETER ChangelogPath
20 Optional. Path to a changelog file to include in the package.
21
22.PARAMETER Channel
23 Optional. Release channel controlling which maturity levels are included.
24 'Stable' (default): Only includes agents with maturity 'stable'.
25 'PreRelease': Includes 'stable', 'preview', and 'experimental' maturity levels.
26
27.PARAMETER DryRun
28 Optional. If specified, shows what would be done without making changes.
29
30.EXAMPLE
31 ./Prepare-Extension.ps1
32 # Prepares stable channel using existing version from package.json
33
34.EXAMPLE
35 ./Prepare-Extension.ps1 -Channel PreRelease
36 # Prepares pre-release channel including experimental agents
37
38.EXAMPLE
39 ./Prepare-Extension.ps1 -ChangelogPath "./CHANGELOG.md"
40 # Prepares with changelog
41
42.NOTES
43 Dependencies: PowerShell-Yaml module
44#>
45
46[CmdletBinding()]
47param(
48 [Parameter(Mandatory = $false)]
49 [string]$ChangelogPath = "",
50
51 [Parameter(Mandatory = $false)]
52 [ValidateSet('Stable', 'PreRelease')]
53 [string]$Channel = 'Stable',
54
55 [Parameter(Mandatory = $false)]
56 [switch]$DryRun,
57
58 [Parameter(Mandatory = $false)]
59 [string]$Collection = ""
60)
61
62$ErrorActionPreference = 'Stop'
63
64Import-Module (Join-Path $PSScriptRoot "../lib/Modules/CIHelpers.psm1") -Force
65Import-Module (Join-Path $PSScriptRoot "../collections/Modules/CollectionHelpers.psm1") -Force
66
67#region Pure Functions
68
69#region Package Generation Functions
70
71function Get-CollectionDisplayName {
72 <#
73 .SYNOPSIS
74 Resolves a display name from a collection manifest.
75 .DESCRIPTION
76 Returns the displayName field if set, derives one from the name field,
77 or falls back to a default value.
78 .PARAMETER CollectionManifest
79 Parsed collection manifest hashtable.
80 .PARAMETER DefaultValue
81 Fallback display name when the manifest provides neither displayName nor name.
82 .OUTPUTS
83 [string] Resolved display name.
84 #>
85 [CmdletBinding()]
86 [OutputType([string])]
87 param(
88 [Parameter(Mandatory = $true)]
89 [hashtable]$CollectionManifest,
90
91 [Parameter(Mandatory = $true)]
92 [string]$DefaultValue
93 )
94
95 if ($CollectionManifest.ContainsKey('displayName') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionManifest.displayName)) {
96 return [string]$CollectionManifest.displayName
97 }
98
99 if ($CollectionManifest.ContainsKey('name') -and -not [string]::IsNullOrWhiteSpace([string]$CollectionManifest.name)) {
100 return "HVE Core - $($CollectionManifest.name)"
101 }
102
103 return $DefaultValue
104}
105
106function Copy-TemplateWithOverrides {
107 <#
108 .SYNOPSIS
109 Clones a template object and applies field overrides.
110 .DESCRIPTION
111 Copies all properties from Template, replacing any whose key appears in
112 Overrides. Additional override keys not in the template are appended.
113 .PARAMETER Template
114 Source PSCustomObject to clone.
115 .PARAMETER Overrides
116 Hashtable of field values to override or add.
117 .OUTPUTS
118 [pscustomobject] New object with overrides applied.
119 #>
120 [CmdletBinding()]
121 [OutputType([pscustomobject])]
122 param(
123 [Parameter(Mandatory = $true)]
124 [pscustomobject]$Template,
125
126 [Parameter(Mandatory = $true)]
127 [hashtable]$Overrides
128 )
129
130 $output = [ordered]@{}
131
132 foreach ($propertyName in $Template.PSObject.Properties.Name) {
133 if ($Overrides.ContainsKey($propertyName)) {
134 $output[$propertyName] = $Overrides[$propertyName]
135 }
136 else {
137 $output[$propertyName] = $Template.$propertyName
138 }
139 }
140
141 foreach ($propertyName in $Overrides.Keys | Sort-Object) {
142 if (-not $output.Contains($propertyName)) {
143 $output[$propertyName] = $Overrides[$propertyName]
144 }
145 }
146
147 return [pscustomobject]$output
148}
149
150function Set-JsonFile {
151 <#
152 .SYNOPSIS
153 Writes an object to a JSON file with UTF-8 encoding.
154 .DESCRIPTION
155 Serializes Content to JSON and writes to Path, creating parent
156 directories as needed.
157 .PARAMETER Path
158 Destination file path.
159 .PARAMETER Content
160 Object to serialize.
161 #>
162 [CmdletBinding()]
163 param(
164 [Parameter(Mandatory = $true)]
165 [string]$Path,
166
167 [Parameter(Mandatory = $true)]
168 [object]$Content
169 )
170
171 $parent = Split-Path -Path $Path -Parent
172 if (-not (Test-Path -Path $parent)) {
173 New-Item -Path $parent -ItemType Directory -Force | Out-Null
174 }
175
176 $json = $Content | ConvertTo-Json -Depth 30
177 Set-Content -Path $Path -Value $json -Encoding utf8NoBOM
178}
179
180function Remove-StaleGeneratedFiles {
181 <#
182 .SYNOPSIS
183 Removes generated collection package files that are no longer expected.
184 .DESCRIPTION
185 Scans extension/ for package.*.json files and removes any not in the
186 expected set, keeping the directory clean of orphaned collection templates.
187 .PARAMETER RepoRoot
188 Repository root path.
189 .PARAMETER ExpectedFiles
190 Array of absolute paths that should be retained.
191 #>
192 [CmdletBinding()]
193 param(
194 [Parameter(Mandatory = $true)]
195 [string]$RepoRoot,
196
197 [Parameter(Mandatory = $true)]
198 [AllowEmptyCollection()]
199 [string[]]$ExpectedFiles
200 )
201
202 $expected = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
203 foreach ($file in $ExpectedFiles) {
204 $null = $expected.Add([System.IO.Path]::GetFullPath($file))
205 }
206
207 $extensionDir = Join-Path $RepoRoot 'extension'
208 Get-ChildItem -Path $extensionDir -Filter 'package.*.json' -File | ForEach-Object {
209 $fullPath = [System.IO.Path]::GetFullPath($_.FullName)
210 if (-not $expected.Contains($fullPath)) {
211 Remove-Item -Path $_.FullName -Force
212 }
213 }
214}
215
216function Invoke-ExtensionCollectionsGeneration {
217 <#
218 .SYNOPSIS
219 Generates collection package files from root collection manifests.
220 .DESCRIPTION
221 Reads the package template and each collections/*.collection.yml file,
222 producing extension/package.json (for hve-core) and
223 extension/package.{id}.json for every other collection. Stale collection
224 files are removed.
225 .PARAMETER RepoRoot
226 Repository root path containing collections/ and extension/templates/.
227 .PARAMETER Channel
228 Release channel controlling maturity filtering for README generation.
229 .OUTPUTS
230 [string[]] Array of generated file paths.
231 #>
232 [CmdletBinding()]
233 [OutputType([string[]])]
234 param(
235 [Parameter(Mandatory = $true)]
236 [string]$RepoRoot,
237
238 [ValidateSet('Stable', 'PreRelease')]
239 [string]$Channel = 'Stable'
240 )
241
242 $collectionsDir = Join-Path $RepoRoot 'collections'
243 $templatesDir = Join-Path $RepoRoot 'extension/templates'
244
245 $allowedMaturities = Get-AllowedMaturities -Channel $Channel
246
247 $packageTemplatePath = Join-Path $templatesDir 'package.template.json'
248
249 if (-not (Test-Path $packageTemplatePath)) {
250 throw "Package template not found: $packageTemplatePath"
251 }
252
253 if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) {
254 throw "Required module 'PowerShell-Yaml' is not installed."
255 }
256
257 Import-Module PowerShell-Yaml -ErrorAction Stop
258
259 $packageTemplate = Get-Content -Path $packageTemplatePath -Raw | ConvertFrom-Json
260
261 $collectionFiles = Get-ChildItem -Path $collectionsDir -Filter '*.collection.yml' -File | Sort-Object Name
262 if ($collectionFiles.Count -eq 0) {
263 throw "No root collection files found in $collectionsDir"
264 }
265
266 $expectedFiles = @()
267
268 foreach ($collectionFile in $collectionFiles) {
269 $collection = Get-CollectionManifest -CollectionPath $collectionFile.FullName
270 if ($collection -isnot [hashtable]) {
271 throw "Collection manifest must be a hashtable: $($collectionFile.FullName)"
272 }
273
274 $collectionId = [string]$collection.id
275 if ([string]::IsNullOrWhiteSpace($collectionId)) {
276 throw "Collection id is required: $($collectionFile.FullName)"
277 }
278
279 $collectionDescription = if ($collection.ContainsKey('description')) { [string]$collection.description } else { [string]$packageTemplate.description }
280
281 $extensionName = switch ($collectionId) {
282 'hve-core' { [string]$packageTemplate.name }
283 'hve-core-all' { 'hve-core-all' }
284 default { "hve-$collectionId" }
285 }
286 $extensionDisplayName = switch ($collectionId) {
287 'hve-core' { [string]$packageTemplate.displayName }
288 'hve-core-all' { 'HVE Core - All' }
289 default { Get-CollectionDisplayName -CollectionManifest $collection -DefaultValue ([string]$packageTemplate.displayName) }
290 }
291
292 $packageTemplateOutput = Copy-TemplateWithOverrides -Template $packageTemplate -Overrides @{
293 name = $extensionName
294 displayName = $extensionDisplayName
295 description = $collectionDescription
296 }
297
298 $packagePath = switch ($collectionId) {
299 'hve-core' { Join-Path $RepoRoot 'extension/package.json' }
300 'hve-core-all' { Join-Path $RepoRoot 'extension/package.hve-core-all.json' }
301 default { Join-Path $RepoRoot "extension/package.$collectionId.json" }
302 }
303
304 Set-JsonFile -Path $packagePath -Content $packageTemplateOutput
305 $expectedFiles += $packagePath
306 }
307
308 Remove-StaleGeneratedFiles -RepoRoot $RepoRoot -ExpectedFiles $expectedFiles
309
310 # Generate README files for each collection
311 $readmeTemplatePath = Join-Path $templatesDir 'README.template.md'
312 foreach ($collectionFile in $collectionFiles) {
313 $collection = Get-CollectionManifest -CollectionPath $collectionFile.FullName
314 $collectionId = [string]$collection.id
315
316 $collectionMdPath = Join-Path $collectionsDir "$collectionId.collection.md"
317 if (-not (Test-Path $collectionMdPath)) {
318 continue
319 }
320
321 $readmePath = switch ($collectionId) {
322 'hve-core' { Join-Path $RepoRoot 'extension/README.md' }
323 'hve-core-all' { Join-Path $RepoRoot 'extension/README.hve-core-all.md' }
324 default { Join-Path $RepoRoot "extension/README.$collectionId.md" }
325 }
326
327 New-CollectionReadme -Collection $collection -CollectionMdPath $collectionMdPath -TemplatePath $readmeTemplatePath -RepoRoot $RepoRoot -OutputPath $readmePath -AllowedMaturities $allowedMaturities
328 }
329
330 return $expectedFiles
331}
332
333function New-CollectionReadme {
334 <#
335 .SYNOPSIS
336 Generates a README.md for an extension collection from a template.
337 .DESCRIPTION
338 Reads a README template and replaces placeholder tokens with collection
339 metadata, hand-authored body content, and auto-generated artifact tables
340 with descriptions read from each artifact's YAML frontmatter.
341 Tokens: {{DISPLAY_NAME}}, {{DESCRIPTION}}, {{BODY}}, {{ARTIFACTS}},
342 {{FULL_EDITION}}.
343 When the collection markdown file contains BEGIN/END markers, the
344 generated artifact section is written back into the source file via
345 Set-ContentIfChanged so the collection.md stays in sync.
346 .PARAMETER Collection
347 Parsed collection manifest hashtable.
348 .PARAMETER CollectionMdPath
349 Path to the collection markdown body file. When markers are present,
350 this file is updated in place with the generated artifact section.
351 .PARAMETER TemplatePath
352 Path to the README template file containing placeholder tokens.
353 .PARAMETER RepoRoot
354 Repository root path for resolving artifact file paths.
355 .PARAMETER OutputPath
356 Destination path for the generated README.
357 .PARAMETER AllowedMaturities
358 Maturity levels to include in artifact tables. Defaults to stable only.
359 #>
360 [CmdletBinding()]
361 param(
362 [Parameter(Mandatory = $true)]
363 [hashtable]$Collection,
364
365 [Parameter(Mandatory = $true)]
366 [string]$CollectionMdPath,
367
368 [Parameter(Mandatory = $true)]
369 [string]$TemplatePath,
370
371 [Parameter(Mandatory = $true)]
372 [string]$RepoRoot,
373
374 [Parameter(Mandatory = $true)]
375 [string]$OutputPath,
376
377 [ValidateNotNullOrEmpty()]
378 [string[]]$AllowedMaturities = @('stable')
379 )
380
381 $collectionId = [string]$Collection.id
382 $displayName = switch ($collectionId) {
383 'hve-core' { 'HVE Core' }
384 'hve-core-all' { 'HVE Core - All' }
385 default { Get-CollectionDisplayName -CollectionManifest $Collection -DefaultValue "HVE Core - $collectionId" }
386 }
387 $description = if ($Collection.ContainsKey('description')) { [string]$Collection.description } else { '' }
388
389 $collectionMaturity = if ($Collection.ContainsKey('maturity') -and -not [string]::IsNullOrWhiteSpace([string]$Collection.maturity)) {
390 [string]$Collection.maturity
391 } else { 'stable' }
392
393 $maturityNotice = if ($collectionMaturity -eq 'experimental') {
394 '> **⚠️ Experimental** — This collection is experimental and available only in the Pre-Release channel. Contents may change or be removed without notice.'
395 } else { '' }
396
397 $bodyContent = Get-Content -Path $CollectionMdPath -Raw
398 $parsed = Split-CollectionMdByMarkers -Content $bodyContent
399
400 if ($parsed.HasMarkers) {
401 $bodyForTemplate = $parsed.Intro
402 if (-not [string]::IsNullOrWhiteSpace($parsed.Footer)) {
403 $bodyForTemplate = $bodyForTemplate + "`n`n" + $parsed.Footer.TrimEnd()
404 }
405 } else {
406 $bodyForTemplate = $bodyContent.Trim()
407 }
408
409 # Collect artifacts with descriptions grouped by kind
410 $agents = @()
411 $prompts = @()
412 $instructions = @()
413 $skills = @()
414
415 if ($Collection.ContainsKey('items')) {
416 foreach ($item in $Collection.items) {
417 if (-not $item.ContainsKey('kind') -or -not $item.ContainsKey('path')) {
418 continue
419 }
420 $maturity = Resolve-CollectionItemMaturity -Maturity $item.maturity
421 if ($AllowedMaturities -and $AllowedMaturities -notcontains $maturity) {
422 continue
423 }
424 $kind = [string]$item.kind
425 $path = [string]$item.path
426 $artifactName = Get-CollectionArtifactKey -Kind $kind -Path $path
427
428 # Resolve full file path for frontmatter reading
429 $resolvedPath = Join-Path $RepoRoot ($path -replace '^\./', '')
430 if ($kind -eq 'skill') {
431 $resolvedPath = Join-Path $resolvedPath 'SKILL.md'
432 }
433 $artifactDesc = Get-ArtifactDescription -FilePath $resolvedPath
434
435 $entry = @{ Name = $artifactName; Description = $artifactDesc }
436 switch ($kind) {
437 'agent' { $agents += $entry }
438 'prompt' { $prompts += $entry }
439 'instruction' { $instructions += $entry }
440 'skill' { $skills += $entry }
441 }
442 }
443 }
444
445 # Build markdown tables for each artifact kind
446 $artifactSections = [System.Text.StringBuilder]::new()
447
448 foreach ($section in @(
449 @{ Title = 'Chat Agents'; Items = $agents },
450 @{ Title = 'Prompts'; Items = $prompts },
451 @{ Title = 'Instructions'; Items = $instructions },
452 @{ Title = 'Skills'; Items = $skills }
453 )) {
454 if ($section.Items.Count -eq 0) { continue }
455
456 $null = $artifactSections.AppendLine("### $($section.Title)")
457 $null = $artifactSections.AppendLine()
458 $null = $artifactSections.AppendLine('| Name | Description |')
459 $null = $artifactSections.AppendLine('|------|-------------|')
460 foreach ($entry in ($section.Items | Sort-Object { $_.Name })) {
461 $null = $artifactSections.AppendLine("| **$($entry.Name)** | $($entry.Description) |")
462 }
463 $null = $artifactSections.AppendLine()
464 }
465
466 # Write back updated artifact section into collection.md when markers are present
467 if ($parsed.HasMarkers) {
468 $generatedBlock = $artifactSections.ToString().TrimEnd()
469 $updatedCollectionMd = "$($parsed.Intro)`n`n$($CollectionMdBeginMarker)`n`n$generatedBlock`n`n$($CollectionMdEndMarker)"
470 if (-not [string]::IsNullOrWhiteSpace($parsed.Footer)) {
471 $updatedCollectionMd += "`n`n$($parsed.Footer.TrimEnd())"
472 }
473 $updatedCollectionMd += "`n"
474 Set-ContentIfChanged -Path $CollectionMdPath -Value $updatedCollectionMd
475 }
476
477 $fullEdition = if ($collectionId -notin @('hve-core', 'hve-core-all')) {
478 "## Full Edition`n`nLooking for more agents covering additional domains? Check out the full [HVE Core](https://marketplace.visualstudio.com/items?itemName=ise-hve-essentials.hve-core) extension."
479 }
480 else {
481 ''
482 }
483
484 # Read template and replace tokens
485 $template = Get-Content -Path $TemplatePath -Raw
486 $readmeContent = $template `
487 -replace '\{\{DISPLAY_NAME\}\}', $displayName `
488 -replace '\{\{DESCRIPTION\}\}', $description `
489 -replace '\{\{MATURITY_NOTICE\}\}', $maturityNotice `
490 -replace '\{\{BODY\}\}', $bodyForTemplate `
491 -replace '\{\{ARTIFACTS\}\}', $artifactSections.ToString().TrimEnd() `
492 -replace '\{\{FULL_EDITION\}\}', $fullEdition
493
494 # Clean up blank lines left by empty token replacements
495 $readmeContent = $readmeContent -replace '(\r?\n){3,}', "`n`n"
496 $readmeContent = $readmeContent.TrimEnd() + "`n"
497
498 Set-Content -Path $OutputPath -Value $readmeContent -Encoding utf8NoBOM -NoNewline
499}
500
501#endregion Package Generation Functions
502
503function Get-AllowedMaturities {
504 <#
505 .SYNOPSIS
506 Returns allowed maturity levels based on release channel.
507 .DESCRIPTION
508 Pure function that determines which maturity levels (stable, preview, experimental)
509 are included in the extension package based on the specified channel.
510 .PARAMETER Channel
511 Release channel. 'Stable' returns only stable; 'PreRelease' includes all levels.
512 .OUTPUTS
513 [string[]] Array of allowed maturity level strings.
514 #>
515 [CmdletBinding()]
516 [OutputType([string[]])]
517 param(
518 [Parameter(Mandatory = $true)]
519 [ValidateSet('Stable', 'PreRelease')]
520 [string]$Channel
521 )
522
523 if ($Channel -eq 'PreRelease') {
524 return @('stable', 'preview', 'experimental')
525 }
526 return @('stable')
527}
528
529function Test-CollectionMaturityEligible {
530 <#
531 .SYNOPSIS
532 Checks whether a collection is eligible for the specified release channel.
533 .DESCRIPTION
534 Pure function that evaluates collection-level maturity against channel rules.
535 Experimental collections are eligible only for PreRelease. Deprecated collections
536 are excluded from all channels.
537 .PARAMETER CollectionManifest
538 Parsed collection manifest hashtable.
539 .PARAMETER Channel
540 Release channel ('Stable' or 'PreRelease').
541 .OUTPUTS
542 [hashtable] With IsEligible bool and Reason string.
543 #>
544 [CmdletBinding()]
545 [OutputType([hashtable])]
546 param(
547 [Parameter(Mandatory = $true)]
548 [hashtable]$CollectionManifest,
549
550 [Parameter(Mandatory = $true)]
551 [ValidateSet('Stable', 'PreRelease')]
552 [string]$Channel
553 )
554
555 $maturity = 'stable'
556 if ($CollectionManifest.ContainsKey('maturity') -and $CollectionManifest['maturity']) {
557 $maturity = $CollectionManifest['maturity']
558 }
559
560 switch ($maturity) {
561 'removed' {
562 return @{
563 IsEligible = $false
564 Reason = "Collection '$($CollectionManifest.id)' is removed and excluded from all channels"
565 }
566 }
567 'deprecated' {
568 return @{
569 IsEligible = $false
570 Reason = "Collection '$($CollectionManifest.id)' is deprecated and excluded from all channels"
571 }
572 }
573 'experimental' {
574 if ($Channel -eq 'Stable') {
575 return @{
576 IsEligible = $false
577 Reason = "Collection '$($CollectionManifest.id)' is experimental and excluded from Stable channel"
578 }
579 }
580 return @{ IsEligible = $true; Reason = '' }
581 }
582 'preview' {
583 return @{ IsEligible = $true; Reason = '' }
584 }
585 'stable' {
586 return @{ IsEligible = $true; Reason = '' }
587 }
588 default {
589 return @{
590 IsEligible = $false
591 Reason = "Collection '$($CollectionManifest.id)' has invalid maturity value: $maturity"
592 }
593 }
594 }
595}
596
597function Test-GlobMatch {
598 <#
599 .SYNOPSIS
600 Tests whether a name matches any of the provided glob patterns.
601 .DESCRIPTION
602 Uses PowerShell's -like operator to test glob pattern matching with
603 * (any characters) and ? (single character) wildcards.
604 .PARAMETER Name
605 The artifact name to test against patterns.
606 .PARAMETER Patterns
607 Array of glob patterns to match against.
608 .OUTPUTS
609 [bool] True if name matches any pattern, false otherwise.
610 #>
611 [CmdletBinding()]
612 [OutputType([bool])]
613 param(
614 [Parameter(Mandatory = $true)]
615 [string]$Name,
616
617 [Parameter(Mandatory = $true)]
618 [string[]]$Patterns
619 )
620
621 foreach ($pattern in $Patterns) {
622 if ($Name -like $pattern) {
623 return $true
624 }
625 }
626 return $false
627}
628
629function Get-CollectionArtifacts {
630 <#
631 .SYNOPSIS
632 Filters collection artifacts by collection item metadata and channel maturity.
633 .DESCRIPTION
634 Applies collection-level filtering to manifest items, returning artifact
635 names that match allowed maturities. Item-level maturity is used when
636 present; otherwise artifacts default to stable.
637 .PARAMETER Collection
638 Collection manifest hashtable with items.
639 .PARAMETER AllowedMaturities
640 Array of maturity levels to include.
641 .OUTPUTS
642 [hashtable] With Agents, Prompts, Instructions, Skills arrays of matching artifact names.
643 #>
644 [CmdletBinding()]
645 [OutputType([hashtable])]
646 param(
647 [Parameter(Mandatory = $true)]
648 [hashtable]$Collection,
649
650 [Parameter(Mandatory = $true)]
651 [string[]]$AllowedMaturities
652 )
653
654 $result = @{
655 Agents = @()
656 Prompts = @()
657 Instructions = @()
658 Skills = @()
659 }
660
661 if (-not $Collection.ContainsKey('items') -or @($Collection.items).Count -eq 0) {
662 return $result
663 }
664
665 foreach ($item in $Collection.items) {
666 if (-not $item.ContainsKey('kind') -or -not $item.ContainsKey('path')) {
667 continue
668 }
669
670 $kind = [string]$item.kind
671 $path = [string]$item.path
672
673 $maturity = Resolve-CollectionItemMaturity -Maturity $item.maturity
674 if ($AllowedMaturities -notcontains $maturity) {
675 continue
676 }
677
678 $artifactKey = Get-CollectionArtifactKey -Kind $kind -Path $path
679 switch ($kind) {
680 'agent' { $result.Agents += $artifactKey }
681 'prompt' { $result.Prompts += $artifactKey }
682 'instruction' { $result.Instructions += $artifactKey }
683 'skill' { $result.Skills += $artifactKey }
684 }
685 }
686
687 return $result
688}
689
690function Resolve-HandoffDependencies {
691 <#
692 .SYNOPSIS
693 Resolves transitive agent handoff dependencies using BFS traversal.
694 .DESCRIPTION
695 Starting from seed agents, performs breadth-first traversal of agent handoff
696 declarations in YAML frontmatter to compute the transitive closure of
697 all agents reachable through handoff chains.
698
699 Handoff targets in frontmatter use display names (e.g., "Task Planner")
700 while agent files use kebab-case stems (e.g., task-planner.agent.md).
701 This function builds a name index to resolve both formats.
702 .PARAMETER SeedAgents
703 Initial agent names (file stems) to start BFS from.
704 .PARAMETER AgentsDir
705 Path to the agents directory containing .agent.md files.
706 .OUTPUTS
707 [string[]] Complete set of agent file stems including seed agents and all transitive handoff targets.
708 #>
709 [CmdletBinding()]
710 [OutputType([string[]])]
711 param(
712 [Parameter(Mandatory = $true)]
713 [string[]]$SeedAgents,
714
715 [Parameter(Mandatory = $true)]
716 [string]$AgentsDir
717 )
718
719 # Build index: map display names and file stems to agent file objects.
720 # Handoff targets use display names from frontmatter (e.g., "RPI Agent")
721 # while seed agents and collection keys use file stems (e.g., "rpi-agent").
722 $agentIndex = @{}
723 $allAgentFiles = Get-ChildItem -Path $AgentsDir -Filter "*.agent.md" -Recurse -File
724 foreach ($af in $allAgentFiles) {
725 $stem = $af.BaseName -replace '\.agent$', ''
726 $agentIndex[$stem] = $af
727
728 $fc = Get-Content -Path $af.FullName -Raw
729 if ($fc -match '(?s)^---\s*\r?\n(.*?)\r?\n---') {
730 $yml = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n"
731 try {
732 $meta = ConvertFrom-Yaml -Yaml $yml
733 if ($meta.ContainsKey('name') -and $meta.name -is [string] -and $meta.name -ne '') {
734 if (-not $agentIndex.ContainsKey($meta.name)) {
735 $agentIndex[$meta.name] = $af
736 }
737 }
738 }
739 catch {
740 Write-Verbose "Skipping display name index for $($af.Name): $_"
741 }
742 }
743 }
744
745 $visited = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
746 $queue = [System.Collections.Generic.Queue[string]]::new()
747
748 foreach ($agent in $SeedAgents) {
749 if ($visited.Add($agent)) {
750 $queue.Enqueue($agent)
751 }
752 }
753
754 while ($queue.Count -gt 0) {
755 $current = $queue.Dequeue()
756 $agentFile = $agentIndex[$current]
757
758 if (-not $agentFile) {
759 Write-Warning "Handoff target agent file not found: $current"
760 continue
761 }
762
763 # Normalize visited entry to file stem for consistent collection filtering
764 $fileStem = $agentFile.BaseName -replace '\.agent$', ''
765 if ($fileStem -ne $current) {
766 $visited.Add($fileStem) | Out-Null
767 }
768
769 # Parse handoffs from frontmatter
770 $content = Get-Content -Path $agentFile.FullName -Raw
771 if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') {
772 $yamlContent = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n"
773 try {
774 $data = ConvertFrom-Yaml -Yaml $yamlContent
775 if ($data.ContainsKey('handoffs') -and $data.handoffs -is [System.Collections.IEnumerable] -and $data.handoffs -isnot [string]) {
776 foreach ($handoff in $data.handoffs) {
777 # Handle both string format and object format (with 'agent' field).
778 # Handoff targets bypass maturity filtering by design.
779 # See docs/contributing/ai-artifacts-common.md
780 # "Handoff vs Requires Maturity Filtering" for rationale.
781 $targetAgent = $null
782 if ($handoff -is [string]) {
783 $targetAgent = $handoff
784 } elseif ($handoff -is [hashtable] -and $handoff.ContainsKey('agent')) {
785 $targetAgent = $handoff.agent
786 }
787 if ($targetAgent -and $visited.Add($targetAgent)) {
788 $queue.Enqueue($targetAgent)
789 }
790 }
791 }
792 }
793 catch {
794 Write-Warning "Failed to parse handoffs from $($agentFile.Name): $_"
795 }
796 }
797 }
798
799 return @($visited)
800}
801
802function Resolve-RequiresDependencies {
803 <#
804 .SYNOPSIS
805 Resolves transitive artifact dependencies from collection item requires blocks.
806 .DESCRIPTION
807 Walks requires blocks in collection items to compute the complete set of
808 dependent artifacts across all types (agents, prompts, instructions, skills).
809 .PARAMETER ArtifactNames
810 Hashtable with initial artifact name arrays keyed by type (agents, prompts, instructions, skills).
811 .PARAMETER AllowedMaturities
812 Array of maturity levels to include.
813 .PARAMETER CollectionRequires
814 Per-type map of artifact requires blocks keyed by artifact name.
815 .PARAMETER CollectionMaturities
816 Optional per-type maturity map keyed by artifact name.
817 .OUTPUTS
818 [hashtable] With Agents, Prompts, Instructions, Skills arrays containing resolved names.
819 #>
820 [CmdletBinding()]
821 [OutputType([hashtable])]
822 param(
823 [Parameter(Mandatory = $true)]
824 [hashtable]$ArtifactNames,
825
826 [Parameter(Mandatory = $true)]
827 [string[]]$AllowedMaturities,
828
829 [Parameter(Mandatory = $false)]
830 [hashtable]$CollectionRequires = @{},
831
832 [Parameter(Mandatory = $false)]
833 [hashtable]$CollectionMaturities = @{}
834 )
835
836 $resolved = @{
837 Agents = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
838 Prompts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
839 Instructions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
840 Skills = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
841 }
842
843 $typeMap = @{
844 agents = 'Agents'
845 prompts = 'Prompts'
846 instructions = 'Instructions'
847 skills = 'Skills'
848 }
849
850 # Seed with initial artifact names
851 foreach ($type in @('agents', 'prompts', 'instructions', 'skills')) {
852 $capitalType = $typeMap[$type]
853 if ($ArtifactNames.ContainsKey($type)) {
854 foreach ($name in $ArtifactNames[$type]) {
855 $null = $resolved[$capitalType].Add($name)
856 }
857 }
858 }
859
860 $changed = $true
861 while ($changed) {
862 $changed = $false
863
864 foreach ($sourceType in @('agents', 'prompts', 'instructions', 'skills')) {
865 if (-not $CollectionRequires.ContainsKey($sourceType)) {
866 continue
867 }
868
869 $sourceCapitalType = $typeMap[$sourceType]
870 foreach ($sourceName in @($resolved[$sourceCapitalType])) {
871 if (-not $CollectionRequires[$sourceType].ContainsKey($sourceName)) {
872 continue
873 }
874
875 $requires = $CollectionRequires[$sourceType][$sourceName]
876 if (-not $requires) {
877 continue
878 }
879
880 foreach ($targetType in @('agents', 'prompts', 'instructions', 'skills')) {
881 if (-not $requires.ContainsKey($targetType)) {
882 continue
883 }
884
885 $targetCapitalType = $typeMap[$targetType]
886 foreach ($dep in @($requires[$targetType])) {
887 $depMaturity = 'stable'
888 if ($CollectionMaturities.ContainsKey($targetType) -and $CollectionMaturities[$targetType].ContainsKey($dep)) {
889 $depMaturity = $CollectionMaturities[$targetType][$dep]
890 }
891
892 if ($AllowedMaturities -notcontains $depMaturity) {
893 continue
894 }
895
896 if ($resolved[$targetCapitalType].Add($dep)) {
897 $changed = $true
898 }
899 }
900 }
901 }
902 }
903 }
904
905 # Convert HashSets to arrays
906 return @{
907 Agents = @($resolved.Agents)
908 Prompts = @($resolved.Prompts)
909 Instructions = @($resolved.Instructions)
910 Skills = @($resolved.Skills)
911 }
912}
913
914function Test-PathsExist {
915 <#
916 .SYNOPSIS
917 Validates that required paths exist for extension preparation.
918 .DESCRIPTION
919 Validation function that checks whether extension directory, package.json,
920 and .github directory exist at the specified locations.
921 .PARAMETER ExtensionDir
922 Path to the extension directory.
923 .PARAMETER PackageJsonPath
924 Path to package.json file.
925 .PARAMETER GitHubDir
926 Path to .github directory.
927 .OUTPUTS
928 [hashtable] With IsValid bool, MissingPaths array, and ErrorMessages array.
929 #>
930 [CmdletBinding()]
931 [OutputType([hashtable])]
932 param(
933 [Parameter(Mandatory = $true)]
934 [string]$ExtensionDir,
935
936 [Parameter(Mandatory = $true)]
937 [string]$PackageJsonPath,
938
939 [Parameter(Mandatory = $true)]
940 [string]$GitHubDir
941 )
942
943 $missingPaths = @()
944 $errorMessages = @()
945
946 if (-not (Test-Path $ExtensionDir)) {
947 $missingPaths += $ExtensionDir
948 $errorMessages += "Extension directory not found: $ExtensionDir"
949 }
950 if (-not (Test-Path $PackageJsonPath)) {
951 $missingPaths += $PackageJsonPath
952 $errorMessages += "package.json not found: $PackageJsonPath"
953 }
954 if (-not (Test-Path $GitHubDir)) {
955 $missingPaths += $GitHubDir
956 $errorMessages += ".github directory not found: $GitHubDir"
957 }
958
959 return @{
960 IsValid = ($missingPaths.Count -eq 0)
961 MissingPaths = $missingPaths
962 ErrorMessages = $errorMessages
963 }
964}
965
966function Get-DiscoveredAgents {
967 <#
968 .SYNOPSIS
969 Discovers chat agent files from the agents directory.
970 .DESCRIPTION
971 Discovery function that scans the agents directory for .agent.md files,
972 filters by exclusion list, and returns structured agent objects.
973 .PARAMETER AgentsDir
974 Path to the agents directory.
975 .PARAMETER AllowedMaturities
976 Array of maturity levels to include.
977 .PARAMETER ExcludedAgents
978 Array of agent names to exclude from packaging.
979 .OUTPUTS
980 [hashtable] With Agents array, Skipped array, and DirectoryExists bool.
981 #>
982 [CmdletBinding()]
983 [OutputType([hashtable])]
984 param(
985 [Parameter(Mandatory = $true)]
986 [string]$AgentsDir,
987
988 [Parameter(Mandatory = $true)]
989 [string[]]$AllowedMaturities,
990
991 [Parameter(Mandatory = $false)]
992 [string[]]$ExcludedAgents = @()
993 )
994
995 $result = @{
996 Agents = @()
997 Skipped = @()
998 DirectoryExists = (Test-Path $AgentsDir)
999 }
1000
1001 if (-not $result.DirectoryExists) {
1002 return $result
1003 }
1004
1005 $agentFiles = Get-ChildItem -Path $AgentsDir -Filter "*.agent.md" -Recurse | Sort-Object Name
1006 $agentFiles = $agentFiles | Where-Object { -not (Test-DeprecatedPath -Path $_.FullName) }
1007
1008 foreach ($agentFile in $agentFiles) {
1009 $agentRelPath = [System.IO.Path]::GetRelativePath($AgentsDir, $agentFile.FullName) -replace '\\', '/'
1010
1011 if (Test-HveCoreRepoSpecificPath -RelativePath $agentRelPath) {
1012 $agentName = $agentFile.BaseName -replace '\.agent$', ''
1013 $result.Skipped += @{ Name = $agentName; Reason = 'repo-specific (root-level)' }
1014 continue
1015 }
1016
1017 $agentName = $agentFile.BaseName -replace '\.agent$', ''
1018
1019 if ($ExcludedAgents -contains $agentName) {
1020 $result.Skipped += @{ Name = $agentName; Reason = 'excluded' }
1021 continue
1022 }
1023
1024 $maturity = "stable"
1025
1026 if ($AllowedMaturities -notcontains $maturity) {
1027 $result.Skipped += @{ Name = $agentName; Reason = "maturity: $maturity" }
1028 continue
1029 }
1030 $result.Agents += [PSCustomObject]@{
1031 name = $agentName
1032 path = "./.github/agents/$agentRelPath"
1033 }
1034 }
1035
1036 return $result
1037}
1038
1039function Get-DiscoveredPrompts {
1040 <#
1041 .SYNOPSIS
1042 Discovers prompt files from the prompts directory.
1043 .DESCRIPTION
1044 Discovery function that scans the prompts directory for .prompt.md files,
1045 and returns structured prompt objects with relative paths.
1046 .PARAMETER PromptsDir
1047 Path to the prompts directory.
1048 .PARAMETER GitHubDir
1049 Path to the .github directory for relative path calculation.
1050 .PARAMETER AllowedMaturities
1051 Array of maturity levels to include.
1052 .OUTPUTS
1053 [hashtable] With Prompts array, Skipped array, and DirectoryExists bool.
1054 #>
1055 [CmdletBinding()]
1056 [OutputType([hashtable])]
1057 param(
1058 [Parameter(Mandatory = $true)]
1059 [string]$PromptsDir,
1060
1061 [Parameter(Mandatory = $true)]
1062 [string]$GitHubDir,
1063
1064 [Parameter(Mandatory = $true)]
1065 [string[]]$AllowedMaturities
1066 )
1067
1068 $result = @{
1069 Prompts = @()
1070 Skipped = @()
1071 DirectoryExists = (Test-Path $PromptsDir)
1072 }
1073
1074 if (-not $result.DirectoryExists) {
1075 return $result
1076 }
1077
1078 $promptFiles = Get-ChildItem -Path $PromptsDir -Filter "*.prompt.md" -Recurse | Sort-Object Name
1079 $promptFiles = $promptFiles | Where-Object { -not (Test-DeprecatedPath -Path $_.FullName) }
1080
1081 foreach ($promptFile in $promptFiles) {
1082 $promptName = $promptFile.BaseName -replace '\.prompt$', ''
1083
1084 $promptRelPath = [System.IO.Path]::GetRelativePath($PromptsDir, $promptFile.FullName) -replace '\\', '/'
1085 if (Test-HveCoreRepoSpecificPath -RelativePath $promptRelPath) {
1086 $result.Skipped += @{ Name = $promptName; Reason = 'repo-specific (root-level)' }
1087 continue
1088 }
1089
1090 $maturity = "stable"
1091
1092 if ($AllowedMaturities -notcontains $maturity) {
1093 $result.Skipped += @{ Name = $promptName; Reason = "maturity: $maturity" }
1094 continue
1095 }
1096
1097 $relativePath = [System.IO.Path]::GetRelativePath($GitHubDir, $promptFile.FullName) -replace '\\', '/'
1098
1099 $result.Prompts += [PSCustomObject]@{
1100 name = $promptName
1101 path = "./.github/$relativePath"
1102 }
1103 }
1104
1105 return $result
1106}
1107
1108function Get-DiscoveredInstructions {
1109 <#
1110 .SYNOPSIS
1111 Discovers instruction files from the instructions directory.
1112 .DESCRIPTION
1113 Discovery function that scans the instructions directory for .instructions.md files,
1114 and returns structured instruction objects with normalized paths.
1115 .PARAMETER InstructionsDir
1116 Path to the instructions directory.
1117 .PARAMETER GitHubDir
1118 Path to the .github directory for relative path calculation.
1119 .PARAMETER AllowedMaturities
1120 Array of maturity levels to include.
1121 .OUTPUTS
1122 [hashtable] With Instructions array, Skipped array, and DirectoryExists bool.
1123 #>
1124 [CmdletBinding()]
1125 [OutputType([hashtable])]
1126 param(
1127 [Parameter(Mandatory = $true)]
1128 [string]$InstructionsDir,
1129
1130 [Parameter(Mandatory = $true)]
1131 [string]$GitHubDir,
1132
1133 [Parameter(Mandatory = $true)]
1134 [string[]]$AllowedMaturities
1135 )
1136
1137 $result = @{
1138 Instructions = @()
1139 Skipped = @()
1140 DirectoryExists = (Test-Path $InstructionsDir)
1141 }
1142
1143 if (-not $result.DirectoryExists) {
1144 return $result
1145 }
1146
1147 $instructionFiles = Get-ChildItem -Path $InstructionsDir -Filter "*.instructions.md" -Recurse | Sort-Object Name
1148 $instructionFiles = $instructionFiles | Where-Object { -not (Test-DeprecatedPath -Path $_.FullName) }
1149
1150 foreach ($instrFile in $instructionFiles) {
1151 $instrRelPath = [System.IO.Path]::GetRelativePath($InstructionsDir, $instrFile.FullName) -replace '\\', '/'
1152 if (Test-HveCoreRepoSpecificPath -RelativePath $instrRelPath) {
1153 $result.Skipped += @{ Name = $instrFile.BaseName; Reason = 'repo-specific (root-level)' }
1154 continue
1155 }
1156 $baseName = $instrFile.BaseName -replace '\.instructions$', ''
1157 $instrName = "$baseName-instructions"
1158
1159 $maturity = "stable"
1160
1161 if ($AllowedMaturities -notcontains $maturity) {
1162 $result.Skipped += @{ Name = $instrName; Reason = "maturity: $maturity" }
1163 continue
1164 }
1165
1166 $relativePathFromGitHub = [System.IO.Path]::GetRelativePath($GitHubDir, $instrFile.FullName)
1167 $normalizedRelativePath = (Join-Path ".github" $relativePathFromGitHub) -replace '\\', '/'
1168
1169 $result.Instructions += [PSCustomObject]@{
1170 name = $instrName
1171 path = "./$normalizedRelativePath"
1172 }
1173 }
1174
1175 return $result
1176}
1177
1178function Get-DiscoveredSkills {
1179 <#
1180 .SYNOPSIS
1181 Discovers skill packages from the skills directory.
1182 .DESCRIPTION
1183 Discovery function that scans the skills directory for subdirectories
1184 containing SKILL.md files and returns structured skill objects.
1185 .PARAMETER SkillsDir
1186 Path to the skills directory.
1187 .PARAMETER AllowedMaturities
1188 Array of maturity levels to include.
1189 .OUTPUTS
1190 [hashtable] With Skills array, Skipped array, and DirectoryExists bool.
1191 #>
1192 [CmdletBinding()]
1193 [OutputType([hashtable])]
1194 param(
1195 [Parameter(Mandatory = $true)]
1196 [string]$SkillsDir,
1197
1198 [Parameter(Mandatory = $true)]
1199 [string[]]$AllowedMaturities
1200 )
1201
1202 $result = @{
1203 Skills = @()
1204 Skipped = @()
1205 DirectoryExists = (Test-Path $SkillsDir)
1206 }
1207
1208 if (-not $result.DirectoryExists) {
1209 return $result
1210 }
1211
1212 $skillFiles = Get-ChildItem -Path $SkillsDir -Filter "SKILL.md" -File -Recurse | Sort-Object { $_.Directory.FullName }
1213 $skillFiles = $skillFiles | Where-Object { -not (Test-DeprecatedPath -Path $_.FullName) }
1214
1215 foreach ($skillFile in $skillFiles) {
1216 $skillDir = $skillFile.Directory
1217 $skillName = $skillDir.Name
1218 $skillRelPath = [System.IO.Path]::GetRelativePath($SkillsDir, $skillDir.FullName) -replace '\\', '/'
1219
1220 if (Test-HveCoreRepoSpecificPath -RelativePath $skillRelPath) {
1221 $result.Skipped += @{ Name = $skillName; Reason = 'repo-specific (root-level)' }
1222 continue
1223 }
1224
1225 $maturity = "stable"
1226
1227 if ($AllowedMaturities -notcontains $maturity) {
1228 $result.Skipped += @{ Name = $skillName; Reason = "maturity: $maturity" }
1229 continue
1230 }
1231
1232 $result.Skills += [PSCustomObject]@{
1233 name = $skillName
1234 path = "./.github/skills/$skillRelPath/SKILL.md"
1235 }
1236 }
1237
1238 return $result
1239}
1240
1241function Update-PackageJsonContributes {
1242 <#
1243 .SYNOPSIS
1244 Updates package.json contributes section with discovered components.
1245 .DESCRIPTION
1246 Pure function that takes a package.json object and discovered components,
1247 returning a new object with the contributes section updated. Handles
1248 chatAgents, chatPromptFiles, chatInstructions, and chatSkills.
1249 .PARAMETER PackageJson
1250 The package.json object to update.
1251 .PARAMETER ChatAgents
1252 Array of discovered chat agent objects.
1253 .PARAMETER ChatPromptFiles
1254 Array of discovered prompt objects.
1255 .PARAMETER ChatInstructions
1256 Array of discovered instruction objects.
1257 .PARAMETER ChatSkills
1258 Array of discovered skill objects.
1259 .OUTPUTS
1260 [PSCustomObject] Updated package.json object.
1261 #>
1262 [CmdletBinding()]
1263 [OutputType([PSCustomObject])]
1264 param(
1265 [Parameter(Mandatory = $true)]
1266 [PSCustomObject]$PackageJson,
1267
1268 [Parameter(Mandatory = $true)]
1269 [AllowEmptyCollection()]
1270 [array]$ChatAgents,
1271
1272 [Parameter(Mandatory = $true)]
1273 [AllowEmptyCollection()]
1274 [array]$ChatPromptFiles,
1275
1276 [Parameter(Mandatory = $true)]
1277 [AllowEmptyCollection()]
1278 [array]$ChatInstructions,
1279
1280 [Parameter(Mandatory = $true)]
1281 [AllowEmptyCollection()]
1282 [array]$ChatSkills
1283 )
1284
1285 # Clone the object to avoid modifying the original
1286 $updated = $PackageJson | ConvertTo-Json -Depth 10 | ConvertFrom-Json
1287
1288 # Strip name and description; VS Code reads these from the files directly
1289 $ChatAgents = @($ChatAgents | Select-Object -Property path)
1290 $ChatPromptFiles = @($ChatPromptFiles | Select-Object -Property path)
1291 $ChatInstructions = @($ChatInstructions | Select-Object -Property path)
1292 $ChatSkills = @($ChatSkills | Select-Object -Property path)
1293
1294 # Ensure contributes section exists
1295 if (-not $updated.contributes) {
1296 $updated | Add-Member -NotePropertyName "contributes" -NotePropertyValue ([PSCustomObject]@{})
1297 }
1298
1299 # Add or update contributes properties
1300 if ($null -eq $updated.contributes.chatAgents) {
1301 $updated.contributes | Add-Member -NotePropertyName "chatAgents" -NotePropertyValue $ChatAgents -Force
1302 } else {
1303 $updated.contributes.chatAgents = $ChatAgents
1304 }
1305
1306 if ($null -eq $updated.contributes.chatPromptFiles) {
1307 $updated.contributes | Add-Member -NotePropertyName "chatPromptFiles" -NotePropertyValue $ChatPromptFiles -Force
1308 } else {
1309 $updated.contributes.chatPromptFiles = $ChatPromptFiles
1310 }
1311
1312 if ($null -eq $updated.contributes.chatInstructions) {
1313 $updated.contributes | Add-Member -NotePropertyName "chatInstructions" -NotePropertyValue $ChatInstructions -Force
1314 } else {
1315 $updated.contributes.chatInstructions = $ChatInstructions
1316 }
1317
1318 if ($null -eq $updated.contributes.chatSkills) {
1319 $updated.contributes | Add-Member -NotePropertyName "chatSkills" -NotePropertyValue $ChatSkills -Force
1320 } else {
1321 $updated.contributes.chatSkills = $ChatSkills
1322 }
1323
1324 return $updated
1325}
1326
1327function New-PrepareResult {
1328 <#
1329 .SYNOPSIS
1330 Creates a standardized result object for extension preparation operations.
1331 .DESCRIPTION
1332 Factory function that creates a hashtable with consistent properties
1333 for reporting preparation operation outcomes.
1334 .PARAMETER Success
1335 Indicates whether the operation completed successfully.
1336 .PARAMETER Version
1337 The version string from package.json.
1338 .PARAMETER AgentCount
1339 Number of agents discovered and included.
1340 .PARAMETER PromptCount
1341 Number of prompts discovered and included.
1342 .PARAMETER InstructionCount
1343 Number of instructions discovered and included.
1344 .PARAMETER SkillCount
1345 Number of skills discovered and included.
1346 .PARAMETER ErrorMessage
1347 Error description when Success is false.
1348 .OUTPUTS
1349 Hashtable with Success, Version, AgentCount, PromptCount,
1350 InstructionCount, SkillCount, and ErrorMessage properties.
1351 #>
1352 [CmdletBinding()]
1353 [OutputType([hashtable])]
1354 param(
1355 [Parameter(Mandatory = $true)]
1356 [bool]$Success,
1357
1358 [Parameter(Mandatory = $false)]
1359 [string]$Version = "",
1360
1361 [Parameter(Mandatory = $false)]
1362 [int]$AgentCount = 0,
1363
1364 [Parameter(Mandatory = $false)]
1365 [int]$PromptCount = 0,
1366
1367 [Parameter(Mandatory = $false)]
1368 [int]$InstructionCount = 0,
1369
1370 [Parameter(Mandatory = $false)]
1371 [int]$SkillCount = 0,
1372
1373 [Parameter(Mandatory = $false)]
1374 [string]$ErrorMessage = ""
1375 )
1376
1377 return @{
1378 Success = $Success
1379 Version = $Version
1380 AgentCount = $AgentCount
1381 PromptCount = $PromptCount
1382 InstructionCount = $InstructionCount
1383 SkillCount = $SkillCount
1384 ErrorMessage = $ErrorMessage
1385 }
1386}
1387
1388function Test-TemplateConsistency {
1389 <#
1390 .SYNOPSIS
1391 Validates collection template metadata against its collection manifest.
1392 .DESCRIPTION
1393 Compares name, displayName, and description fields between a collection
1394 package template (e.g. package.developer.json) and the corresponding
1395 collection manifest. Emits warnings for divergences and returns a list
1396 of mismatches.
1397 .PARAMETER TemplatePath
1398 Path to the collection package template JSON file.
1399 .PARAMETER CollectionManifest
1400 Parsed collection manifest hashtable with name, displayName, description.
1401 .OUTPUTS
1402 [hashtable] With Mismatches array and IsConsistent bool.
1403 #>
1404 [CmdletBinding()]
1405 [OutputType([hashtable])]
1406 param(
1407 [Parameter(Mandatory = $true)]
1408 [ValidateNotNullOrEmpty()]
1409 [string]$TemplatePath,
1410
1411 [Parameter(Mandatory = $true)]
1412 [hashtable]$CollectionManifest
1413 )
1414
1415 $result = @{
1416 Mismatches = @()
1417 IsConsistent = $true
1418 }
1419
1420 if (-not (Test-Path $TemplatePath)) {
1421 $result.Mismatches += @{
1422 Field = 'file'
1423 Template = $TemplatePath
1424 Manifest = 'N/A'
1425 Message = "Template file not found: $TemplatePath"
1426 }
1427 $result.IsConsistent = $false
1428 return $result
1429 }
1430
1431 try {
1432 $template = Get-Content -Path $TemplatePath -Raw | ConvertFrom-Json
1433 }
1434 catch {
1435 $result.Mismatches += @{
1436 Field = 'file'
1437 Template = $TemplatePath
1438 Manifest = 'N/A'
1439 Message = "Failed to parse template: $($_.Exception.Message)"
1440 }
1441 $result.IsConsistent = $false
1442 return $result
1443 }
1444
1445 $fieldsToCheck = @('name', 'displayName', 'description')
1446 foreach ($field in $fieldsToCheck) {
1447 $templateValue = $null
1448 $manifestValue = $null
1449
1450 if ($template.PSObject.Properties[$field]) {
1451 $templateValue = $template.$field
1452 }
1453 if ($CollectionManifest.ContainsKey($field)) {
1454 $manifestValue = $CollectionManifest[$field]
1455 }
1456
1457 if ($null -ne $templateValue -and $null -ne $manifestValue -and $templateValue -ne $manifestValue) {
1458 $result.Mismatches += @{
1459 Field = $field
1460 Template = $templateValue
1461 Manifest = $manifestValue
1462 Message = "$field diverges: template='$templateValue' manifest='$manifestValue'"
1463 }
1464 $result.IsConsistent = $false
1465 }
1466 }
1467
1468 return $result
1469}
1470
1471function Invoke-PrepareExtension {
1472 <#
1473 .SYNOPSIS
1474 Orchestrates VS Code extension preparation with full error handling.
1475 .DESCRIPTION
1476 Executes the complete preparation workflow: validates paths, discovers
1477 agents/prompts/instructions, updates package.json, and handles changelog.
1478 Returns a result object instead of using exit codes.
1479 .PARAMETER ExtensionDirectory
1480 Absolute path to the extension directory containing package.json.
1481 .PARAMETER RepoRoot
1482 Absolute path to the repository root directory.
1483 .PARAMETER Channel
1484 Release channel controlling maturity filter ('Stable' or 'PreRelease').
1485 .PARAMETER ChangelogPath
1486 Optional path to changelog file to include.
1487 .PARAMETER DryRun
1488 When specified, shows what would be done without making changes.
1489 .OUTPUTS
1490 Hashtable with Success, Version, AgentCount, PromptCount,
1491 InstructionCount, SkillCount, and ErrorMessage properties.
1492 #>
1493 [CmdletBinding()]
1494 [OutputType([hashtable])]
1495 param(
1496 [Parameter(Mandatory = $true)]
1497 [ValidateNotNullOrEmpty()]
1498 [string]$ExtensionDirectory,
1499
1500 [Parameter(Mandatory = $true)]
1501 [ValidateNotNullOrEmpty()]
1502 [string]$RepoRoot,
1503
1504 [Parameter(Mandatory = $false)]
1505 [ValidateSet('Stable', 'PreRelease')]
1506 [string]$Channel = 'Stable',
1507
1508 [Parameter(Mandatory = $false)]
1509 [string]$ChangelogPath = "",
1510
1511 [Parameter(Mandatory = $false)]
1512 [switch]$DryRun,
1513
1514 [Parameter(Mandatory = $false)]
1515 [string]$Collection = ""
1516 )
1517
1518 # Derive paths
1519 $GitHubDir = Join-Path $RepoRoot ".github"
1520 $PackageJsonPath = Join-Path $ExtensionDirectory "package.json"
1521
1522 # Generate collection package files from root collection manifests.
1523 # This ensures extension/package.json and extension/package.*.json exist
1524 # with the correct version from the template before any reads occur.
1525 try {
1526 $generated = Invoke-ExtensionCollectionsGeneration -RepoRoot $RepoRoot -Channel $Channel
1527 Write-Host "Generated $($generated.Count) collection package file(s)" -ForegroundColor Green
1528 }
1529 catch {
1530 return New-PrepareResult -Success $false -ErrorMessage "Package generation failed: $($_.Exception.Message)"
1531 }
1532
1533 # Validate required paths exist (package.json now guaranteed by generation)
1534 $pathValidation = Test-PathsExist -ExtensionDir $ExtensionDirectory `
1535 -PackageJsonPath $PackageJsonPath `
1536 -GitHubDir $GitHubDir
1537 if (-not $pathValidation.IsValid) {
1538 $missingPaths = $pathValidation.MissingPaths -join ', '
1539 return New-PrepareResult -Success $false -ErrorMessage "Required paths not found: $missingPaths"
1540 }
1541
1542 # Read and parse package.json
1543 try {
1544 $packageJsonContent = Get-Content -Path $PackageJsonPath -Raw
1545 $packageJson = $packageJsonContent | ConvertFrom-Json
1546 }
1547 catch {
1548 return New-PrepareResult -Success $false -ErrorMessage "Failed to parse package.json at '$PackageJsonPath'. Check the file for JSON syntax errors. Underlying error: $($_.Exception.Message)"
1549 }
1550
1551 # Validate version field
1552 if (-not $packageJson.PSObject.Properties['version']) {
1553 return New-PrepareResult -Success $false -ErrorMessage "package.json does not contain a 'version' field"
1554 }
1555 $version = $packageJson.version
1556 if ($version -notmatch '^\d+\.\d+\.\d+$') {
1557 return New-PrepareResult -Success $false -ErrorMessage "Invalid version format in package.json: $version"
1558 }
1559
1560 # Get allowed maturities for channel
1561 $allowedMaturities = Get-AllowedMaturities -Channel $Channel
1562
1563 Write-Host "`n=== Prepare Extension ===" -ForegroundColor Cyan
1564 Write-Host "Extension Directory: $ExtensionDirectory"
1565 Write-Host "Repository Root: $RepoRoot"
1566 Write-Host "Channel: $Channel"
1567 Write-Host "Allowed Maturities: $($allowedMaturities -join ', ')"
1568 Write-Host "Version: $version"
1569 if ($DryRun) {
1570 Write-Host "[DRY RUN] No changes will be made" -ForegroundColor Yellow
1571 }
1572
1573 # Load collection manifest if specified
1574 $collectionManifest = $null
1575 $collectionArtifactNames = $null
1576 $collectionMaturities = @{}
1577 $collectionRequires = @{}
1578
1579 if ($Collection -and $Collection -ne "") {
1580 $collectionManifest = Get-CollectionManifest -CollectionPath $Collection
1581 Write-Host "Collection: $($collectionManifest.displayName) ($($collectionManifest.id))"
1582
1583 $artifactCollectionManifest = $collectionManifest
1584 if (-not $artifactCollectionManifest.ContainsKey('items') -or @($artifactCollectionManifest.items).Count -eq 0) {
1585 # When the manifest lacks items (e.g., a generated JSON template),
1586 # resolve from the root YAML collection by ID.
1587 $rootCollectionPath = Join-Path $RepoRoot "collections/$($collectionManifest.id).collection.yml"
1588 if (Test-Path $rootCollectionPath) {
1589 $artifactCollectionManifest = Get-CollectionManifest -CollectionPath $rootCollectionPath
1590 Write-Host "Using root collection for items: $rootCollectionPath"
1591 }
1592 else {
1593 Write-Warning "No root collection found for '$($collectionManifest.id)' at $rootCollectionPath"
1594 }
1595 }
1596
1597 # Check collection-level maturity eligibility
1598 $collectionEligibility = Test-CollectionMaturityEligible -CollectionManifest $collectionManifest -Channel $Channel
1599 if (-not $collectionEligibility.IsEligible) {
1600 Write-Host "`n⏭️ $($collectionEligibility.Reason)" -ForegroundColor Yellow
1601 return New-PrepareResult -Success $true -Version $version
1602 }
1603
1604 $collectionMaturity = if ($collectionManifest.ContainsKey('maturity')) { $collectionManifest['maturity'] } else { 'stable' }
1605 Write-Host "Collection maturity: $collectionMaturity"
1606
1607 # Build collection maturity map and channel-filtered artifact names
1608 $collectionMaturities = @{}
1609 $collectionRequires = @{}
1610
1611 if ($artifactCollectionManifest.ContainsKey('items')) {
1612 foreach ($item in $artifactCollectionManifest.items) {
1613 if (-not $item.ContainsKey('kind') -or -not $item.ContainsKey('path')) {
1614 continue
1615 }
1616
1617 $itemKind = [string]$item.kind
1618 $itemPath = [string]$item.path
1619 $artifactKey = Get-CollectionArtifactKey -Kind $itemKind -Path $itemPath
1620 $effectiveMaturity = Resolve-CollectionItemMaturity -Maturity $item.maturity
1621 if (-not $collectionMaturities.ContainsKey("${itemKind}s") -or $null -eq $collectionMaturities["${itemKind}s"]) {
1622 $collectionMaturities["${itemKind}s"] = @{}
1623 }
1624 $collectionMaturities["${itemKind}s"][$artifactKey] = $effectiveMaturity
1625
1626 if ($item.ContainsKey('requires') -and $item.requires) {
1627 if (-not $collectionRequires.ContainsKey("${itemKind}s") -or $null -eq $collectionRequires["${itemKind}s"]) {
1628 $collectionRequires["${itemKind}s"] = @{}
1629 }
1630 $collectionRequires["${itemKind}s"][$artifactKey] = $item.requires
1631 }
1632 }
1633 }
1634
1635 $collectionArtifactNames = Get-CollectionArtifacts -Collection $artifactCollectionManifest -AllowedMaturities $allowedMaturities
1636
1637 # Resolve handoff dependencies (agents only)
1638 if (@($collectionArtifactNames.Agents).Count -gt 0) {
1639 $agentsDir = Join-Path $GitHubDir "agents"
1640 $expandedAgents = Resolve-HandoffDependencies -SeedAgents $collectionArtifactNames.Agents -AgentsDir $agentsDir
1641 $collectionArtifactNames.Agents = $expandedAgents
1642 }
1643
1644 # Resolve requires dependencies
1645 $resolvedNames = Resolve-RequiresDependencies -ArtifactNames @{
1646 agents = $collectionArtifactNames.Agents
1647 prompts = $collectionArtifactNames.Prompts
1648 instructions = $collectionArtifactNames.Instructions
1649 skills = $collectionArtifactNames.Skills
1650 } -AllowedMaturities $allowedMaturities -CollectionRequires $collectionRequires -CollectionMaturities $collectionMaturities
1651
1652 $collectionArtifactNames = @{
1653 Agents = $resolvedNames.Agents
1654 Prompts = $resolvedNames.Prompts
1655 Instructions = $resolvedNames.Instructions
1656 Skills = $resolvedNames.Skills
1657 }
1658 }
1659
1660 # Discover artifacts
1661 $discoveryAllowedMaturities = if ($null -ne $collectionArtifactNames) {
1662 @('stable', 'preview', 'experimental', 'deprecated')
1663 }
1664 else {
1665 $allowedMaturities
1666 }
1667
1668 $agentsDir = Join-Path $GitHubDir "agents"
1669 $agentResult = Get-DiscoveredAgents -AgentsDir $agentsDir -AllowedMaturities $discoveryAllowedMaturities -ExcludedAgents @()
1670 $chatAgents = $agentResult.Agents
1671 $excludedAgents = $agentResult.Skipped
1672
1673 Write-Host "`n--- Chat Agents ---" -ForegroundColor Green
1674 Write-Host "Found $($chatAgents.Count) agent(s) matching criteria"
1675 if ($excludedAgents.Count -gt 0) {
1676 Write-Host "Excluded $($excludedAgents.Count) agent(s) due to maturity filter" -ForegroundColor Yellow
1677 }
1678
1679 # Discover prompts
1680 $promptsDir = Join-Path $GitHubDir "prompts"
1681 $promptResult = Get-DiscoveredPrompts -PromptsDir $promptsDir -GitHubDir $GitHubDir -AllowedMaturities $discoveryAllowedMaturities
1682 $chatPrompts = $promptResult.Prompts
1683 $excludedPrompts = $promptResult.Skipped
1684
1685 Write-Host "`n--- Chat Prompts ---" -ForegroundColor Green
1686 Write-Host "Found $($chatPrompts.Count) prompt(s) matching criteria"
1687 if ($excludedPrompts.Count -gt 0) {
1688 Write-Host "Excluded $($excludedPrompts.Count) prompt(s) due to maturity filter" -ForegroundColor Yellow
1689 }
1690
1691 # Discover instructions
1692 $instructionsDir = Join-Path $GitHubDir "instructions"
1693 $instructionResult = Get-DiscoveredInstructions -InstructionsDir $instructionsDir -GitHubDir $GitHubDir -AllowedMaturities $discoveryAllowedMaturities
1694 $chatInstructions = $instructionResult.Instructions
1695 $excludedInstructions = $instructionResult.Skipped
1696
1697 Write-Host "`n--- Chat Instructions ---" -ForegroundColor Green
1698 Write-Host "Found $($chatInstructions.Count) instruction(s) matching criteria"
1699 if ($excludedInstructions.Count -gt 0) {
1700 Write-Host "Excluded $($excludedInstructions.Count) instruction(s) due to maturity filter" -ForegroundColor Yellow
1701 }
1702
1703 # Discover skills
1704 $skillsDir = Join-Path $GitHubDir "skills"
1705 $skillResult = Get-DiscoveredSkills -SkillsDir $skillsDir -AllowedMaturities $discoveryAllowedMaturities
1706 $chatSkills = $skillResult.Skills
1707 $excludedSkills = $skillResult.Skipped
1708
1709 Write-Host "`n--- Chat Skills ---" -ForegroundColor Green
1710 Write-Host "Found $($chatSkills.Count) skill(s) matching criteria"
1711 if ($excludedSkills.Count -gt 0) {
1712 Write-Host "Excluded $($excludedSkills.Count) skill(s) due to maturity filter" -ForegroundColor Yellow
1713 }
1714
1715 # Apply collection filtering to discovered artifacts
1716 if ($null -ne $collectionArtifactNames) {
1717 $chatAgents = @($chatAgents | Where-Object { $collectionArtifactNames.Agents -contains $_.name })
1718 $chatPrompts = @($chatPrompts | Where-Object { $collectionArtifactNames.Prompts -contains $_.name })
1719 $instrBaseNames = @($collectionArtifactNames.Instructions | ForEach-Object { ($_ -split '/')[-1] })
1720 $chatInstructions = @($chatInstructions | Where-Object {
1721 $instrBaseName = $_.name -replace '-instructions$', ''
1722 $instrBaseNames -contains $instrBaseName
1723 })
1724 $chatSkills = @($chatSkills | Where-Object { $collectionArtifactNames.Skills -contains $_.name })
1725
1726 Write-Host "`n--- Collection Filtering ---" -ForegroundColor Magenta
1727 Write-Host "Agents after filter: $($chatAgents.Count)"
1728 Write-Host "Prompts after filter: $($chatPrompts.Count)"
1729 Write-Host "Instructions after filter: $($chatInstructions.Count)"
1730 Write-Host "Skills after filter: $($chatSkills.Count)"
1731 }
1732
1733 # Apply collection template when building a non-default collection
1734 if ($null -ne $collectionManifest -and $collectionManifest.id -ne 'hve-core') {
1735 $collectionId = $collectionManifest.id
1736 $templatePath = Join-Path $ExtensionDirectory "package.$collectionId.json"
1737 if (-not (Test-Path $templatePath)) {
1738 return New-PrepareResult -Success $false -ErrorMessage "Collection template not found: $templatePath"
1739 }
1740
1741 # Validate template consistency against collection manifest
1742 $consistency = Test-TemplateConsistency -TemplatePath $templatePath -CollectionManifest $collectionManifest
1743 if (-not $consistency.IsConsistent) {
1744 Write-Host "`n--- Template Consistency Warnings ---" -ForegroundColor Yellow
1745 foreach ($mismatch in $consistency.Mismatches) {
1746 Write-Warning "Template/manifest mismatch: $($mismatch.Message)"
1747 Write-CIAnnotation -Message "Template/manifest mismatch ($collectionId): $($mismatch.Message)" -Level Warning
1748 }
1749 }
1750
1751 # Back up canonical package.json for later restore
1752 $backupPath = Join-Path $ExtensionDirectory "package.json.bak"
1753 Copy-Item -Path $PackageJsonPath -Destination $backupPath -Force
1754
1755 # Copy collection template over package.json
1756 Copy-Item -Path $templatePath -Destination $PackageJsonPath -Force
1757
1758 # Re-read template as the working package.json
1759 $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json
1760 Write-Host "Applied collection template: package.$collectionId.json" -ForegroundColor Green
1761 }
1762
1763 # Update package.json with generated contributes
1764 $packageJson = Update-PackageJsonContributes -PackageJson $packageJson `
1765 -ChatAgents $chatAgents `
1766 -ChatPromptFiles $chatPrompts `
1767 -ChatInstructions $chatInstructions `
1768 -ChatSkills $chatSkills
1769
1770 # Write updated package.json
1771 if (-not $DryRun) {
1772 $packageJson | ConvertTo-Json -Depth 10 | Set-Content -Path $PackageJsonPath -Encoding UTF8NoBOM
1773 Write-Host "`nUpdated package.json with discovered artifacts" -ForegroundColor Green
1774 }
1775 else {
1776 Write-Host "`n[DRY RUN] Would update package.json with discovered artifacts" -ForegroundColor Yellow
1777 }
1778
1779 # Handle changelog
1780 if ($ChangelogPath -and (Test-Path $ChangelogPath)) {
1781 $destChangelog = Join-Path $ExtensionDirectory "CHANGELOG.md"
1782 if (-not $DryRun) {
1783 Copy-Item -Path $ChangelogPath -Destination $destChangelog -Force
1784 Write-Host "Copied changelog to extension directory" -ForegroundColor Green
1785 }
1786 else {
1787 Write-Host "[DRY RUN] Would copy changelog to extension directory" -ForegroundColor Yellow
1788 }
1789 }
1790 elseif ($ChangelogPath) {
1791 Write-Warning "Changelog path specified but file not found: $ChangelogPath"
1792 }
1793
1794 Write-Host "`n=== Preparation Complete ===" -ForegroundColor Cyan
1795
1796 return New-PrepareResult -Success $true `
1797 -Version $version `
1798 -AgentCount $chatAgents.Count `
1799 -PromptCount $chatPrompts.Count `
1800 -InstructionCount $chatInstructions.Count `
1801 -SkillCount $chatSkills.Count
1802}
1803
1804#endregion Pure Functions
1805
1806#region Main Execution
1807if ($MyInvocation.InvocationName -ne '.') {
1808 try {
1809 # Verify PowerShell-Yaml module is available
1810 if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) {
1811 throw "Required module 'PowerShell-Yaml' is not installed."
1812 }
1813 Import-Module PowerShell-Yaml -ErrorAction Stop
1814
1815 # Resolve paths using $MyInvocation (must stay in entry point)
1816 $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
1817 $RepoRoot = (Get-Item "$ScriptDir/../..").FullName
1818 $ExtensionDir = Join-Path $RepoRoot "extension"
1819
1820 # Resolve changelog path if provided
1821 $resolvedChangelogPath = ""
1822 if ($ChangelogPath) {
1823 $resolvedChangelogPath = if ([System.IO.Path]::IsPathRooted($ChangelogPath)) {
1824 $ChangelogPath
1825 }
1826 else {
1827 Join-Path $RepoRoot $ChangelogPath
1828 }
1829 }
1830
1831 # Default to hve-core collection when no collection is specified.
1832 # package.json is identity-mapped to the hve-core collection, so the
1833 # default build must apply hve-core filtering rather than including all
1834 # artifacts (hve-core-all behavior). Use -Collection with
1835 # hve-core-all.collection.yml explicitly to include everything.
1836 if (-not $Collection) {
1837 $Collection = Join-Path $RepoRoot 'collections/hve-core.collection.yml'
1838 }
1839
1840 Write-Host "📦 HVE Core Extension Preparer" -ForegroundColor Cyan
1841 Write-Host "==============================" -ForegroundColor Cyan
1842 Write-Host " Channel: $Channel" -ForegroundColor Cyan
1843 Write-Host " Collection: $Collection" -ForegroundColor Cyan
1844 Write-Host ""
1845
1846 # Call orchestration function
1847 $result = Invoke-PrepareExtension `
1848 -ExtensionDirectory $ExtensionDir `
1849 -RepoRoot $RepoRoot `
1850 -Channel $Channel `
1851 -ChangelogPath $resolvedChangelogPath `
1852 -DryRun:$DryRun `
1853 -Collection $Collection
1854
1855 if (-not $result.Success) {
1856 throw $result.ErrorMessage
1857 }
1858
1859 Write-Host ""
1860 Write-Host "🎉 Done!" -ForegroundColor Green
1861 Write-Host ""
1862 Write-Host "📊 Summary:" -ForegroundColor Cyan
1863 Write-Host " Agents: $($result.AgentCount)"
1864 Write-Host " Prompts: $($result.PromptCount)"
1865 Write-Host " Instructions: $($result.InstructionCount)"
1866 Write-Host " Skills: $($result.SkillCount)"
1867 Write-Host " Version: $($result.Version)"
1868
1869 exit 0
1870 }
1871 catch {
1872 Write-Error -ErrorAction Continue "Prepare-Extension failed: $($_.Exception.Message)"
1873 Write-CIAnnotation -Message $_.Exception.Message -Level Error
1874 exit 1
1875 }
1876}
1877#endregion Main Execution
1878