feat: mkdir command creates folders via _index.md
Uses a dedicated /api/cli/folders endpoint (separate from notes because _index slugs contain underscores which the notes slug validator rejects). Derives a readable title from the slug, navigates into the new folder immediately even before any notes exist.
This commit is contained in:
parent
5a26de8a11
commit
e9217ecb77
1 changed files with 80 additions and 0 deletions
80
NozCli/Commands/MkdirCommand.cs
Normal file
80
NozCli/Commands/MkdirCommand.cs
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
using NozCli.Core.Security;
|
||||||
|
using NozCli.Repl;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace NozCli.Commands;
|
||||||
|
|
||||||
|
public class MkdirCommand : ICommand
|
||||||
|
{
|
||||||
|
public string Name => "mkdir";
|
||||||
|
public string Description => "Create a new folder (writes _index.md)";
|
||||||
|
public string Usage => "mkdir <folder-name>";
|
||||||
|
|
||||||
|
public async Task ExecuteAsync(string[] args, ReplContext ctx, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var folderArg = args.FirstOrDefault();
|
||||||
|
if (folderArg is null)
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("[red]Usage:[/] mkdir <folder-name>");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve full folder path relative to current navigation
|
||||||
|
var folderPath = ResolveFolderPath(folderArg.TrimEnd('/'), ctx);
|
||||||
|
|
||||||
|
if (!SlugGuard.IsValid(folderPath))
|
||||||
|
{
|
||||||
|
AnsiConsole.MarkupLine("[red]Ungültiger Ordnername.[/] Nur Kleinbuchstaben, Ziffern und Bindestriche erlaubt.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive a readable title from the last segment
|
||||||
|
var lastSegment = folderPath.Split('/')[^1];
|
||||||
|
var title = ToTitle(lastSegment);
|
||||||
|
|
||||||
|
await AnsiConsole.Status().StartAsync($"Erstelle [cyan]{folderPath}[/]…", async _ =>
|
||||||
|
await ctx.Api.PutFolderIndexAsync(folderPath, title, ct: ct));
|
||||||
|
|
||||||
|
// Navigate into the new folder
|
||||||
|
NavigateInto(folderPath, ctx);
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine($"[green]✓[/] Ordner erstellt → {ctx.CurrentPath}");
|
||||||
|
AnsiConsole.MarkupLine($"[dim]Erstelle deine erste Notiz mit [white]new <name>[/][/]");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveFolderPath(string segment, ReplContext ctx)
|
||||||
|
{
|
||||||
|
// Already a full multi-segment path (user typed full slug)
|
||||||
|
if (segment.Contains('/')) return segment;
|
||||||
|
|
||||||
|
if (ctx.CurrentTopic is null)
|
||||||
|
return segment;
|
||||||
|
|
||||||
|
if (ctx.CurrentSubPath is null)
|
||||||
|
return $"{ctx.CurrentTopic}/{segment}";
|
||||||
|
|
||||||
|
return $"{ctx.CurrentTopic}/{ctx.CurrentSubPath}/{segment}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void NavigateInto(string folderPath, ReplContext ctx)
|
||||||
|
{
|
||||||
|
var parts = folderPath.Split('/');
|
||||||
|
if (parts.Length == 1)
|
||||||
|
{
|
||||||
|
ctx.CurrentTopic = parts[0];
|
||||||
|
ctx.CurrentSubPath = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ctx.CurrentTopic = parts[0];
|
||||||
|
ctx.CurrentSubPath = string.Join('/', parts[1..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// "new-subfolder" → "New Subfolder"
|
||||||
|
private static string ToTitle(string slug) =>
|
||||||
|
string.Join(' ', slug.Split('-').Select(Capitalize));
|
||||||
|
|
||||||
|
private static string Capitalize(string s) =>
|
||||||
|
s.Length == 0 ? s : char.ToUpperInvariant(s[0]) + s[1..];
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue