microsoft/hve-core

Public

mirrored from https://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/brd-skills-refactor

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/extension/Prepare-Extension.ps1

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