You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
173 lines
6.1 KiB
173 lines
6.1 KiB
[CmdletBinding()]
|
|
param(
|
|
[string] $SourceRoot = 'C:\Users\dkucinski\.scout\m-skills',
|
|
[string] $RepositoryRoot = $PSScriptRoot
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$repositoryRoot = (Resolve-Path -LiteralPath $RepositoryRoot).Path
|
|
$sourceRoot = (Resolve-Path -LiteralPath $SourceRoot).Path
|
|
$logDirectory = Join-Path $repositoryRoot 'logs'
|
|
$logPath = Join-Path $logDirectory 'sync.log'
|
|
$allowedExtensions = @('.gitignore', '.js', '.json', '.mjs', '.ps1', '.psm1', '.yaml', '.yml')
|
|
$excludedDirectories = @('.git', 'cache', 'caches', 'logs', 'node_modules')
|
|
$excludedFiles = @('.token-cache.json', 'skills-metadata.json')
|
|
|
|
New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null
|
|
|
|
function Write-SyncLog {
|
|
param(
|
|
[ValidateSet('INFO', 'WARN', 'ERROR')]
|
|
[string] $Level,
|
|
[string] $Message
|
|
)
|
|
|
|
$entry = '{0:o} [{1}] {2}' -f (Get-Date), $Level, $Message
|
|
Add-Content -LiteralPath $logPath -Value $entry
|
|
Write-Output $entry
|
|
}
|
|
|
|
function Invoke-Git {
|
|
param([string[]] $Arguments)
|
|
|
|
& git -C $repositoryRoot @Arguments
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw "Git command failed: git $($Arguments -join ' ')"
|
|
}
|
|
}
|
|
|
|
function Test-AllowedSourceFile {
|
|
param(
|
|
[System.IO.FileInfo] $File,
|
|
[string] $SkillRoot
|
|
)
|
|
|
|
$relativePath = $File.FullName.Substring($SkillRoot.Length).TrimStart('\')
|
|
$segments = $relativePath -split '\\'
|
|
if ($segments | Where-Object {
|
|
$_ -like '.pw-profile*' -or $excludedDirectories -contains $_
|
|
}) {
|
|
return $false
|
|
}
|
|
|
|
if ($excludedFiles -contains $File.Name) {
|
|
return $false
|
|
}
|
|
|
|
return $File.Name -eq 'SKILL.md' -or $allowedExtensions -contains $File.Extension.ToLowerInvariant()
|
|
}
|
|
|
|
function Get-SkillDescription {
|
|
param([string] $SkillFile)
|
|
|
|
$content = Get-Content -LiteralPath $SkillFile
|
|
$descriptionLine = $content | Select-String -Pattern '^\s*description:\s*["'']?(.*?)["'']?\s*$' | Select-Object -First 1
|
|
if ($null -eq $descriptionLine) {
|
|
return 'No description provided.'
|
|
}
|
|
|
|
return $descriptionLine.Matches[0].Groups[1].Value.Trim()
|
|
}
|
|
|
|
$stagingRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("PromptWorks-{0}" -f [guid]::NewGuid())
|
|
|
|
try {
|
|
if (-not (Test-Path -LiteralPath (Join-Path $repositoryRoot '.git'))) {
|
|
throw "'$repositoryRoot' is not a Git repository."
|
|
}
|
|
|
|
Write-SyncLog -Level INFO -Message "Starting sync from '$sourceRoot'."
|
|
Invoke-Git -Arguments @('fetch', 'origin', 'main')
|
|
$remoteAhead = [int](& git -C $repositoryRoot rev-list --count 'main..origin/main')
|
|
if ($LASTEXITCODE -ne 0) {
|
|
throw 'Unable to determine whether the remote branch is ahead.'
|
|
}
|
|
if ($remoteAhead -gt 0) {
|
|
throw "Remote branch origin/main is ahead by $remoteAhead commit(s); merge it before syncing."
|
|
}
|
|
|
|
New-Item -ItemType Directory -Path $stagingRoot | Out-Null
|
|
|
|
$sourceSkills = Get-ChildItem -LiteralPath $sourceRoot -Directory | Sort-Object Name
|
|
foreach ($skill in $sourceSkills) {
|
|
$stagingSkillRoot = Join-Path $stagingRoot $skill.Name
|
|
$copiedFiles = 0
|
|
|
|
Get-ChildItem -LiteralPath $skill.FullName -Recurse -File | Where-Object {
|
|
Test-AllowedSourceFile -File $_ -SkillRoot $skill.FullName
|
|
} | ForEach-Object {
|
|
$relativePath = $_.FullName.Substring($skill.FullName.Length).TrimStart('\')
|
|
$destination = Join-Path $stagingSkillRoot $relativePath
|
|
New-Item -ItemType Directory -Path (Split-Path -Parent $destination) -Force | Out-Null
|
|
Copy-Item -LiteralPath $_.FullName -Destination $destination
|
|
$copiedFiles++
|
|
}
|
|
|
|
if (-not (Test-Path -LiteralPath (Join-Path $stagingSkillRoot 'SKILL.md'))) {
|
|
throw "Skill '$($skill.Name)' has no allowed SKILL.md file."
|
|
}
|
|
|
|
Write-SyncLog -Level INFO -Message "Staged skill '$($skill.Name)' ($copiedFiles files)."
|
|
}
|
|
|
|
$existingSkills = Get-ChildItem -LiteralPath $repositoryRoot -Directory | Where-Object {
|
|
Test-Path -LiteralPath (Join-Path $_.FullName 'SKILL.md')
|
|
}
|
|
foreach ($skill in $existingSkills) {
|
|
if ($skill.Name -notin $sourceSkills.Name) {
|
|
Remove-Item -LiteralPath $skill.FullName -Recurse -Force
|
|
Write-SyncLog -Level INFO -Message "Removed stale skill '$($skill.Name)'."
|
|
}
|
|
}
|
|
|
|
foreach ($skill in $sourceSkills) {
|
|
$destination = Join-Path $repositoryRoot $skill.Name
|
|
if (Test-Path -LiteralPath $destination) {
|
|
Remove-Item -LiteralPath $destination -Recurse -Force
|
|
}
|
|
Move-Item -LiteralPath (Join-Path $stagingRoot $skill.Name) -Destination $destination
|
|
}
|
|
|
|
$readmeLines = @(
|
|
'# PromptWorks',
|
|
'',
|
|
'Private version-controlled mirror of AI skills used in my daily workflow.',
|
|
'',
|
|
'Generated by `sync-skills.ps1`; do not edit manually.',
|
|
'',
|
|
'## Skills',
|
|
'',
|
|
'| Skill | Description |',
|
|
'| --- | --- |'
|
|
)
|
|
foreach ($skill in $sourceSkills) {
|
|
$description = (Get-SkillDescription -SkillFile (Join-Path (Join-Path $repositoryRoot $skill.Name) 'SKILL.md')).Replace('|', '\|')
|
|
$readmeLines += "| [$($skill.Name)]($($skill.Name)/SKILL.md) | $description |"
|
|
}
|
|
Set-Content -LiteralPath (Join-Path $repositoryRoot 'README.md') -Value $readmeLines
|
|
|
|
Invoke-Git -Arguments @('add', '--all')
|
|
& git -C $repositoryRoot diff --cached --quiet
|
|
if ($LASTEXITCODE -eq 0) {
|
|
Write-SyncLog -Level INFO -Message 'Sync completed with no changes.'
|
|
exit 0
|
|
}
|
|
if ($LASTEXITCODE -ne 1) {
|
|
throw 'Unable to determine whether staged changes exist.'
|
|
}
|
|
|
|
$commitMessage = 'chore: sync Scout skills {0:yyyy-MM-dd}' -f (Get-Date)
|
|
Invoke-Git -Arguments @('commit', '-m', $commitMessage)
|
|
Invoke-Git -Arguments @('push', '-u', 'origin', 'main')
|
|
Write-SyncLog -Level INFO -Message "Sync completed and pushed commit '$commitMessage'."
|
|
}
|
|
catch {
|
|
Write-SyncLog -Level ERROR -Message $_.Exception.Message
|
|
throw
|
|
}
|
|
finally {
|
|
if (Test-Path -LiteralPath $stagingRoot) {
|
|
Remove-Item -LiteralPath $stagingRoot -Recurse -Force
|
|
}
|
|
}
|
|
|