diff --git a/ClaudeDo.slnx b/ClaudeDo.slnx new file mode 100644 index 0000000..e680914 --- /dev/null +++ b/ClaudeDo.slnx @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/schema/schema.sql b/schema/schema.sql new file mode 100644 index 0000000..83a09e2 --- /dev/null +++ b/schema/schema.sql @@ -0,0 +1,62 @@ +-- ClaudeDo SQLite schema (single source of truth, 3NF) +-- Applied by Worker on first startup. WAL mode is set via PRAGMA after open. + +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS lists ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + created_at TIMESTAMP NOT NULL, + working_dir TEXT NULL, + default_commit_type TEXT NOT NULL DEFAULT 'chore' +); + +CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE, + title TEXT NOT NULL, + description TEXT NULL, + status TEXT NOT NULL CHECK (status IN ('manual','queued','running','done','failed')), + scheduled_for TIMESTAMP NULL, + result TEXT NULL, + log_path TEXT NULL, + created_at TIMESTAMP NOT NULL, + started_at TIMESTAMP NULL, + finished_at TIMESTAMP NULL, + commit_type TEXT NOT NULL DEFAULT 'chore' +); + +CREATE INDEX IF NOT EXISTS idx_tasks_list_id ON tasks(list_id); +CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status); + +CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS list_tags ( + list_id TEXT NOT NULL REFERENCES lists(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (list_id, tag_id) +); + +CREATE TABLE IF NOT EXISTS task_tags ( + task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE, + tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, + PRIMARY KEY (task_id, tag_id) +); + +CREATE TABLE IF NOT EXISTS worktrees ( + task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE, + path TEXT NOT NULL, + branch_name TEXT NOT NULL, + base_commit TEXT NOT NULL, + head_commit TEXT NULL, + diff_stat TEXT NULL, + state TEXT NOT NULL DEFAULT 'active' CHECK (state IN ('active','merged','discarded','kept')), + created_at TIMESTAMP NOT NULL +); + +-- Seed: minimal tag set (ignored if already present) +INSERT OR IGNORE INTO tags (name) VALUES ('agent'); +INSERT OR IGNORE INTO tags (name) VALUES ('manual'); diff --git a/src/ClaudeDo.App/App.axaml b/src/ClaudeDo.App/App.axaml new file mode 100644 index 0000000..c2f87da --- /dev/null +++ b/src/ClaudeDo.App/App.axaml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/ClaudeDo.App/App.axaml.cs b/src/ClaudeDo.App/App.axaml.cs new file mode 100644 index 0000000..c655858 --- /dev/null +++ b/src/ClaudeDo.App/App.axaml.cs @@ -0,0 +1,31 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using System.Linq; +using Avalonia.Markup.Xaml; +using ClaudeDo.Ui.ViewModels; +using ClaudeDo.Ui.Views; + +namespace ClaudeDo.App; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow + { + DataContext = new MainWindowViewModel(), + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/src/ClaudeDo.App/Assets/avalonia-logo.ico b/src/ClaudeDo.App/Assets/avalonia-logo.ico new file mode 100644 index 0000000..f7da8bb Binary files /dev/null and b/src/ClaudeDo.App/Assets/avalonia-logo.ico differ diff --git a/src/ClaudeDo.App/ClaudeDo.App.csproj b/src/ClaudeDo.App/ClaudeDo.App.csproj new file mode 100644 index 0000000..852bd5f --- /dev/null +++ b/src/ClaudeDo.App/ClaudeDo.App.csproj @@ -0,0 +1,30 @@ + + + WinExe + net8.0 + enable + app.manifest + true + + + + + + + + + + + + + + None + All + + + + + + + + diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs new file mode 100644 index 0000000..039b475 --- /dev/null +++ b/src/ClaudeDo.App/Program.cs @@ -0,0 +1,24 @@ +using Avalonia; +using System; + +namespace ClaudeDo.App; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() +#if DEBUG + .WithDeveloperTools() +#endif + .WithInterFont() + .LogToTrace(); +} diff --git a/src/ClaudeDo.App/ViewLocator.cs b/src/ClaudeDo.App/ViewLocator.cs new file mode 100644 index 0000000..86eee3c --- /dev/null +++ b/src/ClaudeDo.App/ViewLocator.cs @@ -0,0 +1,37 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using ClaudeDo.Ui.ViewModels; + +namespace ClaudeDo.App; + +/// +/// Given a view model, returns the corresponding view if possible. +/// +[RequiresUnreferencedCode( + "Default implementation of ViewLocator involves reflection which may be trimmed away.", + Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")] +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? param) + { + if (param is null) + return null; + + var name = param.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = typeof(ViewModelBase).Assembly.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} diff --git a/src/ClaudeDo.App/app.manifest b/src/ClaudeDo.App/app.manifest new file mode 100644 index 0000000..b80bdb3 --- /dev/null +++ b/src/ClaudeDo.App/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/src/ClaudeDo.Data/ClaudeDo.Data.csproj b/src/ClaudeDo.Data/ClaudeDo.Data.csproj new file mode 100644 index 0000000..624930a --- /dev/null +++ b/src/ClaudeDo.Data/ClaudeDo.Data.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj new file mode 100644 index 0000000..fae1598 --- /dev/null +++ b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + net8.0 + enable + enable + true + + + diff --git a/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs b/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..f753981 --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,6 @@ +namespace ClaudeDo.Ui.ViewModels; + +public partial class MainWindowViewModel : ViewModelBase +{ + public string Greeting { get; } = "Welcome to Avalonia!"; +} diff --git a/src/ClaudeDo.Ui/ViewModels/ViewModelBase.cs b/src/ClaudeDo.Ui/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..32e48dc --- /dev/null +++ b/src/ClaudeDo.Ui/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace ClaudeDo.Ui.ViewModels; + +public abstract class ViewModelBase : ObservableObject +{ +} diff --git a/src/ClaudeDo.Ui/Views/MainWindow.axaml b/src/ClaudeDo.Ui/Views/MainWindow.axaml new file mode 100644 index 0000000..e0d243f --- /dev/null +++ b/src/ClaudeDo.Ui/Views/MainWindow.axaml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/src/ClaudeDo.Ui/Views/MainWindow.axaml.cs b/src/ClaudeDo.Ui/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..6a1e516 --- /dev/null +++ b/src/ClaudeDo.Ui/Views/MainWindow.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace ClaudeDo.Ui.Views; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj b/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj new file mode 100644 index 0000000..4cf11ce --- /dev/null +++ b/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + net8.0 + enable + enable + + + diff --git a/src/ClaudeDo.Worker/Program.cs b/src/ClaudeDo.Worker/Program.cs new file mode 100644 index 0000000..1760df1 --- /dev/null +++ b/src/ClaudeDo.Worker/Program.cs @@ -0,0 +1,6 @@ +var builder = WebApplication.CreateBuilder(args); +var app = builder.Build(); + +app.MapGet("/", () => "Hello World!"); + +app.Run(); diff --git a/src/ClaudeDo.Worker/Properties/launchSettings.json b/src/ClaudeDo.Worker/Properties/launchSettings.json new file mode 100644 index 0000000..446b5e4 --- /dev/null +++ b/src/ClaudeDo.Worker/Properties/launchSettings.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:25359", + "sslPort": 44337 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:5095", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7295;http://localhost:5095", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/ClaudeDo.Worker/appsettings.Development.json b/src/ClaudeDo.Worker/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/ClaudeDo.Worker/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/ClaudeDo.Worker/appsettings.json b/src/ClaudeDo.Worker/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/ClaudeDo.Worker/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj b/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj new file mode 100644 index 0000000..21410a0 --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj @@ -0,0 +1,28 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + +