microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/1637-d-skill-paths

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/extension/Prepare-Extension.ps1

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