Skip to content

Commit 8f16e52

Browse files
committed
feat(Sync-ClaudeConfig): 改进配置同步脚本处理符号链接
- 替换原始 Copy-Item 调用为自定义 Copy-FileSystemEntry 函数 - 添加对符号链接的特殊处理,避免在失效链接上中断迁移过程 - 实现 Copy-ReparsePointItem 函数专门处理符号链接复制 - 对目录链接进行跳过处理,防止循环或无权限路径问题 - 优先复制可解析的文件链接内容,失效链接仅警告不失败 - 添加相关测试用例验证符号链接处理逻辑
1 parent 8a0c200 commit 8f16e52

2 files changed

Lines changed: 95 additions & 1 deletion

File tree

ai/coding/claude/Sync-ClaudeConfig.ps1

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,67 @@ function Copy-DirectoryContents {
264264
}
265265

266266
foreach ($item in Get-ChildItem -LiteralPath $SourcePath -Force) {
267-
Copy-Item -LiteralPath $item.FullName -Destination (Join-Path $DestinationPath $item.Name) -Recurse -Force
267+
Copy-FileSystemEntry -Item $item -DestinationPath (Join-Path $DestinationPath $item.Name)
268+
}
269+
}
270+
271+
function Copy-FileSystemEntry {
272+
param(
273+
[Parameter(Mandatory)]
274+
[System.IO.FileSystemInfo]$Item,
275+
276+
[Parameter(Mandatory)]
277+
[string]$DestinationPath
278+
)
279+
280+
# 旧版 ~/.claude 整目录软链接里可能混入 runtime 生成的符号链接。
281+
# 单独分流可避免 Copy-Item -Recurse 在失效链接上中断整个迁移。
282+
if ($Item.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint)) {
283+
Copy-ReparsePointItem -Item $Item -DestinationPath $DestinationPath
284+
return
285+
}
286+
287+
if ($Item.PSIsContainer) {
288+
if (-not (Test-Path -LiteralPath $DestinationPath)) {
289+
New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null
290+
}
291+
292+
Copy-DirectoryContents -SourcePath $Item.FullName -DestinationPath $DestinationPath
293+
return
294+
}
295+
296+
Copy-Item -LiteralPath $Item.FullName -Destination $DestinationPath -Force
297+
}
298+
299+
function Copy-ReparsePointItem {
300+
param(
301+
[Parameter(Mandatory)]
302+
[System.IO.FileSystemInfo]$Item,
303+
304+
[Parameter(Mandatory)]
305+
[string]$DestinationPath
306+
)
307+
308+
$linkTarget = Get-LinkTargetText -Item $Item
309+
if ($Item.PSIsContainer) {
310+
# 目录链接可能指向外部目录或形成环;迁移旧 ~/.claude 时跳过它们,
311+
# 可以避免把整个备份流程拖进循环或无权限路径。
312+
Write-Warning "备份时跳过目录链接:$($Item.FullName) -> $linkTarget"
313+
return
314+
}
315+
316+
try {
317+
# 对文件链接优先复制当前可解析的内容,这样像 latest.log 之类的快捷入口
318+
# 若仍然有效,就会在备份中落成普通文件;若链接已失效则只告警不失败。
319+
Copy-Item -LiteralPath $Item.FullName -Destination $DestinationPath -Force -ErrorAction Stop
320+
}
321+
catch {
322+
if ([string]::IsNullOrWhiteSpace($linkTarget)) {
323+
Write-Warning "备份时跳过无法解析目标的链接:$($Item.FullName)"
324+
return
325+
}
326+
327+
Write-Warning "备份时跳过无法复制的链接:$($Item.FullName) -> $linkTarget$($_.Exception.Message)"
268328
}
269329
}
270330

tests/Sync-ClaudeConfig.Tests.ps1

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,40 @@ Describe 'Sync-ClaudeConfig' {
176176
Test-Path -LiteralPath (Join-Path $script:GlobalClaudePath 'settings.json') | Should -BeTrue
177177
}
178178

179+
It 'migrates legacy symlink directories even when runtime data contains broken symbolic links' {
180+
& $script:WriteTestJson -Path (Join-Path $script:SourceRoot 'config/settings.json') -Value @{
181+
env = @{
182+
API_TIMEOUT_MS = '3000'
183+
}
184+
}
185+
186+
$legacyManagedRoot = Join-Path $script:SourceRoot '.claude'
187+
$brokenLinkPath = Join-Path $legacyManagedRoot 'debug/latest'
188+
$missingDebugFile = Join-Path $legacyManagedRoot 'debug/missing-session.txt'
189+
190+
New-Item -ItemType Directory -Path (Join-Path $legacyManagedRoot 'debug') -Force | Out-Null
191+
New-Item -ItemType Directory -Path (Split-Path -Parent $script:GlobalClaudePath) -Force | Out-Null
192+
193+
try {
194+
# 这里故意创建一个失效符号链接,用来复现用户目录里已有运行态快捷入口时的迁移问题。
195+
New-Item -ItemType SymbolicLink -Path $brokenLinkPath -Target $missingDebugFile | Out-Null
196+
New-Item -ItemType SymbolicLink -Path $script:GlobalClaudePath -Target $legacyManagedRoot | Out-Null
197+
}
198+
catch {
199+
Set-ItResult -Skipped -Because "当前环境无法稳定创建符号链接:$($_.Exception.Message)"
200+
return
201+
}
202+
203+
Set-Content -Path (Join-Path $script:GlobalClaudePath 'history.jsonl') -Value 'runtime-history' -Encoding utf8NoBOM
204+
205+
{ & $script:SyncScriptPath -SourceRoot $script:SourceRoot -GlobalClaudePath $script:GlobalClaudePath -BackupRoot $script:BackupRoot } | Should -Not -Throw
206+
207+
$targetItem = Get-Item -LiteralPath $script:GlobalClaudePath -Force
208+
$targetItem.Attributes.HasFlag([System.IO.FileAttributes]::ReparsePoint) | Should -BeFalse
209+
Test-Path -LiteralPath (Join-Path $script:GlobalClaudePath 'history.jsonl') | Should -BeTrue
210+
Test-Path -LiteralPath (Join-Path $script:GlobalClaudePath 'settings.json') | Should -BeTrue
211+
}
212+
179213
It 'removes stale managed files without deleting unmanaged files in the same directory' {
180214
& $script:WriteTestJson -Path (Join-Path $script:SourceRoot 'config/settings.json') -Value @{
181215
env = @{

0 commit comments

Comments
 (0)