using System.Diagnostics; using System.Security.Cryptography; using System.Text; using NozCli.Repl; using Spectre.Console; namespace NozCli.Commands; public class EditCommand : ICommand { public string Name => "edit"; public string Description => "Open a note in your default editor, then offer to push"; public string Usage => "edit "; public async Task ExecuteAsync(string[] args, ReplContext ctx) { var slug = args.FirstOrDefault(); if (slug is null) { AnsiConsole.MarkupLine("[red]Usage: edit [/]"); return; } slug = ctx.ResolveSlug(slug); // 1. Fetch note from server NozCli.Core.Models.NoteContent note = default!; await AnsiConsole.Status().StartAsync("Fetching…", async _ => note = await ctx.Api.GetNoteAsync(slug)); // 2. Write to temp file var tmp = Path.Combine(Path.GetTempPath(), $"noz-{slug.Replace('/', '-')}.md"); await File.WriteAllTextAsync(tmp, note.Markdown); // 3. Open editor var editor = ResolveEditor(); AnsiConsole.MarkupLine($"[dim]Opening {editor} …[/]"); var proc = Process.Start(new ProcessStartInfo { FileName = editor, Arguments = $"\"{tmp}\"", UseShellExecute = !OperatingSystem.IsWindows(), }); proc?.WaitForExit(); // 4. Compare hashes var edited = await File.ReadAllTextAsync(tmp); var newHash = HashContent(edited); File.Delete(tmp); if (newHash == note.Hash) { AnsiConsole.MarkupLine("[dim]No changes.[/]"); return; } // 5. Ask to push AnsiConsole.MarkupLine("[green]Changes detected.[/]"); var push = AnsiConsole.Confirm("Push changes to server?"); if (!push) { AnsiConsole.MarkupLine("[dim]Discarded.[/]"); return; } await AnsiConsole.Status().StartAsync("Pushing…", async _ => await ctx.Api.PutNoteAsync(slug, edited)); AnsiConsole.MarkupLine($"[green]✓[/] {slug} updated"); } private static string ResolveEditor() { // Priority: $EDITOR → $VISUAL → VS Code → nano → vi var env = Environment.GetEnvironmentVariable("EDITOR") ?? Environment.GetEnvironmentVariable("VISUAL"); if (env is not null) return env; foreach (var candidate in new[] { "code", "nano", "vi", "notepad" }) if (IsOnPath(candidate)) return candidate; return "nano"; } private static bool IsOnPath(string name) { try { var result = Process.Start(new ProcessStartInfo { FileName = OperatingSystem.IsWindows() ? "where" : "which", Arguments = name, RedirectStandardOutput = true, UseShellExecute = false, }); result?.WaitForExit(); return result?.ExitCode == 0; } catch { return false; } } private static string HashContent(string content) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(content))).ToLower(); }