noz-cli/NozCli/Commands/MvCommand.cs
kryptomrx 5a26de8a11 feat: mv command with atomic note renaming and link rewriting
NoteRenamer fetches inbound graph edges, creates the note under the new
slug (patching the topic frontmatter field if the prefix changes),
rewrites all wikilinks and markdown links in linking notes, then deletes
the old slug. Tests cover wikilink/markdown-link replacement and topic
patching.
2026-05-17 00:27:53 +02:00

88 lines
2.7 KiB
C#

using NozCli.Core.Client;
using NozCli.Core.Security;
using NozCli.Repl;
using Spectre.Console;
namespace NozCli.Commands;
public class MvCommand : ICommand
{
public string Name => "mv";
public string Description => "Rename a note and update all inbound links";
public string Usage => "mv <old-name> <new-name>";
public async Task ExecuteAsync(string[] args, ReplContext ctx, CancellationToken ct = default)
{
if (args.Length < 2)
{
AnsiConsole.MarkupLine("[red]Usage:[/] mv <old-name> <new-name>");
return;
}
// Resolve short names to full slugs
var oldSlug = ctx.ResolveSlug(args[0]);
var newSlug = ctx.ResolveNewSlug(args[1]);
if (!SlugGuard.IsValid(oldSlug) || !SlugGuard.IsValid(newSlug))
{
AnsiConsole.MarkupLine("[red]Ungültiger Slug.[/] Nur Kleinbuchstaben, Ziffern und Bindestriche erlaubt.");
return;
}
if (oldSlug == newSlug)
{
AnsiConsole.MarkupLine("[yellow]Quelle und Ziel sind identisch — nichts zu tun.[/]");
return;
}
if (!ctx.NoteCache.Any(n => n.Slug == oldSlug))
{
AnsiConsole.MarkupLine($"[red]Notiz nicht gefunden:[/] {oldSlug}");
return;
}
if (ctx.NoteCache.Any(n => n.Slug == newSlug))
{
AnsiConsole.MarkupLine($"[red]Ziel existiert bereits:[/] {newSlug}");
return;
}
AnsiConsole.MarkupLine($"[dim]Umbenennen:[/] [cyan]{oldSlug}[/] → [green]{newSlug}[/]");
NoteRenamer.Result result = default!;
await AnsiConsole.Progress()
.AutoClear(false)
.HideCompleted(false)
.StartAsync(async prog =>
{
var task = prog.AddTask("Umbenennen…", maxValue: 100);
result = await NoteRenamer.ExecuteAsync(
ctx.Api, oldSlug, newSlug,
onProgress: msg =>
{
task.Description = msg;
task.Increment(20);
},
ct: ct);
task.Value = 100;
task.Description = "Fertig";
});
// Update local cache: swap old entry for a patched new one
var oldEntry = ctx.NoteCache.FirstOrDefault(n => n.Slug == oldSlug);
if (oldEntry is not null)
{
ctx.NoteCache.Remove(oldEntry);
ctx.NoteCache.Add(oldEntry with { Slug = newSlug });
}
var linkInfo = result.LinksUpdated > 0
? $" [dim]· {result.LinksUpdated} Link(s) aktualisiert[/]"
: "";
AnsiConsole.MarkupLine($"[green]✓[/] {oldSlug} → {newSlug}{linkInfo}");
}
}