microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/a11y-pr1-scripts-validators

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/extension/Prepare-Extension.ps1

1878lines · 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 # Collection files keep the parent h2 outside the generated marker block.
468 if ($parsed.HasMarkers) {
469 $generatedBlock = $artifactSections.ToString().TrimEnd()
470 $updatedCollectionMd = "$($parsed.Intro)`n`n$($CollectionMdBeginMarker)`n`n$generatedBlock`n`n$($CollectionMdEndMarker)"
471 if (-not [string]::IsNullOrWhiteSpace($parsed.Footer)) {
472 $updatedCollectionMd += "`n`n$($parsed.Footer.TrimEnd())"
473 }
474 $updatedCollectionMd += "`n"
475 Set-ContentIfChanged -Path $CollectionMdPath -Value $updatedCollectionMd
476 }
477
478 $fullEdition = if ($collectionId -notin @('hve-core', 'hve-core-all')) {
479 "## 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."
480 }
481 else {
482 ''
483 }
484
485 # Read template and replace tokens
486 $template = Get-Content -Path $TemplatePath -Raw
487 $readmeContent = $template `
488 -replace '\{\{DISPLAY_NAME\}\}', $displayName `
489 -replace '\{\{DESCRIPTION\}\}', $description `
490 -replace '\{\{MATURITY_NOTICE\}\}', $maturityNotice `
491 -replace '\{\{BODY\}\}', $bodyForTemplate `
492 -replace '\{\{ARTIFACTS\}\}', $artifactSections.ToString().TrimEnd() `
493 -replace '\{\{FULL_EDITION\}\}', $fullEdition
494
495 # Clean up blank lines left by empty token replacements
496 $readmeContent = $readmeContent -replace '(\r?\n){3,}', "`n`n"
497 $readmeContent = $readmeContent.TrimEnd() + "`n"
498
499 Set-Content -Path $OutputPath -Value $readmeContent -Encoding utf8NoBOM -NoNewline
500}
501
502#endregion Package Generation Functions
503
504function Get-AllowedMaturities {
505 <#
506 .SYNOPSIS
507 Returns allowed maturity levels based on release channel.
508 .DESCRIPTION
509 Pure function that determines which maturity levels (stable, preview, experimental)
510 are included in the extension package based on the specified channel.
511 .PARAMETER Channel
512 Release channel. 'Stable' returns only stable; 'PreRelease' includes all levels.
513 .OUTPUTS
514 [string[]] Array of allowed maturity level strings.
515 #>
516 [CmdletBinding()]
517 [OutputType([string[]])]
518 param(
519 [Parameter(Mandatory = $true)]
520 [ValidateSet('Stable', 'PreRelease')]
521 [string]$Channel
522 )
523
524 if ($Channel -eq 'PreRelease') {
525 return @('stable', 'preview', 'experimental')
526 }
527 return @('stable')
528}
529
530function Test-CollectionMaturityEligible {
531 <#
532 .SYNOPSIS
533 Checks whether a collection is eligible for the specified release channel.
534 .DESCRIPTION
535 Pure function that evaluates collection-level maturity against channel rules.
536 Experimental collections are eligible only for PreRelease. Deprecated collections
537 are excluded from all channels.
538 .PARAMETER CollectionManifest
539 Parsed collection manifest hashtable.
540 .PARAMETER Channel
541 Release channel ('Stable' or 'PreRelease').
542 .OUTPUTS
543 [hashtable] With IsEligible bool and Reason string.
544 #>
545 [CmdletBinding()]
546 [OutputType([hashtable])]
547 param(
548 [Parameter(Mandatory = $true)]
549 [hashtable]$CollectionManifest,
550
551 [Parameter(Mandatory = $true)]
552 [ValidateSet('Stable', 'PreRelease')]
553 [string]$Channel
554 )
555
556 $maturity = 'stable'
557 if ($CollectionManifest.ContainsKey('maturity') -and $CollectionManifest['maturity']) {
558 $maturity = $CollectionManifest['maturity']
559 }
560
561 switch ($maturity) {
562 'removed' {
563 return @{
564 IsEligible = $false
565 Reason = "Collection '$($CollectionManifest.id)' is removed and excluded from all channels"
566 }
567 }
568 'deprecated' {
569 return @{
570 IsEligible = $false
571 Reason = "Collection '$($CollectionManifest.id)' is deprecated and excluded from all channels"
572 }
573 }
574 'experimental' {
575 if ($Channel -eq 'Stable') {
576 return @{
577 IsEligible = $false
578 Reason = "Collection '$($CollectionManifest.id)' is experimental and excluded from Stable channel"
579 }
580 }
581 return @{ IsEligible = $true; Reason = '' }
582 }
583 'preview' {
584 return @{ IsEligible = $true; Reason = '' }
585 }
586 'stable' {
587 return @{ IsEligible = $true; Reason = '' }
588 }
589 default {
590 return @{
591 IsEligible = $false
592 Reason = "Collection '$($CollectionManifest.id)' has invalid maturity value: $maturity"
593 }
594 }
595 }
596}
597
598function Test-GlobMatch {
599 <#
600 .SYNOPSIS
601 Tests whether a name matches any of the provided glob patterns.
602 .DESCRIPTION
603 Uses PowerShell's -like operator to test glob pattern matching with
604 * (any characters) and ? (single character) wildcards.
605 .PARAMETER Name
606 The artifact name to test against patterns.
607 .PARAMETER Patterns
608 Array of glob patterns to match against.
609 .OUTPUTS
610 [bool] True if name matches any pattern, false otherwise.
611 #>
612 [CmdletBinding()]
613 [OutputType([bool])]
614 param(
615 [Parameter(Mandatory = $true)]
616 [string]$Name,
617
618 [Parameter(Mandatory = $true)]
619 [string[]]$Patterns
620 )
621
622 foreach ($pattern in $Patterns) {
623 if ($Name -like $pattern) {
624 return $true
625 }
626 }
627 return $false
628}
629
630function Get-CollectionArtifacts {
631 <#
632 .SYNOPSIS
633 Filters collection artifacts by collection item metadata and channel maturity.
634 .DESCRIPTION
635 Applies collection-level filtering to manifest items, returning artifact
636 names that match allowed maturities. Item-level maturity is used when
637 present; otherwise artifacts default to stable.
638 .PARAMETER Collection
639 Collection manifest hashtable with items.
640 .PARAMETER AllowedMaturities
641 Array of maturity levels to include.
642 .OUTPUTS
643 [hashtable] With Agents, Prompts, Instructions, Skills arrays of matching artifact names.
644 #>
645 [CmdletBinding()]
646 [OutputType([hashtable])]
647 param(
648 [Parameter(Mandatory = $true)]
649 [hashtable]$Collection,
650
651 [Parameter(Mandatory = $true)]
652 [string[]]$AllowedMaturities
653 )
654
655 $result = @{
656 Agents = @()
657 Prompts = @()
658 Instructions = @()
659 Skills = @()
660 }
661
662 if (-not $Collection.ContainsKey('items') -or @($Collection.items).Count -eq 0) {
663 return $result
664 }
665
666 foreach ($item in $Collection.items) {
667 if (-not $item.ContainsKey('kind') -or -not $item.ContainsKey('path')) {
668 continue
669 }
670
671 $kind = [string]$item.kind
672 $path = [string]$item.path
673
674 $maturity = Resolve-CollectionItemMaturity -Maturity $item.maturity
675 if ($AllowedMaturities -notcontains $maturity) {
676 continue
677 }
678
679 $artifactKey = Get-CollectionArtifactKey -Kind $kind -Path $path
680 switch ($kind) {
681 'agent' { $result.Agents += $artifactKey }
682 'prompt' { $result.Prompts += $artifactKey }
683 'instruction' { $result.Instructions += $artifactKey }
684 'skill' { $result.Skills += $artifactKey }
685 }
686 }
687
688 return $result
689}
690
691function Resolve-HandoffDependencies {
692 <#
693 .SYNOPSIS
694 Resolves transitive agent handoff dependencies using BFS traversal.
695 .DESCRIPTION
696 Starting from seed agents, performs breadth-first traversal of agent handoff
697 declarations in YAML frontmatter to compute the transitive closure of
698 all agents reachable through handoff chains.
699
700 Handoff targets in frontmatter use display names (e.g., "Task Planner")
701 while agent files use kebab-case stems (e.g., task-planner.agent.md).
702 This function builds a name index to resolve both formats.
703 .PARAMETER SeedAgents
704 Initial agent names (file stems) to start BFS from.
705 .PARAMETER AgentsDir
706 Path to the agents directory containing .agent.md files.
707 .OUTPUTS
708 [string[]] Complete set of agent file stems including seed agents and all transitive handoff targets.
709 #>
710 [CmdletBinding()]
711 [OutputType([string[]])]
712 param(
713 [Parameter(Mandatory = $true)]
714 [string[]]$SeedAgents,
715
716 [Parameter(Mandatory = $true)]
717 [string]$AgentsDir
718 )
719
720 # Build index: map display names and file stems to agent file objects.
721 # Handoff targets use display names from frontmatter (e.g., "RPI Agent")
722 # while seed agents and collection keys use file stems (e.g., "rpi-agent").
723 $agentIndex = @{}
724 $allAgentFiles = Get-ChildItem -Path $AgentsDir -Filter "*.agent.md" -Recurse -File
725 foreach ($af in $allAgentFiles) {
726 $stem = $af.BaseName -replace '\.agent$', ''
727 $agentIndex[$stem] = $af
728
729 $fc = Get-Content -Path $af.FullName -Raw
730 if ($fc -match '(?s)^---\s*\r?\n(.*?)\r?\n---') {
731 $yml = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n"
732 try {
733 $meta = ConvertFrom-Yaml -Yaml $yml
734 if ($meta.ContainsKey('name') -and $meta.name -is [string] -and $meta.name -ne '') {
735 if (-not $agentIndex.ContainsKey($meta.name)) {
736 $agentIndex[$meta.name] = $af
737 }
738 }
739 }
740 catch {
741 Write-Verbose "Skipping display name index for $($af.Name): $_"
742 }
743 }
744 }
745
746 $visited = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
747 $queue = [System.Collections.Generic.Queue[string]]::new()
748
749 foreach ($agent in $SeedAgents) {
750 if ($visited.Add($agent)) {
751 $queue.Enqueue($agent)
752 }
753 }
754
755 while ($queue.Count -gt 0) {
756 $current = $queue.Dequeue()
757 $agentFile = $agentIndex[$current]
758
759 if (-not $agentFile) {
760 Write-Warning "Handoff target agent file not found: $current"
761 continue
762 }
763
764 # Normalize visited entry to file stem for consistent collection filtering
765 $fileStem = $agentFile.BaseName -replace '\.agent$', ''
766 if ($fileStem -ne $current) {
767 $visited.Add($fileStem) | Out-Null
768 }
769
770 # Parse handoffs from frontmatter
771 $content = Get-Content -Path $agentFile.FullName -Raw
772 if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') {
773 $yamlContent = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n"
774 try {
775 $data = ConvertFrom-Yaml -Yaml $yamlContent
776 if ($data.ContainsKey('handoffs') -and $data.handoffs -is [System.Collections.IEnumerable] -and $data.handoffs -isnot [string]) {
777 foreach ($handoff in $data.handoffs) {
778 # Handle both string format and object format (with 'agent' field).
779 # Handoff targets bypass maturity filtering by design.
780 # See docs/contributing/ai-artifacts-common.md
781 # "Handoff vs Requires Maturity Filtering" for rationale.
782 $targetAgent = $null
783 if ($handoff -is [string]) {
784 $targetAgent = $handoff
785 } elseif ($handoff -is [hashtable] -and $handoff.ContainsKey('agent')) {
786 $targetAgent = $handoff.agent
787 }
788 if ($targetAgent -and $visited.Add($targetAgent)) {
789 $queue.Enqueue($targetAgent)
790 }
791 }
792 }
793 }
794 catch {
795 Write-Warning "Failed to parse handoffs from $($agentFile.Name): $_"
796 }
797 }
798 }
799
800 return @($visited)
801}
802
803function Resolve-RequiresDependencies {
804 <#
805 .SYNOPSIS
806 Resolves transitive artifact dependencies from collection item requires blocks.
807 .DESCRIPTION
808 Walks requires blocks in collection items to compute the complete set of
809 dependent artifacts across all types (agents, prompts, instructions, skills).
810 .PARAMETER ArtifactNames
811 Hashtable with initial artifact name arrays keyed by type (agents, prompts, instructions, skills).
812 .PARAMETER AllowedMaturities
813 Array of maturity levels to include.
814 .PARAMETER CollectionRequires
815 Per-type map of artifact requires blocks keyed by artifact name.
816 .PARAMETER CollectionMaturities
817 Optional per-type maturity map keyed by artifact name.
818 .OUTPUTS
819 [hashtable] With Agents, Prompts, Instructions, Skills arrays containing resolved names.
820 #>
821 [CmdletBinding()]
822 [OutputType([hashtable])]
823 param(
824 [Parameter(Mandatory = $true)]
825 [hashtable]$ArtifactNames,
826
827 [Parameter(Mandatory = $true)]
828 [string[]]$AllowedMaturities,
829
830 [Parameter(Mandatory = $false)]
831 [hashtable]$CollectionRequires = @{},
832
833 [Parameter(Mandatory = $false)]
834 [hashtable]$CollectionMaturities = @{}
835 )
836
837 $resolved = @{
838 Agents = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
839 Prompts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
840 Instructions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
841 Skills = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
842 }
843
844 $typeMap = @{
845 agents = 'Agents'
846 prompts = 'Prompts'
847 instructions = 'Instructions'
848 skills = 'Skills'
849 }
850
851 # Seed with initial artifact names
852 foreach ($type in @('agents', 'prompts', 'instructions', 'skills')) {
853 $capitalType = $typeMap[$type]
854 if ($ArtifactNames.ContainsKey($type)) {
855 foreach ($name in $ArtifactNames[$type]) {
856 $null = $resolved[$capitalType].Add($name)
857 }
858 }
859 }
860
861 $changed = $true
862 while ($changed) {
863 $changed = $false
864
865 foreach ($sourceType in @('agents', 'prompts', 'instructions', 'skills')) {
866 if (-not $CollectionRequires.ContainsKey($sourceType)) {
867 continue
868 }
869
870 $sourceCapitalType = $typeMap[$sourceType]
871 foreach ($sourceName in @($resolved[$sourceCapitalType])) {
872 if (-not $CollectionRequires[$sourceType].ContainsKey($sourceName)) {
873 continue
874 }
875
876 $requires = $CollectionRequires[$sourceType][$sourceName]
877 if (-not $requires) {
878 continue
879 }
880
881 foreach ($targetType in @('agents', 'prompts', 'instructions', 'skills')) {
882 if (-not $requires.ContainsKey($targetType)) {
883 continue
884 }
885
886 $targetCapitalType = $typeMap[$targetType]
887 foreach ($dep in @($requires[$targetType])) {
888 $depMaturity = 'stable'
889 if ($CollectionMaturities.ContainsKey($targetType) -and $CollectionMaturities[$targetType].ContainsKey($dep)) {
890 $depMaturity = $CollectionMaturities[$targetType][$dep]
891 }
892
893 if ($AllowedMaturities -notcontains $depMaturity) {
894 continue
895 }
896
897 if ($resolved[$targetCapitalType].Add($dep)) {
898 $changed = $true
899 }
900 }
901 }
902 }
903 }
904 }
905
906 # Convert HashSets to arrays
907 return @{
908 Agents = @($resolved.Agents)
909 Prompts = @($resolved.Prompts)
910 Instructions = @($resolved.Instructions)
911 Skills = @($resolved.Skills)
912 }
913}
914
915function Test-PathsExist {
916 <#
917 .SYNOPSIS
918 Validates that required paths exist for extension preparation.
919 .DESCRIPTION
920 Validation function that checks whether extension directory, package.json,
921 and .github directory exist at the specified locations.
922 .PARAMETER ExtensionDir
923 Path to the extension directory.
924 .PARAMETER PackageJsonPath
925 Path to package.json file.
926 .PARAMETER GitHubDir
927 Path to .github directory.
928 .OUTPUTS
929 [hashtable] With IsValid bool, MissingPaths array, and ErrorMessages array.
930 #>
931 [CmdletBinding()]
932 [OutputType([hashtable])]
933 param(
934 [Parameter(Mandatory = $true)]
935 [string]$ExtensionDir,
936
937 [Parameter(Mandatory = $true)]
938 [string]$PackageJsonPath,
939
940 [Parameter(Mandatory = $true)]
941 [string]$GitHubDir
942 )
943
944 $missingPaths = @()
945 $errorMessages = @()
946
947 if (-not (Test-Path $ExtensionDir)) {
948 $missingPaths += $ExtensionDir
949 $errorMessages += "Extension directory not found: $ExtensionDir"
950 }
951 if (-not (Test-Path $PackageJsonPath)) {
952 $missingPaths += $PackageJsonPath
953 $errorMessages += "package.json not found: $PackageJsonPath"
954 }
955 if (-not (Test-Path $GitHubDir)) {
956 $missingPaths += $GitHubDir
957 $errorMessages += ".github directory not found: $GitHubDir"
958 }
959
960 return @{
961 IsValid = ($missingPaths.Count -eq 0)
962 MissingPaths = $missingPaths
963 ErrorMessages = $errorMessages
964 }
965}
966
967function Get-DiscoveredAgents {
968 <#
969 .SYNOPSIS
970 Discovers chat agent files from the agents directory.
971 .DESCRIPTION
972 Discovery function that scans the agents directory for .agent.md files,
973 filters by exclusion list, and returns structured agent objects.
974 .PARAMETER AgentsDir
975 Path to the agents directory.
976 .PARAMETER AllowedMaturities
977 Array of maturity levels to include.
978 .PARAMETER ExcludedAgents
979 Array of agent names to exclude from packaging.
980 .OUTPUTS
981 [hashtable] With Agents array, Skipped array, and DirectoryExists bool.
982 #>
983 [CmdletBinding()]
984 [OutputType([hashtable])]
985 param(
986 [Parameter(Mandatory = $true)]
987 [string]$AgentsDir,
988
989 [Parameter(Mandatory = $true)]
990 [string[]]$AllowedMaturities,
991
992 [Parameter(Mandatory = $false)]
993 [string[]]$ExcludedAgents = @()
994 )
995
996 $result = @{
997 Agents = @()
998 Skipped = @()
999 DirectoryExists = (Test-Path $AgentsDir)
1000 }
1001
1002 if (-not $result.DirectoryExists) {
1003 return $result
1004 }
1005
1006 $agentFiles = Get-ChildItem -Path $AgentsDir -Filter "*.agent.md" -Recurse | Sort-Object Name
1007 $agentFiles = $agentFiles | Where-Object { -not (Test-DeprecatedPath -Path $_.FullName) }
1008
1009 foreach ($agentFile in $agentFiles) {
1010 $agentRelPath = [System.IO.Path]::GetRelativePath($AgentsDir, $agentFile.FullName) -replace '\\', '/'
1011
1012 if (Test-HveCoreRepoSpecificPath -RelativePath $agentRelPath) {
1013 $agentName = $agentFile.BaseName -replace '\.agent$', ''
1014 $result.Skipped += @{ Name = $agentName; Reason = 'repo-specific (root-level)' }
1015 continue
1016 }
1017
1018 $agentName = $agentFile.BaseName -replace '\.agent$', ''
1019
1020 if ($ExcludedAgents -contains $agentName) {
1021 $result.Skipped += @{ Name = $agentName; Reason = 'excluded' }
1022 continue
1023 }
1024
1025 $maturity = "stable"
1026
1027 if ($AllowedMaturities -notcontains $maturity) {
1028 $result.Skipped += @{ Name = $agentName; Reason = "maturity: $maturity" }
1029 continue
1030 }
1031 $result.Agents += [PSCustomObject]@{
1032 name = $agentName
1033 path = "./.github/agents/$agentRelPath"
1034 }
1035 }
1036
1037 return $result
1038}
1039
1040function Get-DiscoveredPrompts {
1041 <#
1042 .SYNOPSIS
1043 Discovers prompt files from the prompts directory.
1044 .DESCRIPTION
1045 Discovery function that scans the prompts directory for .prompt.md files,
1046 and returns structured prompt objects with relative paths.
1047 .PARAMETER PromptsDir
1048 Path to the prompts directory.
1049 .PARAMETER GitHubDir
1050 Path to the .github directory for relative path calculation.
1051 .PARAMETER AllowedMaturities
1052 Array of maturity levels to include.
1053 .OUTPUTS
1054 [hashtable] With Prompts array, Skipped array, and DirectoryExists bool.
1055 #>
1056 [CmdletBinding()]
1057 [OutputType([hashtable])]
1058 param(
1059 [Parameter(Mandatory = $true)]
1060 [string]$PromptsDir,
1061
1062 [Parameter(Mandatory = $true)]
1063 [string]$GitHubDir,
1064
1065 [Parameter(Mandatory = $true)]
1066 [string[]]$AllowedMaturities
1067 )
1068
1069 $result = @{
1070 Prompts = @()
1071 Skipped = @()
1072 DirectoryExists = (Test-Path $PromptsDir)
1073 }
1074
1075 if (-not $result.DirectoryExists) {
1076 return $result
1077 }
1078
1079 $promptFiles = Get-ChildItem -Path $PromptsDir -Filter "*.prompt.md" -Recurse | Sort-Object Name
1080 $promptFiles = $promptFiles | Where-Object { -not (Test-DeprecatedPath -Path $_.FullName) }
1081
1082 foreach ($promptFile in $promptFiles) {
1083 $promptName = $promptFile.BaseName -replace '\.prompt$', ''
1084
1085 $promptRelPath = [System.IO.Path]::GetRelativePath($PromptsDir, $promptFile.FullName) -replace '\\', '/'
1086 if (Test-HveCoreRepoSpecificPath -RelativePath $promptRelPath) {
1087 $result.Skipped += @{ Name = $promptName; Reason = 'repo-specific (root-level)' }
1088 continue
1089 }
1090
1091 $maturity = "stable"
1092
1093 if ($AllowedMaturities -notcontains $maturity) {
1094 $result.Skipped += @{ Name = $promptName; Reason = "maturity: $maturity" }
1095 continue
1096 }
1097
1098 $relativePath = [System.IO.Path]::GetRelativePath($GitHubDir, $promptFile.FullName) -replace '\\', '/'
1099
1100 $result.Prompts += [PSCustomObject]@{
1101 name = $promptName
1102 path = "./.github/$relativePath"
1103 }
1104 }
1105
1106 return $result
1107}
1108
1109function Get-DiscoveredInstructions {
1110 <#
1111 .SYNOPSIS
1112 Discovers instruction files from the instructions directory.
1113 .DESCRIPTION
1114 Discovery function that scans the instructions directory for .instructions.md files,
1115 and returns structured instruction objects with normalized paths.
1116 .PARAMETER InstructionsDir
1117 Path to the instructions directory.
1118 .PARAMETER GitHubDir
1119 Path to the .github directory for relative path calculation.
1120 .PARAMETER AllowedMaturities
1121 Array of maturity levels to include.
1122 .OUTPUTS
1123 [hashtable] With Instructions array, Skipped array, and DirectoryExists bool.
1124 #>
1125 [CmdletBinding()]
1126 [OutputType([hashtable])]
1127 param(
1128 [Parameter(Mandatory = $true)]
1129 [string]$InstructionsDir,
1130
1131 [Parameter(Mandatory = $true)]
1132 [string]$GitHubDir,
1133
1134 [Parameter(Mandatory = $true)]
1135 [string[]]$AllowedMaturities
1136 )
1137
1138 $result = @{
1139 Instructions = @()
1140 Skipped = @()
1141 DirectoryExists = (Test-Path $InstructionsDir)
1142 }
1143
1144 if (-not $result.DirectoryExists) {
1145 return $result
1146 }
1147
1148 $instructionFiles = Get-ChildItem -Path $InstructionsDir -Filter "*.instructions.md" -Recurse | Sort-Object Name
1149 $instructionFiles = $instructionFiles | Where-Object { -not (Test-DeprecatedPath -Path $_.FullName) }
1150
1151 foreach ($instrFile in $instructionFiles) {
1152 $instrRelPath = [System.IO.Path]::GetRelativePath($InstructionsDir, $instrFile.FullName) -replace '\\', '/'
1153 if (Test-HveCoreRepoSpecificPath -RelativePath $instrRelPath) {
1154 $result.Skipped += @{ Name = $instrFile.BaseName; Reason = 'repo-specific (root-level)' }
1155 continue
1156 }
1157 $baseName = $instrFile.BaseName -replace '\.instructions$', ''
1158 $instrName = "$baseName-instructions"
1159
1160 $maturity = "stable"
1161
1162 if ($AllowedMaturities -notcontains $maturity) {
1163 $result.Skipped += @{ Name = $instrName; Reason = "maturity: $maturity" }
1164 continue
1165 }
1166
1167 $relativePathFromGitHub = [System.IO.Path]::GetRelativePath($GitHubDir, $instrFile.FullName)
1168 $normalizedRelativePath = (Join-Path ".github" $relativePathFromGitHub) -replace '\\', '/'
1169
1170 $result.Instructions += [PSCustomObject]@{
1171 name = $instrName
1172 path = "./$normalizedRelativePath"
1173 }
1174 }
1175
1176 return $result
1177}
1178
1179function Get-DiscoveredSkills {
1180 <#
1181 .SYNOPSIS
1182 Discovers skill packages from the skills directory.
1183 .DESCRIPTION
1184 Discovery function that scans the skills directory for subdirectories
1185 containing SKILL.md files and returns structured skill objects.
1186 .PARAMETER SkillsDir
1187 Path to the skills directory.
1188 .PARAMETER AllowedMaturities
1189 Array of maturity levels to include.
1190 .OUTPUTS
1191 [hashtable] With Skills array, Skipped array, and DirectoryExists bool.
1192 #>
1193 [CmdletBinding()]
1194 [OutputType([hashtable])]
1195 param(
1196 [Parameter(Mandatory = $true)]
1197 [string]$SkillsDir,
1198
1199 [Parameter(Mandatory = $true)]
1200 [string[]]$AllowedMaturities
1201 )
1202
1203 $result = @{
1204 Skills = @()
1205 Skipped = @()
1206 DirectoryExists = (Test-Path $SkillsDir)
1207 }
1208
1209 if (-not $result.DirectoryExists) {
1210 return $result
1211 }
1212
1213 $skillFiles = Get-ChildItem -Path $SkillsDir -Filter "SKILL.md" -File -Recurse | Sort-Object { $_.Directory.FullName }
1214 $skillFiles = $skillFiles | Where-Object { -not (Test-DeprecatedPath -Path $_.FullName) }
1215
1216 foreach ($skillFile in $skillFiles) {
1217 $skillDir = $skillFile.Directory
1218 $skillName = $skillDir.Name
1219 $skillRelPath = [System.IO.Path]::GetRelativePath($SkillsDir, $skillDir.FullName) -replace '\\', '/'
1220
1221 if (Test-HveCoreRepoSpecificPath -RelativePath $skillRelPath) {
1222 $result.Skipped += @{ Name = $skillName; Reason = 'repo-specific (root-level)' }
1223 continue
1224 }
1225
1226 $maturity = "stable"
1227
1228 if ($AllowedMaturities -notcontains $maturity) {
1229 $result.Skipped += @{ Name = $skillName; Reason = "maturity: $maturity" }
1230 continue
1231 }
1232
1233 $result.Skills += [PSCustomObject]@{
1234 name = $skillName
1235 path = "./.github/skills/$skillRelPath/SKILL.md"
1236 }
1237 }
1238
1239 return $result
1240}
1241
1242function Update-PackageJsonContributes {
1243 <#
1244 .SYNOPSIS
1245 Updates package.json contributes section with discovered components.
1246 .DESCRIPTION
1247 Pure function that takes a package.json object and discovered components,
1248 returning a new object with the contributes section updated. Handles
1249 chatAgents, chatPromptFiles, chatInstructions, and chatSkills.
1250 .PARAMETER PackageJson
1251 The package.json object to update.
1252 .PARAMETER ChatAgents
1253 Array of discovered chat agent objects.
1254 .PARAMETER ChatPromptFiles
1255 Array of discovered prompt objects.
1256 .PARAMETER ChatInstructions
1257 Array of discovered instruction objects.
1258 .PARAMETER ChatSkills
1259 Array of discovered skill objects.
1260 .OUTPUTS
1261 [PSCustomObject] Updated package.json object.
1262 #>
1263 [CmdletBinding()]
1264 [OutputType([PSCustomObject])]
1265 param(
1266 [Parameter(Mandatory = $true)]
1267 [PSCustomObject]$PackageJson,
1268
1269 [Parameter(Mandatory = $true)]
1270 [AllowEmptyCollection()]
1271 [array]$ChatAgents,
1272
1273 [Parameter(Mandatory = $true)]
1274 [AllowEmptyCollection()]
1275 [array]$ChatPromptFiles,
1276
1277 [Parameter(Mandatory = $true)]
1278 [AllowEmptyCollection()]
1279 [array]$ChatInstructions,
1280
1281 [Parameter(Mandatory = $true)]
1282 [AllowEmptyCollection()]
1283 [array]$ChatSkills
1284 )
1285
1286 # Clone the object to avoid modifying the original
1287 $updated = $PackageJson | ConvertTo-Json -Depth 10 | ConvertFrom-Json
1288
1289 # Strip name and description; VS Code reads these from the files directly
1290 $ChatAgents = @($ChatAgents | Select-Object -Property path)
1291 $ChatPromptFiles = @($ChatPromptFiles | Select-Object -Property path)
1292 $ChatInstructions = @($ChatInstructions | Select-Object -Property path)
1293 $ChatSkills = @($ChatSkills | Select-Object -Property path)
1294
1295 # Ensure contributes section exists
1296 if (-not $updated.contributes) {
1297 $updated | Add-Member -NotePropertyName "contributes" -NotePropertyValue ([PSCustomObject]@{})
1298 }
1299
1300 # Add or update contributes properties
1301 if ($null -eq $updated.contributes.chatAgents) {
1302 $updated.contributes | Add-Member -NotePropertyName "chatAgents" -NotePropertyValue $ChatAgents -Force
1303 } else {
1304 $updated.contributes.chatAgents = $ChatAgents
1305 }
1306
1307 if ($null -eq $updated.contributes.chatPromptFiles) {
1308 $updated.contributes | Add-Member -NotePropertyName "chatPromptFiles" -NotePropertyValue $ChatPromptFiles -Force
1309 } else {
1310 $updated.contributes.chatPromptFiles = $ChatPromptFiles
1311 }
1312
1313 if ($null -eq $updated.contributes.chatInstructions) {
1314 $updated.contributes | Add-Member -NotePropertyName "chatInstructions" -NotePropertyValue $ChatInstructions -Force
1315 } else {
1316 $updated.contributes.chatInstructions = $ChatInstructions
1317 }
1318
1319 if ($null -eq $updated.contributes.chatSkills) {
1320 $updated.contributes | Add-Member -NotePropertyName "chatSkills" -NotePropertyValue $ChatSkills -Force
1321 } else {
1322 $updated.contributes.chatSkills = $ChatSkills
1323 }
1324
1325 return $updated
1326}
1327
1328function New-PrepareResult {
1329 <#
1330 .SYNOPSIS
1331 Creates a standardized result object for extension preparation operations.
1332 .DESCRIPTION
1333 Factory function that creates a hashtable with consistent properties
1334 for reporting preparation operation outcomes.
1335 .PARAMETER Success
1336 Indicates whether the operation completed successfully.
1337 .PARAMETER Version
1338 The version string from package.json.
1339 .PARAMETER AgentCount
1340 Number of agents discovered and included.
1341 .PARAMETER PromptCount
1342 Number of prompts discovered and included.
1343 .PARAMETER InstructionCount
1344 Number of instructions discovered and included.
1345 .PARAMETER SkillCount
1346 Number of skills discovered and included.
1347 .PARAMETER ErrorMessage
1348 Error description when Success is false.
1349 .OUTPUTS
1350 Hashtable with Success, Version, AgentCount, PromptCount,
1351 InstructionCount, SkillCount, and ErrorMessage properties.
1352 #>
1353 [CmdletBinding()]
1354 [OutputType([hashtable])]
1355 param(
1356 [Parameter(Mandatory = $true)]
1357 [bool]$Success,
1358
1359 [Parameter(Mandatory = $false)]
1360 [string]$Version = "",
1361
1362 [Parameter(Mandatory = $false)]
1363 [int]$AgentCount = 0,
1364
1365 [Parameter(Mandatory = $false)]
1366 [int]$PromptCount = 0,
1367
1368 [Parameter(Mandatory = $false)]
1369 [int]$InstructionCount = 0,
1370
1371 [Parameter(Mandatory = $false)]
1372 [int]$SkillCount = 0,
1373
1374 [Parameter(Mandatory = $false)]
1375 [string]$ErrorMessage = ""
1376 )
1377
1378 return @{
1379 Success = $Success
1380 Version = $Version
1381 AgentCount = $AgentCount
1382 PromptCount = $PromptCount
1383 InstructionCount = $InstructionCount
1384 SkillCount = $SkillCount
1385 ErrorMessage = $ErrorMessage
1386 }
1387}
1388
1389function Test-TemplateConsistency {
1390 <#
1391 .SYNOPSIS
1392 Validates collection template metadata against its collection manifest.
1393 .DESCRIPTION
1394 Compares name, displayName, and description fields between a collection
1395 package template (e.g. package.developer.json) and the corresponding
1396 collection manifest. Emits warnings for divergences and returns a list
1397 of mismatches.
1398 .PARAMETER TemplatePath
1399 Path to the collection package template JSON file.
1400 .PARAMETER CollectionManifest
1401 Parsed collection manifest hashtable with name, displayName, description.
1402 .OUTPUTS
1403 [hashtable] With Mismatches array and IsConsistent bool.
1404 #>
1405 [CmdletBinding()]
1406 [OutputType([hashtable])]
1407 param(
1408 [Parameter(Mandatory = $true)]
1409 [ValidateNotNullOrEmpty()]
1410 [string]$TemplatePath,
1411
1412 [Parameter(Mandatory = $true)]
1413 [hashtable]$CollectionManifest
1414 )
1415
1416 $result = @{
1417 Mismatches = @()
1418 IsConsistent = $true
1419 }
1420
1421 if (-not (Test-Path $TemplatePath)) {
1422 $result.Mismatches += @{
1423 Field = 'file'
1424 Template = $TemplatePath
1425 Manifest = 'N/A'
1426 Message = "Template file not found: $TemplatePath"
1427 }
1428 $result.IsConsistent = $false
1429 return $result
1430 }
1431
1432 try {
1433 $template = Get-Content -Path $TemplatePath -Raw | ConvertFrom-Json
1434 }
1435 catch {
1436 $result.Mismatches += @{
1437 Field = 'file'
1438 Template = $TemplatePath
1439 Manifest = 'N/A'
1440 Message = "Failed to parse template: $($_.Exception.Message)"
1441 }
1442 $result.IsConsistent = $false
1443 return $result
1444 }
1445
1446 $fieldsToCheck = @('name', 'displayName', 'description')
1447 foreach ($field in $fieldsToCheck) {
1448 $templateValue = $null
1449 $manifestValue = $null
1450
1451 if ($template.PSObject.Properties[$field]) {
1452 $templateValue = $template.$field
1453 }
1454 if ($CollectionManifest.ContainsKey($field)) {
1455 $manifestValue = $CollectionManifest[$field]
1456 }
1457
1458 if ($null -ne $templateValue -and $null -ne $manifestValue -and $templateValue -ne $manifestValue) {
1459 $result.Mismatches += @{
1460 Field = $field
1461 Template = $templateValue
1462 Manifest = $manifestValue
1463 Message = "$field diverges: template='$templateValue' manifest='$manifestValue'"
1464 }
1465 $result.IsConsistent = $false
1466 }
1467 }
1468
1469 return $result
1470}
1471
1472function Invoke-PrepareExtension {
1473 <#
1474 .SYNOPSIS
1475 Orchestrates VS Code extension preparation with full error handling.
1476 .DESCRIPTION
1477 Executes the complete preparation workflow: validates paths, discovers
1478 agents/prompts/instructions, updates package.json, and handles changelog.
1479 Returns a result object instead of using exit codes.
1480 .PARAMETER ExtensionDirectory
1481 Absolute path to the extension directory containing package.json.
1482 .PARAMETER RepoRoot
1483 Absolute path to the repository root directory.
1484 .PARAMETER Channel
1485 Release channel controlling maturity filter ('Stable' or 'PreRelease').
1486 .PARAMETER ChangelogPath
1487 Optional path to changelog file to include.
1488 .PARAMETER DryRun
1489 When specified, shows what would be done without making changes.
1490 .OUTPUTS
1491 Hashtable with Success, Version, AgentCount, PromptCount,
1492 InstructionCount, SkillCount, and ErrorMessage properties.
1493 #>
1494 [CmdletBinding()]
1495 [OutputType([hashtable])]
1496 param(
1497 [Parameter(Mandatory = $true)]
1498 [ValidateNotNullOrEmpty()]
1499 [string]$ExtensionDirectory,
1500
1501 [Parameter(Mandatory = $true)]
1502 [ValidateNotNullOrEmpty()]
1503 [string]$RepoRoot,
1504
1505 [Parameter(Mandatory = $false)]
1506 [ValidateSet('Stable', 'PreRelease')]
1507 [string]$Channel = 'Stable',
1508
1509 [Parameter(Mandatory = $false)]
1510 [string]$ChangelogPath = "",
1511
1512 [Parameter(Mandatory = $false)]
1513 [switch]$DryRun,
1514
1515 [Parameter(Mandatory = $false)]
1516 [string]$Collection = ""
1517 )
1518
1519 # Derive paths
1520 $GitHubDir = Join-Path $RepoRoot ".github"
1521 $PackageJsonPath = Join-Path $ExtensionDirectory "package.json"
1522
1523 # Generate collection package files from root collection manifests.
1524 # This ensures extension/package.json and extension/package.*.json exist
1525 # with the correct version from the template before any reads occur.
1526 try {
1527 $generated = Invoke-ExtensionCollectionsGeneration -RepoRoot $RepoRoot -Channel $Channel
1528 Write-Host "Generated $($generated.Count) collection package file(s)" -ForegroundColor Green
1529 }
1530 catch {
1531 return New-PrepareResult -Success $false -ErrorMessage "Package generation failed: $($_.Exception.Message)"
1532 }
1533
1534 # Validate required paths exist (package.json now guaranteed by generation)
1535 $pathValidation = Test-PathsExist -ExtensionDir $ExtensionDirectory `
1536 -PackageJsonPath $PackageJsonPath `
1537 -GitHubDir $GitHubDir
1538 if (-not $pathValidation.IsValid) {
1539 $missingPaths = $pathValidation.MissingPaths -join ', '
1540 return New-PrepareResult -Success $false -ErrorMessage "Required paths not found: $missingPaths"
1541 }
1542
1543 # Read and parse package.json
1544 try {
1545 $packageJsonContent = Get-Content -Path $PackageJsonPath -Raw
1546 $packageJson = $packageJsonContent | ConvertFrom-Json
1547 }
1548 catch {
1549 return New-PrepareResult -Success $false -ErrorMessage "Failed to parse package.json at '$PackageJsonPath'. Check the file for JSON syntax errors. Underlying error: $($_.Exception.Message)"
1550 }
1551
1552 # Validate version field
1553 if (-not $packageJson.PSObject.Properties['version']) {
1554 return New-PrepareResult -Success $false -ErrorMessage "package.json does not contain a 'version' field"
1555 }
1556 $version = $packageJson.version
1557 if ($version -notmatch '^\d+\.\d+\.\d+$') {
1558 return New-PrepareResult -Success $false -ErrorMessage "Invalid version format in package.json: $version"
1559 }
1560
1561 # Get allowed maturities for channel
1562 $allowedMaturities = Get-AllowedMaturities -Channel $Channel
1563
1564 Write-Host "`n=== Prepare Extension ===" -ForegroundColor Cyan
1565 Write-Host "Extension Directory: $ExtensionDirectory"
1566 Write-Host "Repository Root: $RepoRoot"
1567 Write-Host "Channel: $Channel"
1568 Write-Host "Allowed Maturities: $($allowedMaturities -join ', ')"
1569 Write-Host "Version: $version"
1570 if ($DryRun) {
1571 Write-Host "[DRY RUN] No changes will be made" -ForegroundColor Yellow
1572 }
1573
1574 # Load collection manifest if specified
1575 $collectionManifest = $null
1576 $collectionArtifactNames = $null
1577 $collectionMaturities = @{}
1578 $collectionRequires = @{}
1579
1580 if ($Collection -and $Collection -ne "") {
1581 $collectionManifest = Get-CollectionManifest -CollectionPath $Collection
1582 Write-Host "Collection: $($collectionManifest.displayName) ($($collectionManifest.id))"
1583
1584 $artifactCollectionManifest = $collectionManifest
1585 if (-not $artifactCollectionManifest.ContainsKey('items') -or @($artifactCollectionManifest.items).Count -eq 0) {
1586 # When the manifest lacks items (e.g., a generated JSON template),
1587 # resolve from the root YAML collection by ID.
1588 $rootCollectionPath = Join-Path $RepoRoot "collections/$($collectionManifest.id).collection.yml"
1589 if (Test-Path $rootCollectionPath) {
1590 $artifactCollectionManifest = Get-CollectionManifest -CollectionPath $rootCollectionPath
1591 Write-Host "Using root collection for items: $rootCollectionPath"
1592 }
1593 else {
1594 Write-Warning "No root collection found for '$($collectionManifest.id)' at $rootCollectionPath"
1595 }
1596 }
1597
1598 # Check collection-level maturity eligibility
1599 $collectionEligibility = Test-CollectionMaturityEligible -CollectionManifest $collectionManifest -Channel $Channel
1600 if (-not $collectionEligibility.IsEligible) {
1601 Write-Host "`n⏭️ $($collectionEligibility.Reason)" -ForegroundColor Yellow
1602 return New-PrepareResult -Success $true -Version $version
1603 }
1604
1605 $collectionMaturity = if ($collectionManifest.ContainsKey('maturity')) { $collectionManifest['maturity'] } else { 'stable' }
1606 Write-Host "Collection maturity: $collectionMaturity"
1607
1608 # Build collection maturity map and channel-filtered artifact names
1609 $collectionMaturities = @{}
1610 $collectionRequires = @{}
1611
1612 if ($artifactCollectionManifest.ContainsKey('items')) {
1613 foreach ($item in $artifactCollectionManifest.items) {
1614 if (-not $item.ContainsKey('kind') -or -not $item.ContainsKey('path')) {
1615 continue
1616 }
1617
1618 $itemKind = [string]$item.kind
1619 $itemPath = [string]$item.path
1620 $artifactKey = Get-CollectionArtifactKey -Kind $itemKind -Path $itemPath
1621 $effectiveMaturity = Resolve-CollectionItemMaturity -Maturity $item.maturity
1622 if (-not $collectionMaturities.ContainsKey("${itemKind}s") -or $null -eq $collectionMaturities["${itemKind}s"]) {
1623 $collectionMaturities["${itemKind}s"] = @{}
1624 }
1625 $collectionMaturities["${itemKind}s"][$artifactKey] = $effectiveMaturity
1626
1627 if ($item.ContainsKey('requires') -and $item.requires) {
1628 if (-not $collectionRequires.ContainsKey("${itemKind}s") -or $null -eq $collectionRequires["${itemKind}s"]) {
1629 $collectionRequires["${itemKind}s"] = @{}
1630 }
1631 $collectionRequires["${itemKind}s"][$artifactKey] = $item.requires
1632 }
1633 }
1634 }
1635
1636 $collectionArtifactNames = Get-CollectionArtifacts -Collection $artifactCollectionManifest -AllowedMaturities $allowedMaturities
1637
1638 # Resolve handoff dependencies (agents only)
1639 if (@($collectionArtifactNames.Agents).Count -gt 0) {
1640 $agentsDir = Join-Path $GitHubDir "agents"
1641 $expandedAgents = Resolve-HandoffDependencies -SeedAgents $collectionArtifactNames.Agents -AgentsDir $agentsDir
1642 $collectionArtifactNames.Agents = $expandedAgents
1643 }
1644
1645 # Resolve requires dependencies
1646 $resolvedNames = Resolve-RequiresDependencies -ArtifactNames @{
1647 agents = $collectionArtifactNames.Agents
1648 prompts = $collectionArtifactNames.Prompts
1649 instructions = $collectionArtifactNames.Instructions
1650 skills = $collectionArtifactNames.Skills
1651 } -AllowedMaturities $allowedMaturities -CollectionRequires $collectionRequires -CollectionMaturities $collectionMaturities
1652
1653 $collectionArtifactNames = @{
1654 Agents = $resolvedNames.Agents
1655 Prompts = $resolvedNames.Prompts
1656 Instructions = $resolvedNames.Instructions
1657 Skills = $resolvedNames.Skills
1658 }
1659 }
1660
1661 # Discover artifacts
1662 $discoveryAllowedMaturities = if ($null -ne $collectionArtifactNames) {
1663 @('stable', 'preview', 'experimental', 'deprecated')
1664 }
1665 else {
1666 $allowedMaturities
1667 }
1668
1669 $agentsDir = Join-Path $GitHubDir "agents"
1670 $agentResult = Get-DiscoveredAgents -AgentsDir $agentsDir -AllowedMaturities $discoveryAllowedMaturities -ExcludedAgents @()
1671 $chatAgents = $agentResult.Agents
1672 $excludedAgents = $agentResult.Skipped
1673
1674 Write-Host "`n--- Chat Agents ---" -ForegroundColor Green
1675 Write-Host "Found $($chatAgents.Count) agent(s) matching criteria"
1676 if ($excludedAgents.Count -gt 0) {
1677 Write-Host "Excluded $($excludedAgents.Count) agent(s) due to maturity filter" -ForegroundColor Yellow
1678 }
1679
1680 # Discover prompts
1681 $promptsDir = Join-Path $GitHubDir "prompts"
1682 $promptResult = Get-DiscoveredPrompts -PromptsDir $promptsDir -GitHubDir $GitHubDir -AllowedMaturities $discoveryAllowedMaturities
1683 $chatPrompts = $promptResult.Prompts
1684 $excludedPrompts = $promptResult.Skipped
1685
1686 Write-Host "`n--- Chat Prompts ---" -ForegroundColor Green
1687 Write-Host "Found $($chatPrompts.Count) prompt(s) matching criteria"
1688 if ($excludedPrompts.Count -gt 0) {
1689 Write-Host "Excluded $($excludedPrompts.Count) prompt(s) due to maturity filter" -ForegroundColor Yellow
1690 }
1691
1692 # Discover instructions
1693 $instructionsDir = Join-Path $GitHubDir "instructions"
1694 $instructionResult = Get-DiscoveredInstructions -InstructionsDir $instructionsDir -GitHubDir $GitHubDir -AllowedMaturities $discoveryAllowedMaturities
1695 $chatInstructions = $instructionResult.Instructions
1696 $excludedInstructions = $instructionResult.Skipped
1697
1698 Write-Host "`n--- Chat Instructions ---" -ForegroundColor Green
1699 Write-Host "Found $($chatInstructions.Count) instruction(s) matching criteria"
1700 if ($excludedInstructions.Count -gt 0) {
1701 Write-Host "Excluded $($excludedInstructions.Count) instruction(s) due to maturity filter" -ForegroundColor Yellow
1702 }
1703
1704 # Discover skills
1705 $skillsDir = Join-Path $GitHubDir "skills"
1706 $skillResult = Get-DiscoveredSkills -SkillsDir $skillsDir -AllowedMaturities $discoveryAllowedMaturities
1707 $chatSkills = $skillResult.Skills
1708 $excludedSkills = $skillResult.Skipped
1709
1710 Write-Host "`n--- Chat Skills ---" -ForegroundColor Green
1711 Write-Host "Found $($chatSkills.Count) skill(s) matching criteria"
1712 if ($excludedSkills.Count -gt 0) {
1713 Write-Host "Excluded $($excludedSkills.Count) skill(s) due to maturity filter" -ForegroundColor Yellow
1714 }
1715
1716 # Apply collection filtering to discovered artifacts
1717 if ($null -ne $collectionArtifactNames) {
1718 $chatAgents = @($chatAgents | Where-Object { $collectionArtifactNames.Agents -contains $_.name })
1719 $chatPrompts = @($chatPrompts | Where-Object { $collectionArtifactNames.Prompts -contains $_.name })
1720 $instrBaseNames = @($collectionArtifactNames.Instructions | ForEach-Object { ($_ -split '/')[-1] })
1721 $chatInstructions = @($chatInstructions | Where-Object {
1722 $instrBaseName = $_.name -replace '-instructions$', ''
1723 $instrBaseNames -contains $instrBaseName
1724 })
1725 $chatSkills = @($chatSkills | Where-Object { $collectionArtifactNames.Skills -contains $_.name })
1726
1727 Write-Host "`n--- Collection Filtering ---" -ForegroundColor Magenta
1728 Write-Host "Agents after filter: $($chatAgents.Count)"
1729 Write-Host "Prompts after filter: $($chatPrompts.Count)"
1730 Write-Host "Instructions after filter: $($chatInstructions.Count)"
1731 Write-Host "Skills after filter: $($chatSkills.Count)"
1732 }
1733
1734 # Apply collection template when building a non-default collection
1735 if ($null -ne $collectionManifest -and $collectionManifest.id -ne 'hve-core') {
1736 $collectionId = $collectionManifest.id
1737 $templatePath = Join-Path $ExtensionDirectory "package.$collectionId.json"
1738 if (-not (Test-Path $templatePath)) {
1739 return New-PrepareResult -Success $false -ErrorMessage "Collection template not found: $templatePath"
1740 }
1741
1742 # Validate template consistency against collection manifest
1743 $consistency = Test-TemplateConsistency -TemplatePath $templatePath -CollectionManifest $collectionManifest
1744 if (-not $consistency.IsConsistent) {
1745 Write-Host "`n--- Template Consistency Warnings ---" -ForegroundColor Yellow
1746 foreach ($mismatch in $consistency.Mismatches) {
1747 Write-Warning "Template/manifest mismatch: $($mismatch.Message)"
1748 Write-CIAnnotation -Message "Template/manifest mismatch ($collectionId): $($mismatch.Message)" -Level Warning
1749 }
1750 }
1751
1752 # Back up canonical package.json for later restore
1753 $backupPath = Join-Path $ExtensionDirectory "package.json.bak"
1754 Copy-Item -Path $PackageJsonPath -Destination $backupPath -Force
1755
1756 # Copy collection template over package.json
1757 Copy-Item -Path $templatePath -Destination $PackageJsonPath -Force
1758
1759 # Re-read template as the working package.json
1760 $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json
1761 Write-Host "Applied collection template: package.$collectionId.json" -ForegroundColor Green
1762 }
1763
1764 # Update package.json with generated contributes
1765 $packageJson = Update-PackageJsonContributes -PackageJson $packageJson `
1766 -ChatAgents $chatAgents `
1767 -ChatPromptFiles $chatPrompts `
1768 -ChatInstructions $chatInstructions `
1769 -ChatSkills $chatSkills
1770
1771 # Write updated package.json
1772 if (-not $DryRun) {
1773 $packageJson | ConvertTo-Json -Depth 10 | Set-Content -Path $PackageJsonPath -Encoding UTF8NoBOM
1774 Write-Host "`nUpdated package.json with discovered artifacts" -ForegroundColor Green
1775 }
1776 else {
1777 Write-Host "`n[DRY RUN] Would update package.json with discovered artifacts" -ForegroundColor Yellow
1778 }
1779
1780 # Handle changelog
1781 if ($ChangelogPath -and (Test-Path $ChangelogPath)) {
1782 $destChangelog = Join-Path $ExtensionDirectory "CHANGELOG.md"
1783 if (-not $DryRun) {
1784 Copy-Item -Path $ChangelogPath -Destination $destChangelog -Force
1785 Write-Host "Copied changelog to extension directory" -ForegroundColor Green
1786 }
1787 else {
1788 Write-Host "[DRY RUN] Would copy changelog to extension directory" -ForegroundColor Yellow
1789 }
1790 }
1791 elseif ($ChangelogPath) {
1792 Write-Warning "Changelog path specified but file not found: $ChangelogPath"
1793 }
1794
1795 Write-Host "`n=== Preparation Complete ===" -ForegroundColor Cyan
1796
1797 return New-PrepareResult -Success $true `
1798 -Version $version `
1799 -AgentCount $chatAgents.Count `
1800 -PromptCount $chatPrompts.Count `
1801 -InstructionCount $chatInstructions.Count `
1802 -SkillCount $chatSkills.Count
1803}
1804
1805#endregion Pure Functions
1806
1807#region Main Execution
1808if ($MyInvocation.InvocationName -ne '.') {
1809 try {
1810 # Verify PowerShell-Yaml module is available
1811 if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) {
1812 throw "Required module 'PowerShell-Yaml' is not installed."
1813 }
1814 Import-Module PowerShell-Yaml -ErrorAction Stop
1815
1816 # Resolve paths using $MyInvocation (must stay in entry point)
1817 $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
1818 $RepoRoot = (Get-Item "$ScriptDir/../..").FullName
1819 $ExtensionDir = Join-Path $RepoRoot "extension"
1820
1821 # Resolve changelog path if provided
1822 $resolvedChangelogPath = ""
1823 if ($ChangelogPath) {
1824 $resolvedChangelogPath = if ([System.IO.Path]::IsPathRooted($ChangelogPath)) {
1825 $ChangelogPath
1826 }
1827 else {
1828 Join-Path $RepoRoot $ChangelogPath
1829 }
1830 }
1831
1832 # Default to hve-core collection when no collection is specified.
1833 # package.json is identity-mapped to the hve-core collection, so the
1834 # default build must apply hve-core filtering rather than including all
1835 # artifacts (hve-core-all behavior). Use -Collection with
1836 # hve-core-all.collection.yml explicitly to include everything.
1837 if (-not $Collection) {
1838 $Collection = Join-Path $RepoRoot 'collections/hve-core.collection.yml'
1839 }
1840
1841 Write-Host "📦 HVE Core Extension Preparer" -ForegroundColor Cyan
1842 Write-Host "==============================" -ForegroundColor Cyan
1843 Write-Host " Channel: $Channel" -ForegroundColor Cyan
1844 Write-Host " Collection: $Collection" -ForegroundColor Cyan
1845 Write-Host ""
1846
1847 # Call orchestration function
1848 $result = Invoke-PrepareExtension `
1849 -ExtensionDirectory $ExtensionDir `
1850 -RepoRoot $RepoRoot `
1851 -Channel $Channel `
1852 -ChangelogPath $resolvedChangelogPath `
1853 -DryRun:$DryRun `
1854 -Collection $Collection
1855
1856 if (-not $result.Success) {
1857 throw $result.ErrorMessage
1858 }
1859
1860 Write-Host ""
1861 Write-Host "🎉 Done!" -ForegroundColor Green
1862 Write-Host ""
1863 Write-Host "📊 Summary:" -ForegroundColor Cyan
1864 Write-Host " Agents: $($result.AgentCount)"
1865 Write-Host " Prompts: $($result.PromptCount)"
1866 Write-Host " Instructions: $($result.InstructionCount)"
1867 Write-Host " Skills: $($result.SkillCount)"
1868 Write-Host " Version: $($result.Version)"
1869
1870 exit 0
1871 }
1872 catch {
1873 Write-Error -ErrorAction Continue "Prepare-Extension failed: $($_.Exception.Message)"
1874 Write-CIAnnotation -Message $_.Exception.Message -Level Error
1875 exit 1
1876 }
1877}
1878#endregion Main Execution
1879