using NozCli.Repl; using NozCli.Core.Security; using Spectre.Console; namespace NozCli.Commands; /// push — reads .md files from a local directory and uploads changed ones. public class PushCommand : ICommand { public string Name => "push"; public string Description => "Push local markdown files to the server"; public string Usage => "push [--all]"; public async Task ExecuteAsync(string[] args, ReplContext ctx, CancellationToken ct = default) { var localPath = args.FirstOrDefault(a => !a.StartsWith("--")); var force = args.Contains("--all"); if (localPath is null) { AnsiConsole.MarkupLine("[red]Usage: push [--all][/]"); return; } localPath = Path.GetFullPath(localPath); if (!Directory.Exists(localPath)) { AnsiConsole.MarkupLine($"[red]Directory not found: {localPath}[/]"); return; } var files = Directory.EnumerateFiles(localPath, "*.md", SearchOption.AllDirectories) .Where(f => !Path.GetFileName(f).StartsWith("_")) .ToList(); if (files.Count == 0) { AnsiConsole.MarkupLine("[yellow]No markdown files found.[/]"); return; } // Build slug → server hash map for incremental push var serverHashes = ctx.NoteCache.ToDictionary(n => n.Slug, n => n.Hash ?? ""); int pushed = 0, skipped = 0, failed = 0; await AnsiConsole.Progress() .Columns(new TaskDescriptionColumn(), new ProgressBarColumn(), new SpinnerColumn()) .StartAsync(async progress => { var task = progress.AddTask("Pushing notes…", maxValue: files.Count); foreach (var file in files) { var relative = Path.GetRelativePath(localPath, file); var slug = relative.Replace(Path.DirectorySeparatorChar, '/').Replace(".md", ""); if (!SlugGuard.IsValid(slug)) { AnsiConsole.MarkupLine($"[yellow]⚠ Skipped invalid slug:[/] {slug}"); task.Increment(1); continue; } var markdown = await File.ReadAllTextAsync(file); var localHash = NozCli.Core.Client.HashHelper.Sha256(markdown); if (!force && serverHashes.TryGetValue(slug, out var serverHash) && serverHash == localHash) { skipped++; task.Increment(1); continue; } try { await ctx.Api.PutNoteAsync(slug, markdown, ct); pushed++; } catch (Exception ex) { AnsiConsole.MarkupLine($"[red]✗ {slug}: {ex.Message}[/]"); failed++; } task.Increment(1); } }); AnsiConsole.MarkupLine($"[green]↑ {pushed} pushed[/] [dim]{skipped} unchanged {(failed > 0 ? $"[red]{failed} failed[/]" : "")}[/]"); // Refresh note cache ctx.NoteCache = await ctx.Api.GetNotesAsync(ct); } }