microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
8b197250063fc1629244f661f78baf9022cebbb0

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/plugins/Generate-Plugins.ps1

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