feat(data,worker): add db schema init and signalr hub skeleton
Data layer: Paths helper with ~/%USERPROFILE% expansion, SqliteConnectionFactory (WAL + foreign keys), SchemaInitializer that applies the embedded schema.sql, and POCO entities for lists/tasks/tags/worktrees. Worker: WorkerConfig loader (~/.todo-app/worker.config.json with defaults), WorkerHub exposing Ping(), and Program.cs wiring Kestrel to 127.0.0.1:<port>, SignalR at /hub, schema applied on startup. Pins Microsoft.Data.Sqlite and Microsoft.Extensions.Hosting to 8.x for net8.0 compatibility. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
||||
70
src/ClaudeDo.Worker/Config/WorkerConfig.cs
Normal file
70
src/ClaudeDo.Worker/Config/WorkerConfig.cs
Normal file
@@ -0,0 +1,70 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using ClaudeDo.Data;
|
||||
|
||||
namespace ClaudeDo.Worker.Config;
|
||||
|
||||
public sealed class WorkerConfig
|
||||
{
|
||||
[JsonPropertyName("db_path")]
|
||||
public string DbPath { get; set; } = "~/.todo-app/todo.db";
|
||||
|
||||
[JsonPropertyName("sandbox_root")]
|
||||
public string SandboxRoot { get; set; } = "~/.todo-app/sandbox";
|
||||
|
||||
[JsonPropertyName("log_root")]
|
||||
public string LogRoot { get; set; } = "~/.todo-app/logs";
|
||||
|
||||
/// <summary>"sibling" → place worktrees next to the target repo; "central" → under <see cref="CentralWorktreeRoot"/>.</summary>
|
||||
[JsonPropertyName("worktree_root_strategy")]
|
||||
public string WorktreeRootStrategy { get; set; } = "sibling";
|
||||
|
||||
[JsonPropertyName("central_worktree_root")]
|
||||
public string CentralWorktreeRoot { get; set; } = "~/.todo-app/worktrees";
|
||||
|
||||
[JsonPropertyName("queue_backstop_interval_ms")]
|
||||
public int QueueBackstopIntervalMs { get; set; } = 30_000;
|
||||
|
||||
[JsonPropertyName("signalr_port")]
|
||||
public int SignalRPort { get; set; } = 47_821;
|
||||
|
||||
[JsonPropertyName("claude_bin")]
|
||||
public string ClaudeBin { get; set; } = "claude";
|
||||
|
||||
public static string DefaultConfigPath =>
|
||||
Path.Combine(Paths.AppDataRoot(), "worker.config.json");
|
||||
|
||||
/// <summary>
|
||||
/// Loads the config from <paramref name="path"/> (defaults to <see cref="DefaultConfigPath"/>).
|
||||
/// Missing file → returns defaults. Resolves all path-typed fields to absolute paths.
|
||||
/// </summary>
|
||||
public static WorkerConfig Load(string? path = null)
|
||||
{
|
||||
path ??= DefaultConfigPath;
|
||||
|
||||
WorkerConfig cfg;
|
||||
if (File.Exists(path))
|
||||
{
|
||||
var json = File.ReadAllText(path);
|
||||
cfg = JsonSerializer.Deserialize<WorkerConfig>(json, JsonOpts)
|
||||
?? throw new InvalidOperationException($"Failed to parse {path}");
|
||||
}
|
||||
else
|
||||
{
|
||||
cfg = new WorkerConfig();
|
||||
}
|
||||
|
||||
cfg.DbPath = Paths.Expand(cfg.DbPath);
|
||||
cfg.SandboxRoot = Paths.Expand(cfg.SandboxRoot);
|
||||
cfg.LogRoot = Paths.Expand(cfg.LogRoot);
|
||||
cfg.CentralWorktreeRoot = Paths.Expand(cfg.CentralWorktreeRoot);
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
ReadCommentHandling = JsonCommentHandling.Skip,
|
||||
AllowTrailingCommas = true,
|
||||
};
|
||||
}
|
||||
16
src/ClaudeDo.Worker/Hub/WorkerHub.cs
Normal file
16
src/ClaudeDo.Worker/Hub/WorkerHub.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace ClaudeDo.Worker.Hub;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR hub the UI connects to. Only <see cref="Ping"/> is implemented at this stage;
|
||||
/// RunNow/CancelTask/WakeQueue/GetActive land here once QueueService exists.
|
||||
/// </summary>
|
||||
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
{
|
||||
private static readonly string Version =
|
||||
Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0";
|
||||
|
||||
public string Ping() => $"pong v{Version}";
|
||||
}
|
||||
@@ -1,6 +1,27 @@
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Worker.Config;
|
||||
using ClaudeDo.Worker.Hub;
|
||||
|
||||
var cfg = WorkerConfig.Load();
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Initialize DB schema before the host starts accepting connections.
|
||||
var dbFactory = new SqliteConnectionFactory(cfg.DbPath);
|
||||
SchemaInitializer.Apply(dbFactory);
|
||||
|
||||
builder.Services.AddSingleton(cfg);
|
||||
builder.Services.AddSingleton(dbFactory);
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
// Loopback-only bind. Firewall is irrelevant for 127.0.0.1.
|
||||
builder.WebHost.UseUrls($"http://127.0.0.1:{cfg.SignalRPort}");
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/", () => "Hello World!");
|
||||
app.MapHub<WorkerHub>("/hub");
|
||||
|
||||
app.Logger.LogInformation("ClaudeDo.Worker listening on http://127.0.0.1:{Port} (db: {Db})",
|
||||
cfg.SignalRPort, cfg.DbPath);
|
||||
|
||||
app.Run();
|
||||
|
||||
Reference in New Issue
Block a user