diff --git a/desktop/build/installer.nsh b/desktop/build/installer.nsh index 9d765a47..8d1a1699 100644 --- a/desktop/build/installer.nsh +++ b/desktop/build/installer.nsh @@ -198,7 +198,7 @@ FunctionEnd StrCpy $1 "Recovery helper failed without diagnostic output (exit code $0)" ${EndIf} StrCpy $R2 "$1" 360 - MessageBox MB_ICONSTOP|MB_OK "Claude Code Haha stopped setup before removing the old version. Reason: $R2$\r$\n$\r$\nClose the app and retry. If the reason mentions an elevated installer, launch setup normally instead of using Run as administrator.$\r$\n$\r$\nClaude Code Haha 已在删除旧版本前停止安装。原因:$R2$\r$\n$\r$\n请关闭旧程序后重试;如果原因提到安装器权限过高,请直接双击运行,不要使用“以管理员身份运行”。旧版本和原数据尚未删除。" + MessageBox MB_ICONSTOP|MB_OK "Claude Code Haha stopped setup before removing the old version. Reason: $R2$\r$\n$\r$\nClose the app and retry. If the reason mentions an elevated installer, launch setup normally instead of using Run as administrator.$\r$\n$\r$\nClaude Code Haha 已在删除旧版本前停止安装。原因:$R2$\r$\n$\r$\n请关闭旧程序后重试;如果原因提到安装器权限过高,请直接双击运行,不要使用“以管理员身份运行”。旧版本和原数据尚未删除。" /SD IDOK SetErrorLevel 20 Quit ${EndIf} diff --git a/desktop/build/recover-legacy-install-data.ps1 b/desktop/build/recover-legacy-install-data.ps1 index 31b37f2a..766baa76 100644 --- a/desktop/build/recover-legacy-install-data.ps1 +++ b/desktop/build/recover-legacy-install-data.ps1 @@ -636,6 +636,7 @@ function Write-AppModeAtomically { function Invoke-LegacyRecovery { param( [Parameter(Mandatory = $true)][string[]]$InstallDirs, + [string[]]$SharedInstallDirs = @(), [Parameter(Mandatory = $true)][string]$UserDataDir, [Parameter(Mandatory = $true)][string]$RecoveryRoot, [Parameter(Mandatory = $true)][string]$ProcessName, @@ -656,7 +657,11 @@ function Invoke-LegacyRecovery { -UserDataDir $UserDataDir ` -ActiveConfigDir $ActiveConfigDir ` -ActiveConfigManaged $ActiveConfigManaged - Assert-NoUndiscoveredLegacySources -InstallDirs $existingInstallDirs -ActiveSource $source + $potentialSharedInstallDirs = @(Get-PotentialInstallDirs -InstallDirs $SharedInstallDirs) + $existingSharedInstallDirs = @(Get-ExistingInstallDirs -InstallDirs $potentialSharedInstallDirs) + if ($existingSharedInstallDirs.Count -gt 0) { + Assert-NoUndiscoveredLegacySources -InstallDirs $existingSharedInstallDirs -ActiveSource $source + } if ([string]::IsNullOrWhiteSpace([string]$source)) { return $null } @@ -793,6 +798,19 @@ function Run-SelfTest { -ActiveConfigDir '' -InstallerIdentitySafety 'untrusted-elevated' -SkipProcessCheck Assert-SelfTest -Condition ($null -eq $elevatedDefaultResult) -Message 'elevated default-mode reinstall was blocked without legacy data' + $perUserInstall = Join-Path $testRoot 'per-user install' + $perUserExternalTarget = Join-Path $testRoot 'per-user external target' + $perUserAppPayload = Join-Path $perUserInstall 'resources\app payload' + New-Item -ItemType Directory -Path (Join-Path $perUserInstall 'resources') -Force | Out-Null + New-Item -ItemType Directory -Path $perUserExternalTarget -Force | Out-Null + New-Item -ItemType Junction -Path $perUserAppPayload -Target $perUserExternalTarget | Out-Null + $perUserResult = Invoke-LegacyRecovery ` + -InstallDirs @($perUserInstall) ` + -UserDataDir (Join-Path $testRoot 'per-user app data') ` + -RecoveryRoot (Join-Path $testRoot 'per-user recovery') -ProcessName $ProcessName ` + -ActiveConfigDir '' -SkipProcessCheck + Assert-SelfTest -Condition ($null -eq $perUserResult) -Message 'per-user default-mode reinstall scanned the packaged application tree' + $wrongIdentityInstall = Join-Path $testRoot 'registered shared install' $wrongIdentityData = Join-Path $wrongIdentityInstall 'custom data' New-Item -ItemType Directory -Path $wrongIdentityData -Force | Out-Null @@ -801,7 +819,7 @@ function Run-SelfTest { $wrongIdentityFailed = $false try { Invoke-LegacyRecovery ` - -InstallDirs @($wrongIdentityInstall) ` + -InstallDirs @($wrongIdentityInstall) -SharedInstallDirs @($wrongIdentityInstall) ` -UserDataDir (Join-Path $testRoot 'wrong identity app data') ` -RecoveryRoot (Join-Path $testRoot 'wrong identity recovery') -ProcessName $ProcessName ` -ActiveConfigDir '' -InstallerIdentitySafety 'untrusted-elevated' -SkipProcessCheck | Out-Null @@ -833,7 +851,8 @@ function Run-SelfTest { $sharedFailed = $false try { Invoke-LegacyRecovery ` - -InstallDirs @($sharedInstall) -UserDataDir (Join-Path $testRoot 'shared app data') ` + -InstallDirs @($sharedInstall) -SharedInstallDirs @($sharedInstall) ` + -UserDataDir (Join-Path $testRoot 'shared app data') ` -RecoveryRoot (Join-Path $testRoot 'shared recovery') -ProcessName $ProcessName ` -ActiveConfigDir $activeExternal -ActiveConfigManaged '' -SkipProcessCheck | Out-Null } catch { @@ -851,7 +870,8 @@ function Run-SelfTest { $otherUserFailed = $false try { Invoke-LegacyRecovery ` - -InstallDirs @($otherUserInstall) -UserDataDir $currentUserMode ` + -InstallDirs @($otherUserInstall) -SharedInstallDirs @($otherUserInstall) ` + -UserDataDir $currentUserMode ` -RecoveryRoot (Join-Path $testRoot 'other user recovery') -ProcessName $ProcessName ` -ActiveConfigDir '' -SkipProcessCheck | Out-Null } catch { @@ -938,7 +958,8 @@ function Run-SelfTest { $junctionFailed = $false try { Invoke-LegacyRecovery ` - -InstallDirs @($junctionInstall) -UserDataDir (Join-Path $testRoot 'junction app data') ` + -InstallDirs @($junctionInstall) -SharedInstallDirs @($junctionInstall) ` + -UserDataDir (Join-Path $testRoot 'junction app data') ` -RecoveryRoot (Join-Path $testRoot 'junction recovery') -ProcessName $ProcessName ` -ActiveConfigDir '' -SkipProcessCheck | Out-Null } catch { @@ -960,6 +981,7 @@ try { $result = Invoke-LegacyRecovery ` -InstallDirs @($PerUserInstallDir, $PerMachineInstallDir, $CandidateInstallDir) ` + -SharedInstallDirs @($PerMachineInstallDir) ` -UserDataDir $UserDataDir ` -RecoveryRoot $RecoveryRoot ` -ProcessName $ProcessName ` diff --git a/scripts/pr/release-workflow.test.ts b/scripts/pr/release-workflow.test.ts index 4350322d..e1d660f1 100644 --- a/scripts/pr/release-workflow.test.ts +++ b/scripts/pr/release-workflow.test.ts @@ -514,6 +514,7 @@ describe('release desktop workflow', () => { expect(installerHook).toContain('Var ccHahaPerMachineInstallLocation') expect(installerHook).toMatch(/!macro CcHahaRunLegacyRecovery[\s\S]*ReadRegStr \$ccHahaPerUserInstallLocation[\s\S]*\$ccHahaPerUserUninstallString == ""[\s\S]*No registered installation needs legacy data recovery[\s\S]*Call CcHahaRecoverLegacy/) expect(installerHook).toContain('SetErrorLevel 20') + expect(installerHook).toContain('/SD IDOK') expect(installerHook).toContain('Quit') expect(recoveryHelper).toContain('function Get-LegacyActiveSource') expect(recoveryHelper).toContain('function Get-PotentialInstallDirs') @@ -529,6 +530,8 @@ describe('release desktop workflow', () => { expect(recoveryHelper).toContain('Multiple distinct legacy data sources') expect(recoveryHelper).toContain('Active CLAUDE_CONFIG_DIR is managed outside Claude Code Haha') expect(recoveryHelper).toContain('Test-LexicalPathAtOrBelow') + expect(recoveryHelper).toContain('-SharedInstallDirs @($PerMachineInstallDir)') + expect(recoveryHelper).toContain('per-user default-mode reinstall scanned the packaged application tree') expect(recoveryHelper).toContain('untrusted-elevated') expect(recoveryHelper).toContain('External CLAUDE_CONFIG_DIR is active while install-contained legacy data still exists') expect(recoveryHelper).toMatch(/\$source = Get-UnsafeLegacySource[\s\S]*Assert-NoRunningApplication/)