microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
f976449aa637cc0b12669bf6478df53862a94cbc

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/plugins/Generate-Plugins.ps1

399lines · 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 Generates Copilot CLI plugin directories from collection manifests.
9
10.DESCRIPTION
11 Reads collection YAML manifests from the collections/ directory and generates
12 plugin directories under plugins/ with symlinks to source artifacts, plugin.json
13 manifests, and auto-generated README files.
14
15 Supports generating all plugins or specific collections. Use -Refresh to
16 regenerate existing plugins (deletes and recreates).
17
18.PARAMETER CollectionIds
19 Optional. Array of collection IDs to generate. Generates all when omitted.
20
21.PARAMETER Refresh
22 Optional. Deletes and recreates existing plugin directories.
23
24.PARAMETER DryRun
25 Optional. Shows what would be done without making changes.
26
27.PARAMETER Channel
28 Optional. Release channel controlling eligible item maturities.
29 Stable includes only stable items. PreRelease includes stable, preview,
30 and experimental. Deprecated is excluded from both channels.
31
32.EXAMPLE
33 ./Generate-Plugins.ps1
34 # Generates all plugins (default: all + refresh)
35
36.EXAMPLE
37 ./Generate-Plugins.ps1 -CollectionIds rpi,github
38 # Generates only the rpi and github plugins
39
40.EXAMPLE
41 ./Generate-Plugins.ps1 -DryRun
42 # Shows what would be generated without making changes
43
44.EXAMPLE
45 ./Generate-Plugins.ps1 -Channel Stable
46 # Generates plugins with stable-only items
47
48.NOTES
49 Dependencies: PowerShell-Yaml module, scripts/plugins/Modules/PluginHelpers.psm1
50#>
51
52[CmdletBinding()]
53param(
54 [Parameter(Mandatory = $false)]
55 [string[]]$CollectionIds,
56
57 [Parameter(Mandatory = $false)]
58 [switch]$Refresh,
59
60 [Parameter(Mandatory = $false)]
61 [switch]$DryRun,
62
63 [Parameter(Mandatory = $false)]
64 [ValidateSet('Stable', 'PreRelease')]
65 [string]$Channel = 'PreRelease'
66)
67
68$ErrorActionPreference = 'Stop'
69
70Import-Module (Join-Path $PSScriptRoot 'Modules/PluginHelpers.psm1') -Force
71Import-Module (Join-Path $PSScriptRoot '../lib/Modules/CIHelpers.psm1') -Force
72
73#region Orchestration
74
75function Get-AllowedCollectionMaturities {
76 <#
77 .SYNOPSIS
78 Returns allowed collection item maturities for a channel.
79
80 .PARAMETER Channel
81 Release channel ('Stable' or 'PreRelease').
82
83 .OUTPUTS
84 [string[]] Allowed maturity values for collection items.
85 #>
86 [CmdletBinding()]
87 [OutputType([string[]])]
88 param(
89 [Parameter(Mandatory = $true)]
90 [ValidateSet('Stable', 'PreRelease')]
91 [string]$Channel
92 )
93
94 if ($Channel -eq 'Stable') {
95 return @('stable')
96 }
97
98 return @('stable', 'preview', 'experimental')
99}
100
101function Select-CollectionItemsByChannel {
102 <#
103 .SYNOPSIS
104 Filters collection items by channel using item maturity metadata.
105
106 .PARAMETER Collection
107 Collection manifest hashtable.
108
109 .PARAMETER Channel
110 Release channel ('Stable' or 'PreRelease').
111
112 .OUTPUTS
113 [hashtable] Collection clone with filtered items.
114 #>
115 [CmdletBinding()]
116 [OutputType([hashtable])]
117 param(
118 [Parameter(Mandatory = $true)]
119 [hashtable]$Collection,
120
121 [Parameter(Mandatory = $true)]
122 [ValidateSet('Stable', 'PreRelease')]
123 [string]$Channel
124 )
125
126 $allowedMaturities = Get-AllowedCollectionMaturities -Channel $Channel
127 $filteredItems = @()
128
129 foreach ($item in $Collection.items) {
130 $effectiveMaturity = Resolve-CollectionItemMaturity -Maturity $item.maturity
131 if ($allowedMaturities -contains $effectiveMaturity) {
132 $filteredItems += $item
133 }
134 }
135
136 $filteredCollection = @{}
137 foreach ($key in $Collection.Keys) {
138 $filteredCollection[$key] = $Collection[$key]
139 }
140 $filteredCollection['items'] = $filteredItems
141
142 return $filteredCollection
143}
144
145function Invoke-PluginGeneration {
146 <#
147 .SYNOPSIS
148 Orchestrates plugin directory generation from collection manifests.
149
150 .DESCRIPTION
151 Loads collection manifests from the collections/ directory, optionally
152 filters to specified IDs, and generates plugin directory structures
153 under plugins/. Each plugin receives symlinks to source artifacts,
154 a plugin.json manifest, and an auto-generated README.
155
156 .PARAMETER RepoRoot
157 Absolute path to the repository root directory.
158
159 .PARAMETER CollectionIds
160 Optional. Array of collection IDs to generate. Generates all when omitted.
161
162 .PARAMETER Refresh
163 When specified, removes existing plugin directories before regenerating.
164
165 .PARAMETER DryRun
166 When specified, logs actions without creating files or directories.
167
168 .PARAMETER Channel
169 Release channel controlling item maturity eligibility.
170
171 .OUTPUTS
172 Hashtable with Success, PluginCount, and ErrorMessage keys
173 via New-GenerateResult.
174 #>
175 [CmdletBinding()]
176 [OutputType([hashtable])]
177 param(
178 [Parameter(Mandatory = $true)]
179 [ValidateNotNullOrEmpty()]
180 [string]$RepoRoot,
181
182 [Parameter(Mandatory = $false)]
183 [string[]]$CollectionIds,
184
185 [Parameter(Mandatory = $false)]
186 [switch]$Refresh,
187
188 [Parameter(Mandatory = $false)]
189 [switch]$DryRun,
190
191 [Parameter(Mandatory = $false)]
192 [ValidateSet('Stable', 'PreRelease')]
193 [string]$Channel = 'PreRelease'
194 )
195
196 $collectionsDir = Join-Path -Path $RepoRoot -ChildPath 'collections'
197 $pluginsDir = Join-Path -Path $RepoRoot -ChildPath 'plugins'
198
199 # Read repo version from package.json for plugin manifests
200 $packageJsonPath = Join-Path -Path $RepoRoot -ChildPath 'package.json'
201 $repoVersion = (Get-Content -Path $packageJsonPath -Raw | ConvertFrom-Json).version
202
203 # Auto-update hve-core-all collection with discovered artifacts
204 $updateResult = Update-HveCoreAllCollection -RepoRoot $RepoRoot -DryRun:$DryRun
205 Write-Verbose "hve-core-all updated: $($updateResult.ItemCount) items ($($updateResult.AddedCount) added, $($updateResult.RemovedCount) removed)"
206
207 # Load all collection manifests
208 $allCollections = Get-AllCollections -CollectionsDir $collectionsDir
209
210 if ($allCollections.Count -eq 0) {
211 Write-Warning 'No collection manifests found in collections/'
212 return New-GenerateResult -Success $true -PluginCount 0
213 }
214
215 # Filter to requested IDs when provided
216 if ($CollectionIds -and $CollectionIds.Count -gt 0) {
217 $filtered = @($allCollections | Where-Object { $CollectionIds -contains $_.id })
218 $missing = @($CollectionIds | Where-Object { $_ -notin ($allCollections | ForEach-Object { $_.id }) })
219 if ($missing.Count -gt 0) {
220 Write-Warning "Collections not found: $($missing -join ', ')"
221 }
222 $allCollections = $filtered
223 }
224
225 Write-Host "`n=== Plugin Generation ===" -ForegroundColor Cyan
226 Write-Host "Collections: $($allCollections.Count)"
227 Write-Host "Channel: $Channel"
228 Write-Host "Plugins dir: $pluginsDir"
229 if ($DryRun) {
230 Write-Host '[DRY RUN] No changes will be made' -ForegroundColor Yellow
231 }
232
233 $generated = 0
234 $totalAgents = 0
235 $totalCommands = 0
236 $totalInstructions = 0
237 $totalSkills = 0
238
239 foreach ($collection in $allCollections) {
240 $id = $collection.id
241 $pluginDir = Join-Path -Path $pluginsDir -ChildPath $id
242
243 # Skip deprecated collections
244 $collectionMaturity = if ($collection.ContainsKey('maturity') -and $collection.maturity) {
245 [string]$collection.maturity
246 } else { 'stable' }
247
248 if ($collectionMaturity -eq 'deprecated') {
249 Write-Verbose "Skipping deprecated collection: $id"
250 continue
251 }
252
253 # Refresh: remove existing plugin directory
254 if ($Refresh -and (Test-Path -Path $pluginDir)) {
255 if ($DryRun) {
256 Write-Host " [DRY RUN] Would remove $pluginDir" -ForegroundColor Yellow
257 }
258 else {
259 Remove-Item -Path $pluginDir -Recurse -Force
260 Write-Verbose "Removed existing plugin directory: $pluginDir"
261 }
262 }
263
264 # Generate plugin directory structure
265 $filteredCollection = Select-CollectionItemsByChannel -Collection $collection -Channel $Channel
266
267 $result = Write-PluginDirectory -Collection $filteredCollection `
268 -PluginsDir $pluginsDir `
269 -RepoRoot $RepoRoot `
270 -Version $repoVersion `
271 -Maturity $collectionMaturity `
272 -DryRun:$DryRun
273
274 $itemCount = $filteredCollection.items.Count
275 $totalAgents += $result.AgentCount
276 $totalCommands += $result.CommandCount
277 $totalInstructions += $result.InstructionCount
278 $totalSkills += $result.SkillCount
279 $generated++
280
281 Write-Host " $id ($itemCount items)" -ForegroundColor Green
282 }
283
284 # Generate marketplace.json from all collections
285 Write-MarketplaceManifest `
286 -RepoRoot $RepoRoot `
287 -Collections $allCollections `
288 -DryRun:$DryRun
289
290 Write-Host "`n--- Summary ---" -ForegroundColor Cyan
291 Write-Host " Plugins generated: $generated"
292 Write-Host " Agents: $totalAgents"
293 Write-Host " Commands: $totalCommands"
294 Write-Host " Instructions: $totalInstructions"
295 Write-Host " Skills: $totalSkills"
296
297 return New-GenerateResult -Success $true -PluginCount $generated
298}
299
300#endregion Orchestration
301
302#region Main Execution
303
304function Start-PluginGeneration {
305 <#
306 .SYNOPSIS
307 Entry point for CLI invocation. Returns 0 on success, 1 on failure.
308
309 .PARAMETER ScriptPath
310 Absolute path to this script file, used to resolve the repo root.
311
312 .PARAMETER CollectionIds
313 Optional collection IDs forwarded to Invoke-PluginGeneration.
314
315 .PARAMETER Refresh
316 Forwarded refresh switch.
317
318 .PARAMETER DryRun
319 Forwarded dry-run switch.
320
321 .PARAMETER Channel
322 Forwarded channel parameter.
323
324 .OUTPUTS
325 [int] Exit code: 0 for success, 1 for failure.
326 #>
327 [CmdletBinding()]
328 [OutputType([int])]
329 param(
330 [Parameter(Mandatory = $true)]
331 [string]$ScriptPath,
332
333 [Parameter(Mandatory = $false)]
334 [string[]]$CollectionIds,
335
336 [Parameter(Mandatory = $false)]
337 [switch]$Refresh,
338
339 [Parameter(Mandatory = $false)]
340 [switch]$DryRun,
341
342 [Parameter(Mandatory = $false)]
343 [ValidateSet('Stable', 'PreRelease')]
344 [string]$Channel = 'PreRelease'
345 )
346
347 try {
348 # Verify PowerShell-Yaml module
349 if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) {
350 throw "Required module 'PowerShell-Yaml' is not installed."
351 }
352 Import-Module PowerShell-Yaml -ErrorAction Stop
353
354 # Resolve paths
355 $ScriptDir = Split-Path -Parent $ScriptPath
356 $RepoRoot = (Get-Item "$ScriptDir/../..").FullName
357
358 Write-Host 'HVE Core Plugin Generator' -ForegroundColor Cyan
359 Write-Host '==========================' -ForegroundColor Cyan
360
361 # Default to all + refresh when no args
362 $effectiveRefresh = $Refresh
363 if (-not $CollectionIds -and -not $Refresh.IsPresent -and -not $DryRun.IsPresent) {
364 $effectiveRefresh = [switch]::new($true)
365 }
366
367 $result = Invoke-PluginGeneration `
368 -RepoRoot $RepoRoot `
369 -CollectionIds $CollectionIds `
370 -Refresh:$effectiveRefresh `
371 -DryRun:$DryRun `
372 -Channel $Channel
373
374 if (-not $result.Success) {
375 throw $result.ErrorMessage
376 }
377
378 Write-Host ''
379 Write-Host 'Done!' -ForegroundColor Green
380 Write-Host " $($result.PluginCount) plugin(s) generated."
381
382 return 0
383 }
384 catch {
385 Write-Error "Plugin generation failed: $($_.Exception.Message)"
386 Write-CIAnnotation -Message $_.Exception.Message -Level Error
387 return 1
388 }
389}
390
391if ($MyInvocation.InvocationName -ne '.') {
392 exit (Start-PluginGeneration `
393 -ScriptPath $MyInvocation.MyCommand.Path `
394 -CollectionIds $CollectionIds `
395 -Refresh:$Refresh `
396 -DryRun:$DryRun `
397 -Channel $Channel)
398}
399#endregion
400