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:
parent
b971d45e78
commit
db9aabd2b7
12 changed files with 340 additions and 1 deletions
41
Noz.PowerShell/Cmdlets/GetNozNoteCmdlet.cs
Normal file
41
Noz.PowerShell/Cmdlets/GetNozNoteCmdlet.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Noz.PowerShell/Cmdlets/GetNozNoteContentCmdlet.cs
Normal file
32
Noz.PowerShell/Cmdlets/GetNozNoteContentCmdlet.cs
Normal 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;
|
||||||
|
}
|
||||||
59
Noz.PowerShell/Cmdlets/NewNozNoteCmdlet.cs
Normal file
59
Noz.PowerShell/Cmdlets/NewNozNoteCmdlet.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
24
Noz.PowerShell/Cmdlets/RemoveNozNoteCmdlet.cs
Normal file
24
Noz.PowerShell/Cmdlets/RemoveNozNoteCmdlet.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Noz.PowerShell/Cmdlets/SetNozNoteCmdlet.cs
Normal file
31
Noz.PowerShell/Cmdlets/SetNozNoteCmdlet.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Noz.PowerShell/Install-NozModule.ps1
Normal file
29
Noz.PowerShell/Install-NozModule.ps1
Normal 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
|
||||||
20
Noz.PowerShell/Noz.PowerShell.csproj
Normal file
20
Noz.PowerShell/Noz.PowerShell.csproj
Normal 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
26
Noz.PowerShell/Noz.psd1
Normal 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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Noz.PowerShell/NozCmdletBase.cs
Normal file
47
Noz.PowerShell/NozCmdletBase.cs
Normal 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
29
Noz.PowerShell/NozNote.cs
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue