noz-cli/NozCli/Commands/PushCommand.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

87 lines
3.1 KiB
C#

using NozCli.Repl;
using Spectre.Console;
namespace NozCli.Commands;
/// push <local-path> — 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 <local-path> [--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 <local-path> [--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", "");
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);
}
}