From b6ca6072530f6a7e9c399b57c3bdf0b18e5e64c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Feb 2026 03:18:51 +0000 Subject: [PATCH 1/8] Multi-target library for net6.0, net8.0, net9.0, and net10.0 - Update MountAnything and MountAnything.Hosting.Abstractions NuGet packages to produce binaries for all four TFMs - Update MountAnything.Hosting.Build.targets to pass TargetFramework to the inner Host project build and use it in output paths instead of the previously hardcoded net6.0 - Multi-target tests and example project - Update CI and Publish workflows to install .NET 6/8/9/10 SDKs and bump actions/checkout and actions/setup-dotnet to v4 https://claude.ai/code/session_01HeNhhSDXi7aCwMrH9sLkrD --- .github/workflows/ci.yml | 10 +++++++--- .github/workflows/publish.yml | 10 +++++++--- examples/mount-powershell/MountPowershell.csproj | 2 +- .../MountAnything.Hosting.Abstractions.csproj | 2 +- .../build/MountAnything.Hosting.Build.targets | 4 ++-- src/MountAnything/MountAnything.csproj | 2 +- tests/MountAnything.Tests/MountAnything.Tests.csproj | 2 +- 7 files changed, 20 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb5da54..caf2396 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,12 +26,16 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + 10.0.x - name: Build run: dotnet build \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3bb4d1f..0f9ac4a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -24,12 +24,16 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup .NET - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 6.0.x + dotnet-version: | + 6.0.x + 8.0.x + 9.0.x + 10.0.x - name: Build run: dotnet build diff --git a/examples/mount-powershell/MountPowershell.csproj b/examples/mount-powershell/MountPowershell.csproj index 808aa9b..036c711 100644 --- a/examples/mount-powershell/MountPowershell.csproj +++ b/examples/mount-powershell/MountPowershell.csproj @@ -3,7 +3,7 @@ - net6.0 + net6.0;net8.0;net9.0;net10.0 enable enable MountPowershell diff --git a/src/MountAnything.Hosting.Abstractions/MountAnything.Hosting.Abstractions.csproj b/src/MountAnything.Hosting.Abstractions/MountAnything.Hosting.Abstractions.csproj index 3be9d09..4b8d185 100644 --- a/src/MountAnything.Hosting.Abstractions/MountAnything.Hosting.Abstractions.csproj +++ b/src/MountAnything.Hosting.Abstractions/MountAnything.Hosting.Abstractions.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net8.0;net9.0;net10.0 enable enable true diff --git a/src/MountAnything.Hosting.Build/build/MountAnything.Hosting.Build.targets b/src/MountAnything.Hosting.Build/build/MountAnything.Hosting.Build.targets index 7180341..7b5b924 100644 --- a/src/MountAnything.Hosting.Build/build/MountAnything.Hosting.Build.targets +++ b/src/MountAnything.Hosting.Build/build/MountAnything.Hosting.Build.targets @@ -20,10 +20,10 @@ - + - + diff --git a/src/MountAnything/MountAnything.csproj b/src/MountAnything/MountAnything.csproj index 3dd414e..498cb06 100644 --- a/src/MountAnything/MountAnything.csproj +++ b/src/MountAnything/MountAnything.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net8.0;net9.0;net10.0 enable enable true diff --git a/tests/MountAnything.Tests/MountAnything.Tests.csproj b/tests/MountAnything.Tests/MountAnything.Tests.csproj index 4b1efed..a151087 100644 --- a/tests/MountAnything.Tests/MountAnything.Tests.csproj +++ b/tests/MountAnything.Tests/MountAnything.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net6.0;net8.0;net9.0;net10.0 enable false From 48254e27441ee1af8a177c1021898ffc05aaeaef Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Feb 2026 03:29:49 +0000 Subject: [PATCH 2/8] Add pwsh to session start hook and fix stale ItemTests assertion - Add PowerShell installation to the session start hook so the MountPowershell example can build in Claude Code on the web - Fix FirstTypeNameIsOnlyTypeOnFinalPSObject test that was not updated after the intentional breaking change in 19936ed which changed the default TypeName to use the class name instead of the underlying PSObject's first type name https://claude.ai/code/session_01HeNhhSDXi7aCwMrH9sLkrD --- .claude/hooks/session-start.sh | 9 +++++++++ tests/MountAnything.Tests/ItemTests.cs | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index 66604a3..53c3b84 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -17,6 +17,15 @@ fi echo 'export DOTNET_ROLL_FORWARD=LatestMajor' >> "$CLAUDE_ENV_FILE" export DOTNET_ROLL_FORWARD=LatestMajor +# Install PowerShell if not already installed (needed by MountAnything.Hosting.Build targets) +if ! command -v pwsh &>/dev/null; then + wget -q https://packages.microsoft.com/config/ubuntu/24.04/packages-microsoft-prod.deb -O /tmp/packages-microsoft-prod.deb + dpkg -i /tmp/packages-microsoft-prod.deb + apt-get update -qq 2>/dev/null || true + apt-get install -y --no-install-recommends powershell + rm -f /tmp/packages-microsoft-prod.deb +fi + # Restore NuGet packages cd "$CLAUDE_PROJECT_DIR" dotnet restore diff --git a/tests/MountAnything.Tests/ItemTests.cs b/tests/MountAnything.Tests/ItemTests.cs index e48c916..b666d0d 100644 --- a/tests/MountAnything.Tests/ItemTests.cs +++ b/tests/MountAnything.Tests/ItemTests.cs @@ -9,7 +9,7 @@ namespace MountAnything.Tests; public class ItemTests { [Fact] - public void FirstTypeNameIsOnlyTypeOnFinalPSObject() + public void ClassTypeNameIsOnlyTypeOnFinalPSObject() { var underlyingObject = new PSObject(); underlyingObject.TypeNames.Clear(); @@ -20,7 +20,7 @@ public void FirstTypeNameIsOnlyTypeOnFinalPSObject() var pipelineObject = item.ToPipelineObject(p => p.ToString()); pipelineObject.TypeNames.Should().HaveCount(1); - pipelineObject.TypeNames.Single().Should().Be("MyType"); + pipelineObject.TypeNames.Single().Should().Be(typeof(TestItem).FullName); } [Fact] From 92904bd4286e49793ecf269dc3635fcb5eb68389 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Feb 2026 03:54:52 +0000 Subject: [PATCH 3/8] Fix multi-target build by guarding BuildHost against outer build dispatch When using TargetFrameworks (plural), MSBuild runs an outer build that dispatches to per-TFM inner builds. The BuildHost and PublishModule targets need Condition="'$(TargetFramework)' != ''" to skip the outer build where TargetFramework is empty. Also add RestoreRecursive=false to prevent the nested host project restore from overwriting the Hosting.Abstractions multi-TFM project.assets.json with a single-TFM version. https://claude.ai/code/session_01HeNhhSDXi7aCwMrH9sLkrD --- .../build/MountAnything.Hosting.Build.targets | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MountAnything.Hosting.Build/build/MountAnything.Hosting.Build.targets b/src/MountAnything.Hosting.Build/build/MountAnything.Hosting.Build.targets index 7b5b924..5b5dbad 100644 --- a/src/MountAnything.Hosting.Build/build/MountAnything.Hosting.Build.targets +++ b/src/MountAnything.Hosting.Build/build/MountAnything.Hosting.Build.targets @@ -10,7 +10,7 @@ - + - + @@ -71,7 +71,7 @@ WorkingDirectory="$(ModuleOutputDir)" /> - + From 5e3fc4005f7f68d393ac6dd7157a42396ddfc73e Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Feb 2026 04:01:14 +0000 Subject: [PATCH 4/8] Upgrade System.Management.Automation per TFM to fix NETSDK1206 RID warnings Use TFM-appropriate PowerShell SDK versions instead of a single 7.2.0 for all targets. The old 7.2.0 transitively pulls in Microsoft.Management.Infrastructure.Runtime.Win which uses deprecated version-specific RIDs (win7-x64, win8-x86, etc.) that trigger NETSDK1206 warnings on net8.0+. - net6.0: System.Management.Automation 7.2.24 (PowerShell 7.2) - net8.0: System.Management.Automation 7.4.13 (PowerShell 7.4) - net9.0/net10.0: System.Management.Automation 7.5.4 (PowerShell 7.5) https://claude.ai/code/session_01HeNhhSDXi7aCwMrH9sLkrD --- .../MountAnything.Hosting.Abstractions.csproj | 10 ++++++++-- src/MountAnything/MountAnything.csproj | 10 +++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/MountAnything.Hosting.Abstractions/MountAnything.Hosting.Abstractions.csproj b/src/MountAnything.Hosting.Abstractions/MountAnything.Hosting.Abstractions.csproj index 4b8d185..4bb106e 100644 --- a/src/MountAnything.Hosting.Abstractions/MountAnything.Hosting.Abstractions.csproj +++ b/src/MountAnything.Hosting.Abstractions/MountAnything.Hosting.Abstractions.csproj @@ -20,8 +20,14 @@ README.md - - + + + + + + + + diff --git a/src/MountAnything/MountAnything.csproj b/src/MountAnything/MountAnything.csproj index 498cb06..864cc6a 100644 --- a/src/MountAnything/MountAnything.csproj +++ b/src/MountAnything/MountAnything.csproj @@ -21,7 +21,15 @@ - + + + + + + + + + From c1a9c2045bf0f217b0e70b537bb212d2409731ae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Feb 2026 04:07:14 +0000 Subject: [PATCH 5/8] Upgrade MSBuild packages to fix NU1903 vulnerability warnings Upgrade Microsoft.Build.Tasks.Core and Microsoft.Build.Utilities.Core from 17.1.0 to 17.8.43 to resolve: - GHSA-h4j7-5rxr-p4wc (CVE-2025-26646, spoofing via DownloadFile task) - GHSA-w3q9-fxm7-j8fq (CVE-2025-55247, DoS via predictable temp paths) https://claude.ai/code/session_01HeNhhSDXi7aCwMrH9sLkrD --- .../MountAnything.Hosting.Build.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MountAnything.Hosting.Build/MountAnything.Hosting.Build.csproj b/src/MountAnything.Hosting.Build/MountAnything.Hosting.Build.csproj index b77e359..df467df 100644 --- a/src/MountAnything.Hosting.Build/MountAnything.Hosting.Build.csproj +++ b/src/MountAnything.Hosting.Build/MountAnything.Hosting.Build.csproj @@ -33,10 +33,10 @@ - + all - + all From 94b31549d6bb710d5309876dd97db4675cb30cdc Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 26 Feb 2026 04:19:06 +0000 Subject: [PATCH 6/8] Fix nullability annotations to match PowerShell provider interfaces Update IProviderImpl, ProviderImpl, and the Provider template to use nullable annotations matching the PowerShell SDK interfaces: - GetProperty/GetPropertyDynamicParameters: Collection? parameter - NewProperty/NewPropertyDynamicParameters: object? value parameter - RemovePropertyDynamicParameters: non-nullable object return type Eliminates all CS8766/CS8767 nullability warnings (25 warnings across all TFMs). https://claude.ai/code/session_01HeNhhSDXi7aCwMrH9sLkrD --- .../IProviderImpl.cs | 8 ++++---- src/MountAnything.Hosting.Templates/Provider.cs | 12 ++++++------ src/MountAnything/ProviderImpl.cs | 12 ++++++------ 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/MountAnything.Hosting.Abstractions/IProviderImpl.cs b/src/MountAnything.Hosting.Abstractions/IProviderImpl.cs index 8e1399a..b3f242e 100644 --- a/src/MountAnything.Hosting.Abstractions/IProviderImpl.cs +++ b/src/MountAnything.Hosting.Abstractions/IProviderImpl.cs @@ -947,9 +947,9 @@ public interface IProviderImpl object? ClearPropertyDynamicParameters(string path, Collection propertyToClear); - void GetProperty(string path, Collection providerSpecificPickList); + void GetProperty(string path, Collection? providerSpecificPickList); - object? GetPropertyDynamicParameters(string path, Collection providerSpecificPickList); + object? GetPropertyDynamicParameters(string path, Collection? providerSpecificPickList); void SetProperty(string path, PSObject propertyValue); @@ -1106,7 +1106,7 @@ void MoveProperty( /// the user unless the Force property is set to true. An error should be sent to the WriteError method if /// the path represents an item that is hidden from the user and Force is set to false. /// - void NewProperty(string path, string propertyName, string propertyTypeName, object value); + void NewProperty(string path, string propertyName, string propertyTypeName, object? value); /// /// Gives the provider an opportunity to attach additional parameters to the @@ -1136,7 +1136,7 @@ void MoveProperty( string path, string propertyName, string propertyTypeName, - object value); + object? value); /// Removes a property on the item specified by the path. /// diff --git a/src/MountAnything.Hosting.Templates/Provider.cs b/src/MountAnything.Hosting.Templates/Provider.cs index 4765cce..7aff820 100644 --- a/src/MountAnything.Hosting.Templates/Provider.cs +++ b/src/MountAnything.Hosting.Templates/Provider.cs @@ -307,12 +307,12 @@ public void ClearProperty(string path, Collection propertyToClear) return ProviderImpl.ClearPropertyDynamicParameters(path, propertyToClear); } - public void GetProperty(string path, Collection providerSpecificPickList) + public void GetProperty(string path, Collection? providerSpecificPickList) { ProviderImpl.GetProperty(path, providerSpecificPickList); } - public object? GetPropertyDynamicParameters(string path, Collection providerSpecificPickList) + public object? GetPropertyDynamicParameters(string path, Collection? providerSpecificPickList) { return ProviderImpl.GetPropertyDynamicParameters(path, providerSpecificPickList); } @@ -356,12 +356,12 @@ public void MoveProperty(string sourcePath, string sourceProperty, string destin return ProviderImpl.MovePropertyDynamicParameters(sourcePath, sourceProperty, destinationPath, destinationProperty); } - public void NewProperty(string path, string propertyName, string propertyTypeName, object value) + public void NewProperty(string path, string propertyName, string propertyTypeName, object? value) { ProviderImpl.NewProperty(path, propertyName, propertyTypeName, value); } - public object? NewPropertyDynamicParameters(string path, string propertyName, string propertyTypeName, object value) + public object? NewPropertyDynamicParameters(string path, string propertyName, string propertyTypeName, object? value) { return ProviderImpl.NewPropertyDynamicParameters(path, propertyName, propertyTypeName, value); } @@ -371,9 +371,9 @@ public void RemoveProperty(string path, string propertyName) ProviderImpl.RemoveProperty(path, propertyName); } - public object? RemovePropertyDynamicParameters(string path, string propertyName) + public object RemovePropertyDynamicParameters(string path, string propertyName) { - return ProviderImpl.RemovePropertyDynamicParameters(path, propertyName); + return ProviderImpl.RemovePropertyDynamicParameters(path, propertyName)!; } public void RenameProperty(string path, string sourceProperty, string destinationProperty) diff --git a/src/MountAnything/ProviderImpl.cs b/src/MountAnything/ProviderImpl.cs index c6eed07..36cfc12 100644 --- a/src/MountAnything/ProviderImpl.cs +++ b/src/MountAnything/ProviderImpl.cs @@ -568,12 +568,12 @@ public void ClearProperty(string path, Collection propertyToClear) return GetDynamicParameters(path, typeof(IClearItemPropertiesParameters<>)); } - public void GetProperty(string path, Collection providerSpecificPickList) + public void GetProperty(string path, Collection? providerSpecificPickList) { WithPathHandler(path, handler => { handler.SetDynamicParameters(typeof(IGetItemPropertiesParameters<>), DynamicParameters); - var propertyNames = providerSpecificPickList.ToHashSet(); + var propertyNames = providerSpecificPickList?.ToHashSet() ?? new HashSet(); var itemProperties = handler .GetItemProperties(propertyNames, ToFullyQualifiedProviderPath) .WherePropertiesMatch(propertyNames); @@ -587,7 +587,7 @@ public void GetProperty(string path, Collection providerSpecificPickList }); } - public object? GetPropertyDynamicParameters(string path, Collection providerSpecificPickList) + public object? GetPropertyDynamicParameters(string path, Collection? providerSpecificPickList) { return GetDynamicParameters(path, typeof(IGetItemPropertiesParameters<>)); } @@ -640,7 +640,7 @@ public void MoveProperty(string sourcePath, string sourceProperty, string destin return null; } - public void NewProperty(string path, string propertyName, string propertyTypeName, object value) + public void NewProperty(string path, string propertyName, string propertyTypeName, object? value) { WriteDebug($"NewProperty({path}, {propertyName}, {propertyTypeName}, )"); WithPathHandler(path, handler => @@ -648,7 +648,7 @@ public void NewProperty(string path, string propertyName, string propertyTypeNam if (handler is INewItemPropertyHandler newPropertyHandler) { handler.SetDynamicParameters(typeof(INewItemPropertyParameters<>), DynamicParameters); - newPropertyHandler.NewItemProperty(propertyName, propertyTypeName, value); + newPropertyHandler.NewItemProperty(propertyName, propertyTypeName, value!); WritePropertyObject(new Hashtable { [propertyName] = value }, path); } else @@ -658,7 +658,7 @@ public void NewProperty(string path, string propertyName, string propertyTypeNam }); } - public object? NewPropertyDynamicParameters(string path, string propertyName, string propertyTypeName, object value) + public object? NewPropertyDynamicParameters(string path, string propertyName, string propertyTypeName, object? value) { return GetDynamicParameters(path, typeof(INewItemPropertyParameters<>)); } From 3de34fc15be9a2e144613959a494f23ce011146a Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 17:05:53 +0000 Subject: [PATCH 7/8] Add comprehensive documentation with docs/ folder and streamlined README Restructure the README as a concise entry point with links to detailed documentation pages. Create 8 docs pages covering: - Getting Started guide using the in-repo example project - Routing (all Map variants, route hierarchy, regex composition) - Path Handlers (required/optional methods, context, constructor injection) - Items (Item, Item, IItem, custom properties, links, aliases) - Caching (freshness strategies, partial items, cache control) - Dependency Injection (Autofac, TypedString, ancestor items) - Handler Interfaces (write operations, content read/write, dynamic params) - Advanced Topics (ItemPath, LinkGenerator, ItemNavigator, assembly isolation) https://claude.ai/code/session_01HeNhhSDXi7aCwMrH9sLkrD --- README.md | 178 +++++++++++------------------------ docs/advanced.md | 147 +++++++++++++++++++++++++++++ docs/caching.md | 90 ++++++++++++++++++ docs/dependency-injection.md | 172 +++++++++++++++++++++++++++++++++ docs/getting-started.md | 174 ++++++++++++++++++++++++++++++++++ docs/handler-interfaces.md | 144 ++++++++++++++++++++++++++++ docs/items.md | 167 ++++++++++++++++++++++++++++++++ docs/path-handlers.md | 121 ++++++++++++++++++++++++ docs/routing.md | 170 +++++++++++++++++++++++++++++++++ 9 files changed, 1239 insertions(+), 124 deletions(-) create mode 100644 docs/advanced.md create mode 100644 docs/caching.md create mode 100644 docs/dependency-injection.md create mode 100644 docs/getting-started.md create mode 100644 docs/handler-interfaces.md create mode 100644 docs/items.md create mode 100644 docs/path-handlers.md create mode 100644 docs/routing.md diff --git a/README.md b/README.md index 1ac9e96..fbfe2fe 100644 --- a/README.md +++ b/README.md @@ -1,150 +1,80 @@ # MountAnything -A framework for building powershell providers to make it easy to navigate arbitrary API's as a hierarchical virtual filesystem of objects. +A framework for building PowerShell providers that expose arbitrary APIs as hierarchical virtual filesystems. Navigate any API with familiar commands like `cd`, `ls`, `Get-Item`, and `Get-ChildItem`. -## Getting started +The primary consumer of this framework is [MountAws](https://github.com/andyalm/mount-aws), which exposes AWS services as a virtual drive. -1. Reference the `MountAnything` and `MountAnything.Hosting.Build` nuget packages in your csproj project that will contain your powershell provider. -2. Create a class that implements the `IMountAnythingProvider` interface. -3. Implement the `CreateRouter` method. For information on creating a router, see the [Router](#Router) section below. -4. When you build your project, it will output a powershell .psd1 module file in a Module subdirectory of your projects output path (e.g. `bin/Debug/net6.0/Module`). You can test it out by importing that module into your powershell session and then using the `New-PSDrive` command to mount your provider to a drive. If you would like your provider to automatically mount a drive when the module is loaded, you can implement the optional `GetDefaultDrives` method in your `IMountAnythingProvider` implementation. +## What it looks like -## Key abstractions +```powershell +# Navigate AWS ECS resources as a filesystem +cd aws:\us-east-1\ecs\clusters\my-cluster\services +ls -There are three key abstractions that drive MountAnything. The `Router`, `PathHandler`'s, and `Item`'s: +# Inspect a specific resource +Get-Item my-service -### Router - -Every path in the virtual filesystem is processed by the `Router` to determine which `PathHandler` will process the command. -The Router API composes a nested hierarchy of routes. Under the hood, routes are regex based, but you usually can use a more convenient -extension method to avoid needing to actually deal with regex's. Here is an example of the routing api from the [MountAws](https://github.com/andyalm/mount-aws) project: - -```c# -router.MapRegex("(?[a-z0-9-]+)", region => -{ - region.MapLiteral("ecs", ecs => - { - ecs.MapLiteral("task-definitions", taskDefinitions => - { - taskDefinitions.Map(); - }); - ecs.MapLiteral("clusters", clusters => - { - clusters.Map(cluster => - { - cluster.MapLiteral("services", services => - { - services.Map(); - }); - }); - }); - }); -}); +# Or navigate PowerShell itself (included example project) +cd pwsh:\modules\Microsoft.PowerShell.Utility +ls # lists commands in the module ``` -In the example, you can see a few different variations of `Map` methods used. All of them take a generic type argument that corresponds to the `IPathHandler` that will be invoked for matching routes. They are: - -* `MapLiteral` - This matches on the literal string (e.g. constant) passed into it. Only that literal string will match the route. -* `Map` - This matches any supported character (pretty much anything besides a `/`, which is used as the path separator) at this hierarchy level. You can optionally pass in a string as the first argument to this method if you would like to capture the value of the matched value. The captured value will be given the name that is passed as the argument. The captured value can be used for dependency injection into the `PathHander` of this or any child route. -* `Map` - This is similar to the `Map` above, except it contains a second type parameter that is a [TypedString](src/MountAnything/TypedString.cs) whose value will be populated from the matched route value and can be injected into the constructor of this or any child `PathHandler`. -* `MapRegex` - This is the lower level method that the above two methods call under the hood. Any regex is acceptable, so long as it does not contain the `^` or `$` characters for declaring the beginning or end of a string. Those are implicitly added by the router as necessary. It is important to note that any regex you are adding is implicitly concatenated with the regex's built by parent and child routes when the router is matching. Named captures are allowed in the regex and those captured values can be used for dependency injection into the `PathHandler` of this or any child route. - -### PathHandler - -The `PathHandler` is in charge of processing a command to the powershell provider. -While there is an `IPathHandler`, it is expected that 99% of the time you will want to use -the `PathHandler` abstract base class instead for convenience. It will automatically handle -things like caching for you, which helps make things like tab completion as performant as possible. - -The `PathHandler` base class has only two methods that you are required to implement: - -* `GetItemImpl` - This is called when the `Get-Item` command is called. It should return the `IItem` that corresponds to the path that this `PathHandler` is processing. If no item exists at this path, it should return `null`. -* `GetChildItemsImpl` - This is called when the `Get-ChildItems` command. Its also used to support tab completion by default. It should return all of the child items of the item returned by the `GetItemImpl` method. - -In addition, you can optionally override the following methods when helpful/necessary: - -* `ExistsImpl` - By default, existence is checked by calling `GetItem` and determining if it returned `null` or not. However, if you can provide a more performant/optimal implementation, you can override this method. -* `GetChildItems(string filter)` - This method supports tab completion, as well as when the `-Filter` argument is used on the `Get-ChildItems` command. By default, the `GetChildItemsImpl` method is called and the filter as applied to entire set of items returned. However, if you can provide a more performant implementation that does not require fetching all items first, you are encouraged to do so by overriding this method. -* `CacheChildren` - By default, the paths of the child items returned by `GetChildItemsImpl` are cached to help make things like tab completion faster. However, if there are potentially a very large number of child items for this handler, you may want to tell it not to do this by overriding this property and returning `false`. -* `GetItemCommandDefaultFreshness` - This allows you to customize when the cache is used for `Get-Item` commands. -* `GetChildItemsCommandDefaultFreshness` - This allows you to customize when the cache is used for `Get-ChildItems` commands. - -### Item - -This represents the object/item that is returned to the console by `Get-Item` and `Get-ChildItems` commands. It is generally a wrapper -class around an underlying object that will be sent to the console. There is a generic version of `Item` where the type -argument represents the .net type of the item that will be sent to the console. If you inherit from the non-generic `Item`, the -underlying type will be a `PSObject`. Either way, all properties on the underlying type will be written to the powershell pipeline. The -`Item` class has a couple methods that need to be implemented in the subclass to tell the powershell provider what the path of the item is: - -* `ItemName` - This identifies the virtual "filename" of the item. It should be something that naturally identifies the item. Prefer human friendly names if they are guaranteed to be unique. -* `IsContainer` - This indicates whether this could have child items or not. +## Quick start -Here is an example of a simple `Item` implementation: +1. Reference the NuGet packages: -```c# -public class SecurityGroupItem : Item -{ - public SecurityGroupItem(ItemPath parentPath, SecurityGroup securityGroup) : base(parentPath, securityGroup) {} - - public override string ItemName => UnderlyingObject.GroupId; - - public override bool IsContainer => false; -} +```xml + + ``` -## Dependency Injection +2. Implement `IMountAnythingProvider` to define your virtual filesystem's route structure: -All `IPathHandler` instances support dependency injection, powered by [Autofac](https://autofac.readthedocs.io/). -The Router provides a `RegisterServices` method that allows you to use Autofac's [ContainerBuilder](https://autofac.readthedocs.io/en/latest/register/registration.html) -to register additional services that can be injected into your `PathHandler`'s. Services can be registered at any point in the routing -hierarchy and a registration further down in the hierarchy will override one that happens higher up. For example, take this example: - -```c# -// registers the default implementation of RegionEndpoint to be us-east-1 -router.RegisterServices(builder => builder.Register(_ => RegionEndpoint.UsEast1)); -router.MapLiteral("regions", regions => +```csharp +public class MyProvider : IMountAnythingProvider { - regions.Map("Region", region => + public Router CreateRouter() { - region.RegisterServices((match, builder) => + var router = Router.Create(); + router.MapLiteral("modules", modules => { - // overrides the default region registration above - builder.Register(_ => RegionEndpoint.FromSystemName(match.Values["Region"]); + modules.Map(); }); - }); -}); + return router; + } +} ``` -In the above example, any PathHandler underneath the `/regions` path will be injected the region from the current path. Any PathHandler -outside of the `/regions` path will have the `us-east-1` region injected. +3. Implement `PathHandler` subclasses that return `Item` objects for each path. -### Injecting an ancestor item +4. Build your project — the `MountAnything.Hosting.Build` package auto-generates a PowerShell module in your output directory. Import it and start navigating. -Sometimes `PathHandler`'s need to know something about a specific item above them in the path hierarchy. You can have an ancestor item -injected into your `PathHandler`'s constructor by using the `IItemAncestor` interface. For example, in this theoretical example, -an EcsService handler wants to know what ECS cluster it belongs to, so it declares `IItemAncestor` as a constructor dependency: +See the [Getting Started guide](docs/getting-started.md) for a full walkthrough with the included example project. -```c# -public class EcsServiceHandler : PathHandler -{ - private readonly IItemAncestor _cluster; - private readonly IEcsApi _ecs; +## Key concepts - public EcsServiceHandler(ItemPath path, IPathHandlerContext context, IItemAncestor cluster, IEcsApi ecs) : base(path, context) - { - _cluster = cluster; - _ecs = ecs; - } - - protected override IItem GetItemImpl() - { - var ecsService = _ecs.DescribeService(serviceName: ItemName, clusterName: _cluster.Name); - - return new EcsServiceItem(ParentPath, ecsService); - } -} +- **[Router](docs/routing.md)** — Maps URL-like paths to handlers using composable route definitions (`MapLiteral`, `Map`, `MapRegex`). +- **[PathHandler](docs/path-handlers.md)** — Processes `Get-Item` and `Get-ChildItem` for a matched path. Supports automatic caching, dependency injection, and optional write operations. +- **[Item](docs/items.md)** — Wraps a .NET object for the PowerShell pipeline. Defines the virtual filename and whether the item can have children. + +## Documentation + +- [Getting Started](docs/getting-started.md) — step-by-step guide +- [Routing](docs/routing.md) — all `Map` variants and route hierarchy +- [Path Handlers](docs/path-handlers.md) — handler context, optional overrides, constructor injection +- [Items](docs/items.md) — custom properties, aliases, links +- [Caching](docs/caching.md) — freshness strategies and cache control +- [Dependency Injection](docs/dependency-injection.md) — service registration, typed captures, ancestor items +- [Handler Interfaces](docs/handler-interfaces.md) — supporting `New-Item`, `Remove-Item`, `Get-Content`, etc. +- [Advanced Topics](docs/advanced.md) — `ItemPath`, `LinkGenerator`, `ItemNavigator`, assembly isolation + +## Building from source + +```bash +dotnet build +dotnet test ``` -This example assumes there is a `IPathHandler` higher in the routing hierarchy whose `GetItem` implementation returns an item of type `ClusterItem`. -The `IItemAncestor` implementation walks up the hierarchy looking for an item whose type matches the one declared as `TItem`. \ No newline at end of file +## License + +See [LICENSE](LICENSE). diff --git a/docs/advanced.md b/docs/advanced.md new file mode 100644 index 0000000..9e36245 --- /dev/null +++ b/docs/advanced.md @@ -0,0 +1,147 @@ +# Advanced Topics + +This page covers utility types and architectural details for advanced use cases. + +## `ItemPath` + +`ItemPath` is an immutable path type used throughout MountAnything. It normalizes backslashes to forward slashes and strips leading/trailing separators. + +```csharp +var path = new ItemPath(@"us-east-1\ecs\clusters"); +// path.FullName == "us-east-1/ecs/clusters" +``` + +### Key members + +| Member | Description | +|---|---| +| `FullName` | The normalized path string | +| `Name` | The last segment (e.g., `"clusters"`) | +| `Parent` | The parent path (e.g., `"us-east-1/ecs"`) | +| `Parts` | Array of path segments | +| `IsRoot` | Whether this is the empty root path | +| `Combine(parts)` | Append segments to the path | +| `Ancestor(name)` | Walk up to find an ancestor with the given name | +| `IsAncestorOf(path)` | Check if this path is an ancestor of another | +| `MatchesPattern(pattern)` | Wildcard matching (supports `*`) | + +### Constants and conversions + +```csharp +ItemPath.Root // The empty root path +ItemPath.Separator // '/' + +// Explicit casts (not implicit, to avoid accidental conversions) +var path = (ItemPath)"some/path"; +var str = (string)path; +``` + +## `LinkGenerator` + +`LinkGenerator` helps construct paths to items elsewhere in the hierarchy. It's available as a property on every `PathHandler`. + +```csharp +// Construct a path using the first N parts of the current path as a base +var taskDefPath = LinkGenerator.ConstructPath( + numberOfParentPathParts: 2, // e.g., "us-east-1/ecs" + childPath: $"task-definitions/{taskDefName}" +); +// Result: "us-east-1/ecs/task-definitions/my-task-def" +``` + +This is useful for creating cross-references in item `LinkPaths`: + +```csharp +public class ServiceItem : Item +{ + public ServiceItem(ItemPath parentPath, Service service, LinkGenerator linkGenerator) + : base(parentPath, service) + { + LinkPaths = new Dictionary + { + ["TaskDefinition"] = linkGenerator.ConstructPath(2, $"task-definitions/{service.TaskDef}") + }; + } +} +``` + +## `ItemNavigator` + +`ItemNavigator` converts a flat list of items with hierarchical paths into a directory-like structure. This is useful when an API returns a flat list (e.g., S3 object keys) that you want to present as nested directories. + +Subclass `ItemNavigator` and implement: + +| Method | Purpose | +|---|---| +| `CreateDirectoryItem(parentPath, directoryPath)` | Create a virtual directory item | +| `CreateItem(parentPath, model)` | Create a leaf item from a model | +| `GetPath(model)` | Extract the hierarchical path from a model | +| `ListItems(pathPrefix)` | Fetch all models (optionally filtered by prefix) | + +Then call `ListChildItems(parentPath)` to get the items for a given level: + +```csharp +public class S3Navigator : ItemNavigator +{ + protected override IItem CreateDirectoryItem(ItemPath parentPath, ItemPath dirPath) + => new S3DirectoryItem(parentPath, dirPath.Name); + + protected override IItem CreateItem(ItemPath parentPath, S3Object obj) + => new S3ObjectItem(parentPath, obj); + + protected override ItemPath GetPath(S3Object obj) + => new ItemPath(obj.Key); + + protected override IEnumerable ListItems(ItemPath? pathPrefix) + => _s3.ListObjects(prefix: pathPrefix?.FullName); +} +``` + +## `MountAnythingProvider` + +For providers that need custom parameters on `New-PSDrive`, inherit from `MountAnythingProvider` instead of implementing `IMountAnythingProvider` directly: + +```csharp +public class MyDriveParameters +{ + [Parameter(Mandatory = true)] + public string Profile { get; set; } = ""; + + [Parameter] + public string Region { get; set; } = "us-east-1"; +} + +public class MyProvider : MountAnythingProvider +{ + public override Router CreateRouter() { /* ... */ } + + protected override PSDriveInfo NewDrive(PSDriveInfo driveInfo, MyDriveParameters parameters) + { + return new MyPsDriveInfo(driveInfo, parameters.Profile, parameters.Region); + } +} +``` + +Users can then pass custom parameters when mounting: + +```powershell +New-PSDrive -Name aws -PSProvider MyProvider -Root '' -Profile production -Region eu-west-1 +``` + +## Assembly Load Context isolation + +MountAnything uses .NET's `AssemblyLoadContext` to isolate provider assemblies from the PowerShell host process. This prevents version conflicts between provider dependencies and PowerShell's own dependencies. + +The architecture: + +- **`MountAnything.Hosting.Abstractions`** stays in the global (default) `AssemblyLoadContext`. It contains only two small interfaces (`IProviderImpl` and `IProviderHost`) to minimize coupling. +- **`MountAnything`** and your provider assembly are loaded into an isolated `AssemblyLoadContext` created at module import time. +- **`MountAnything.Hosting.Build`** generates a bridge class at build time (from the template in `MountAnything.Hosting.Templates`) that lives in the global context and delegates to `IProviderImpl` across the isolation boundary. + +This means your provider can use any version of any NuGet package without conflicting with other providers or PowerShell itself. + +## See also + +- [Items](items.md) — `Links`, `LinkPaths`, and `Aliases` +- [Routing](routing.md) — `MapRecursive` for hierarchical paths +- [Dependency Injection](dependency-injection.md) — `TypedString` and `TypedItemPath` diff --git a/docs/caching.md b/docs/caching.md new file mode 100644 index 0000000..98a5be5 --- /dev/null +++ b/docs/caching.md @@ -0,0 +1,90 @@ +# Caching + +MountAnything includes an in-memory cache that makes tab completion fast and reduces redundant API calls. The `PathHandler` base class integrates with the cache automatically — you generally don't need to interact with it directly, but understanding how it works helps you tune performance. + +> **Prerequisite:** [Path Handlers](path-handlers.md) + +## How it works + +When a `PathHandler` calls `GetItem()` or `GetChildItems()`, the base class checks the cache before calling your `GetItemImpl()` or `GetChildItemsImpl()` methods. If a fresh cached result exists, your implementation is skipped entirely. + +Items are cached by path (case-insensitive). Child item lists are cached as a set of paths associated with the parent item. + +## Freshness strategies + +The `Freshness` class determines whether a cached item is "fresh enough" to use. There are four built-in strategies: + +| Strategy | Behavior | Use case | +|---|---|---| +| `Freshness.Default` | Uses cache unless `-Force` is specified | Standard reads | +| `Freshness.Guaranteed` | Uses cache only if it's less than 15 seconds old and `-Force` is not set | The default for `Get-Item` and `Get-ChildItem` commands | +| `Freshness.Fastest` | Uses cache if it's less than 4 hours old, ignoring `-Force` | Tab completion and path expansion, where speed matters more than freshness | +| `Freshness.NoPartial` | Uses cache only if the item is not marked as partial | `GetItem()` default — ensures full item data is fetched when a list API returned partial data | + +## Controlling cache behavior + +### `GetItemCommandDefaultFreshness` and `GetChildItemsCommandDefaultFreshness` + +Override these properties to change when the cache is used for `Get-Item` and `Get-ChildItem` commands: + +```csharp +public class ExpensiveHandler : PathHandler +{ + // Allow cached results for up to 15 seconds + public override Freshness GetItemCommandDefaultFreshness => Freshness.Guaranteed; + + // Use cache aggressively for child items + public override Freshness GetChildItemsCommandDefaultFreshness => Freshness.Default; +} +``` + +Both default to `Freshness.Guaranteed` (15-second TTL). + +### `CacheChildren` + +By default, child item paths are cached when `GetChildItemsImpl()` returns. This enables fast tab completion. Override this property to `false` if the number of children is very large and caching them would waste memory: + +```csharp +protected override bool CacheChildren => false; +``` + +### Partial items (`IsPartial`) + +Many APIs return less data in list operations than in detail operations. Mark list-derived items as partial so the cache knows to re-fetch when full data is needed: + +```csharp +public class ServiceItem : Item +{ + private readonly bool _isPartial; + + public ServiceItem(ItemPath parentPath, Service service, bool isPartial = false) + : base(parentPath, service) + { + _isPartial = isPartial; + } + + public override bool IsPartial => _isPartial; + // ... +} +``` + +When `GetItem()` is called with `Freshness.NoPartial` (the default), a partial cached item is treated as a cache miss, causing `GetItemImpl()` to be called to fetch the full item. + +## Cache invalidation + +After write operations (e.g., `New-Item`, `Remove-Item`), you may need to invalidate cached entries: + +```csharp +Cache.RemoveItem(path); +``` + +This removes the item and its associated child list from the cache. + +## Alias resolution + +The cache supports alias resolution via `Cache.ResolveAlias(string identifierOrAlias)`. If an item declares aliases (see [Items](items.md)), both the primary name and aliases are stored in the cache, and `ResolveAlias` maps aliases back to the canonical name. + +## See also + +- [Path Handlers](path-handlers.md) — freshness overrides and `CacheChildren` +- [Items](items.md) — `IsPartial` and `Aliases` diff --git a/docs/dependency-injection.md b/docs/dependency-injection.md new file mode 100644 index 0000000..eb5415e --- /dev/null +++ b/docs/dependency-injection.md @@ -0,0 +1,172 @@ +# Dependency Injection + +MountAnything uses [Autofac](https://autofac.readthedocs.io/) as its DI container. All `PathHandler` instances are resolved through DI, so you can inject services, route-captured values, and ancestor items into handler constructors. + +> **Prerequisite:** [Routing](routing.md), [Path Handlers](path-handlers.md) + +## Registering services + +### Top-level registration + +Register services on the `Router` that are available to all handlers: + +```csharp +public Router CreateRouter() +{ + var router = Router.Create(); + + // Microsoft.Extensions.DependencyInjection style + router.ConfigureServices(services => + { + services.AddSingleton(); + }); + + // Autofac-native style (for advanced scenarios) + router.ConfigureContainer(builder => + { + builder.Register(_ => RegionEndpoint.UsEast1); + }); + + return router; +} +``` + +### Route-scoped registration + +Register services at a specific point in the route hierarchy. These override top-level registrations for handlers at this level and below: + +```csharp +router.Map("Region", region => +{ + region.ConfigureServices((services, match) => + { + // Override the default region with the one from the URL + services.AddTransient(_ => RegionEndpoint.FromSystemName(match.Values["Region"])); + }); +}); +``` + +The `match` parameter provides access to captured route values via `match.Values["name"]`. + +**Hierarchy rule:** A registration at a child route overrides the same service registered at a parent route. For example: + +```csharp +// Default: us-east-1 for all handlers +router.ConfigureContainer(builder => builder.Register(_ => RegionEndpoint.UsEast1)); + +router.Map("Region", region => +{ + // Override: use the region from the path for handlers under /regions/* + region.ConfigureServices((services, match) => + { + services.AddTransient(_ => RegionEndpoint.FromSystemName(match.Values["Region"])); + }); +}); +``` + +## Route captures as injectable services + +Named captures from routing are automatically available for DI. When you use `Map("Name", ...)`, the captured string is registered under that name. + +### Named string captures + +```csharp +router.Map("Region", region => { ... }); +``` + +Any handler at this level or below can receive the captured value. However, since it's registered as a plain `string`, this can be ambiguous when multiple captures exist. + +### `TypedString` captures + +For type-safe injection, use `Map` which registers a strongly-typed wrapper: + +```csharp +public class Cluster : TypedString +{ + public Cluster(string value) : base(value) { } +} + +// In the router: +clusters.Map(cluster => { ... }); +``` + +Now handlers can inject `Cluster` directly: + +```csharp +public class ServiceHandler : PathHandler +{ + private readonly Cluster _cluster; + + public ServiceHandler(ItemPath path, IPathHandlerContext context, Cluster cluster) + : base(path, context) + { + _cluster = cluster; + } +} +``` + +`TypedString` has an implicit conversion to `string`, so you can use it wherever a string is expected. + +### `TypedItemPath` captures + +For `MapRecursive`, the captured multi-segment path is wrapped in a `TypedItemPath`: + +```csharp +public class S3Key : TypedItemPath +{ + public S3Key(ItemPath path) : base(path) { } +} + +router.MapRecursive(); +``` + +## Injecting ancestor items + +Sometimes a handler needs context from an item higher in the path hierarchy. Use `IItemAncestor` to inject it: + +```csharp +public class EcsServiceHandler : PathHandler +{ + private readonly IItemAncestor _cluster; + private readonly IEcsApi _ecs; + + public EcsServiceHandler(ItemPath path, IPathHandlerContext context, + IItemAncestor cluster, IEcsApi ecs) : base(path, context) + { + _cluster = cluster; + _ecs = ecs; + } + + protected override IItem? GetItemImpl() + { + var service = _ecs.DescribeService( + serviceName: ItemName, + clusterName: _cluster.Item.ItemName); + + return new EcsServiceItem(ParentPath, service); + } +} +``` + +**How it works:** When `IItemAncestor` is resolved, the `ItemAncestorResolver` walks up the path hierarchy, calling `GetItem()` on each ancestor handler until it finds one that returns an item of type `ClusterItem`. + +This requires that a handler higher in the routing hierarchy returns a `ClusterItem` from its `GetItemImpl()` method. + +## What's automatically registered + +The following services are always available for injection: + +| Service | Description | +|---|---| +| `ItemPath` | The current path being handled | +| `IPathHandlerContext` | The handler context (cache, debug logging, etc.) | +| `Router` | The router instance | +| `IItemAncestor` | Ancestor item resolver (for any `T : IItem`) | + +Any concrete class not explicitly registered is also resolved automatically (via Autofac's `AnyConcreteTypeNotAlreadyRegisteredSource`), so handler classes don't need manual registration. + +## See also + +- [Routing](routing.md) — how captures are defined +- [Path Handlers](path-handlers.md) — constructor injection +- [Advanced Topics](advanced.md) — `TypedString` and `TypedItemPath` details diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..813c4a5 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,174 @@ +# Getting Started + +MountAnything is a framework for building PowerShell providers that expose arbitrary APIs as hierarchical virtual filesystems. This guide walks through creating a provider from scratch, using the included `mount-powershell` example as a reference. + +> **Prerequisites:** .NET 6.0+ SDK, PowerShell 7+ + +## 1. Create a project and add NuGet references + +Create a new class library project and reference the two required packages: + +```xml + + +``` + +`MountAnything` is the core framework. `MountAnything.Hosting.Build` is an MSBuild integration that auto-generates the PowerShell module files at build time. + +## 2. Implement `IMountAnythingProvider` + +Create a class implementing `IMountAnythingProvider`. The only required method is `CreateRouter()`, which defines the virtual filesystem's URL-like path structure: + +```csharp +using MountAnything; +using MountAnything.Routing; + +public class MountPowershellProvider : IMountAnythingProvider +{ + public Router CreateRouter() + { + var router = Router.Create(); + router.MapLiteral("modules", modules => + { + modules.Map(module => + { + module.Map(); + }); + }); + router.MapLiteral("commands", commands => + { + commands.Map(); + }); + return router; + } + + public IEnumerable GetDefaultDrives() + { + yield return new DefaultDrive("pwsh") + { + Description = "Navigate powershell objects as a hierarchical virtual drive" + }; + } +} +``` + +`GetDefaultDrives` is optional — it automatically mounts a PSDrive when the module is imported. Without it, users must call `New-PSDrive` manually. + +## 3. Implement path handlers + +Each path in the virtual filesystem is handled by a `PathHandler` subclass. You need to implement two methods: + +- `GetItemImpl()` — returns the item at this path (or `null` if it doesn't exist) +- `GetChildItemsImpl()` — returns the child items listed by `Get-ChildItem` + +Here's a handler that lists PowerShell modules: + +```csharp +using MountAnything; + +public class ModulesHandler : PathHandler +{ + public ModulesHandler(ItemPath path, IPathHandlerContext context) : base(path, context) { } + + protected override IItem? GetItemImpl() + { + return new GenericContainerItem(ParentPath, "modules"); + } + + protected override IEnumerable GetChildItemsImpl() + { + return Context.InvokeCommand.InvokeScript("Get-Module") + .Select(m => new ModuleItem(Path, m)); + } +} +``` + +And a handler for an individual module: + +```csharp +public class ModuleHandler : PathHandler +{ + public ModuleHandler(ItemPath path, IPathHandlerContext context) : base(path, context) { } + + protected override IItem? GetItemImpl() + { + var module = Context.InvokeCommand + .InvokeScript($"Get-Module -Name {ItemName} -ErrorAction SilentlyContinue") + .SingleOrDefault(); + + return module != null ? new ModuleItem(ParentPath, module) : null; + } + + protected override IEnumerable GetChildItemsImpl() + { + return Context.InvokeCommand.InvokeScript($"Get-Command -Module {ItemName}") + .Select(c => new CommandItem(Path, c)); + } +} +``` + +See [Path Handlers](path-handlers.md) for details on optional overrides like caching control and filter support. + +## 4. Implement items + +Items represent the objects returned to the PowerShell pipeline. Subclass `Item` (or `Item` for `PSObject`-backed items) and provide: + +- `ItemName` — the virtual "filename" that identifies this item +- `IsContainer` — whether this item can have children + +```csharp +using System.Management.Automation; +using MountAnything; + +public class ModuleItem : Item +{ + public ModuleItem(ItemPath parentPath, PSObject underlyingObject) : base(parentPath, underlyingObject) + { + ItemName = underlyingObject.Property("Name")!; + } + + public override string ItemName { get; } + public override bool IsContainer => true; +} +``` + +See [Items](items.md) for details on custom properties, aliases, links, and the `[ItemProperty]` attribute. + +## 5. Build and test + +Build the project: + +```bash +dotnet build +``` + +The `MountAnything.Hosting.Build` package generates a PowerShell module in `bin/Debug/net6.0/Module/` (or whichever target framework you're using). This includes a `.psd1` manifest and the compiled provider DLL. + +Import and test the module: + +```powershell +Import-Module ./bin/Debug/net6.0/Module/MountPowershell.psd1 + +# If you implemented GetDefaultDrives, the drive is already mounted: +cd pwsh: + +# Otherwise, mount it manually: +New-PSDrive -Name pwsh -PSProvider MountPowershell -Root '' + +# Navigate the virtual filesystem +cd pwsh:\modules +Get-ChildItem +Get-Item Microsoft.PowerShell.Utility +cd Microsoft.PowerShell.Utility +Get-ChildItem # lists commands in the module +``` + +## Next steps + +- [Routing](routing.md) — all `Map` variants and route configuration +- [Path Handlers](path-handlers.md) — optional overrides and the handler context +- [Items](items.md) — custom properties, aliases, and links +- [Caching](caching.md) — freshness strategies and cache control +- [Dependency Injection](dependency-injection.md) — registering services and injecting ancestor items +- [Handler Interfaces](handler-interfaces.md) — supporting write operations (`New-Item`, `Remove-Item`, etc.) +- [Advanced Topics](advanced.md) — `ItemPath`, `LinkGenerator`, `ItemNavigator`, and more diff --git a/docs/handler-interfaces.md b/docs/handler-interfaces.md new file mode 100644 index 0000000..b9296ac --- /dev/null +++ b/docs/handler-interfaces.md @@ -0,0 +1,144 @@ +# Handler Interfaces + +By default, `PathHandler` supports `Get-Item` and `Get-ChildItem`. To support additional PowerShell commands, implement the corresponding optional interface on your handler class. + +> **Prerequisite:** [Path Handlers](path-handlers.md) + +## Item operations + +| Interface | PowerShell Command | Method | +|---|---|---| +| `INewItemHandler` | `New-Item` | `IItem NewItem(string? itemTypeName, object? newItemValue)` | +| `IRemoveItemHandler` | `Remove-Item` | `void RemoveItem()` | +| `ISetItemHandler` | `Set-Item` | `void SetItem(object value)` | +| `IClearItemHandler` | `Clear-Item` | `void ClearItem()` | +| `IInvokeDefaultActionHandler` | `Invoke-Item` | `IEnumerable? InvokeDefaultAction()` | + +### Example + +```csharp +public class BucketObjectHandler : PathHandler, INewItemHandler, IRemoveItemHandler +{ + private readonly IS3Api _s3; + + public BucketObjectHandler(ItemPath path, IPathHandlerContext context, IS3Api s3) + : base(path, context) + { + _s3 = s3; + } + + protected override IItem? GetItemImpl() { /* ... */ } + protected override IEnumerable GetChildItemsImpl() { /* ... */ } + + public IItem NewItem(string? itemTypeName, object? newItemValue) + { + _s3.PutObject(ItemName, newItemValue?.ToString()); + Cache.RemoveItem(ParentPath); // invalidate parent's child cache + return GetItem()!; + } + + public void RemoveItem() + { + _s3.DeleteObject(ItemName); + Cache.RemoveItem(Path); + Cache.RemoveItem(ParentPath); + } +} +``` + +## Content operations + +For `Get-Content` and `Set-Content` support, implement these interfaces on your handler: + +| Interface | PowerShell Command | Method | +|---|---|---| +| `IContentReaderHandler` | `Get-Content` | `IStreamContentReader GetContentReader()` | +| `IContentWriterHandler` | `Set-Content` | `IStreamContentWriter GetContentWriter()` | + +### `IStreamContentReader` + +Return an object that provides a `Stream` for reading: + +```csharp +public interface IStreamContentReader : IDisposable +{ + Stream GetContentStream(); +} +``` + +Built-in implementations: +- `StreamContentReader` — wraps any `Stream` +- `HttpResponseContentReader` — wraps an `HttpResponseMessage` +- `EmptyContentReader` — returns an empty stream + +### `IStreamContentWriter` + +Return an object that provides a writable `Stream` and a callback for when writing is complete: + +```csharp +public interface IStreamContentWriter +{ + Stream GetWriterStream(); + void WriterFinished(Stream stream); +} +``` + +Built-in implementation: +- `StreamContentWriter` — writes to a `MemoryStream` and calls your callback with the result + +### Example + +```csharp +public class FileHandler : PathHandler, IContentReaderHandler, IContentWriterHandler +{ + public IStreamContentReader GetContentReader() + { + var stream = FetchFileContent(); + return new StreamContentReader(stream); + } + + public IStreamContentWriter GetContentWriter() + { + return new StreamContentWriter(stream => + { + UploadFileContent(stream); + Cache.RemoveItem(Path); + }); + } +} +``` + +## Item property operations + +| Interface | PowerShell Command | Method | +|---|---|---| +| `ISetItemPropertiesHandler` | `Set-ItemProperty` | `void SetItemProperties(PSObject properties)` | +| `IClearItemPropertiesHandler` | `Clear-ItemProperty` | `void ClearItemProperties(IEnumerable propertyNames)` | +| `INewItemPropertyHandler` | `New-ItemProperty` | `void NewItemProperty(string name, string? typeName, object value)` | +| `IRemoveItemPropertyHandler` | `Remove-ItemProperty` | `void RemoveItemProperty(string name)` | + +## Dynamic parameters + +Each command has a corresponding dynamic parameters interface that lets you add custom PowerShell parameters: + +| Interface | For Command | +|---|---| +| `IGetItemParameters` | `Get-Item` | +| `IGetChildItemParameters` | `Get-ChildItem` | +| `INewItemParameters` | `New-Item` | +| `IRemoveItemParameters` | `Remove-Item` | +| `ISetItemParameters` | `Set-Item` | +| `IClearItemParameters` | `Clear-Item` | +| `IInvokeDefaultActionParameters` | `Invoke-Item` | +| `IGetItemPropertiesParameters` | `Get-ItemProperty` | +| `ISetItemPropertiesParameters` | `Set-ItemProperty` | +| `IClearItemPropertiesParameters` | `Clear-ItemProperty` | +| `INewItemPropertyParameters` | `New-ItemProperty` | +| `IRemoveItemPropertyParameters` | `Remove-ItemProperty` | + +The type parameter `T` is a class whose properties are decorated with `[Parameter]` attributes. These become additional parameters on the PowerShell command when the provider is active. + +## See also + +- [Path Handlers](path-handlers.md) — the base handler class +- [Caching](caching.md) — invalidating cache after write operations diff --git a/docs/items.md b/docs/items.md new file mode 100644 index 0000000..f5b7d65 --- /dev/null +++ b/docs/items.md @@ -0,0 +1,167 @@ +# Items + +Items are the objects returned to the PowerShell pipeline by `Get-Item` and `Get-ChildItem`. They wrap an underlying .NET object and define how it appears in the virtual filesystem. + +> **Prerequisite:** [Path Handlers](path-handlers.md) + +## `Item` — wrapping a typed object + +The most common base class. The type parameter `T` is the .NET type of the object being wrapped: + +```csharp +public class SecurityGroupItem : Item +{ + public SecurityGroupItem(ItemPath parentPath, SecurityGroup sg) : base(parentPath, sg) { } + + public override string ItemName => UnderlyingObject.GroupId; + public override bool IsContainer => false; +} +``` + +All public properties on `T` are automatically written to the PowerShell pipeline object. + +## `Item` — wrapping a `PSObject` + +A convenience subclass of `Item` for when you're working directly with PowerShell objects (e.g., results from `InvokeScript`): + +```csharp +public class ModuleItem : Item +{ + public ModuleItem(ItemPath parentPath, PSObject underlyingObject) : base(parentPath, underlyingObject) + { + ItemName = underlyingObject.Property("Name")!; + } + + public override string ItemName { get; } + public override bool IsContainer => true; +} +``` + +The non-generic `Item` also provides a helper `Property(string name)` method for accessing properties on the underlying `PSObject`. + +## `IItem` — lightweight interface + +For simple cases (like a root node that doesn't wrap a real object), you can implement `IItem` directly: + +```csharp +public class RootItem : IItem +{ + public ItemPath FullPath => ItemPath.Root; + public bool IsContainer => true; + + public PSObject ToPipelineObject(Func pathResolver) + { + return new PSObject(); + } +} +``` + +## Required members + +| Member | Description | +|---|---| +| `ItemName` | The virtual "filename" that identifies this item. Prefer human-friendly names if they're guaranteed to be unique. | +| `IsContainer` | Whether this item can have children (determines if `cd` works on it). | + +## Optional members + +| Member | Default | Description | +|---|---|---| +| `IsPartial` | `false` | Marks the item as a partial representation (e.g., from a list API that returns fewer fields than a detail API). The cache uses this to decide when to refresh. | +| `ItemType` | `null` | A type string added to the pipeline object (e.g., `"Directory"`, `"File"`). | +| `TypeName` | The item class's full name | Controls the PowerShell type name on the pipeline object. | +| `Aliases` | Empty | Alternative names that resolve to this item (useful for IDs vs. display names). | +| `CustomizePSObject(PSObject)` | No-op | Hook to modify the pipeline object before it's returned. | + +## Adding custom properties + +### `[ItemProperty]` attribute + +The simplest way to add properties to the pipeline object. Decorate a public property on your item class: + +```csharp +public class ClusterItem : Item +{ + public ClusterItem(ItemPath parentPath, Cluster cluster) : base(parentPath, cluster) { } + + public override string ItemName => UnderlyingObject.Name; + public override bool IsContainer => true; + + [ItemProperty] + public int RunningTaskCount => UnderlyingObject.RunningTasksCount; + + [ItemProperty("Status")] + public string ClusterStatus => UnderlyingObject.Status; +} +``` + +The optional `PropertyName` parameter lets you control the property name as it appears in PowerShell. Without it, the C# property name is used. + +### `CustomizePSObject` + +For more control, override `CustomizePSObject` to modify the `PSObject` directly: + +```csharp +protected override void CustomizePSObject(PSObject psObject) +{ + psObject.Properties.Add(new PSNoteProperty("ComputedField", ComputeSomething())); +} +``` + +## Links and cross-references + +Items can link to related items at different paths in the virtual filesystem. This is useful when an item logically relates to items elsewhere in the hierarchy. + +### `Links` + +A dictionary of named links to other `IItem` instances. The linked items are embedded as nested objects on the pipeline output, and their paths appear in the `Links` property: + +```csharp +public class ServiceItem : Item +{ + public ServiceItem(ItemPath parentPath, Service service, TaskDefinitionItem taskDef) + : base(parentPath, service) + { + Links = new Dictionary + { + ["TaskDefinition"] = taskDef + }; + } +} +``` + +### `LinkPaths` + +A lighter-weight alternative that stores just the path without embedding the full item: + +```csharp +public class ServiceItem : Item +{ + public ServiceItem(ItemPath parentPath, Service service, LinkGenerator linkGenerator) + : base(parentPath, service) + { + LinkPaths = new Dictionary + { + ["TaskDefinition"] = linkGenerator.ConstructPath(2, $"task-definitions/{service.TaskDefinition}") + }; + } +} +``` + +Both `Links` and `LinkPaths` are surfaced through a `Links` property on the pipeline object, making cross-references navigable in PowerShell. + +## Aliases + +Items can declare alternative names that resolve to the same item. This is useful when resources have both a human-friendly name and a technical ID: + +```csharp +public override IEnumerable Aliases => new[] { UnderlyingObject.Arn }; +``` + +The cache stores entries for all aliases, so users can navigate by either name. + +## See also + +- [Path Handlers](path-handlers.md) — the handlers that return items +- [Caching](caching.md) — how `IsPartial` and aliases interact with the cache +- [Advanced Topics](advanced.md) — `LinkGenerator` for constructing cross-reference paths diff --git a/docs/path-handlers.md b/docs/path-handlers.md new file mode 100644 index 0000000..6200eca --- /dev/null +++ b/docs/path-handlers.md @@ -0,0 +1,121 @@ +# Path Handlers + +A `PathHandler` processes PowerShell provider commands (`Get-Item`, `Get-ChildItem`, etc.) for a matched path. When the [Router](routing.md) matches a path, it resolves the corresponding handler and delegates the command to it. + +> **Prerequisite:** [Routing](routing.md) + +## The `PathHandler` base class + +Inherit from `PathHandler` rather than implementing `IPathHandler` directly. The base class provides automatic caching, debug logging, and convenience properties: + +```csharp +public class ModuleHandler : PathHandler +{ + public ModuleHandler(ItemPath path, IPathHandlerContext context) : base(path, context) { } + + protected override IItem? GetItemImpl() + { + var module = Context.InvokeCommand + .InvokeScript($"Get-Module -Name {ItemName} -ErrorAction SilentlyContinue") + .SingleOrDefault(); + + return module != null ? new ModuleItem(ParentPath, module) : null; + } + + protected override IEnumerable GetChildItemsImpl() + { + return Context.InvokeCommand.InvokeScript($"Get-Command -Module {ItemName}") + .Select(c => new CommandItem(Path, c)); + } +} +``` + +## Required methods + +| Method | Called by | Purpose | +|---|---|---| +| `GetItemImpl()` | `Get-Item` | Return the item at this path, or `null` if it doesn't exist | +| `GetChildItemsImpl()` | `Get-ChildItem` | Return all child items of this path | + +## Optional overrides + +| Member | Default | Purpose | +|---|---|---| +| `ExistsImpl()` | Calls `GetItem(Freshness.Fastest)` | Override for a more efficient existence check | +| `GetChildItems(string filter)` | Calls `GetChildItemsImpl()` then filters | Override to support efficient server-side filtering (used by tab completion and `-Filter`) | +| `CacheChildren` | `true` | Set to `false` if child items are too numerous to cache | +| `GetItemCommandDefaultFreshness` | `Freshness.Guaranteed` | Controls when `Get-Item` uses cached results | +| `GetChildItemsCommandDefaultFreshness` | `Freshness.Guaranteed` | Controls when `Get-ChildItem` uses cached results | + +See [Caching](caching.md) for details on freshness strategies. + +## Available properties + +These properties are available in any `PathHandler` subclass: + +| Property | Type | Description | +|---|---|---| +| `Path` | `ItemPath` | The full path being handled | +| `ParentPath` | `ItemPath` | The parent of the current path | +| `ItemName` | `string` | The last segment of the path (the "filename") | +| `Context` | `IPathHandlerContext` | Access to cache, PowerShell engine, and debug logging | +| `Cache` | `Cache` | Shorthand for `Context.Cache` | +| `LinkGenerator` | `LinkGenerator` | Helper for constructing cross-reference paths | + +## `IPathHandlerContext` + +The context provides access to the PowerShell engine and provider state: + +| Member | Type | Description | +|---|---|---| +| `Cache` | `Cache` | The in-memory item cache | +| `WriteDebug(string)` | — | Write a debug message (visible with `-Debug` flag) | +| `WriteWarning(string)` | — | Write a warning message | +| `Force` | `bool` | Whether the `-Force` flag was specified | +| `InvokeCommand` | `CommandInvocationIntrinsics` | Execute PowerShell commands from within a handler | +| `DriveInfo` | `PSDriveInfo` | The current PSDrive (useful for accessing drive-level configuration) | + +### Calling PowerShell from handlers + +Use `Context.InvokeCommand.InvokeScript()` to execute PowerShell commands and get results as `PSObject` collections: + +```csharp +protected override IEnumerable GetChildItemsImpl() +{ + return Context.InvokeCommand.InvokeScript("Get-Module") + .Select(m => new ModuleItem(Path, m)); +} +``` + +## Constructor injection + +Handler constructors support dependency injection. Besides the required `ItemPath` and `IPathHandlerContext`, you can inject any service registered with the router: + +```csharp +public class ServiceHandler : PathHandler +{ + private readonly IItemAncestor _cluster; + private readonly IEcsApi _ecs; + + public ServiceHandler(ItemPath path, IPathHandlerContext context, + IItemAncestor cluster, IEcsApi ecs) : base(path, context) + { + _cluster = cluster; + _ecs = ecs; + } +} +``` + +See [Dependency Injection](dependency-injection.md) for details on service registration, route captures, and ancestor item injection. + +## Write operations + +By default, handlers only support read operations (`Get-Item`, `Get-ChildItem`). To support commands like `New-Item`, `Remove-Item`, `Get-Content`, etc., implement optional handler interfaces. See [Handler Interfaces](handler-interfaces.md). + +## See also + +- [Routing](routing.md) — how paths are matched to handlers +- [Items](items.md) — the objects handlers return +- [Caching](caching.md) — controlling cache freshness +- [Dependency Injection](dependency-injection.md) — constructor injection +- [Handler Interfaces](handler-interfaces.md) — write operations diff --git a/docs/routing.md b/docs/routing.md new file mode 100644 index 0000000..2290ef4 --- /dev/null +++ b/docs/routing.md @@ -0,0 +1,170 @@ +# Routing + +The `Router` maps virtual filesystem paths to `PathHandler` types using a composable, regex-based routing system. Every path that a user navigates to is matched against the router to determine which handler processes the command. + +## Creating a router + +Create a router in your `IMountAnythingProvider.CreateRouter()` method. The root handler is the handler invoked when the user is at the drive root: + +```csharp +public Router CreateRouter() +{ + var router = Router.Create(); + // define child routes here + return router; +} +``` + +## Map methods + +Routes form a nested hierarchy. At each level, you choose a mapping method that determines how a path segment is matched. + +### `MapLiteral` + +Matches an exact, case-insensitive string: + +```csharp +router.MapLiteral("modules", modules => +{ + // child routes under /modules/... +}); +``` + +Only the literal string `modules` will match. This is ideal for fixed navigation nodes like `ecs`, `s3`, `modules`, etc. + +### `Map` + +Matches any path segment using the pattern `[a-z0-9-_.:]+`: + +```csharp +modules.Map(module => +{ + // child routes under /modules//... +}); +``` + +This is the most common way to match dynamic path segments (resource names, IDs, etc.). + +### `Map` with named capture + +Captures the matched value under a name so it can be injected into handler constructors via dependency injection: + +```csharp +router.Map("Region", region => +{ + // "Region" captured value is now available for DI +}); +``` + +The captured value is registered as a `string` keyed by the name `"Region"`. Any `PathHandler` at this level or below can receive it through constructor injection. + +### `Map` + +Like named capture, but wraps the value in a strongly-typed `TypedString` subclass: + +```csharp +router.Map(cluster => +{ + // Cluster typed string is available for DI +}); +``` + +Where `Cluster` is: + +```csharp +public class Cluster : TypedString +{ + public Cluster(string value) : base(value) { } +} +``` + +The type name is used as the capture name. This gives you type safety when injecting route values — you can distinguish between a `Cluster` and a `ServiceName` even though both are strings. See [Dependency Injection](dependency-injection.md) for more on `TypedString`. + +### `MapRegex` + +The low-level method that all other `Map` methods call under the hood. Accepts any regex pattern: + +```csharp +router.MapRegex("(?[a-z0-9-]+)", region => +{ + // child routes +}); +``` + +**Important:** Do not include `^` or `$` anchors — they are added automatically by the router. Named capture groups in the regex are available for dependency injection. + +### `MapRecursive` + +Matches a multi-segment path (including `/` separators), useful for tree structures like file paths or hierarchical keys: + +```csharp +router.MapRecursive(); +``` + +Where `S3Key` extends `TypedItemPath`: + +```csharp +public class S3Key : TypedItemPath +{ + public S3Key(ItemPath path) : base(path) { } +} +``` + +The entire remaining path is captured as a `TypedItemPath` and injected into the handler. + +## How routes compose + +Routes are hierarchically composed. When a child route is defined inside a parent route, the parent's regex pattern is prepended with a `/` separator: + +```csharp +router.MapLiteral("clusters", clusters => +{ + clusters.Map(); // matches: clusters/[a-z0-9-_.:]+ +}); +``` + +The router tries child routes first (most specific match), then falls back to the parent. This means deeper, more specific routes take priority. + +## Full example + +From the [MountAws](https://github.com/andyalm/mount-aws) project: + +```csharp +router.MapRegex("(?[a-z0-9-]+)", region => +{ + region.MapLiteral("ecs", ecs => + { + ecs.MapLiteral("task-definitions", taskDefinitions => + { + taskDefinitions.Map(); + }); + ecs.MapLiteral("clusters", clusters => + { + clusters.Map(cluster => + { + cluster.MapLiteral("services", services => + { + services.Map(); + }); + }); + }); + }); +}); +``` + +This creates a filesystem like: + +``` +/us-east-1/ecs/task-definitions/my-task +/us-east-1/ecs/clusters/my-cluster/services/my-service +``` + +## Route-level service registration + +You can register services at any point in the routing hierarchy. See [Dependency Injection](dependency-injection.md) for details. + +## See also + +- [Getting Started](getting-started.md) — creating your first provider +- [Path Handlers](path-handlers.md) — what handlers do once a route matches +- [Dependency Injection](dependency-injection.md) — registering services and injecting route captures From 752822e0940d49b1315a54103d8fc8ce773de3cd Mon Sep 17 00:00:00 2001 From: Andy Alm Date: Sat, 28 Feb 2026 11:57:54 -0800 Subject: [PATCH 8/8] docs: Update README with expanded provider examples, clarify dependency usage, refine item property examples, and update target framework to net8.0. --- README.md | 14 ++++++++------ docs/getting-started.md | 6 +++--- docs/items.md | 9 +++------ 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index fbfe2fe..12d096a 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ # MountAnything -A framework for building PowerShell providers that expose arbitrary APIs as hierarchical virtual filesystems. Navigate any API with familiar commands like `cd`, `ls`, `Get-Item`, and `Get-ChildItem`. +A framework for building PowerShell providers that expose arbitrary APIs as hierarchical virtual filesystems. Navigate any API with familiar commands like `cd`, `ls`, `Get-Item`, and `Get-Content`. -The primary consumer of this framework is [MountAws](https://github.com/andyalm/mount-aws), which exposes AWS services as a virtual drive. +Some example providers built with this framework include: + +- [MountAws](https://github.com/andyalm/mount-aws) - AWS services +- [MountGitlab](https://github.com/andyalm/mount-gitlab) - Gitlab +- [MountConsul](https://github.com/andyalm/mount-consul) - Consul KV store +- [MountVault](https://github.com/andyalm/mount-vault) - HashiCorp Vault +- [MountArtifactory](https://github.com/andyalm/mount-artifactory) - JFrog Artifactory ## What it looks like @@ -13,10 +19,6 @@ ls # Inspect a specific resource Get-Item my-service - -# Or navigate PowerShell itself (included example project) -cd pwsh:\modules\Microsoft.PowerShell.Utility -ls # lists commands in the module ``` ## Quick start diff --git a/docs/getting-started.md b/docs/getting-started.md index 813c4a5..214d162 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -13,7 +13,7 @@ Create a new class library project and reference the two required packages: ``` -`MountAnything` is the core framework. `MountAnything.Hosting.Build` is an MSBuild integration that auto-generates the PowerShell module files at build time. +`MountAnything` is the core framework. `MountAnything.Hosting.Build` is an MSBuild integration that auto-generates the PowerShell module files at build time. The main project should reference both `MountAnything.Hosting.Build` and `MountAnything`. If you want to create supporting class libraries, they can just reference `MountAnything`. ## 2. Implement `IMountAnythingProvider` @@ -142,12 +142,12 @@ Build the project: dotnet build ``` -The `MountAnything.Hosting.Build` package generates a PowerShell module in `bin/Debug/net6.0/Module/` (or whichever target framework you're using). This includes a `.psd1` manifest and the compiled provider DLL. +The `MountAnything.Hosting.Build` package generates a PowerShell module in `bin/Debug/net8.0/Module/` (or whichever target framework you're using). This includes a `.psd1` manifest and the compiled provider DLL. Import and test the module: ```powershell -Import-Module ./bin/Debug/net6.0/Module/MountPowershell.psd1 +Import-Module ./bin/Debug/net8.0/Module/MountPowershell.psd1 # If you implemented GetDefaultDrives, the drive is already mounted: cd pwsh: diff --git a/docs/items.md b/docs/items.md index f5b7d65..e082ec9 100644 --- a/docs/items.md +++ b/docs/items.md @@ -88,10 +88,7 @@ public class ClusterItem : Item public override bool IsContainer => true; [ItemProperty] - public int RunningTaskCount => UnderlyingObject.RunningTasksCount; - - [ItemProperty("Status")] - public string ClusterStatus => UnderlyingObject.Status; + public bool HasRunningTasks => UnderlyingObject.RunningTasksCount > 0; } ``` @@ -152,10 +149,10 @@ Both `Links` and `LinkPaths` are surfaced through a `Links` property on the pipe ## Aliases -Items can declare alternative names that resolve to the same item. This is useful when resources have both a human-friendly name and a technical ID: +Items can declare alternative names that resolve to the same item. This is useful when resources have more than one way of uniquely identifying them: ```csharp -public override IEnumerable Aliases => new[] { UnderlyingObject.Arn }; +public override IEnumerable Aliases => new[] { UnderlyingObject.AnotherId }; ``` The cache stores entries for all aliases, so users can navigate by either name.