Initial shared Copilot resources scaffold

This commit is contained in:
2026-04-23 15:46:34 -04:00
commit adfcb83ab6
44 changed files with 2249 additions and 0 deletions

372
install/publish.ps1 Normal file
View File

@@ -0,0 +1,372 @@
param(
[Parameter(Mandatory = $true)]
[string]$Source,
[string]$Kind,
[string]$Name,
[switch]$Force
)
$ErrorActionPreference = 'Stop'
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RepoRoot = Split-Path -Parent $ScriptDir
$StateDir = if ($env:COPILOT_RESOURCES_STATE_DIR) { $env:COPILOT_RESOURCES_STATE_DIR } else { Join-Path $HOME '.copilot-resources-state' }
function Normalize-Stem {
param([string]$Value)
$Normalized = $Value.ToLowerInvariant()
$Normalized = [regex]::Replace($Normalized, '[\s_]+', '-')
$Normalized = [regex]::Replace($Normalized, '[^a-z0-9-]+', '-')
$Normalized = [regex]::Replace($Normalized, '-+', '-')
$Normalized = $Normalized.Trim('-')
if ([string]::IsNullOrWhiteSpace($Normalized)) {
return 'resource'
}
return $Normalized
}
function Strip-KindSuffix {
param(
[string]$Kind,
[string]$Value
)
switch ($Kind) {
'prompt' { return ($Value -replace '\.prompt\.md$', '') }
'instruction' { return ($Value -replace '\.instructions\.md$', '') }
'agent' { return ($Value -replace '\.agent\.md$', '') }
'hook' { return ($Value -replace '\.json$', '') }
default { return $Value }
}
}
function Get-KindSuffix {
param([string]$Kind)
switch ($Kind) {
'prompt' { return '.prompt.md' }
'instruction' { return '.instructions.md' }
'agent' { return '.agent.md' }
'hook' { return '.json' }
'skill' { return '' }
default { throw "Unsupported kind: $Kind" }
}
}
function Get-KindRoot {
param([string]$Kind)
switch ($Kind) {
'skill' { return (Join-Path $RepoRoot 'resources\skills') }
'prompt' { return (Join-Path $RepoRoot 'resources\prompts') }
'instruction' { return (Join-Path $RepoRoot 'resources\instructions') }
'agent' { return (Join-Path $RepoRoot 'resources\agents') }
'hook' { return (Join-Path $RepoRoot 'resources\hooks') }
default { throw "Unsupported kind: $Kind" }
}
}
function Get-FrontmatterField {
param(
[string]$Path,
[string]$Field
)
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
return $null
}
$Lines = Get-Content -LiteralPath $Path
if ($Lines.Count -eq 0 -or $Lines[0].Trim() -ne '---') {
return $null
}
$Pattern = '^[\s]*' + [regex]::Escape($Field) + ':[\s]*(.+?)\s*$'
for ($Index = 1; $Index -lt $Lines.Count; $Index++) {
if ($Lines[$Index].Trim() -eq '---') {
break
}
if ($Lines[$Index] -match $Pattern) {
$Value = $Matches[1] -replace '\s+#.*$', ''
return $Value.Trim('"', "'")
}
}
return $null
}
function Set-SkillNameField {
param(
[string]$Path,
[string]$Name
)
$Lines = [System.Collections.Generic.List[string]]::new()
$Lines.AddRange([string[]](Get-Content -LiteralPath $Path))
if ($Lines.Count -eq 0 -or $Lines[0].Trim() -ne '---') {
throw "Skill frontmatter must start with ---: $Path"
}
$FrontmatterEnd = -1
for ($Index = 1; $Index -lt $Lines.Count; $Index++) {
if ($Lines[$Index].Trim() -eq '---') {
$FrontmatterEnd = $Index
break
}
}
if ($FrontmatterEnd -lt 0) {
throw "Skill frontmatter is incomplete: $Path"
}
$Replaced = $false
for ($Index = 1; $Index -lt $FrontmatterEnd; $Index++) {
if ($Lines[$Index] -match '^\s*name\s*:') {
$Lines[$Index] = "name: $Name"
$Replaced = $true
break
}
}
if (-not $Replaced) {
$Lines.Insert(1, "name: $Name")
}
Set-Content -LiteralPath $Path -Value $Lines
}
function Get-StringHash {
param([string]$Value)
$Bytes = [System.Text.Encoding]::UTF8.GetBytes($Value)
$Sha = [System.Security.Cryptography.SHA256]::Create()
try {
return ([System.Convert]::ToHexString($Sha.ComputeHash($Bytes))).ToLowerInvariant()
}
finally {
$Sha.Dispose()
}
}
function Get-ArtifactFingerprint {
param([string]$Path)
if (Test-Path -LiteralPath $Path -PathType Container) {
$Manifest = foreach ($File in (Get-ChildItem -LiteralPath $Path -Recurse -File | Sort-Object FullName)) {
$Relative = [System.IO.Path]::GetRelativePath($Path, $File.FullName).Replace('\', '/')
$Hash = (Get-FileHash -LiteralPath $File.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
"$Hash $Relative"
}
return Get-StringHash -Value ($Manifest -join "`n")
}
return (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLowerInvariant()
}
function Get-ArtifactDisplayName {
param(
[string]$Kind,
[string]$Path
)
switch ($Kind) {
'skill' {
$Name = Get-FrontmatterField -Path (Join-Path $Path 'SKILL.md') -Field 'name'
if ($Name) { return $Name }
return (Split-Path -Leaf $Path)
}
'prompt' {
$Name = Get-FrontmatterField -Path $Path -Field 'name'
if ($Name) { return $Name }
return Strip-KindSuffix -Kind $Kind -Value (Split-Path -Leaf $Path)
}
'instruction' {
$Name = Get-FrontmatterField -Path $Path -Field 'name'
if ($Name) { return $Name }
return Strip-KindSuffix -Kind $Kind -Value (Split-Path -Leaf $Path)
}
'agent' {
$Name = Get-FrontmatterField -Path $Path -Field 'name'
if ($Name) { return $Name }
return Strip-KindSuffix -Kind $Kind -Value (Split-Path -Leaf $Path)
}
'hook' {
return Strip-KindSuffix -Kind $Kind -Value (Split-Path -Leaf $Path)
}
default {
throw "Unsupported kind: $Kind"
}
}
}
function Get-KindArtifacts {
param([string]$Kind)
$Root = Get-KindRoot -Kind $Kind
if (-not (Test-Path -LiteralPath $Root)) {
return @()
}
switch ($Kind) {
'skill' { return (Get-ChildItem -LiteralPath $Root -Directory | Sort-Object FullName | Select-Object -ExpandProperty FullName) }
'prompt' { return (Get-ChildItem -LiteralPath $Root -File -Filter '*.prompt.md' | Sort-Object FullName | Select-Object -ExpandProperty FullName) }
'instruction' { return (Get-ChildItem -LiteralPath $Root -File -Filter '*.instructions.md' | Sort-Object FullName | Select-Object -ExpandProperty FullName) }
'agent' { return (Get-ChildItem -LiteralPath $Root -File -Filter '*.agent.md' | Sort-Object FullName | Select-Object -ExpandProperty FullName) }
'hook' { return (Get-ChildItem -LiteralPath $Root -File -Filter '*.json' | Sort-Object FullName | Select-Object -ExpandProperty FullName) }
default { throw "Unsupported kind: $Kind" }
}
}
function Write-PublishLog {
param(
[string]$Kind,
[string]$Source,
[string]$Target,
[string]$Origin,
[string]$Fingerprint,
[string]$Outcome
)
New-Item -ItemType Directory -Path $StateDir -Force | Out-Null
$Timestamp = [DateTime]::UtcNow.ToString('o')
"$Timestamp`t$Kind`t$Source`t$Target`t$Origin`t$Fingerprint`t$Outcome" | Add-Content -LiteralPath (Join-Path $StateDir 'publish-log.tsv')
}
function Detect-Kind {
param([string]$Path)
if ((Test-Path -LiteralPath $Path -PathType Container) -and (Test-Path -LiteralPath (Join-Path $Path 'SKILL.md'))) {
return 'skill'
}
switch -Regex ((Split-Path -Leaf $Path)) {
'\.prompt\.md$' { return 'prompt' }
'\.instructions\.md$' { return 'instruction' }
'\.agent\.md$' { return 'agent' }
'\.json$' { return 'hook' }
'^SKILL\.md$' { return 'skill' }
default { throw "Could not detect resource kind for: $Path" }
}
}
if (-not (Test-Path -LiteralPath $Source)) {
throw "Source does not exist: $Source"
}
if (-not $Kind) {
$Kind = Detect-Kind -Path $Source
}
$TempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString('N'))
New-Item -ItemType Directory -Path $TempDir -Force | Out-Null
try {
switch ($Kind) {
'skill' {
if (Test-Path -LiteralPath $Source -PathType Leaf) {
$Source = Split-Path -Parent $Source
}
if (-not (Test-Path -LiteralPath (Join-Path $Source 'SKILL.md'))) {
throw "Skill publish requires a directory containing SKILL.md"
}
if (-not $Name) { $Name = Get-FrontmatterField -Path (Join-Path $Source 'SKILL.md') -Field 'name' }
if (-not $Name) { $Name = Split-Path -Leaf $Source }
$NormalizedName = Normalize-Stem -Value $Name
$Target = Join-Path $RepoRoot (Join-Path 'resources\skills' $NormalizedName)
$Candidate = Join-Path $TempDir $NormalizedName
Copy-Item -LiteralPath $Source -Destination $Candidate -Recurse -Force
Set-SkillNameField -Path (Join-Path $Candidate 'SKILL.md') -Name $NormalizedName
}
'prompt' {
if (-not $Name) { $Name = Split-Path -Leaf $Source }
$NormalizedName = Normalize-Stem -Value (Strip-KindSuffix -Kind $Kind -Value $Name)
$Target = Join-Path $RepoRoot (Join-Path 'resources\prompts' ($NormalizedName + (Get-KindSuffix -Kind $Kind)))
$Candidate = Join-Path $TempDir ($NormalizedName + (Get-KindSuffix -Kind $Kind))
Copy-Item -LiteralPath $Source -Destination $Candidate -Force
}
'instruction' {
if (-not $Name) { $Name = Split-Path -Leaf $Source }
$NormalizedName = Normalize-Stem -Value (Strip-KindSuffix -Kind $Kind -Value $Name)
$Target = Join-Path $RepoRoot (Join-Path 'resources\instructions' ($NormalizedName + (Get-KindSuffix -Kind $Kind)))
$Candidate = Join-Path $TempDir ($NormalizedName + (Get-KindSuffix -Kind $Kind))
Copy-Item -LiteralPath $Source -Destination $Candidate -Force
}
'agent' {
if (-not $Name) { $Name = Split-Path -Leaf $Source }
$NormalizedName = Normalize-Stem -Value (Strip-KindSuffix -Kind $Kind -Value $Name)
$Target = Join-Path $RepoRoot (Join-Path 'resources\agents' ($NormalizedName + (Get-KindSuffix -Kind $Kind)))
$Candidate = Join-Path $TempDir ($NormalizedName + (Get-KindSuffix -Kind $Kind))
Copy-Item -LiteralPath $Source -Destination $Candidate -Force
}
'hook' {
if (-not $Name) { $Name = Split-Path -Leaf $Source }
$NormalizedName = Normalize-Stem -Value (Strip-KindSuffix -Kind $Kind -Value $Name)
$Target = Join-Path $RepoRoot (Join-Path 'resources\hooks' ($NormalizedName + (Get-KindSuffix -Kind $Kind)))
$Candidate = Join-Path $TempDir ($NormalizedName + (Get-KindSuffix -Kind $Kind))
Copy-Item -LiteralPath $Source -Destination $Candidate -Force
}
default {
throw "Unsupported kind: $Kind"
}
}
$CandidateFingerprint = Get-ArtifactFingerprint -Path $Candidate
$CandidateDisplayName = Get-ArtifactDisplayName -Kind $Kind -Path $Candidate
if (Test-Path -LiteralPath $Target) {
$ExistingTargetFingerprint = Get-ArtifactFingerprint -Path $Target
if ($ExistingTargetFingerprint -eq $CandidateFingerprint) {
Write-PublishLog -Kind $Kind -Source $Source -Target $Target -Origin 'local-first' -Fingerprint $CandidateFingerprint -Outcome 'noop'
Write-Host "No publish needed. Shared artifact already exists at: $Target"
Write-Host "Fingerprint: $CandidateFingerprint"
Write-Host "Display name: $CandidateDisplayName"
return
}
if (-not $Force) {
throw "Target already exists with different content: $Target"
}
}
foreach ($ExistingPath in (Get-KindArtifacts -Kind $Kind)) {
if ($ExistingPath -eq $Target) {
continue
}
$ExistingFingerprint = Get-ArtifactFingerprint -Path $ExistingPath
if ($ExistingFingerprint -eq $CandidateFingerprint) {
throw "Duplicate $Kind content already exists at: $ExistingPath"
}
$ExistingDisplayName = Get-ArtifactDisplayName -Kind $Kind -Path $ExistingPath
if ($ExistingDisplayName -eq $CandidateDisplayName) {
throw "Duplicate $Kind display name '$CandidateDisplayName' already exists at: $ExistingPath"
}
}
New-Item -ItemType Directory -Path (Split-Path -Parent $Target) -Force | Out-Null
if (Test-Path -LiteralPath $Target) {
Remove-Item -LiteralPath $Target -Recurse -Force
}
if (Test-Path -LiteralPath $Candidate -PathType Container) {
Copy-Item -LiteralPath $Candidate -Destination $Target -Recurse -Force
} else {
Copy-Item -LiteralPath $Candidate -Destination $Target -Force
}
Write-PublishLog -Kind $Kind -Source $Source -Target $Target -Origin 'local-first' -Fingerprint $CandidateFingerprint -Outcome 'published'
Write-Host "Published $Kind into shared repo: $Target"
Write-Host "Fingerprint: $CandidateFingerprint"
Write-Host "Display name: $CandidateDisplayName"
}
finally {
if (Test-Path -LiteralPath $TempDir) {
Remove-Item -LiteralPath $TempDir -Recurse -Force
}
}