noz-cli/NozCli/Commands/NewCommand.cs
kryptomrx ae8a961cfd feat(commands): Dual-editor (nano/code), new note, parallel sync pull
- edit: terminal editor (nano/vim) by default, -c flag opens VS Code/Zed
- edit/new: ResolveTerminal() + ResolveCode() auto-detect editors on PATH
- new: create note from template, opens editor, confirms before push
- sync: pull notes from server with Parallel.ForEachAsync (8 concurrent)
- sync: hash diff - only download notes changed since last sync
- sync --dry-run: preview without writing, --all to force full download
- push: hash diff against server cache, Progress bar, --all flag
2026-05-16 23:20:59 +02:00

90 lines
3 KiB
C#

using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using NozCli.Core.Models;
using NozCli.Repl;
using Spectre.Console;
namespace NozCli.Commands;
public class NewCommand : ICommand
{
public string Name => "new";
public string Description => "Create a new note and open it in nano (or -c for VS Code)";
public string Usage => "new [-c] <slug-or-name>";
public async Task ExecuteAsync(string[] args, ReplContext ctx, CancellationToken ct = default)
{
var useCode = args.Contains("-c") || args.Contains("--code");
var nameArg = args.FirstOrDefault(a => !a.StartsWith('-'));
if (nameArg is null) { AnsiConsole.MarkupLine("[red]Usage: new [-c] <slug-or-name>[/]"); return; }
var slug = ctx.ResolveNewSlug(nameArg);
if (ctx.NoteCache.Any(n => n.Slug == slug))
{
AnsiConsole.MarkupLine($"[yellow]Note already exists:[/] {slug} [dim](use 'edit' to modify it)[/]");
return;
}
var topic = ctx.CurrentTopic ?? (slug.Contains('/') ? slug.Split('/')[0] : slug);
var title = ToTitle(slug.Split('/').Last());
var lang = "de";
var template = $"""
---
title: "{title}"
description: ""
topic: {topic}
status: seed
lang: {lang}
---
# {title}
""";
var tmp = Path.Combine(Path.GetTempPath(), $"noz-new-{slug.Replace('/', '-')}.md");
await File.WriteAllTextAsync(tmp, template);
var (editor, editorArgs) = useCode
? EditCommand.ResolveCode(tmp, ctx.Config.CodeEditor)
: EditCommand.ResolveTerminal(tmp, ctx.Config.Editor);
AnsiConsole.MarkupLine($"[dim]Opening {editor} …[/]");
var proc = Process.Start(new ProcessStartInfo
{
FileName = editor,
Arguments = editorArgs,
UseShellExecute = false,
});
proc?.WaitForExit();
var markdown = await File.ReadAllTextAsync(tmp);
File.Delete(tmp);
if (markdown.Trim() == template.Trim())
{
AnsiConsole.MarkupLine("[dim]No changes — note not created.[/]");
return;
}
var push = AnsiConsole.Confirm($"Push [cyan]{slug}[/] to server?");
if (!push) { AnsiConsole.MarkupLine("[dim]Discarded.[/]"); return; }
await AnsiConsole.Status().StartAsync("Creating…", async _ =>
{
await ctx.Api.PutNoteAsync(slug, markdown, ct);
ctx.NoteCache = await ctx.Api.GetNotesAsync(ct);
});
AnsiConsole.MarkupLine($"[green]✓[/] {slug} created");
}
private static string ToTitle(string slug) =>
string.Join(' ', slug.Split('-').Select(w =>
w.Length > 0 ? char.ToUpper(w[0]) + w[1..] : w));
private static string Sha256(string text) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(text))).ToLower();
}