microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
975e862d391b7b0b89b4066927217fa3d327924e

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/plugins/Generate-Plugins.ps1

413lines · 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 # Probe symlink capability once for the entire generation run
208 $symlinkCapable = Test-SymlinkCapability
209 Write-Verbose "Symlink capability: $symlinkCapable ($(if ($symlinkCapable) { 'using symlinks' } else { 'using file copies' }))"
210
211 # Load all collection manifests
212 $allCollections = Get-AllCollections -CollectionsDir $collectionsDir
213
214 if ($allCollections.Count -eq 0) {
215 Write-Warning 'No collection manifests found in collections/'
216 return New-GenerateResult -Success $true -PluginCount 0
217 }
218
219 # Filter to requested IDs when provided
220 if ($CollectionIds -and $CollectionIds.Count -gt 0) {
221 $filtered = @($allCollections | Where-Object { $CollectionIds -contains $_.id })
222 $missing = @($CollectionIds | Where-Object { $_ -notin ($allCollections | ForEach-Object { $_.id }) })
223 if ($missing.Count -gt 0) {
224 Write-Warning "Collections not found: $($missing -join ', ')"
225 }
226 $allCollections = $filtered
227 }
228
229 Write-Host "`n=== Plugin Generation ===" -ForegroundColor Cyan
230 Write-Host "Collections: $($allCollections.Count)"
231 Write-Host "Channel: $Channel"
232 Write-Host "Plugins dir: $pluginsDir"
233 if ($DryRun) {
234 Write-Host '[DRY RUN] No changes will be made' -ForegroundColor Yellow
235 }
236
237 $generated = 0
238 $totalAgents = 0
239 $totalCommands = 0
240 $totalInstructions = 0
241 $totalSkills = 0
242
243 foreach ($collection in $allCollections) {
244 $id = $collection.id
245 $pluginDir = Join-Path -Path $pluginsDir -ChildPath $id
246
247 # Skip deprecated collections
248 $collectionMaturity = if ($collection.ContainsKey('maturity') -and $collection.maturity) {
249 [string]$collection.maturity
250 } else { 'stable' }
251
252 if ($collectionMaturity -eq 'deprecated') {
253 Write-Verbose "Skipping deprecated collection: $id"
254 continue
255 }
256
257 # Refresh: remove existing plugin directory
258 if ($Refresh -and (Test-Path -Path $pluginDir)) {
259 if ($DryRun) {
260 Write-Host " [DRY RUN] Would remove $pluginDir" -ForegroundColor Yellow
261 }
262 else {
263 Remove-Item -Path $pluginDir -Recurse -Force
264 Write-Verbose "Removed existing plugin directory: $pluginDir"
265 }
266 }
267
268 # Generate plugin directory structure
269 $filteredCollection = Select-CollectionItemsByChannel -Collection $collection -Channel $Channel
270
271 $result = Write-PluginDirectory -Collection $filteredCollection `
272 -PluginsDir $pluginsDir `
273 -RepoRoot $RepoRoot `
274 -Version $repoVersion `
275 -Maturity $collectionMaturity `
276 -DryRun:$DryRun `
277 -SymlinkCapable:$symlinkCapable
278
279 $itemCount = $filteredCollection.items.Count
280 $totalAgents += $result.AgentCount
281 $totalCommands += $result.CommandCount
282 $totalInstructions += $result.InstructionCount
283 $totalSkills += $result.SkillCount
284 $generated++
285
286 Write-Host " $id ($itemCount items)" -ForegroundColor Green
287 }
288
289 # Generate marketplace.json from all collections
290 Write-MarketplaceManifest `
291 -RepoRoot $RepoRoot `
292 -Collections $allCollections `
293 -DryRun:$DryRun
294
295 # Fix git index modes for text stubs on non-symlink systems so Linux
296 # checkouts materialize real symbolic links instead of plain files.
297 if (-not $symlinkCapable) {
298 $fixedCount = Repair-PluginSymlinkIndex -PluginsDir $pluginsDir -RepoRoot $RepoRoot -DryRun:$DryRun
299 if ($fixedCount -gt 0) {
300 Write-Host " Symlink index: $fixedCount entries fixed (100644 -> 120000)" -ForegroundColor Green
301 }
302 }
303
304 Write-Host "`n--- Summary ---" -ForegroundColor Cyan
305 Write-Host " Plugins generated: $generated"
306 Write-Host " Agents: $totalAgents"
307 Write-Host " Commands: $totalCommands"
308 Write-Host " Instructions: $totalInstructions"
309 Write-Host " Skills: $totalSkills"
310
311 return New-GenerateResult -Success $true -PluginCount $generated
312}
313
314#endregion Orchestration
315
316#region Main Execution
317
318function Start-PluginGeneration {
319 <#
320 .SYNOPSIS
321 Entry point for CLI invocation. Returns 0 on success, 1 on failure.
322
323 .PARAMETER ScriptPath
324 Absolute path to this script file, used to resolve the repo root.
325
326 .PARAMETER CollectionIds
327 Optional collection IDs forwarded to Invoke-PluginGeneration.
328
329 .PARAMETER Refresh
330 Forwarded refresh switch.
331
332 .PARAMETER DryRun
333 Forwarded dry-run switch.
334
335 .PARAMETER Channel
336 Forwarded channel parameter.
337
338 .OUTPUTS
339 [int] Exit code: 0 for success, 1 for failure.
340 #>
341 [CmdletBinding()]
342 [OutputType([int])]
343 param(
344 [Parameter(Mandatory = $true)]
345 [string]$ScriptPath,
346
347 [Parameter(Mandatory = $false)]
348 [string[]]$CollectionIds,
349
350 [Parameter(Mandatory = $false)]
351 [switch]$Refresh,
352
353 [Parameter(Mandatory = $false)]
354 [switch]$DryRun,
355
356 [Parameter(Mandatory = $false)]
357 [ValidateSet('Stable', 'PreRelease')]
358 [string]$Channel = 'PreRelease'
359 )
360
361 try {
362 # Verify PowerShell-Yaml module
363 if (-not (Get-Module -ListAvailable -Name PowerShell-Yaml)) {
364 throw "Required module 'PowerShell-Yaml' is not installed."
365 }
366 Import-Module PowerShell-Yaml -ErrorAction Stop
367
368 # Resolve paths
369 $ScriptDir = Split-Path -Parent $ScriptPath
370 $RepoRoot = (Get-Item "$ScriptDir/../..").FullName
371
372 Write-Host 'HVE Core Plugin Generator' -ForegroundColor Cyan
373 Write-Host '==========================' -ForegroundColor Cyan
374
375 # Default to all + refresh when no args
376 $effectiveRefresh = $Refresh
377 if (-not $CollectionIds -and -not $Refresh.IsPresent -and -not $DryRun.IsPresent) {
378 $effectiveRefresh = [switch]::new($true)
379 }
380
381 $result = Invoke-PluginGeneration `
382 -RepoRoot $RepoRoot `
383 -CollectionIds $CollectionIds `
384 -Refresh:$effectiveRefresh `
385 -DryRun:$DryRun `
386 -Channel $Channel
387
388 if (-not $result.Success) {
389 throw $result.ErrorMessage
390 }
391
392 Write-Host ''
393 Write-Host 'Done!' -ForegroundColor Green
394 Write-Host " $($result.PluginCount) plugin(s) generated."
395
396 return 0
397 }
398 catch {
399 Write-Error "Plugin generation failed: $($_.Exception.Message)"
400 Write-CIAnnotation -Message $_.Exception.Message -Level Error
401 return 1
402 }
403}
404
405if ($MyInvocation.InvocationName -ne '.') {
406 exit (Start-PluginGeneration `
407 -ScriptPath $MyInvocation.MyCommand.Path `
408 -CollectionIds $CollectionIds `
409 -Refresh:$Refresh `
410 -DryRun:$DryRun `
411 -Channel $Channel)
412}
413#endregion
414