7 Commits

Author SHA1 Message Date
Mika Kuns
fc9029de97 fix(installer): wait for prior service registration to clear before create
After `sc delete`, the service stays in "marked for deletion" state until
every open handle (services.msc, Task Manager Services tab, Event Viewer,
prior sc query process) is closed. The installer used to immediately call
`sc create` and hit a silent hang / confusing "specified service has been
marked for deletion" error.

Poll `sc query` for up to 30s after delete; if the service is still
registered past that, fail with actionable guidance (close the offending
console or reboot). Also translate exit 1072 from `sc create` into the
same human-readable hint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:54:36 +02:00
Mika Kuns
1c764dae3f fix(installer): publish framework-dependent single-file
All checks were successful
Release / release (push) Successful in 27s
Self-contained single-file bundle was crashing at startup with 0xc0000005
in the apphost bootstrap (COR_E_EXECUTIONENGINE), because the Linux Gitea
runner doesn't carry the Microsoft.WindowsDesktop.App runtime pack — the
resulting bundle was missing WPF runtime bits. Disabling compression alone
didn't resolve it.

Switch to framework-dependent single-file: target machines need the .NET 8
Desktop Runtime (x64) installed but the bundle is ~2 MB instead of ~140 MB
and starts reliably. Keep IncludeAllContentForSelfExtract=true so native
deps (e_sqlite3) extract to a temp dir on first run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:11:27 +02:00
Mika Kuns
cfec3297a4 fix(installer): disable single-file compression to prevent WPF startup AV
All checks were successful
Release / release (push) Successful in 30s
Published installer was crashing at launch with 0xc0000005 (access violation)
and exit code 0x80131506 (COR_E_EXECUTIONENGINE), faulting inside the exe's
own bundle-extractor bootstrap before any managed code ran. This is a known
interaction between WPF, single-file publish, and EnableCompressionInSingleFile.

Disable compression (exe grows ~30% but startup becomes reliable).
IncludeAllContentForSelfExtract stays true — WPF still needs on-disk
extraction for XAML resources.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:48:06 +02:00
Mika Kuns
6e1d64b489 Change sdk Version
All checks were successful
Release / release (push) Successful in 39s
2026-04-15 14:23:26 +02:00
Mika Kuns
f599f8d0af fix(installer,worker): service hosting, dark theme, uninstall polish
Some checks failed
Release / release (push) Failing after 0s
Worker:
- Wire UseWindowsService + Microsoft.Extensions.Hosting.WindowsServices so
  SCM's Service Control Protocol handshake succeeds. Previously the binary
  exited immediately under sc start, leaving the service registered but
  never running.

Installer:
- Pin SDK to .NET 9 (global.json) — SDK 10 dropped win-arm from its RID
  graph, breaking restore of the WPF project; .NET 9 keeps win-arm AND
  understands the .slnx solution format.
- Force SelfContained=true and default RID=win-x64 when PublishSingleFile
  is set, so Rider Publish and CLI produce the same bundle.
- Dark theme: set Background/Foreground explicitly on WizardWindow and
  SettingsWindow roots (WPF implicit styles don't cascade to derived
  Window types). Custom ComboBox template + ComboBoxItem style so
  dropdowns honour the dark palette instead of system defaults.
- Throttle download progress to one report per MB and overwrite the same
  UI line (\r prefix marker) instead of appending per chunk.
- Register ClaudeDo in HKLM\...\Uninstall so it appears in Apps & Features.
  Copy installer into InstallDir\uninstaller\ for the UninstallString, and
  schedule a cmd.exe trampoline to handle the self-delete case when
  Apps & Features launches the copy from inside the install dir.
- Treat sc.exe stop exit 1062 (ERROR_SERVICE_NOT_ACTIVE) as success.
- Delete the uninstall registry key during UninstallRunner.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:19:09 +02:00
Mika Kuns
9b928c6217 fix(installer): set EnableWindowsTargeting so Linux Gitea runners can publish
All checks were successful
Release / release (push) Successful in 40s
The release workflow runs on a Linux container; building net8.0-windows +
UseWPF=true requires this opt-in property since .NET 8. No-op on Windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 11:39:23 +02:00
c9e38aef88 Merge pull request 'feat/ui-improvements' (#1) from feat/ui-improvements into main
Some checks failed
Release / release (push) Failing after 19s
Reviewed-on: #1
2026-04-15 09:28:58 +00:00
16 changed files with 323 additions and 6 deletions

View File

@@ -67,7 +67,7 @@ jobs:
-c Release -r win-x64 --self-contained true \
/p:Version=$VERSION -o out/worker
- name: Publish ClaudeDo.Installer (win-x64, single-file)
- name: Publish ClaudeDo.Installer (win-x64, single-file, framework-dependent)
env:
WORK: ${{ steps.ws.outputs.dir }}
VERSION: ${{ steps.ver.outputs.version }}
@@ -75,8 +75,11 @@ jobs:
set -euo pipefail
export PATH="$DOTNET_ROOT:$PATH"
cd "$WORK/src"
# Framework-dependent — WPF runtime pack isn't distributed on Linux SDK;
# the previous self-contained bundle crashed at startup (apphost AV).
# Target machines need .NET 8 Desktop Runtime (x64).
dotnet publish src/ClaudeDo.Installer/ClaudeDo.Installer.csproj \
-c Release -r win-x64 --self-contained true \
-c Release -r win-x64 --self-contained false \
/p:Version=$VERSION /p:PublishSingleFile=true \
-o out/installer

6
global.json Normal file
View File

@@ -0,0 +1,6 @@
{
"sdk": {
"version": "8.0.418",
"rollForward": "latestFeature"
}
}

View File

@@ -111,6 +111,7 @@ public partial class App : Application
sc.AddSingleton<IInstallStep, InitDatabaseStep>();
sc.AddSingleton<IInstallStep, RegisterServiceStep>();
sc.AddSingleton<IInstallStep, CreateShortcutsStep>();
sc.AddSingleton<IInstallStep, WriteUninstallRegistryStep>();
sc.AddSingleton<WriteInstallManifestStep>();
sc.AddSingleton<IInstallStep>(sp => sp.GetRequiredService<WriteInstallManifestStep>());

View File

@@ -6,6 +6,8 @@
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Allow Linux Gitea runners to publish this WPF project for win-x64; no-op on Windows. -->
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<!-- Debug: asInvoker so Rider/VS can debug without elevation -->
@@ -19,10 +21,15 @@
</PropertyGroup>
<PropertyGroup Condition="'$(PublishSingleFile)' == 'true'">
<!-- Framework-dependent: the WPF runtime pack isn't distributed for cross-compile
on Linux CI, which made self-contained bundles crash on startup with AV in the
apphost. Target machines already have the .NET 8 Desktop Runtime. -->
<SelfContained>false</SelfContained>
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-x64</RuntimeIdentifier>
<PublishTrimmed>false</PublishTrimmed>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<EnableCompressionInSingleFile>false</EnableCompressionInSingleFile>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,6 +1,8 @@
using System.Diagnostics;
using System.IO;
using ClaudeDo.Data;
using ClaudeDo.Installer.Steps;
using Microsoft.Win32;
namespace ClaudeDo.Installer.Core;
@@ -36,6 +38,17 @@ public sealed class UninstallRunner
progress.Report("Unregistering service...");
await ProcessRunner.RunAsync("sc.exe", $"delete {StopServiceStep.ServiceName}", null, progress, ct);
// 3b) Remove Apps & Features registry entry (best-effort).
progress.Report("Removing Add/Remove Programs entry...");
try
{
Registry.LocalMachine.DeleteSubKeyTree(WriteUninstallRegistryStep.UninstallKeyPath, throwOnMissingSubKey: false);
}
catch (Exception ex)
{
progress.Report($"Warning: could not delete uninstall registry key: {ex.Message}");
}
// 4) Remove shortcuts (best-effort — a stuck .lnk must not block the rest).
progress.Report("Removing shortcuts...");
TryDeleteFile(Path.Combine(
@@ -63,6 +76,21 @@ public sealed class UninstallRunner
failures.Add($"app data ({appData}): {err}");
}
// 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
// the deletion after this process exits. Best-effort: if this fails the
// user is left with an empty <uninstaller> folder which is harmless.
var runningExe = Environment.ProcessPath;
if (runningExe is not null
&& IsInsideDirectory(runningExe, _context.InstallDirectory)
&& Directory.Exists(_context.InstallDirectory))
{
progress.Report("Scheduling final cleanup after exit...");
TryScheduleTrampolineDelete(_context.InstallDirectory);
// The trampoline will finish the job — clear the residual failure entry for the install dir.
failures.RemoveAll(f => f.StartsWith("install dir"));
}
if (failures.Count > 0)
{
return StepResult.Fail(
@@ -74,6 +102,37 @@ public sealed class UninstallRunner
return StepResult.Ok();
}
private static bool IsInsideDirectory(string filePath, string directory)
{
try
{
var full = Path.GetFullPath(filePath);
var dir = Path.GetFullPath(directory).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
return full.StartsWith(dir, StringComparison.OrdinalIgnoreCase);
}
catch { return false; }
}
private static void TryScheduleTrampolineDelete(string installDir)
{
try
{
var pid = Environment.ProcessId;
// Wait for this process to exit, then recursively remove the install dir.
// /B timeout avoids a visible window; ping as a portable sleep; rmdir /S /Q is silent.
var cmd = $"/C start \"\" /MIN cmd /C \"ping 127.0.0.1 -n 3 >nul & rmdir /S /Q \"\"{installDir}\"\"\"";
Process.Start(new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = cmd,
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
});
}
catch { /* best effort */ }
}
/// <summary>
/// Guards against catastrophic recursive-delete paths. The install dir must be
/// a non-root directory with a non-empty name (e.g. reject "", "C:\\", "/").

View File

@@ -54,6 +54,7 @@ public partial class InstallPageViewModel : ObservableObject, IInstallerPage
Steps.Add(new StepViewModel("Initialize Database"));
Steps.Add(new StepViewModel("Register Windows Service"));
Steps.Add(new StepViewModel("Create Shortcuts"));
Steps.Add(new StepViewModel("Register in Add/Remove Programs"));
Steps.Add(new StepViewModel("Write Install Manifest"));
}
return Task.CompletedTask;
@@ -82,7 +83,21 @@ public partial class InstallPageViewModel : ObservableObject, IInstallerPage
step.Status = p.Status;
if (p.Message is not null)
step.Messages.Add(p.Message);
{
// Messages starting with "\r" overwrite the previous line (live progress).
if (p.Message.StartsWith('\r'))
{
var line = p.Message[1..];
if (step.Messages.Count > 0 && step.Messages[^1].StartsWith(" "))
step.Messages[^1] = line;
else
step.Messages.Add(line);
}
else
{
step.Messages.Add(p.Message);
}
}
if (p.Status is StepStatus.Running && !step.IsExpanded)
step.IsExpanded = true;

View File

@@ -44,9 +44,18 @@ public sealed class DownloadAndExtractStep : IInstallStep
var zipPath = Path.Combine(scratchDir, zipAsset.Name);
var checksumPath = Path.Combine(scratchDir, "checksums.txt");
progress.Report($"Downloading {zipAsset.Name} ({zipAsset.Size / (1024 * 1024)} MB)...");
var totalMb = zipAsset.Size / (1024 * 1024);
progress.Report($"Downloading {zipAsset.Name} ({totalMb} MB)...");
long lastReportedMb = -1;
await _releases.DownloadAsync(zipAsset.BrowserDownloadUrl, zipPath,
new Progress<long>(b => progress.Report($" {b / (1024 * 1024)} MB downloaded")),
new Progress<long>(b =>
{
var mb = b / (1024 * 1024);
if (mb == lastReportedMb) return;
lastReportedMb = mb;
// Leading "\r" tells the UI to overwrite the previous line instead of appending.
progress.Report($"\r {mb} / {totalMb} MB downloaded");
}),
ct);
progress.Report("Downloading checksums...");

View File

@@ -23,6 +23,24 @@ public sealed class RegisterServiceStep : IInstallStep
progress.Report("Removing existing service registration (if any)...");
await RunSc($"delete {ServiceName}", ctx, progress, ct, ignoreErrors: true);
// Wait for the service to actually disappear from SCM. `sc delete` returns
// immediately but the service stays "marked for deletion" until every open
// handle (services.msc, Task Manager, a prior sc query process) is closed.
// Poll up to 30s — then fail with actionable guidance if it's still there.
progress.Report("Waiting for prior service registration to clear...");
for (var i = 0; i < 30; i++)
{
ct.ThrowIfCancellationRequested();
var (queryExit, _) = await RunSc($"query {ServiceName}", ctx, progress, ct, ignoreErrors: true);
if (queryExit != 0) break; // service no longer registered — good
if (i == 29)
return StepResult.Fail(
$"Service '{ServiceName}' is marked for deletion but hasn't cleared after 30s. " +
"Close any open Services console (services.msc), Task Manager Services tab, or " +
"Event Viewer showing the service, then retry. A reboot will also clear it.");
await Task.Delay(1000, ct);
}
// Create service
var startType = ctx.AutoStart ? "auto" : "demand";
var createArgs = $"create {ServiceName} binPath= \"{workerExe}\" start= {startType}";
@@ -35,6 +53,10 @@ public sealed class RegisterServiceStep : IInstallStep
progress.Report("Creating service...");
var (exitCode, output) = await RunSc(createArgs, ctx, progress, ct);
if (exitCode == 1072)
return StepResult.Fail(
$"Service '{ServiceName}' is still marked for deletion. " +
"Close services.msc / Task Manager / Event Viewer and retry, or reboot.");
if (exitCode != 0)
return StepResult.Fail($"sc.exe create failed (exit {exitCode}): {output}");

View File

@@ -27,6 +27,12 @@ public sealed class StopServiceStep : IInstallStep
}
var (stopExit, _) = await ProcessRunner.RunAsync("sc.exe", $"stop {ServiceName}", null, progress, ct);
// 1062 = ERROR_SERVICE_NOT_ACTIVE — registered but not running, treat as already stopped.
if (stopExit == 1062)
{
progress.Report("Service was registered but not running.");
return StepResult.Ok();
}
if (stopExit != 0)
return StepResult.Fail($"sc.exe stop failed with exit code {stopExit}");

View File

@@ -0,0 +1,81 @@
using System.IO;
using ClaudeDo.Installer.Core;
using Microsoft.Win32;
namespace ClaudeDo.Installer.Steps;
/// <summary>
/// Registers ClaudeDo under <c>HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo</c>
/// so it shows up in Windows "Apps &amp; Features" / "Programs and Features".
/// Also copies the running installer into the install directory so there is an exe
/// for UninstallString to reference after the temp-extracted single-file bundle is gone.
/// </summary>
public sealed class WriteUninstallRegistryStep : IInstallStep
{
internal const string UninstallKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\ClaudeDo";
public string Name => "Register in Add/Remove Programs";
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
{
var uninstallDir = Path.Combine(ctx.InstallDirectory, "uninstaller");
Directory.CreateDirectory(uninstallDir);
var targetExe = Path.Combine(uninstallDir, "ClaudeDo.Installer.exe");
// Copy the running installer so Apps & Features has a stable exe to launch —
// the single-file temp extract is gone once this process exits.
var sourceExe = Environment.ProcessPath
?? throw new InvalidOperationException("Cannot resolve running installer path.");
try
{
progress.Report("Copying uninstaller binary...");
File.Copy(sourceExe, targetExe, overwrite: true);
}
catch (Exception ex)
{
return StepResult.Fail($"Failed to copy uninstaller exe: {ex.Message}");
}
progress.Report("Writing Add/Remove Programs entry...");
try
{
using var key = Registry.LocalMachine.CreateSubKey(UninstallKeyPath, writable: true);
if (key is null)
return StepResult.Fail("Could not open uninstall registry key (permission denied?).");
key.SetValue("DisplayName", "ClaudeDo", RegistryValueKind.String);
key.SetValue("DisplayVersion", ctx.InstallerVersion ?? "0.0.0", RegistryValueKind.String);
key.SetValue("Publisher", "Mika Kuns", RegistryValueKind.String);
key.SetValue("InstallLocation", ctx.InstallDirectory, RegistryValueKind.String);
key.SetValue("UninstallString", $"\"{targetExe}\"", RegistryValueKind.String);
key.SetValue("DisplayIcon", targetExe, RegistryValueKind.String);
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
// Best-effort install size (KB) — scan install dir.
try
{
var sizeKb = (int)(DirectorySizeBytes(ctx.InstallDirectory) / 1024);
key.SetValue("EstimatedSize", sizeKb, RegistryValueKind.DWord);
}
catch { /* best-effort only */ }
}
catch (Exception ex)
{
return StepResult.Fail($"Failed to write uninstall registry: {ex.Message}");
}
await Task.CompletedTask;
return StepResult.Ok();
}
private static long DirectorySizeBytes(string path)
{
long total = 0;
foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories))
{
try { total += new FileInfo(file).Length; } catch { /* ignore */ }
}
return total;
}
}

View File

@@ -184,6 +184,34 @@
</Setter>
</Style>
<!-- ComboBox toggle button (dropdown arrow chrome) -->
<ControlTemplate x:Key="ComboBoxToggleButtonTemplate" TargetType="ToggleButton">
<Border x:Name="Bd"
Background="{StaticResource IslandBgBrush}"
BorderBrush="{StaticResource BorderSubtleBrush}"
BorderThickness="1"
CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<Path Grid.Column="1"
HorizontalAlignment="Center" VerticalAlignment="Center"
Fill="{StaticResource TextSecondaryBrush}"
Data="M 0 0 L 4 4 L 8 0 Z"/>
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource AccentBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
<!-- ComboBox -->
<Style TargetType="ComboBox">
<Setter Property="Background" Value="{StaticResource IslandBgBrush}"/>
@@ -191,6 +219,71 @@
<Setter Property="BorderBrush" Value="{StaticResource BorderSubtleBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="SnapsToDevicePixels" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<ToggleButton x:Name="ToggleButton"
Template="{StaticResource ComboBoxToggleButtonTemplate}"
Focusable="False"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
ClickMode="Press"/>
<ContentPresenter x:Name="ContentSite"
IsHitTestVisible="False"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center" HorizontalAlignment="Left"
TextElement.Foreground="{StaticResource TextPrimaryBrush}"/>
<Popup x:Name="Popup"
Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True" Focusable="False"
PopupAnimation="Slide">
<Border x:Name="DropDownBorder"
Background="{StaticResource IslandBgBrush}"
BorderBrush="{StaticResource BorderSubtleBrush}"
BorderThickness="1"
CornerRadius="4"
MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="{TemplateBinding MaxDropDownHeight}">
<ScrollViewer SnapsToDevicePixels="True">
<StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained"/>
</ScrollViewer>
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ComboBoxItem — dark dropdown rows -->
<Style TargetType="ComboBoxItem">
<Setter Property="Background" Value="{StaticResource IslandBgBrush}"/>
<Setter Property="Foreground" Value="{StaticResource TextPrimaryBrush}"/>
<Setter Property="Padding" Value="8,6"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource SelectionHoverBrush}"/>
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource SelectionBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- CheckBox -->

View File

@@ -6,6 +6,10 @@
Width="720" Height="520"
MinWidth="620" MinHeight="460"
WindowStartupLocation="CenterScreen"
Background="{StaticResource WindowBgBrush}"
Foreground="{StaticResource TextPrimaryBrush}"
FontFamily="Segoe UI"
FontSize="13"
d:DataContext="{d:DesignInstance views:SettingsViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

View File

@@ -6,6 +6,10 @@
Width="720" Height="520"
MinWidth="620" MinHeight="460"
WindowStartupLocation="CenterScreen"
Background="{StaticResource WindowBgBrush}"
Foreground="{StaticResource TextPrimaryBrush}"
FontFamily="Segoe UI"
FontSize="13"
d:DataContext="{d:DesignInstance views:WizardViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

View File

@@ -6,6 +6,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="8.0.1" />
</ItemGroup>
<PropertyGroup>

View File

@@ -10,6 +10,10 @@ var cfg = WorkerConfig.Load();
var builder = WebApplication.CreateBuilder(args);
// When launched by the Windows SCM, speak the Service Control Protocol so SCM
// doesn't think we crashed (~30s timeout). No-op when running interactively.
builder.Host.UseWindowsService(o => o.ServiceName = "ClaudeDoWorker");
// Initialize DB schema before the host starts accepting connections.
var dbFactory = new SqliteConnectionFactory(cfg.DbPath);
SchemaInitializer.Apply(dbFactory);

View File

@@ -7,6 +7,8 @@
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<!-- Allow Linux Gitea runners to build this Windows-targeted project; no-op on Windows. -->
<EnableWindowsTargeting>true</EnableWindowsTargeting>
</PropertyGroup>
<ItemGroup>