microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
copilot/research-single-dynamic-rewrite

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/extension/Prepare-Extension.ps1

1605lines · 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
65
66#region Pure Functions
67
68function Get-AllowedMaturities {
69 <#
70 .SYNOPSIS
71 Returns allowed maturity levels based on release channel.
72 .DESCRIPTION
73 Pure function that determines which maturity levels (stable, preview, experimental)
74 are included in the extension package based on the specified channel.
75 .PARAMETER Channel
76 Release channel. 'Stable' returns only stable; 'PreRelease' includes all levels.
77 .OUTPUTS
78 [string[]] Array of allowed maturity level strings.
79 #>
80 [CmdletBinding()]
81 [OutputType([string[]])]
82 param(
83 [Parameter(Mandatory = $true)]
84 [ValidateSet('Stable', 'PreRelease')]
85 [string]$Channel
86 )
87
88 if ($Channel -eq 'PreRelease') {
89 return @('stable', 'preview', 'experimental')
90 }
91 return @('stable')
92}
93
94function Get-RegistryData {
95 <#
96 .SYNOPSIS
97 Loads the AI artifacts registry JSON file.
98 .DESCRIPTION
99 Reads and parses the AI artifacts registry JSON file into a hashtable
100 containing artifact metadata keyed by type (agents, prompts, instructions, skills).
101 .PARAMETER RegistryPath
102 Path to the ai-artifacts-registry.json file.
103 .OUTPUTS
104 [hashtable] Parsed registry data with keys: agents, prompts, instructions, skills, personas, version.
105 #>
106 [CmdletBinding()]
107 [OutputType([hashtable])]
108 param(
109 [Parameter(Mandatory = $true)]
110 [ValidateNotNullOrEmpty()]
111 [string]$RegistryPath
112 )
113
114 if (-not (Test-Path $RegistryPath)) {
115 throw "AI artifacts registry not found: $RegistryPath"
116 }
117
118 $content = Get-Content -Path $RegistryPath -Raw
119 return $content | ConvertFrom-Json -AsHashtable
120}
121
122function Get-CollectionManifest {
123 <#
124 .SYNOPSIS
125 Loads a collection manifest JSON file.
126 .DESCRIPTION
127 Reads and parses a collection manifest JSON file that defines persona-based
128 artifact filtering rules for extension packaging.
129 .PARAMETER CollectionPath
130 Path to the collection manifest JSON file.
131 .OUTPUTS
132 [hashtable] Parsed collection manifest with id, name, displayName, description, personas, and optional include/exclude.
133 #>
134 [CmdletBinding()]
135 [OutputType([hashtable])]
136 param(
137 [Parameter(Mandatory = $true)]
138 [ValidateNotNullOrEmpty()]
139 [string]$CollectionPath
140 )
141
142 if (-not (Test-Path $CollectionPath)) {
143 throw "Collection manifest not found: $CollectionPath"
144 }
145
146 $content = Get-Content -Path $CollectionPath -Raw
147 return $content | ConvertFrom-Json -AsHashtable
148}
149
150function Test-GlobMatch {
151 <#
152 .SYNOPSIS
153 Tests whether a name matches any of the provided glob patterns.
154 .DESCRIPTION
155 Uses PowerShell's -like operator to test glob pattern matching with
156 * (any characters) and ? (single character) wildcards.
157 .PARAMETER Name
158 The artifact name to test against patterns.
159 .PARAMETER Patterns
160 Array of glob patterns to match against.
161 .OUTPUTS
162 [bool] True if name matches any pattern, false otherwise.
163 #>
164 [CmdletBinding()]
165 [OutputType([bool])]
166 param(
167 [Parameter(Mandatory = $true)]
168 [string]$Name,
169
170 [Parameter(Mandatory = $true)]
171 [string[]]$Patterns
172 )
173
174 foreach ($pattern in $Patterns) {
175 if ($Name -like $pattern) {
176 return $true
177 }
178 }
179 return $false
180}
181
182function Get-CollectionArtifacts {
183 <#
184 .SYNOPSIS
185 Filters registry artifacts by collection persona, maturity, and glob patterns.
186 .DESCRIPTION
187 Applies collection-level filtering to the artifact registry, returning artifact
188 names that match the collection's persona requirements, allowed maturities,
189 and optional include/exclude glob patterns.
190 .PARAMETER Registry
191 AI artifacts registry hashtable.
192 .PARAMETER Collection
193 Collection manifest hashtable with personas and optional include/exclude.
194 .PARAMETER AllowedMaturities
195 Array of maturity levels to include.
196 .OUTPUTS
197 [hashtable] With Agents, Prompts, Instructions, Skills arrays of matching artifact names.
198 #>
199 [CmdletBinding()]
200 [OutputType([hashtable])]
201 param(
202 [Parameter(Mandatory = $true)]
203 [hashtable]$Registry,
204
205 [Parameter(Mandatory = $true)]
206 [hashtable]$Collection,
207
208 [Parameter(Mandatory = $true)]
209 [string[]]$AllowedMaturities
210 )
211
212 $result = @{
213 Agents = @()
214 Prompts = @()
215 Instructions = @()
216 Skills = @()
217 }
218
219 $collectionPersonas = $Collection.personas
220
221 foreach ($type in @('agents', 'prompts', 'instructions', 'skills')) {
222 if (-not $Registry.ContainsKey($type)) { continue }
223
224 $includePatterns = @()
225 $excludePatterns = @()
226 if ($Collection.ContainsKey('include') -and $Collection.include.ContainsKey($type)) {
227 $includePatterns = $Collection.include[$type]
228 }
229 if ($Collection.ContainsKey('exclude') -and $Collection.exclude.ContainsKey($type)) {
230 $excludePatterns = $Collection.exclude[$type]
231 }
232
233 foreach ($name in $Registry[$type].Keys) {
234 $entry = $Registry[$type][$name]
235
236 # Persona filter: artifact must belong to at least one collection persona
237 # Empty personas array means universal (all personas)
238 $personaMatch = $false
239 if (@($entry.personas).Count -eq 0) {
240 $personaMatch = $true
241 } else {
242 foreach ($persona in $entry.personas) {
243 if ($collectionPersonas -contains $persona) {
244 $personaMatch = $true
245 break
246 }
247 }
248 }
249 if (-not $personaMatch) { continue }
250
251 # Maturity filter
252 if ($AllowedMaturities -notcontains $entry.maturity) { continue }
253
254 # Include glob filter (if specified)
255 if ($includePatterns.Count -gt 0 -and -not (Test-GlobMatch -Name $name -Patterns $includePatterns)) {
256 continue
257 }
258
259 # Exclude glob filter
260 if ($excludePatterns.Count -gt 0 -and (Test-GlobMatch -Name $name -Patterns $excludePatterns)) {
261 continue
262 }
263
264 $capitalType = @{ agents = 'Agents'; prompts = 'Prompts'; instructions = 'Instructions'; skills = 'Skills' }[$type]
265 $result[$capitalType] += $name
266 }
267 }
268
269 return $result
270}
271
272function Resolve-HandoffDependencies {
273 <#
274 .SYNOPSIS
275 Resolves transitive agent handoff dependencies using BFS traversal.
276 .DESCRIPTION
277 Starting from seed agents, performs breadth-first traversal of agent handoff
278 declarations in YAML frontmatter to compute the transitive closure of
279 all agents reachable through handoff chains.
280 .PARAMETER SeedAgents
281 Initial agent names to start BFS from.
282 .PARAMETER AgentsDir
283 Path to the agents directory containing .agent.md files.
284 .PARAMETER AllowedMaturities
285 Array of maturity levels to include.
286 .PARAMETER Registry
287 AI artifacts registry hashtable for maturity lookup.
288 .OUTPUTS
289 [string[]] Complete set of agent names including seed agents and all transitive handoff targets.
290 #>
291 [CmdletBinding()]
292 [OutputType([string[]])]
293 param(
294 [Parameter(Mandatory = $true)]
295 [string[]]$SeedAgents,
296
297 [Parameter(Mandatory = $true)]
298 [string]$AgentsDir,
299
300 [Parameter(Mandatory = $true)]
301 [string[]]$AllowedMaturities,
302
303 [Parameter(Mandatory = $false)]
304 [hashtable]$Registry = @{}
305 )
306
307 $visited = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
308 $queue = [System.Collections.Generic.Queue[string]]::new()
309
310 foreach ($agent in $SeedAgents) {
311 if ($visited.Add($agent)) {
312 $queue.Enqueue($agent)
313 }
314 }
315
316 while ($queue.Count -gt 0) {
317 $current = $queue.Dequeue()
318 $agentFile = Join-Path $AgentsDir "$current.agent.md"
319
320 if (-not (Test-Path $agentFile)) {
321 Write-Warning "Handoff target agent file not found: $agentFile"
322 continue
323 }
324
325 # Check maturity from registry
326 $maturity = "stable"
327 if ($Registry.Count -gt 0 -and $Registry.ContainsKey('agents') -and $Registry.agents.ContainsKey($current)) {
328 $maturity = $Registry.agents[$current].maturity
329 }
330 if ($AllowedMaturities -notcontains $maturity) { continue }
331
332 # Parse handoffs from frontmatter
333 $content = Get-Content -Path $agentFile -Raw
334 if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') {
335 $yamlContent = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n"
336 try {
337 $data = ConvertFrom-Yaml -Yaml $yamlContent
338 if ($data.ContainsKey('handoffs') -and $data.handoffs -is [System.Collections.IEnumerable] -and $data.handoffs -isnot [string]) {
339 foreach ($handoff in $data.handoffs) {
340 # Handle both string format and object format (with 'agent' field)
341 $targetAgent = $null
342 if ($handoff -is [string]) {
343 $targetAgent = $handoff
344 } elseif ($handoff -is [hashtable] -and $handoff.ContainsKey('agent')) {
345 $targetAgent = $handoff.agent
346 }
347 if ($targetAgent -and $visited.Add($targetAgent)) {
348 $queue.Enqueue($targetAgent)
349 }
350 }
351 }
352 }
353 catch {
354 Write-Warning "Failed to parse handoffs from $current.agent.md: $_"
355 }
356 }
357 }
358
359 return @($visited)
360}
361
362function Resolve-RequiresDependencies {
363 <#
364 .SYNOPSIS
365 Resolves transitive artifact dependencies from registry requires blocks.
366 .DESCRIPTION
367 Walks the requires blocks in agent registry entries to compute the
368 complete set of dependent artifacts across all types (agents, prompts,
369 instructions, skills) using BFS for transitive agent dependencies.
370 .PARAMETER ArtifactNames
371 Hashtable with initial artifact name arrays keyed by type (agents, prompts, instructions, skills).
372 .PARAMETER Registry
373 AI artifacts registry hashtable.
374 .PARAMETER AllowedMaturities
375 Array of maturity levels to include.
376 .OUTPUTS
377 [hashtable] With Agents, Prompts, Instructions, Skills arrays containing resolved names.
378 #>
379 [CmdletBinding()]
380 [OutputType([hashtable])]
381 param(
382 [Parameter(Mandatory = $true)]
383 [hashtable]$ArtifactNames,
384
385 [Parameter(Mandatory = $true)]
386 [hashtable]$Registry,
387
388 [Parameter(Mandatory = $true)]
389 [string[]]$AllowedMaturities
390 )
391
392 $resolved = @{
393 Agents = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
394 Prompts = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
395 Instructions = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
396 Skills = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
397 }
398
399 $typeMap = @{
400 agents = 'Agents'
401 prompts = 'Prompts'
402 instructions = 'Instructions'
403 skills = 'Skills'
404 }
405
406 # Seed with initial artifact names
407 foreach ($type in @('agents', 'prompts', 'instructions', 'skills')) {
408 $capitalType = $typeMap[$type]
409 if ($ArtifactNames.ContainsKey($type)) {
410 foreach ($name in $ArtifactNames[$type]) {
411 $null = $resolved[$capitalType].Add($name)
412 }
413 }
414 }
415
416 # Walk requires for agents (only agents have requires blocks)
417 $processedAgents = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
418 $agentQueue = [System.Collections.Generic.Queue[string]]::new()
419
420 foreach ($agent in $resolved.Agents) {
421 $agentQueue.Enqueue($agent)
422 }
423
424 while ($agentQueue.Count -gt 0) {
425 $current = $agentQueue.Dequeue()
426 if (-not $processedAgents.Add($current)) { continue }
427
428 if (-not $Registry.ContainsKey('agents') -or -not $Registry.agents.ContainsKey($current)) { continue }
429
430 $entry = $Registry.agents[$current]
431 if (-not $entry.ContainsKey('requires')) { continue }
432
433 $requires = $entry.requires
434
435 foreach ($type in @('agents', 'prompts', 'instructions', 'skills')) {
436 if (-not $requires.ContainsKey($type)) { continue }
437 $capitalType = $typeMap[$type]
438
439 foreach ($dep in $requires[$type]) {
440 # Check maturity of dependency
441 if ($Registry.ContainsKey($type) -and $Registry[$type].ContainsKey($dep)) {
442 $depMaturity = $Registry[$type][$dep].maturity
443 if ($AllowedMaturities -notcontains $depMaturity) { continue }
444 }
445
446 if ($resolved[$capitalType].Add($dep)) {
447 if ($type -eq 'agents') {
448 $agentQueue.Enqueue($dep)
449 }
450 }
451 }
452 }
453 }
454
455 # Convert HashSets to arrays
456 return @{
457 Agents = @($resolved.Agents)
458 Prompts = @($resolved.Prompts)
459 Instructions = @($resolved.Instructions)
460 Skills = @($resolved.Skills)
461 }
462}
463
464function Get-FrontmatterData {
465 <#
466 .SYNOPSIS
467 Extracts description from YAML frontmatter.
468 .DESCRIPTION
469 Function that parses YAML frontmatter from a markdown file
470 and returns a hashtable with the description value.
471 .PARAMETER FilePath
472 Path to the markdown file to parse.
473 .PARAMETER FallbackDescription
474 Default description if none found in frontmatter.
475 .OUTPUTS
476 [hashtable] With description key.
477 #>
478 [CmdletBinding()]
479 [OutputType([hashtable])]
480 param(
481 [Parameter(Mandatory = $true)]
482 [string]$FilePath,
483
484 [Parameter(Mandatory = $false)]
485 [string]$FallbackDescription = ""
486 )
487
488 $content = Get-Content -Path $FilePath -Raw
489 $description = ""
490
491 if ($content -match '(?s)^---\s*\r?\n(.*?)\r?\n---') {
492 $yamlContent = $Matches[1] -replace '\r\n', "`n" -replace '\r', "`n"
493 try {
494 $data = ConvertFrom-Yaml -Yaml $yamlContent
495 if ($data.ContainsKey('description')) {
496 $description = $data.description
497 }
498 }
499 catch {
500 Write-Warning "Failed to parse YAML frontmatter in $(Split-Path -Leaf $FilePath): $_"
501 }
502 }
503
504 return @{
505 description = if ($description) { $description } else { $FallbackDescription }
506 }
507}
508
509function Test-PathsExist {
510 <#
511 .SYNOPSIS
512 Validates that required paths exist for extension preparation.
513 .DESCRIPTION
514 Validation function that checks whether extension directory, package.json,
515 and .github directory exist at the specified locations.
516 .PARAMETER ExtensionDir
517 Path to the extension directory.
518 .PARAMETER PackageJsonPath
519 Path to package.json file.
520 .PARAMETER GitHubDir
521 Path to .github directory.
522 .OUTPUTS
523 [hashtable] With IsValid bool, MissingPaths array, and ErrorMessages array.
524 #>
525 [CmdletBinding()]
526 [OutputType([hashtable])]
527 param(
528 [Parameter(Mandatory = $true)]
529 [string]$ExtensionDir,
530
531 [Parameter(Mandatory = $true)]
532 [string]$PackageJsonPath,
533
534 [Parameter(Mandatory = $true)]
535 [string]$GitHubDir
536 )
537
538 $missingPaths = @()
539 $errorMessages = @()
540
541 if (-not (Test-Path $ExtensionDir)) {
542 $missingPaths += $ExtensionDir
543 $errorMessages += "Extension directory not found: $ExtensionDir"
544 }
545 if (-not (Test-Path $PackageJsonPath)) {
546 $missingPaths += $PackageJsonPath
547 $errorMessages += "package.json not found: $PackageJsonPath"
548 }
549 if (-not (Test-Path $GitHubDir)) {
550 $missingPaths += $GitHubDir
551 $errorMessages += ".github directory not found: $GitHubDir"
552 }
553
554 return @{
555 IsValid = ($missingPaths.Count -eq 0)
556 MissingPaths = $missingPaths
557 ErrorMessages = $errorMessages
558 }
559}
560
561function Get-DiscoveredAgents {
562 <#
563 .SYNOPSIS
564 Discovers chat agent files from the agents directory.
565 .DESCRIPTION
566 Discovery function that scans the agents directory for .agent.md files,
567 extracts frontmatter description, filters by registry maturity and exclusion list,
568 and returns structured agent objects.
569 .PARAMETER AgentsDir
570 Path to the agents directory.
571 .PARAMETER AllowedMaturities
572 Array of maturity levels to include.
573 .PARAMETER ExcludedAgents
574 Array of agent names to exclude from packaging.
575 .PARAMETER Registry
576 AI artifacts registry hashtable for maturity lookup.
577 .OUTPUTS
578 [hashtable] With Agents array, Skipped array, and DirectoryExists bool.
579 #>
580 [CmdletBinding()]
581 [OutputType([hashtable])]
582 param(
583 [Parameter(Mandatory = $true)]
584 [string]$AgentsDir,
585
586 [Parameter(Mandatory = $true)]
587 [string[]]$AllowedMaturities,
588
589 [Parameter(Mandatory = $false)]
590 [string[]]$ExcludedAgents = @(),
591
592 [Parameter(Mandatory = $false)]
593 [hashtable]$Registry = @{}
594 )
595
596 $result = @{
597 Agents = @()
598 Skipped = @()
599 DirectoryExists = (Test-Path $AgentsDir)
600 }
601
602 if (-not $result.DirectoryExists) {
603 return $result
604 }
605
606 $agentFiles = Get-ChildItem -Path $AgentsDir -Filter "*.agent.md" | Sort-Object Name
607
608 foreach ($agentFile in $agentFiles) {
609 $agentName = $agentFile.BaseName -replace '\.agent$', ''
610
611 if ($ExcludedAgents -contains $agentName) {
612 $result.Skipped += @{ Name = $agentName; Reason = 'excluded' }
613 continue
614 }
615
616 # Determine maturity from registry if available, else default to stable
617 $maturity = "stable"
618 if ($Registry.Count -gt 0 -and $Registry.ContainsKey('agents') -and $Registry.agents.ContainsKey($agentName)) {
619 $maturity = $Registry.agents[$agentName].maturity
620 }
621
622 $frontmatter = Get-FrontmatterData -FilePath $agentFile.FullName -FallbackDescription "AI agent for $agentName"
623
624 if ($AllowedMaturities -notcontains $maturity) {
625 $result.Skipped += @{ Name = $agentName; Reason = "maturity: $maturity" }
626 continue
627 }
628
629 $result.Agents += [PSCustomObject]@{
630 name = $agentName
631 path = "./.github/agents/$($agentFile.Name)"
632 description = $frontmatter.description
633 }
634 }
635
636 return $result
637}
638
639function Get-DiscoveredPrompts {
640 <#
641 .SYNOPSIS
642 Discovers prompt files from the prompts directory.
643 .DESCRIPTION
644 Discovery function that scans the prompts directory for .prompt.md files,
645 extracts frontmatter description, filters by registry maturity, and returns
646 structured prompt objects with relative paths.
647 .PARAMETER PromptsDir
648 Path to the prompts directory.
649 .PARAMETER GitHubDir
650 Path to the .github directory for relative path calculation.
651 .PARAMETER AllowedMaturities
652 Array of maturity levels to include.
653 .PARAMETER Registry
654 AI artifacts registry hashtable for maturity lookup.
655 .OUTPUTS
656 [hashtable] With Prompts array, Skipped array, and DirectoryExists bool.
657 #>
658 [CmdletBinding()]
659 [OutputType([hashtable])]
660 param(
661 [Parameter(Mandatory = $true)]
662 [string]$PromptsDir,
663
664 [Parameter(Mandatory = $true)]
665 [string]$GitHubDir,
666
667 [Parameter(Mandatory = $true)]
668 [string[]]$AllowedMaturities,
669
670 [Parameter(Mandatory = $false)]
671 [hashtable]$Registry = @{}
672 )
673
674 $result = @{
675 Prompts = @()
676 Skipped = @()
677 DirectoryExists = (Test-Path $PromptsDir)
678 }
679
680 if (-not $result.DirectoryExists) {
681 return $result
682 }
683
684 $promptFiles = Get-ChildItem -Path $PromptsDir -Filter "*.prompt.md" -Recurse | Sort-Object Name
685
686 foreach ($promptFile in $promptFiles) {
687 $promptName = $promptFile.BaseName -replace '\.prompt$', ''
688 $displayName = ($promptName -replace '-', ' ') -replace '(\b\w)', { $_.Groups[1].Value.ToUpper() }
689
690 # Determine maturity from registry if available, else default to stable
691 $maturity = "stable"
692 if ($Registry.Count -gt 0 -and $Registry.ContainsKey('prompts') -and $Registry.prompts.ContainsKey($promptName)) {
693 $maturity = $Registry.prompts[$promptName].maturity
694 }
695
696 $frontmatter = Get-FrontmatterData -FilePath $promptFile.FullName -FallbackDescription "Prompt for $displayName"
697
698 if ($AllowedMaturities -notcontains $maturity) {
699 $result.Skipped += @{ Name = $promptName; Reason = "maturity: $maturity" }
700 continue
701 }
702
703 $relativePath = [System.IO.Path]::GetRelativePath($GitHubDir, $promptFile.FullName) -replace '\\', '/'
704
705 $result.Prompts += [PSCustomObject]@{
706 name = $promptName
707 path = "./.github/$relativePath"
708 description = $frontmatter.description
709 }
710 }
711
712 return $result
713}
714
715function Get-DiscoveredInstructions {
716 <#
717 .SYNOPSIS
718 Discovers instruction files from the instructions directory.
719 .DESCRIPTION
720 Discovery function that scans the instructions directory for .instructions.md files,
721 extracts frontmatter description, filters by registry maturity, and returns
722 structured instruction objects with normalized paths.
723 .PARAMETER InstructionsDir
724 Path to the instructions directory.
725 .PARAMETER GitHubDir
726 Path to the .github directory for relative path calculation.
727 .PARAMETER AllowedMaturities
728 Array of maturity levels to include.
729 .PARAMETER Registry
730 AI artifacts registry hashtable for maturity lookup.
731 .OUTPUTS
732 [hashtable] With Instructions array, Skipped array, and DirectoryExists bool.
733 #>
734 [CmdletBinding()]
735 [OutputType([hashtable])]
736 param(
737 [Parameter(Mandatory = $true)]
738 [string]$InstructionsDir,
739
740 [Parameter(Mandatory = $true)]
741 [string]$GitHubDir,
742
743 [Parameter(Mandatory = $true)]
744 [string[]]$AllowedMaturities,
745
746 [Parameter(Mandatory = $false)]
747 [hashtable]$Registry = @{}
748 )
749
750 $result = @{
751 Instructions = @()
752 Skipped = @()
753 DirectoryExists = (Test-Path $InstructionsDir)
754 }
755
756 if (-not $result.DirectoryExists) {
757 return $result
758 }
759
760 $instructionFiles = Get-ChildItem -Path $InstructionsDir -Filter "*.instructions.md" -Recurse | Sort-Object Name
761
762 foreach ($instrFile in $instructionFiles) {
763 $baseName = $instrFile.BaseName -replace '\.instructions$', ''
764 $instrName = "$baseName-instructions"
765 $displayName = ($baseName -replace '-', ' ') -replace '(\b\w)', { $_.Groups[1].Value.ToUpper() }
766
767 # Determine maturity from registry using relative path key
768 $relPath = [System.IO.Path]::GetRelativePath($InstructionsDir, $instrFile.FullName) -replace '\\', '/'
769 $registryKey = $relPath -replace '\.instructions\.md$', ''
770 $maturity = "stable"
771 if ($Registry.Count -gt 0 -and $Registry.ContainsKey('instructions') -and $Registry.instructions.ContainsKey($registryKey)) {
772 $maturity = $Registry.instructions[$registryKey].maturity
773 }
774
775 $frontmatter = Get-FrontmatterData -FilePath $instrFile.FullName -FallbackDescription "Instructions for $displayName"
776
777 if ($AllowedMaturities -notcontains $maturity) {
778 $result.Skipped += @{ Name = $instrName; Reason = "maturity: $maturity" }
779 continue
780 }
781
782 $relativePathFromGitHub = [System.IO.Path]::GetRelativePath($GitHubDir, $instrFile.FullName)
783 $normalizedRelativePath = (Join-Path ".github" $relativePathFromGitHub) -replace '\\', '/'
784
785 $result.Instructions += [PSCustomObject]@{
786 name = $instrName
787 path = "./$normalizedRelativePath"
788 description = $frontmatter.description
789 }
790 }
791
792 return $result
793}
794
795function Get-DiscoveredSkills {
796 <#
797 .SYNOPSIS
798 Discovers skill packages from the skills directory.
799 .DESCRIPTION
800 Discovery function that scans the skills directory for subdirectories
801 containing SKILL.md files, filters by registry maturity, and returns
802 structured skill objects.
803 .PARAMETER SkillsDir
804 Path to the skills directory.
805 .PARAMETER AllowedMaturities
806 Array of maturity levels to include.
807 .PARAMETER Registry
808 AI artifacts registry hashtable for maturity lookup.
809 .OUTPUTS
810 [hashtable] With Skills array, Skipped array, and DirectoryExists bool.
811 #>
812 [CmdletBinding()]
813 [OutputType([hashtable])]
814 param(
815 [Parameter(Mandatory = $true)]
816 [string]$SkillsDir,
817
818 [Parameter(Mandatory = $true)]
819 [string[]]$AllowedMaturities,
820
821 [Parameter(Mandatory = $false)]
822 [hashtable]$Registry = @{}
823 )
824
825 $result = @{
826 Skills = @()
827 Skipped = @()
828 DirectoryExists = (Test-Path $SkillsDir)
829 }
830
831 if (-not $result.DirectoryExists) {
832 return $result
833 }
834
835 $skillDirs = Get-ChildItem -Path $SkillsDir -Directory | Sort-Object Name
836
837 foreach ($skillDir in $skillDirs) {
838 $skillName = $skillDir.Name
839 $skillFile = Join-Path $skillDir.FullName "SKILL.md"
840
841 if (-not (Test-Path $skillFile)) {
842 $result.Skipped += @{ Name = $skillName; Reason = 'missing SKILL.md' }
843 continue
844 }
845
846 $maturity = "stable"
847 if ($Registry.Count -gt 0 -and $Registry.ContainsKey('skills') -and $Registry.skills.ContainsKey($skillName)) {
848 $maturity = $Registry.skills[$skillName].maturity
849 }
850
851 if ($AllowedMaturities -notcontains $maturity) {
852 $result.Skipped += @{ Name = $skillName; Reason = "maturity: $maturity" }
853 continue
854 }
855
856 $frontmatter = Get-FrontmatterData -FilePath $skillFile -FallbackDescription "Skill for $skillName"
857
858 $result.Skills += [PSCustomObject]@{
859 name = $skillName
860 path = "./.github/skills/$skillName"
861 description = $frontmatter.description
862 }
863 }
864
865 return $result
866}
867
868function Update-PackageJsonContributes {
869 <#
870 .SYNOPSIS
871 Updates package.json contributes section with discovered components.
872 .DESCRIPTION
873 Pure function that takes a package.json object and discovered components,
874 returning a new object with the contributes section updated. Handles
875 chatAgents, chatPromptFiles, chatInstructions, and chatSkills.
876 .PARAMETER PackageJson
877 The package.json object to update.
878 .PARAMETER ChatAgents
879 Array of discovered chat agent objects.
880 .PARAMETER ChatPromptFiles
881 Array of discovered prompt objects.
882 .PARAMETER ChatInstructions
883 Array of discovered instruction objects.
884 .PARAMETER ChatSkills
885 Array of discovered skill objects.
886 .OUTPUTS
887 [PSCustomObject] Updated package.json object.
888 #>
889 [CmdletBinding()]
890 [OutputType([PSCustomObject])]
891 param(
892 [Parameter(Mandatory = $true)]
893 [PSCustomObject]$PackageJson,
894
895 [Parameter(Mandatory = $true)]
896 [AllowEmptyCollection()]
897 [array]$ChatAgents,
898
899 [Parameter(Mandatory = $true)]
900 [AllowEmptyCollection()]
901 [array]$ChatPromptFiles,
902
903 [Parameter(Mandatory = $true)]
904 [AllowEmptyCollection()]
905 [array]$ChatInstructions,
906
907 [Parameter(Mandatory = $true)]
908 [AllowEmptyCollection()]
909 [array]$ChatSkills
910 )
911
912 # Clone the object to avoid modifying the original
913 $updated = $PackageJson | ConvertTo-Json -Depth 10 | ConvertFrom-Json
914
915 # Ensure contributes section exists
916 if (-not $updated.contributes) {
917 $updated | Add-Member -NotePropertyName "contributes" -NotePropertyValue ([PSCustomObject]@{})
918 }
919
920 # Add or update contributes properties
921 if ($null -eq $updated.contributes.chatAgents) {
922 $updated.contributes | Add-Member -NotePropertyName "chatAgents" -NotePropertyValue $ChatAgents -Force
923 } else {
924 $updated.contributes.chatAgents = $ChatAgents
925 }
926
927 if ($null -eq $updated.contributes.chatPromptFiles) {
928 $updated.contributes | Add-Member -NotePropertyName "chatPromptFiles" -NotePropertyValue $ChatPromptFiles -Force
929 } else {
930 $updated.contributes.chatPromptFiles = $ChatPromptFiles
931 }
932
933 if ($null -eq $updated.contributes.chatInstructions) {
934 $updated.contributes | Add-Member -NotePropertyName "chatInstructions" -NotePropertyValue $ChatInstructions -Force
935 } else {
936 $updated.contributes.chatInstructions = $ChatInstructions
937 }
938
939 if ($null -eq $updated.contributes.chatSkills) {
940 $updated.contributes | Add-Member -NotePropertyName "chatSkills" -NotePropertyValue $ChatSkills -Force
941 } else {
942 $updated.contributes.chatSkills = $ChatSkills
943 }
944
945 return $updated
946}
947
948function New-PrepareResult {
949 <#
950 .SYNOPSIS
951 Creates a standardized result object for extension preparation operations.
952 .DESCRIPTION
953 Factory function that creates a hashtable with consistent properties
954 for reporting preparation operation outcomes.
955 .PARAMETER Success
956 Indicates whether the operation completed successfully.
957 .PARAMETER Version
958 The version string from package.json.
959 .PARAMETER AgentCount
960 Number of agents discovered and included.
961 .PARAMETER PromptCount
962 Number of prompts discovered and included.
963 .PARAMETER InstructionCount
964 Number of instructions discovered and included.
965 .PARAMETER SkillCount
966 Number of skills discovered and included.
967 .PARAMETER ErrorMessage
968 Error description when Success is false.
969 .OUTPUTS
970 Hashtable with Success, Version, AgentCount, PromptCount,
971 InstructionCount, SkillCount, and ErrorMessage properties.
972 #>
973 [CmdletBinding()]
974 [OutputType([hashtable])]
975 param(
976 [Parameter(Mandatory = $true)]
977 [bool]$Success,
978
979 [Parameter(Mandatory = $false)]
980 [string]$Version = "",
981
982 [Parameter(Mandatory = $false)]
983 [int]$AgentCount = 0,
984
985 [Parameter(Mandatory = $false)]
986 [int]$PromptCount = 0,
987
988 [Parameter(Mandatory = $false)]
989 [int]$InstructionCount = 0,
990
991 [Parameter(Mandatory = $false)]
992 [int]$SkillCount = 0,
993
994 [Parameter(Mandatory = $false)]
995 [string]$ErrorMessage = ""
996 )
997
998 return @{
999 Success = $Success
1000 Version = $Version
1001 AgentCount = $AgentCount
1002 PromptCount = $PromptCount
1003 InstructionCount = $InstructionCount
1004 SkillCount = $SkillCount
1005 ErrorMessage = $ErrorMessage
1006 }
1007}
1008
1009function Test-CollectionManifestCompleteness {
1010 <#
1011 .SYNOPSIS
1012 Validates collection manifest contains all required fields for packaging.
1013 .DESCRIPTION
1014 Ensures collection manifest has all required fields needed for Option A
1015 dynamic package.json generation: id, name, displayName, description,
1016 publisher, and personas.
1017 .PARAMETER Manifest
1018 Parsed collection manifest hashtable.
1019 .OUTPUTS
1020 None. Throws on validation failure.
1021 #>
1022 [CmdletBinding()]
1023 param(
1024 [Parameter(Mandatory = $true)]
1025 [hashtable]$Manifest
1026 )
1027
1028 $requiredFields = @('id', 'name', 'displayName', 'description', 'publisher', 'personas')
1029 $missing = @()
1030
1031 foreach ($field in $requiredFields) {
1032 if (-not $Manifest.ContainsKey($field) -or [string]::IsNullOrWhiteSpace($Manifest[$field])) {
1033 $missing += $field
1034 }
1035 }
1036
1037 if ($missing.Count -gt 0) {
1038 throw "Collection manifest missing required fields: $($missing -join ', ')"
1039 }
1040
1041 Write-Host "✓ Collection manifest validation passed" -ForegroundColor Green
1042}
1043
1044function New-ReadmeFromRegistry {
1045 <#
1046 .SYNOPSIS
1047 Generates README content from registry and collection manifest.
1048 .DESCRIPTION
1049 Creates a complete README.md file for the extension by pulling artifact
1050 descriptions from the AI artifacts registry. Used for Option A packaging
1051 to eliminate hand-authored per-persona README files.
1052 .PARAMETER CollectionManifest
1053 Collection manifest hashtable containing displayName and description.
1054 .PARAMETER Registry
1055 AI artifacts registry hashtable with agents, prompts, instructions, skills.
1056 .PARAMETER ArtifactNames
1057 Hashtable with Agents, Prompts, Instructions, Skills string arrays.
1058 .PARAMETER Channel
1059 Release channel (Stable or PreRelease) for documentation.
1060 .OUTPUTS
1061 [string] Complete README.md markdown content.
1062 #>
1063 [CmdletBinding()]
1064 [OutputType([string])]
1065 param(
1066 [Parameter(Mandatory = $true)]
1067 [hashtable]$CollectionManifest,
1068
1069 [Parameter(Mandatory = $true)]
1070 [hashtable]$Registry,
1071
1072 [Parameter(Mandatory = $true)]
1073 [hashtable]$ArtifactNames,
1074
1075 [Parameter(Mandatory = $false)]
1076 [ValidateSet('Stable', 'PreRelease')]
1077 [string]$Channel = 'Stable'
1078 )
1079
1080 $readme = @"
1081# $($CollectionManifest.displayName)
1082
1083> $($CollectionManifest.description)
1084
1085This extension provides AI chat agents, prompts, and instructions for use with GitHub Copilot in VS Code.
1086
1087## Features
1088
1089"@
1090
1091 # Add agents section
1092 if ($ArtifactNames.Agents.Count -gt 0) {
1093 $readme += @"
1094
1095### 🤖 Chat Agents
1096
1097| Agent | Description |
1098| ----- | ----------- |
1099
1100"@
1101 foreach ($agentName in ($ArtifactNames.Agents | Sort-Object)) {
1102 $agentDesc = "Agent for $agentName"
1103 if ($Registry.agents -and $Registry.agents[$agentName] -and $Registry.agents[$agentName].description) {
1104 $agentDesc = $Registry.agents[$agentName].description
1105 }
1106 $readme += "| **$agentName** | $agentDesc |`n"
1107 }
1108 }
1109
1110 # Add prompts section
1111 if ($ArtifactNames.Prompts.Count -gt 0) {
1112 $readme += @"
1113
1114### 📝 Prompts
1115
1116| Prompt | Description |
1117| ------ | ----------- |
1118
1119"@
1120 foreach ($promptName in ($ArtifactNames.Prompts | Sort-Object)) {
1121 $promptDesc = "Prompt for $promptName"
1122 if ($Registry.prompts -and $Registry.prompts[$promptName] -and $Registry.prompts[$promptName].description) {
1123 $promptDesc = $Registry.prompts[$promptName].description
1124 }
1125 $readme += "| **$promptName** | $promptDesc |`n"
1126 }
1127 }
1128
1129 # Add instructions section
1130 if ($ArtifactNames.Instructions.Count -gt 0) {
1131 $readme += @"
1132
1133### 📚 Instructions
1134
1135| Instruction | Description |
1136| ----------- | ----------- |
1137
1138"@
1139 foreach ($instructionName in ($ArtifactNames.Instructions | Sort-Object)) {
1140 $instructionDesc = "Instructions for $instructionName"
1141 if ($Registry.instructions -and $Registry.instructions[$instructionName] -and $Registry.instructions[$instructionName].description) {
1142 $instructionDesc = $Registry.instructions[$instructionName].description
1143 }
1144 $readme += "| **$instructionName** | $instructionDesc |`n"
1145 }
1146 }
1147
1148 # Add skills section
1149 if ($ArtifactNames.Skills.Count -gt 0) {
1150 $readme += @"
1151
1152### ⚡ Skills
1153
1154| Skill | Description |
1155| ----- | ----------- |
1156
1157"@
1158 foreach ($skillName in ($ArtifactNames.Skills | Sort-Object)) {
1159 $skillDesc = "Skill for $skillName"
1160 if ($Registry.skills -and $Registry.skills[$skillName] -and $Registry.skills[$skillName].description) {
1161 $skillDesc = $Registry.skills[$skillName].description
1162 }
1163 $readme += "| **$skillName** | $skillDesc |`n"
1164 }
1165 }
1166
1167 # Add getting started section
1168 $readme += @"
1169
1170## Getting Started
1171
1172After installing this extension, the chat agents will be available in GitHub Copilot Chat. You can:
1173
11741. **Use custom agents** by selecting them from the agent picker in Copilot Chat
11752. **Apply prompts** through the Copilot Chat interface
11763. **Reference instructions** — They're automatically applied based on file patterns
1177
1178## Requirements
1179
1180- VS Code version 1.106.1 or higher
1181- GitHub Copilot extension
1182
1183## License
1184
1185MIT License - see [LICENSE](LICENSE) for details
1186
1187## Support
1188
1189For issues, questions, or contributions, please visit the [GitHub repository](https://github.com/microsoft/hve-core).
1190
1191---
1192
1193Brought to you by Microsoft ISE HVE Essentials
1194"@
1195
1196 return $readme
1197}
1198
1199function Invoke-PrepareExtension {
1200 <#
1201 .SYNOPSIS
1202 Orchestrates VS Code extension preparation with full error handling.
1203 .DESCRIPTION
1204 Executes the complete preparation workflow: validates paths, discovers
1205 agents/prompts/instructions, updates package.json, and handles changelog.
1206 Returns a result object instead of using exit codes.
1207 .PARAMETER ExtensionDirectory
1208 Absolute path to the extension directory containing package.json.
1209 .PARAMETER RepoRoot
1210 Absolute path to the repository root directory.
1211 .PARAMETER Channel
1212 Release channel controlling maturity filter ('Stable' or 'PreRelease').
1213 .PARAMETER ChangelogPath
1214 Optional path to changelog file to include.
1215 .PARAMETER DryRun
1216 When specified, shows what would be done without making changes.
1217 .OUTPUTS
1218 Hashtable with Success, Version, AgentCount, PromptCount,
1219 InstructionCount, SkillCount, and ErrorMessage properties.
1220 #>
1221 [CmdletBinding()]
1222 [OutputType([hashtable])]
1223 param(
1224 [Parameter(Mandatory = $true)]
1225 [ValidateNotNullOrEmpty()]
1226 [string]$ExtensionDirectory,
1227
1228 [Parameter(Mandatory = $true)]
1229 [ValidateNotNullOrEmpty()]
1230 [string]$RepoRoot,
1231
1232 [Parameter(Mandatory = $false)]
1233 [ValidateSet('Stable', 'PreRelease')]
1234 [string]$Channel = 'Stable',
1235
1236 [Parameter(Mandatory = $false)]
1237 [string]$ChangelogPath = "",
1238
1239 [Parameter(Mandatory = $false)]
1240 [switch]$DryRun,
1241
1242 [Parameter(Mandatory = $false)]
1243 [string]$Collection = ""
1244 )
1245
1246 # Derive paths
1247 $GitHubDir = Join-Path $RepoRoot ".github"
1248 $PackageJsonPath = Join-Path $ExtensionDirectory "package.json"
1249
1250 # Track whether package.json was modified for restoration
1251 $needsRestore = $false
1252 $modifiedCollectionId = ""
1253
1254 try {
1255 # Validate required paths exist
1256 $pathValidation = Test-PathsExist -ExtensionDir $ExtensionDirectory `
1257 -PackageJsonPath $PackageJsonPath `
1258 -GitHubDir $GitHubDir
1259 if (-not $pathValidation.IsValid) {
1260 $missingPaths = $pathValidation.MissingPaths -join ', '
1261 return New-PrepareResult -Success $false -ErrorMessage "Required paths not found: $missingPaths"
1262 }
1263
1264 # Load AI artifacts registry if available
1265 $registryPath = Join-Path $GitHubDir "ai-artifacts-registry.json"
1266 $registry = @{}
1267 if (Test-Path $registryPath) {
1268 $registry = Get-RegistryData -RegistryPath $registryPath
1269 Write-Host "Registry loaded: $registryPath"
1270 }
1271
1272 # Read and parse package.json
1273 try {
1274 $packageJsonContent = Get-Content -Path $PackageJsonPath -Raw
1275 $packageJson = $packageJsonContent | ConvertFrom-Json
1276 }
1277 catch {
1278 return New-PrepareResult -Success $false -ErrorMessage "Failed to parse package.json at '$PackageJsonPath'. Check the file for JSON syntax errors. Underlying error: $($_.Exception.Message)"
1279 }
1280
1281 # Validate version field
1282 if (-not $packageJson.PSObject.Properties['version']) {
1283 return New-PrepareResult -Success $false -ErrorMessage "package.json does not contain a 'version' field"
1284 }
1285 $version = $packageJson.version
1286 if ($version -notmatch '^\d+\.\d+\.\d+$') {
1287 return New-PrepareResult -Success $false -ErrorMessage "Invalid version format in package.json: $version"
1288 }
1289
1290 # Get allowed maturities for channel
1291 $allowedMaturities = Get-AllowedMaturities -Channel $Channel
1292
1293 Write-Host "`n=== Prepare Extension ===" -ForegroundColor Cyan
1294 Write-Host "Extension Directory: $ExtensionDirectory"
1295 Write-Host "Repository Root: $RepoRoot"
1296 Write-Host "Channel: $Channel"
1297 Write-Host "Allowed Maturities: $($allowedMaturities -join ', ')"
1298 Write-Host "Version: $version"
1299 if ($DryRun) {
1300 Write-Host "[DRY RUN] No changes will be made" -ForegroundColor Yellow
1301 }
1302
1303 # Load collection manifest if specified
1304 $collectionManifest = $null
1305 $collectionArtifactNames = $null
1306
1307 if ($Collection -and $Collection -ne "") {
1308 $collectionManifest = Get-CollectionManifest -CollectionPath $Collection
1309 Write-Host "Collection: $($collectionManifest.displayName) ($($collectionManifest.id))"
1310
1311 # Validate collection manifest completeness before processing
1312 if ($collectionManifest.id -ne 'hve-core-all') {
1313 try {
1314 Test-CollectionManifestCompleteness -Manifest $collectionManifest
1315 }
1316 catch {
1317 return New-PrepareResult -Success $false -ErrorMessage $_.Exception.Message
1318 }
1319 }
1320
1321 # Get persona-filtered artifact names
1322 $collectionArtifactNames = Get-CollectionArtifacts -Registry $registry -Collection $collectionManifest -AllowedMaturities $allowedMaturities
1323
1324 # Resolve handoff dependencies (agents only)
1325 $agentsDir = Join-Path $GitHubDir "agents"
1326 $expandedAgents = Resolve-HandoffDependencies -SeedAgents $collectionArtifactNames.Agents -AgentsDir $agentsDir -AllowedMaturities $allowedMaturities -Registry $registry
1327 $collectionArtifactNames.Agents = $expandedAgents
1328
1329 # Resolve requires dependencies
1330 $resolvedNames = Resolve-RequiresDependencies -ArtifactNames @{
1331 agents = $collectionArtifactNames.Agents
1332 prompts = $collectionArtifactNames.Prompts
1333 instructions = $collectionArtifactNames.Instructions
1334 skills = $collectionArtifactNames.Skills
1335 } -Registry $registry -AllowedMaturities $allowedMaturities
1336
1337 $collectionArtifactNames = @{
1338 Agents = $resolvedNames.Agents
1339 Prompts = $resolvedNames.Prompts
1340 Instructions = $resolvedNames.Instructions
1341 Skills = $resolvedNames.Skills
1342 }
1343 }
1344
1345 # Discover agents
1346 $agentsDir = Join-Path $GitHubDir "agents"
1347 $agentResult = Get-DiscoveredAgents -AgentsDir $agentsDir -AllowedMaturities $allowedMaturities -ExcludedAgents @() -Registry $registry
1348 $chatAgents = $agentResult.Agents
1349 $excludedAgents = $agentResult.Skipped
1350
1351 Write-Host "`n--- Chat Agents ---" -ForegroundColor Green
1352 Write-Host "Found $($chatAgents.Count) agent(s) matching criteria"
1353 if ($excludedAgents.Count -gt 0) {
1354 Write-Host "Excluded $($excludedAgents.Count) agent(s) due to maturity filter" -ForegroundColor Yellow
1355 }
1356
1357 # Discover prompts
1358 $promptsDir = Join-Path $GitHubDir "prompts"
1359 $promptResult = Get-DiscoveredPrompts -PromptsDir $promptsDir -GitHubDir $GitHubDir -AllowedMaturities $allowedMaturities -Registry $registry
1360 $chatPrompts = $promptResult.Prompts
1361 $excludedPrompts = $promptResult.Skipped
1362
1363 Write-Host "`n--- Chat Prompts ---" -ForegroundColor Green
1364 Write-Host "Found $($chatPrompts.Count) prompt(s) matching criteria"
1365 if ($excludedPrompts.Count -gt 0) {
1366 Write-Host "Excluded $($excludedPrompts.Count) prompt(s) due to maturity filter" -ForegroundColor Yellow
1367 }
1368
1369 # Discover instructions
1370 $instructionsDir = Join-Path $GitHubDir "instructions"
1371 $instructionResult = Get-DiscoveredInstructions -InstructionsDir $instructionsDir -GitHubDir $GitHubDir -AllowedMaturities $allowedMaturities -Registry $registry
1372 $chatInstructions = $instructionResult.Instructions
1373 $excludedInstructions = $instructionResult.Skipped
1374
1375 Write-Host "`n--- Chat Instructions ---" -ForegroundColor Green
1376 Write-Host "Found $($chatInstructions.Count) instruction(s) matching criteria"
1377 if ($excludedInstructions.Count -gt 0) {
1378 Write-Host "Excluded $($excludedInstructions.Count) instruction(s) due to maturity filter" -ForegroundColor Yellow
1379 }
1380
1381 # Discover skills
1382 $skillsDir = Join-Path $GitHubDir "skills"
1383 $skillResult = Get-DiscoveredSkills -SkillsDir $skillsDir -AllowedMaturities $allowedMaturities -Registry $registry
1384 $chatSkills = $skillResult.Skills
1385 $excludedSkills = $skillResult.Skipped
1386
1387 Write-Host "`n--- Chat Skills ---" -ForegroundColor Green
1388 Write-Host "Found $($chatSkills.Count) skill(s) matching criteria"
1389 if ($excludedSkills.Count -gt 0) {
1390 Write-Host "Excluded $($excludedSkills.Count) skill(s) due to maturity filter" -ForegroundColor Yellow
1391 }
1392
1393 # Apply collection filtering to discovered artifacts
1394 if ($null -ne $collectionArtifactNames) {
1395 $chatAgents = @($chatAgents | Where-Object { $collectionArtifactNames.Agents -contains $_.name })
1396 $chatPrompts = @($chatPrompts | Where-Object { $collectionArtifactNames.Prompts -contains $_.name })
1397 $instrBaseNames = @($collectionArtifactNames.Instructions | ForEach-Object { ($_ -split '/')[-1] })
1398 $chatInstructions = @($chatInstructions | Where-Object {
1399 $instrBaseName = $_.name -replace '-instructions$', ''
1400 $instrBaseNames -contains $instrBaseName
1401 })
1402 $chatSkills = @($chatSkills | Where-Object { $collectionArtifactNames.Skills -contains $_.name })
1403
1404 Write-Host "`n--- Collection Filtering ---" -ForegroundColor Magenta
1405 Write-Host "Agents after filter: $($chatAgents.Count)"
1406 Write-Host "Prompts after filter: $($chatPrompts.Count)"
1407 Write-Host "Instructions after filter: $($chatInstructions.Count)"
1408 Write-Host "Skills after filter: $($chatSkills.Count)"
1409 }
1410
1411 # Apply persona template when building a non-default collection
1412 if ($null -ne $collectionManifest -and $collectionManifest.id -ne 'hve-core-all') {
1413 # Read canonical package.json
1414 $packageJson = Get-Content -Path $PackageJsonPath -Raw | ConvertFrom-Json
1415
1416 # Override metadata from collection manifest (in-place)
1417 # Use Add-Member to ensure properties exist before setting
1418 if (-not $packageJson.PSObject.Properties['name']) {
1419 $packageJson | Add-Member -NotePropertyName 'name' -NotePropertyValue $collectionManifest.name
1420 } else {
1421 $packageJson.name = $collectionManifest.name
1422 }
1423
1424 if (-not $packageJson.PSObject.Properties['displayName']) {
1425 $packageJson | Add-Member -NotePropertyName 'displayName' -NotePropertyValue $collectionManifest.displayName
1426 } else {
1427 $packageJson.displayName = $collectionManifest.displayName
1428 }
1429
1430 if (-not $packageJson.PSObject.Properties['description']) {
1431 $packageJson | Add-Member -NotePropertyName 'description' -NotePropertyValue $collectionManifest.description
1432 } else {
1433 $packageJson.description = $collectionManifest.description
1434 }
1435
1436 if ($collectionManifest.ContainsKey('publisher')) {
1437 if (-not $packageJson.PSObject.Properties['publisher']) {
1438 $packageJson | Add-Member -NotePropertyName 'publisher' -NotePropertyValue $collectionManifest.publisher
1439 } else {
1440 $packageJson.publisher = $collectionManifest.publisher
1441 }
1442 }
1443
1444 # Mark for restoration after build
1445 $needsRestore = $true
1446 $modifiedCollectionId = $collectionManifest.id
1447
1448 Write-Host "Applied metadata from collection: $($collectionManifest.id)" -ForegroundColor Green
1449
1450 # Write metadata changes immediately so they're visible even in DryRun
1451 # (tests expect to read modified package.json from disk)
1452 $packageJson | ConvertTo-Json -Depth 10 | Set-Content -Path $PackageJsonPath -Encoding UTF8NoBOM
1453 }
1454
1455 # Update package.json with generated contributes
1456 $packageJson = Update-PackageJsonContributes -PackageJson $packageJson `
1457 -ChatAgents $chatAgents `
1458 -ChatPromptFiles $chatPrompts `
1459 -ChatInstructions $chatInstructions `
1460 -ChatSkills $chatSkills
1461
1462 # Write updated package.json
1463 if (-not $DryRun) {
1464 $packageJson | ConvertTo-Json -Depth 10 | Set-Content -Path $PackageJsonPath -Encoding UTF8NoBOM
1465 Write-Host "`nUpdated package.json with discovered artifacts" -ForegroundColor Green
1466 }
1467 else {
1468 Write-Host "`n[DRY RUN] Would update package.json with discovered artifacts" -ForegroundColor Yellow
1469 }
1470
1471 # Generate README from registry when building a collection
1472 if ($null -ne $collectionManifest) {
1473 Write-Host "`nGenerating README from registry..." -ForegroundColor Cyan
1474
1475 # Build artifact names list from discovered artifacts
1476 $artifactNamesForReadme = @{
1477 Agents = @($chatAgents | ForEach-Object { $_.name })
1478 Prompts = @($chatPrompts | ForEach-Object { $_.name })
1479 Instructions = @($chatInstructions | ForEach-Object { $_.name -replace '-instructions$', '' })
1480 Skills = @($chatSkills | ForEach-Object { $_.name })
1481 }
1482
1483 $readmeContent = New-ReadmeFromRegistry `
1484 -CollectionManifest $collectionManifest `
1485 -Registry $registry `
1486 -ArtifactNames $artifactNamesForReadme `
1487 -Channel $Channel
1488
1489 $readmePath = Join-Path $ExtensionDirectory "README.md"
1490 if (-not $DryRun) {
1491 Set-Content -Path $readmePath -Value $readmeContent -Encoding UTF8NoBOM
1492 Write-Host "✓ README generated: $readmePath" -ForegroundColor Green
1493 }
1494 else {
1495 Write-Host "[DRY RUN] Would generate README: $readmePath" -ForegroundColor Yellow
1496 }
1497 }
1498
1499 # Handle changelog
1500 if ($ChangelogPath -and (Test-Path $ChangelogPath)) {
1501 $destChangelog = Join-Path $ExtensionDirectory "CHANGELOG.md"
1502 if (-not $DryRun) {
1503 Copy-Item -Path $ChangelogPath -Destination $destChangelog -Force
1504 Write-Host "Copied changelog to extension directory" -ForegroundColor Green
1505 }
1506 else {
1507 Write-Host "[DRY RUN] Would copy changelog to extension directory" -ForegroundColor Yellow
1508 }
1509 }
1510 elseif ($ChangelogPath) {
1511 Write-Warning "Changelog path specified but file not found: $ChangelogPath"
1512 }
1513
1514 Write-Host "`n=== Preparation Complete ===" -ForegroundColor Cyan
1515
1516 return New-PrepareResult -Success $true `
1517 -Version $version `
1518 -AgentCount $chatAgents.Count `
1519 -PromptCount $chatPrompts.Count `
1520 -InstructionCount $chatInstructions.Count `
1521 -SkillCount $chatSkills.Count
1522 }
1523 finally {
1524 # Restore canonical package.json from git if it was modified
1525 if ($needsRestore -and -not $DryRun) {
1526 Write-Host "`nRestoring canonical package.json from git..." -ForegroundColor Cyan
1527 $gitRestore = & git checkout HEAD -- extension/package.json 2>&1
1528 if ($LASTEXITCODE -eq 0) {
1529 Write-Host "✓ Package.json restored successfully" -ForegroundColor Green
1530 }
1531 else {
1532 Write-Warning "Failed to restore package.json from git. Manual restore may be required."
1533 Write-Warning "Error: $gitRestore"
1534 }
1535 }
1536 }
1537}
1538
1539#endregion Pure Functions
1540
1541#region Main Execution
1542if ($MyInvocation.InvocationName -ne '.') {
1543 try {
1544 # Verify PowerShell-Yaml module is available
1545 if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) {
1546 throw "Required module 'PowerShell-Yaml' is not installed."
1547 }
1548 Import-Module PowerShell-Yaml -ErrorAction Stop
1549
1550 # Resolve paths using $MyInvocation (must stay in entry point)
1551 $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
1552 $RepoRoot = (Get-Item "$ScriptDir/../..").FullName
1553 $ExtensionDir = Join-Path $RepoRoot "extension"
1554
1555 # Resolve changelog path if provided
1556 $resolvedChangelogPath = ""
1557 if ($ChangelogPath) {
1558 $resolvedChangelogPath = if ([System.IO.Path]::IsPathRooted($ChangelogPath)) {
1559 $ChangelogPath
1560 }
1561 else {
1562 Join-Path $RepoRoot $ChangelogPath
1563 }
1564 }
1565
1566 Write-Host "📦 HVE Core Extension Preparer" -ForegroundColor Cyan
1567 Write-Host "==============================" -ForegroundColor Cyan
1568 Write-Host " Channel: $Channel" -ForegroundColor Cyan
1569 if ($Collection) {
1570 Write-Host " Collection: $Collection" -ForegroundColor Cyan
1571 }
1572 Write-Host ""
1573
1574 # Call orchestration function
1575 $result = Invoke-PrepareExtension `
1576 -ExtensionDirectory $ExtensionDir `
1577 -RepoRoot $RepoRoot `
1578 -Channel $Channel `
1579 -ChangelogPath $resolvedChangelogPath `
1580 -DryRun:$DryRun `
1581 -Collection $Collection
1582
1583 if (-not $result.Success) {
1584 throw $result.ErrorMessage
1585 }
1586
1587 Write-Host ""
1588 Write-Host "🎉 Done!" -ForegroundColor Green
1589 Write-Host ""
1590 Write-Host "📊 Summary:" -ForegroundColor Cyan
1591 Write-Host " Agents: $($result.AgentCount)"
1592 Write-Host " Prompts: $($result.PromptCount)"
1593 Write-Host " Instructions: $($result.InstructionCount)"
1594 Write-Host " Skills: $($result.SkillCount)"
1595 Write-Host " Version: $($result.Version)"
1596
1597 exit 0
1598 }
1599 catch {
1600 Write-Error "Prepare Extension failed: $($_.Exception.Message)"
1601 Write-CIAnnotation -Message $_.Exception.Message -Level Error
1602 exit 1
1603 }
1604}
1605#endregion
1606