fix(installer): make user-data deletion on uninstall opt-in

Add bool removeAppData parameter (default false) to UninstallRunner.RunAsync,
gate ~/.todo-app deletion on it, surface a checkbox in SettingsWindow, and
update the confirmation message to reflect whether data will be removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-17 14:29:35 +02:00
parent 7d48f34b15
commit 5f3d41e1f6
3 changed files with 27 additions and 13 deletions

View File

@@ -17,7 +17,7 @@ public sealed class UninstallRunner
_stopService = stopService; _stopService = stopService;
} }
public async Task<StepResult> RunAsync(IProgress<string> progress, CancellationToken ct) public async Task<StepResult> RunAsync(bool removeAppData, IProgress<string> progress, CancellationToken ct)
{ {
// 1) Validate install dir up front — refuse obviously unsafe paths. // 1) Validate install dir up front — refuse obviously unsafe paths.
// Prevents Directory.Delete(recursive:true) from wiping C:\ or C:\Program Files\. // Prevents Directory.Delete(recursive:true) from wiping C:\ or C:\Program Files\.
@@ -67,7 +67,9 @@ public sealed class UninstallRunner
failures.Add($"install dir ({_context.InstallDirectory}): {err}"); failures.Add($"install dir ({_context.InstallDirectory}): {err}");
} }
// 6) Delete ~/.todo-app (config + DB + logs) — user opted into full removal. // 6) Delete ~/.todo-app (config + DB + logs) — only if user opted in.
if (removeAppData)
{
var appData = Paths.AppDataRoot(); var appData = Paths.AppDataRoot();
if (Directory.Exists(appData)) if (Directory.Exists(appData))
{ {
@@ -75,6 +77,7 @@ public sealed class UninstallRunner
if (!TryDeleteDir(appData, out var err)) if (!TryDeleteDir(appData, out var err))
failures.Add($"app data ({appData}): {err}"); failures.Add($"app data ({appData}): {err}");
} }
}
// 7) If we were launched from inside the install dir (Apps & Features case), // 7) If we were launched from inside the install dir (Apps & Features case),
// our own exe is still locked — schedule a cmd.exe trampoline to finish // our own exe is still locked — schedule a cmd.exe trampoline to finish

View File

@@ -29,6 +29,9 @@ public partial class SettingsViewModel : ObservableObject
[ObservableProperty] [ObservableProperty]
private string _versionLabel = ""; private string _versionLabel = "";
[ObservableProperty]
private bool _removeAppData;
public SettingsViewModel( public SettingsViewModel(
PageResolver resolver, PageResolver resolver,
InstallContext context, InstallContext context,
@@ -133,8 +136,12 @@ public partial class SettingsViewModel : ObservableObject
[RelayCommand] [RelayCommand]
private async Task Uninstall() private async Task Uninstall()
{ {
var dataNote = RemoveAppData
? "This will remove ClaudeDo AND delete all of your tasks, configuration, and database.\n\nContinue?"
: "This will remove ClaudeDo. Your tasks, configuration, and database in ~/.todo-app will be kept.\n\nContinue?";
var confirm = MessageBox.Show( var confirm = MessageBox.Show(
"This will remove ClaudeDo AND delete all of your tasks, configuration, and database.\n\nContinue?", dataNote,
"Uninstall ClaudeDo", "Uninstall ClaudeDo",
MessageBoxButton.YesNo, MessageBoxButton.YesNo,
MessageBoxImage.Warning); MessageBoxImage.Warning);
@@ -142,7 +149,7 @@ public partial class SettingsViewModel : ObservableObject
if (confirm != MessageBoxResult.Yes) return; if (confirm != MessageBoxResult.Yes) return;
var progress = new Progress<string>(msg => StatusMessage = msg); var progress = new Progress<string>(msg => StatusMessage = msg);
var r = await _uninstallRunner.RunAsync(progress, CancellationToken.None); var r = await _uninstallRunner.RunAsync(RemoveAppData, progress, CancellationToken.None);
if (!r.Success) if (!r.Success)
{ {

View File

@@ -69,6 +69,7 @@
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Status message / version label --> <!-- Status message / version label -->
@@ -88,14 +89,17 @@
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
<Button Grid.Column="1" Content="Uninstall" Margin="0,0,8,0" <CheckBox Grid.Column="1" IsChecked="{Binding RemoveAppData}"
Content="Remove user data (tasks, logs, configs in ~/.todo-app)"
Margin="0,0,12,0" VerticalAlignment="Center"/>
<Button Grid.Column="2" Content="Uninstall" Margin="0,0,8,0"
Command="{Binding UninstallCommand}"/> Command="{Binding UninstallCommand}"/>
<Button Grid.Column="2" Content="Repair" Margin="0,0,8,0" <Button Grid.Column="3" Content="Repair" Margin="0,0,8,0"
Command="{Binding RepairCommand}"/> Command="{Binding RepairCommand}"/>
<Button Grid.Column="3" Content="Save" Margin="0,0,8,0" <Button Grid.Column="4" Content="Save" Margin="0,0,8,0"
Command="{Binding SaveCommand}" Command="{Binding SaveCommand}"
Style="{StaticResource AccentButton}"/> Style="{StaticResource AccentButton}"/>
<Button Grid.Column="4" Content="Close" <Button Grid.Column="5" Content="Close"
Command="{Binding CloseCommand}"/> Command="{Binding CloseCommand}"/>
</Grid> </Grid>
</Border> </Border>