e3228abb26
При переключении на ветку или коммит без plugin/skills junction'ы навыков повисают битыми, и git checkout падает на середине: часть рабочего дерева уже удалена, а HEAD остаётся на прежней ветке. Remove-AgentSkillLink удаляет сам reparse point через Directory.Delete (без -Recurse), поэтому в target не заходит и содержимое plugin/skills не трогает. Junction опознаётся по атрибуту ReparsePoint, а не по резолву target — иначе режим не работал бы ровно в том случае, ради которого сделан. Настоящий каталог не удаляется, а возвращает Skipped. Режимы скрипта разведены parameter set'ами: -Remove -Check вместе теперь дают ошибку разбора параметров вместо тихого выигрыша одного из ключей. Покрыто пятью кейсами в tools/tests/AgentAssets.Tests.ps1 (включая junction с исчезнувшим источником); порядок действий и восстановление после падения чекаута описаны в CLAUDE.md.
97 lines
3.7 KiB
PowerShell
97 lines
3.7 KiB
PowerShell
Set-StrictMode -Version Latest
|
||
|
||
<#
|
||
.SYNOPSIS
|
||
Проверяет, что LinkPath — junction, указывающий на SourcePath.
|
||
#>
|
||
function Test-AgentSkillLink {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory)] [string] $SourcePath,
|
||
[Parameter(Mandatory)] [string] $LinkPath
|
||
)
|
||
|
||
$item = Get-Item -LiteralPath $LinkPath -Force -ErrorAction SilentlyContinue
|
||
if ($null -eq $item -or $item.LinkType -ne 'Junction') { return $false }
|
||
|
||
$actual = [System.IO.Path]::GetFullPath(($item.Target | Select-Object -First 1))
|
||
$expected = [System.IO.Path]::GetFullPath($SourcePath)
|
||
return [string]::Equals($actual.TrimEnd('\'), $expected.TrimEnd('\'), [StringComparison]::OrdinalIgnoreCase)
|
||
}
|
||
|
||
<#
|
||
.SYNOPSIS
|
||
Раскладывает навык по LinkPath junction'ом. Возвращает Created | AlreadyLinked | Copied.
|
||
Настоящий каталог никогда не удаляется автоматически.
|
||
#>
|
||
function Sync-AgentSkillLink {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory)] [string] $SourcePath,
|
||
[Parameter(Mandatory)] [string] $LinkPath,
|
||
[switch] $AllowCopy
|
||
)
|
||
|
||
if (-not (Test-Path -LiteralPath $SourcePath)) {
|
||
throw "нет исходного навыка: $SourcePath"
|
||
}
|
||
if (Test-AgentSkillLink -SourcePath $SourcePath -LinkPath $LinkPath) {
|
||
return 'AlreadyLinked'
|
||
}
|
||
|
||
$existing = Get-Item -LiteralPath $LinkPath -Force -ErrorAction SilentlyContinue
|
||
if ($null -ne $existing) {
|
||
if ($existing.LinkType -ne 'Junction') {
|
||
throw "$LinkPath существует и не является junction — удалите его вручную и повторите"
|
||
}
|
||
# Ссылка ведёт не туда: удаляем сам reparse point, не заходя в target.
|
||
[System.IO.Directory]::Delete($LinkPath)
|
||
}
|
||
|
||
$parent = Split-Path -Parent $LinkPath
|
||
if (-not (Test-Path -LiteralPath $parent)) {
|
||
New-Item -ItemType Directory -Path $parent -Force | Out-Null
|
||
}
|
||
|
||
try {
|
||
New-Item -ItemType Junction -Path $LinkPath -Value $SourcePath -ErrorAction Stop | Out-Null
|
||
return 'Created'
|
||
}
|
||
catch {
|
||
if (-not $AllowCopy) {
|
||
throw "не удалось создать junction $LinkPath -> $SourcePath ($($_.Exception.Message)). Повторите с -AllowCopy, если копия допустима."
|
||
}
|
||
Copy-Item -LiteralPath $SourcePath -Destination $LinkPath -Recurse -Force -ErrorAction Stop
|
||
return 'Copied'
|
||
}
|
||
}
|
||
|
||
<#
|
||
.SYNOPSIS
|
||
Снимает junction по LinkPath. Возвращает Removed | Missing | Skipped.
|
||
Настоящие каталоги не трогает, в target не заходит.
|
||
#>
|
||
function Remove-AgentSkillLink {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory)] [string] $LinkPath
|
||
)
|
||
|
||
$item = Get-Item -LiteralPath $LinkPath -Force -ErrorAction SilentlyContinue
|
||
if ($null -eq $item) { return 'Missing' }
|
||
|
||
# Проверяем именно атрибут, а не LinkType: junction с удалённым target
|
||
# (ровно наш случай после checkout без plugin/skills) остаётся reparse point,
|
||
# но полагаться на резолв target тут нельзя.
|
||
$isReparse = $item.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)
|
||
if (-not ($item -is [System.IO.DirectoryInfo]) -or -not $isReparse) {
|
||
return 'Skipped'
|
||
}
|
||
|
||
# Удаляем сам reparse point: без -Recurse содержимое target не затрагивается.
|
||
[System.IO.Directory]::Delete($LinkPath)
|
||
return 'Removed'
|
||
}
|
||
|
||
Export-ModuleMember -Function Test-AgentSkillLink, Sync-AgentSkillLink, Remove-AgentSkillLink
|