feat(powershell): Noz PowerShell module with pipeline-native cmdlets

- Get-NozNote: list notes, filter by -Topic, -Status, -Lang
- Get-NozNoteContent: fetch markdown, accepts pipeline input by Slug
- Set-NozNote: update note content, accepts markdown from pipeline
- New-NozNote: create note with frontmatter template
- Remove-NozNote: delete note, ShouldProcess + ConfirmImpact.High for safety
- NozNote output object: clean PS-friendly properties (no internal Hash field)
- NozCmdletBase: shared config loading, API client lifecycle, error translation
- Noz.psd1: module manifest for PS 7.2+
- Install-NozModule.ps1: build + install to user module path
- NozCli.Core retargeted to net8.0 for PS 7.4 LTS compatibility
This commit is contained in:
kryptomrx 2026-05-16 23:42:27 +02:00
parent b971d45e78
commit db9aabd2b7
12 changed files with 340 additions and 1 deletions

View file

@ -0,0 +1,41 @@
using System.Management.Automation;
namespace Noz.PowerShell.Cmdlets;
/// <summary>List notes from the noz server.</summary>
/// <example>
/// Get-NozNote
/// Get-NozNote -Topic psychologie
/// Get-NozNote -Status seed -Lang de
/// Get-NozNote | Group-Object Topic | Sort-Object Count -Descending
/// Get-NozNote | Export-Csv ~/notes.csv
/// </example>
[Cmdlet(VerbsCommon.Get, "NozNote")]
[OutputType(typeof(NozNote))]
public sealed class GetNozNoteCmdlet : NozCmdletBase
{
[Parameter(Position = 0)]
public string? Topic { get; set; }
[Parameter]
[ValidateSet("seed", "budding", "evergreen")]
public string? Status { get; set; }
[Parameter]
[ValidateSet("de", "en")]
public string? Lang { get; set; }
protected override void ProcessRecord()
{
var notes = Run(Api.GetNotesAsync());
var filtered = notes.AsEnumerable();
if (Topic is not null) filtered = filtered.Where(n => n.Topic == Topic);
if (Status is not null) filtered = filtered.Where(n => n.Status == Status);
if (Lang is not null) filtered = filtered.Where(n => n.Lang == Lang);
foreach (var note in filtered.OrderBy(n => n.Slug))
WriteObject(NozNote.From(note));
}
}

View file

@ -0,0 +1,32 @@
using System.Management.Automation;
namespace Noz.PowerShell.Cmdlets;
/// <summary>Get the markdown content of a note.</summary>
/// <example>
/// Get-NozNoteContent -Slug psychologie/flow-theorie
/// "psychologie/flow-theorie" | Get-NozNoteContent
/// Get-NozNote -Topic psychologie | Get-NozNoteContent | Select-Object Slug, Markdown
/// </example>
[Cmdlet(VerbsCommon.Get, "NozNoteContent")]
[OutputType(typeof(NozNoteContent))]
public sealed class GetNozNoteContentCmdlet : NozCmdletBase
{
[Parameter(Mandatory = true, Position = 0,
ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Slug { get; set; } = "";
protected override void ProcessRecord()
{
var content = Run(Api.GetNoteAsync(Slug));
WriteObject(new NozNoteContent { Slug = content.Slug, Markdown = content.Markdown });
}
}
public sealed class NozNoteContent
{
public string Slug { get; init; } = "";
public string Markdown { get; init; } = "";
public override string ToString() => Slug;
}

View file

@ -0,0 +1,59 @@
using System.Management.Automation;
namespace Noz.PowerShell.Cmdlets;
/// <summary>Create a new note on the server.</summary>
/// <example>
/// New-NozNote -Slug psychologie/neue-notiz -Title "Neue Notiz"
/// New-NozNote -Slug biosysteme/pilze -Title "Pilze" -Topic biosysteme -Status budding -Lang de
/// </example>
[Cmdlet(VerbsCommon.New, "NozNote", SupportsShouldProcess = true)]
[OutputType(typeof(NozNote))]
public sealed class NewNozNoteCmdlet : NozCmdletBase
{
[Parameter(Mandatory = true, Position = 0)]
public string Slug { get; set; } = "";
[Parameter(Mandatory = true, Position = 1)]
public string Title { get; set; } = "";
[Parameter]
public string? Topic { get; set; }
[Parameter]
[ValidateSet("seed", "budding", "evergreen")]
public string Status { get; set; } = "seed";
[Parameter]
[ValidateSet("de", "en")]
public string Lang { get; set; } = "de";
[Parameter]
public string Description { get; set; } = "";
protected override void ProcessRecord()
{
if (!ShouldProcess(Slug, "Create note")) return;
var topic = Topic ?? (Slug.Contains('/') ? Slug.Split('/')[0] : Slug);
var markdown = $"""
---
title: "{Title}"
description: "{Description}"
topic: {topic}
status: {Status}
lang: {Lang}
---
# {Title}
""";
Run(Api.PutNoteAsync(Slug, markdown));
var notes = Run(Api.GetNotesAsync());
var created = notes.FirstOrDefault(n => n.Slug == Slug);
if (created is not null)
WriteObject(NozNote.From(created));
}
}

View file

@ -0,0 +1,24 @@
using System.Management.Automation;
namespace Noz.PowerShell.Cmdlets;
/// <summary>Delete a note from the server.</summary>
/// <example>
/// Remove-NozNote -Slug psychologie/alte-notiz
/// Get-NozNote -Status seed | Where-Object Topic -eq "draft" | Remove-NozNote -WhatIf
/// Get-NozNote -Status seed | Where-Object Topic -eq "draft" | Remove-NozNote -Confirm:$false
/// </example>
[Cmdlet(VerbsCommon.Remove, "NozNote", SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.High)]
public sealed class RemoveNozNoteCmdlet : NozCmdletBase
{
[Parameter(Mandatory = true, Position = 0,
ValueFromPipeline = true, ValueFromPipelineByPropertyName = true)]
public string Slug { get; set; } = "";
protected override void ProcessRecord()
{
if (!ShouldProcess(Slug, "Delete note")) return;
Run(Api.DeleteNoteAsync(Slug));
WriteVerbose($"Deleted: {Slug}");
}
}

View file

@ -0,0 +1,31 @@
using System.Management.Automation;
namespace Noz.PowerShell.Cmdlets;
/// <summary>Update the markdown content of an existing note.</summary>
/// <example>
/// Set-NozNote -Slug psychologie/test -Markdown "# Test`n`nHello."
/// Get-Content note.md -Raw | Set-NozNote -Slug psychologie/test
/// </example>
[Cmdlet(VerbsCommon.Set, "NozNote", SupportsShouldProcess = true)]
[OutputType(typeof(NozNote))]
public sealed class SetNozNoteCmdlet : NozCmdletBase
{
[Parameter(Mandatory = true, Position = 0, ValueFromPipelineByPropertyName = true)]
public string Slug { get; set; } = "";
[Parameter(Mandatory = true, Position = 1, ValueFromPipeline = true)]
public string Markdown { get; set; } = "";
protected override void ProcessRecord()
{
if (!ShouldProcess(Slug, "Update note")) return;
Run(Api.PutNoteAsync(Slug, Markdown));
var notes = Run(Api.GetNotesAsync());
var updated = notes.FirstOrDefault(n => n.Slug == Slug);
if (updated is not null)
WriteObject(NozNote.From(updated));
}
}

View file

@ -0,0 +1,29 @@
#Requires -Version 7.2
<#
.SYNOPSIS
Builds and installs the Noz PowerShell module for the current user.
.EXAMPLE
./Install-NozModule.ps1
./Install-NozModule.ps1 -Verbose
#>
$ErrorActionPreference = 'Stop'
$moduleDir = Join-Path ([Environment]::GetFolderPath('MyDocuments')) 'PowerShell' 'Modules' 'Noz'
Write-Host "Building Noz.PowerShell..." -ForegroundColor Cyan
dotnet build "$PSScriptRoot/Noz.PowerShell.csproj" -c Release -v quiet
$src = "$PSScriptRoot/bin/Release/net8.0"
Write-Host "Installing to: $moduleDir" -ForegroundColor Cyan
New-Item -ItemType Directory -Force -Path $moduleDir | Out-Null
# Copy DLLs and manifest
Copy-Item "$src/Noz.PowerShell.dll" $moduleDir -Force
Copy-Item "$src/NozCli.Core.dll" $moduleDir -Force
Copy-Item "$PSScriptRoot/Noz.psd1" $moduleDir -Force
Write-Host "Done. Import with:" -ForegroundColor Green
Write-Host " Import-Module Noz" -ForegroundColor White
Write-Host " Get-NozNote -Topic psychologie" -ForegroundColor White

View file

@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\NozCli.Core\NozCli.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="PowerShellStandard.Library" Version="5.1.1" PrivateAssets="all" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AssemblyName>Noz.PowerShell</AssemblyName>
<RootNamespace>Noz.PowerShell</RootNamespace>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
</PropertyGroup>
</Project>

26
Noz.PowerShell/Noz.psd1 Normal file
View file

@ -0,0 +1,26 @@
@{
ModuleVersion = '0.1.0'
GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
Author = 'Bruno Deanoz'
Description = 'PowerShell module for noz — a self-hosted digital garden'
PowerShellVersion = '7.2'
RootModule = 'Noz.PowerShell.dll'
CmdletsToExport = @(
'Get-NozNote',
'Get-NozNoteContent',
'Set-NozNote',
'New-NozNote',
'Remove-NozNote'
)
FunctionsToExport = @()
AliasesToExport = @()
PrivateData = @{
PSData = @{
Tags = @('noz', 'digital-garden', 'notes', 'markdown')
ProjectUri = 'https://git.kryptomrx.de/krypto/noz-cli'
}
}
}

View file

@ -0,0 +1,47 @@
using System.Management.Automation;
using NozCli.Core.Client;
using NozCli.Core.Config;
namespace Noz.PowerShell;
public abstract class NozCmdletBase : PSCmdlet
{
protected LocalConfig Config { get; private set; } = null!;
protected NozApiClient Api { get; private set; } = null!;
protected override void BeginProcessing()
{
var cfg = LocalConfig.Load();
if (cfg is null)
ThrowTerminatingError(new ErrorRecord(
new InvalidOperationException(
"noz is not initialized. Run: noz init <server-url> <token>"),
"NozNotInitialized",
ErrorCategory.NotSpecified,
targetObject: null));
Config = cfg!;
Api = new NozApiClient(cfg!);
}
protected override void StopProcessing() => Api?.Dispose();
protected T Run<T>(Task<T> task)
{
try { return task.GetAwaiter().GetResult(); }
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
ThrowTerminatingError(new ErrorRecord(ex, "NozUnauthorized",
ErrorCategory.AuthenticationError, null));
throw;
}
catch (HttpRequestException ex)
{
ThrowTerminatingError(new ErrorRecord(ex, "NozApiError",
ErrorCategory.ConnectionError, null));
throw;
}
}
protected void Run(Task task) => Run(task.ContinueWith(_ => true));
}

29
Noz.PowerShell/NozNote.cs Normal file
View file

@ -0,0 +1,29 @@
using NozCli.Core.Models;
namespace Noz.PowerShell;
/// PS-friendly output object for a noz note.
/// Clean property set — no internal fields like Hash.
public sealed class NozNote
{
public string Slug { get; init; } = "";
public string Title { get; init; } = "";
public string Status { get; init; } = "";
public string Topic { get; init; } = "";
public string Lang { get; init; } = "";
public string? Date { get; init; }
public bool Private { get; init; }
public override string ToString() => Slug;
internal static NozNote From(NoteInfo n) => new()
{
Slug = n.Slug,
Title = n.Title,
Status = n.Status,
Topic = n.Topic,
Lang = n.Lang,
Date = n.Date,
Private = n.Private,
};
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net10.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View file

@ -1,4 +1,5 @@
<Solution> <Solution>
<Project Path="NozCli.Core/NozCli.Core.csproj" /> <Project Path="NozCli.Core/NozCli.Core.csproj" />
<Project Path="NozCli/NozCli.csproj" /> <Project Path="NozCli/NozCli.csproj" />
<Project Path="Noz.PowerShell/Noz.PowerShell.csproj" />
</Solution> </Solution>