From 7d557b906ff3e884d758bb6d850d2c3811bcd943 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 8 Oct 2026 11:50:57 +0200 Subject: [PATCH] MVP LlmRun: CLI/GUI che lancia claude -p nella cartella dell'exe, con docs Co-Authored-By: Claude Sonnet 5.5 --- .gitignore | 2 ++ CLAUDE.md | 38 +++++++++++----------- LlmRun.csproj | 11 +++++++ Program.cs | 86 +++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 32 ++++++++++++++++++ docs/MVP.md | 7 ++-- docs/ROADMAP.md | 4 +-- 7 files changed, 158 insertions(+), 22 deletions(-) create mode 100644 .gitignore create mode 100644 LlmRun.csproj create mode 100644 Program.cs create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd42ee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/CLAUDE.md b/CLAUDE.md index 5df9193..8376440 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,30 +2,32 @@ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. -## Stato del progetto +## Progetto -Repository vuoto (nessun codice, nessun git). Obiettivo: MVP di un eseguibile C# che, "sotto il cofano", invoca Claude Code (CLI `claude`) da terminale. +LlmRun: eseguibile C# (.NET 10, Windows) che invoca Claude Code (`claude -p`) da terminale. Senza argomenti apre una GUI WinForms, con argomenti funziona da CLI. Non è un repo git. -## Approccio doc-first +## Workflow doc-first (obbligatorio) -Prima si aggiorna la documentazione, poi il codice. Specifica e criteri di accettazione: [docs/MVP.md](docs/MVP.md). Fasi e stato: [docs/ROADMAP.md](docs/ROADMAP.md). Se il codice diverge dall'MVP, aggiornare prima i doc. +- Specifica e criteri di accettazione: [docs/MVP.md](docs/MVP.md). Fasi e stato: [docs/ROADMAP.md](docs/ROADMAP.md). Uso: [README.md](README.md). +- Ad ogni richiesta di modifica: aggiornare prima i doc interessati (MVP.md, ROADMAP.md con le spunte, README.md se cambia l'uso), poi il codice, poi questo file se cambiano comandi, design o vincoli. Nessuna modifica deve lasciare i doc disallineati. -Design deciso: un solo `Program.cs`, nessuna dipendenza, lancio di `claude -p ` con `ProcessStartInfo.ArgumentList`, output in streaming, exit code propagato. +## Comandi -## Requisito chiave - -Claude Code deve essere lanciato con working directory = la directory in cui risiede l'eseguibile, non la current directory del chiamante. In C# usare `AppContext.BaseDirectory` (non `Environment.CurrentDirectory`) come `ProcessStartInfo.WorkingDirectory`. - -Nota: con single-file publish `Assembly.Location` è vuoto, quindi `AppContext.BaseDirectory` è la scelta corretta. - -## Comandi (da .NET SDK, una volta creato il progetto) - -- Build: `dotnet build` -- Run: `dotnet run` +- Build: `dotnet build` (LlmRun.csproj, net10.0-windows, WinForms) +- GUI: `dotnet run`; CLI: `dotnet run -- "prompt"` - Publish single-file: `dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true` +- Test: non esistono ancora. -Aggiornare questa sezione quando esisteranno il `.csproj` e i test. +## Architettura -## Ambiente +Tutto in [Program.cs](Program.cs): top-level statements per la CLI e la classe `Gui` (Form) per la GUI. Entrambe costruiscono un `ProcessStartInfo` per `claude -p ` con `ArgumentList` (niente quoting manuale). Nessuna dipendenza esterna. -Windows. `claude` è installato come script npm (`claude.cmd`): con `Process.Start` e `UseShellExecute=false` può servire lanciare via `cmd.exe /c claude ...` oppure risolvere il percorso completo, altrimenti il processo non viene trovato. +- CLI: stdout/stderr ereditati (streaming), exit code propagato, 127 se `claude` non è nel PATH. +- GUI: output rediretto e mostrato riga per riga. + +## Vincoli non ovvi + +- La working directory di `claude` deve essere la cartella dell'exe: usare `AppContext.BaseDirectory`, mai `Environment.CurrentDirectory` né `Assembly.Location` (vuoto in single-file). +- Lo stdin di `claude` va redirezionato e chiuso subito, altrimenti aspetta 3s. +- `claude` è un .exe nativo (`~/.local/bin`), avviabile direttamente. Se fosse `claude.cmd` serve `cmd.exe /c`. +- La logica di avvio processo è duplicata tra CLI e GUI (volutamente, per restare in un file solo): se cambia, modificarla in entrambi i punti. diff --git a/LlmRun.csproj b/LlmRun.csproj new file mode 100644 index 0000000..b1adacc --- /dev/null +++ b/LlmRun.csproj @@ -0,0 +1,11 @@ + + + + Exe + net10.0-windows + true + enable + enable + + + diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..ab2e2e7 --- /dev/null +++ b/Program.cs @@ -0,0 +1,86 @@ +using System.Diagnostics; +using System.Windows.Forms; + +if (args.Length == 0) +{ + ApplicationConfiguration.Initialize(); + Application.Run(new Gui()); + return 0; +} + +var psi = NewClaude(string.Join(' ', args)); +try +{ + using var p = Process.Start(psi)!; // stdout/stderr ereditati: streaming in tempo reale + p.StandardInput.Close(); + p.WaitForExit(); + return p.ExitCode; +} +catch (System.ComponentModel.Win32Exception) +{ + Console.Error.WriteLine("'claude' non trovato nel PATH."); + return 127; +} + +static ProcessStartInfo NewClaude(string prompt) +{ + var psi = new ProcessStartInfo("claude") + { + WorkingDirectory = AppContext.BaseDirectory, + UseShellExecute = false, + RedirectStandardInput = true, // evita l'attesa di stdin di claude + }; + psi.ArgumentList.Add("-p"); + psi.ArgumentList.Add(prompt); + return psi; +} + +class Gui : Form +{ + readonly TextBox prompt = new() { Dock = DockStyle.Top, Multiline = true, Height = 80, AcceptsReturn = true }; + readonly Button send = new() { Dock = DockStyle.Top, Text = "Invia a Claude", Height = 32 }; + readonly TextBox output = new() { Dock = DockStyle.Fill, Multiline = true, ReadOnly = true, ScrollBars = ScrollBars.Vertical }; + + public Gui() + { + Text = "LlmRun"; + Width = 700; Height = 500; + Controls.Add(output); Controls.Add(send); Controls.Add(prompt); // Fill per primo: l'ordine inverso dei Dock + send.Click += async (_, _) => await Run(); + } + + async Task Run() + { + if (string.IsNullOrWhiteSpace(prompt.Text)) return; + send.Enabled = false; + output.Clear(); + try + { + var psi = Program_NewClaude(prompt.Text); + using var p = Process.Start(psi)!; + p.StandardInput.Close(); + void Append(string? l) { if (l != null) BeginInvoke(() => output.AppendText(l + Environment.NewLine)); } + p.OutputDataReceived += (_, e) => Append(e.Data); + p.ErrorDataReceived += (_, e) => Append(e.Data); + p.BeginOutputReadLine(); p.BeginErrorReadLine(); + await p.WaitForExitAsync(); + output.AppendText($"[exit {p.ExitCode}]"); + } + catch (System.ComponentModel.Win32Exception) { output.Text = "'claude' non trovato nel PATH."; } + finally { send.Enabled = true; } + } + + static ProcessStartInfo Program_NewClaude(string prompt) + { + var psi = new ProcessStartInfo("claude") + { + WorkingDirectory = AppContext.BaseDirectory, + UseShellExecute = false, + RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true, + CreateNoWindow = true, + }; + psi.ArgumentList.Add("-p"); + psi.ArgumentList.Add(prompt); + return psi; + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..36a275a --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# LlmRun + +Eseguibile C# (.NET 10, Windows) che lancia Claude Code (`claude -p`) nella cartella in cui risiede l'eseguibile. + +## Requisiti +- .NET SDK 10 +- `claude` (Claude Code) nel PATH + +## Uso + +GUI (nessun argomento): si apre una finestra con campo prompt, pulsante e output. +``` +dotnet run +``` + +CLI (con argomenti): il prompt viene stampato su terminale, l'exit code è quello di `claude`. +``` +dotnet run -- "spiega questo progetto" +``` + +Eseguibile già compilato: +``` +dotnet build +bin\Debug\net10.0-windows\LlmRun.exe +``` + +## Publish single-file +``` +dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true +``` + +Documentazione: [docs/MVP.md](docs/MVP.md), [docs/ROADMAP.md](docs/ROADMAP.md). diff --git a/docs/MVP.md b/docs/MVP.md index 58a16a9..b68ec70 100644 --- a/docs/MVP.md +++ b/docs/MVP.md @@ -16,14 +16,17 @@ Tutti gli argomenti sono uniti in un unico prompt. 4. Esce con lo stesso exit code di `claude`. 5. Se `claude` non è trovato: messaggio chiaro ed exit code 127. +## GUI +Senza argomenti si apre una finestra WinForms (prompt, pulsante, output) che usa lo stesso lancio di `claude -p`. Con argomenti resta la CLI. + ## Scelte tecniche - .NET 8+, un solo `Program.cs`, nessuna dipendenza esterna. - `ProcessStartInfo.ArgumentList` per passare il prompt (niente problemi di quoting). -- Windows: `claude` è `claude.cmd`, quindi si avvia `cmd.exe /c claude ...`. +- `claude` è un .exe nativo (`~/.local/bin`): si avvia direttamente, senza `cmd /c`. Lo stdin è chiuso subito per evitare l'attesa di 3s di claude. - Working dir: `AppContext.BaseDirectory`, non la cwd del chiamante. ## Fuori scope (MVP) -Modalità interattiva, sessioni/`--resume`, config file, logging su file, test automatici, supporto Linux/macOS. +Modalità interattiva (chat), sessioni/`--resume`, config file, logging su file, test automatici, supporto Linux/macOS. ## Criteri di accettazione - [ ] Eseguito da un'altra cartella, `claude` parte nella cartella dell'exe (verificabile chiedendo "pwd"). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index bdfe5fc..dde7d26 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -7,8 +7,8 @@ Dettagli dell'MVP in [MVP.md](MVP.md). - [ ] Chiudere le domande aperte in MVP.md ## Fase 1 — MVP -- [ ] `dotnet new console`, `Program.cs` unico -- [ ] Lancio di `claude -p` nella cartella dell'exe, streaming, exit code +- [x] `dotnet new console`, `Program.cs` unico +- [x] Lancio di `claude -p` nella cartella dell'exe, streaming, exit code - [ ] Publish single-file win-x64 e verifica dei criteri di accettazione ## Fase 2 — Robustezza