{"id":49245801,"url":"https://github.com/nullean/argh","last_synced_at":"2026-05-11T18:04:32.356Z","repository":{"id":352084212,"uuid":"1204180681","full_name":"nullean/argh","owner":"nullean","description":"A source generator for command line parsing and command invocations.","archived":false,"fork":false,"pushed_at":"2026-05-04T20:49:33.000Z","size":1420,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-04T21:04:59.316Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nullean.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-07T19:09:26.000Z","updated_at":"2026-05-04T20:49:15.000Z","dependencies_parsed_at":"2026-04-24T22:00:25.146Z","dependency_job_id":null,"html_url":"https://github.com/nullean/argh","commit_stats":null,"previous_names":["nullean/argh"],"tags_count":32,"template":false,"template_full_name":null,"purl":"pkg:github/nullean/argh","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nullean%2Fargh","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nullean%2Fargh/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nullean%2Fargh/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nullean%2Fargh/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nullean","download_url":"https://codeload.github.com/nullean/argh/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nullean%2Fargh/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32906515,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-11T17:09:15.040Z","status":"ssl_error","status_checked_at":"2026-05-11T17:08:45.420Z","response_time":120,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-04-24T22:00:17.253Z","updated_at":"2026-05-11T18:04:32.343Z","avatar_url":"https://github.com/nullean.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Nullean.Argh\n\nBuild full-featured .NET CLIs without writing a parser.\n\nMethods become commands, XML docs become help text, records become option sets. A Roslyn source generator emits parsing, routing, dispatch, and help into your assembly at build time — no reflection, no runtime overhead, trimming- and AOT-safe by default.\n\nWrite vanilla C# and get a fully functional CLI in return: rich `--help` output, shell tab-completions for bash, zsh, and fish, and a machine-readable JSON schema ready for agentic use cases — all without writing a single line of plumbing code for any of it.\n\n***Heavily** Inspired by [ConsoleAppFramework](https://github.com/Cysharp/ConsoleAppFramework) (Cysharp) — rewritten from scratch with a different feature set, but ConsoleAppFramework laid out the path for source-generated CLI's in .NET.*\n\n![Sample CLI help output (XmlDocShowcase)](https://cdn.jsdelivr.net/gh/nullean/argh@main/docs/assets/xml-doc-showcase-help.gif)\n\n**Table of contents**\n\n- [Features](#features)\n- [Packages](#packages)\n- [Quick start](#quick-start)\n- [Registration model](#registration-model)\n- [Namespaces](#namespaces)\n- [Parameters and binding](#parameters-and-binding)\n  - [Arguments (positional)](#arguments-positional)\n  - [Flags (named options)](#flags-named-options)\n  - [Long name override](#long-name-override)\n  - [Supported types](#supported-types)\n- [CancellationToken (Ctrl+C)](#cancellationtoken-ctrlc)\n- [Object binding](#object-binding)\n- [Fuzzy matching](#fuzzy-matching)\n- [Help and XML documentation](#help-and-xml-documentation)\n- [Middleware](#middleware)\n- [Dependency injection](#dependency-injection)\n- [Hosting](#hosting)\n  - [Intrinsic commands and log suppression](#intrinsic-commands-and-log-suppression)\n- [Routing API](#routing-api)\n- [Validation](#validation)\n- [Shell completions](#shell-completions)\n- [Schema JSON](#schema-json)\n- [License and links](#license-and-links)\n\n## Features\n\n- **XML docs are your help text**\n  - Summaries, param descriptions, remarks, and `\u003cexample\u003e` blocks appear in `--help` automatically\n  - No separate attribute layer, no string duplication\n- **Everything is generated C#**\n  - Typed dispatch tree, option parsers, and help printers emitted directly into your assembly\n  - Read it, step through it in a debugger, ship it trimmed or AOT-compiled\n- **`MapGroup`-style namespaces**\n  - Nested command groups with their own help pages and scoped option types\n  - Immediately familiar if you've used ASP.NET minimal APIs\n- **DTO binding with `[AsParameters]`**\n  - Records and classes expand into flags without a custom bind loop\n  - Optional prefix (`[AsParameters(\"app\")]`) namespaces all long names\n- **Shell completions built-in**\n  - Generated lookup tables for subcommands, namespaces, and flags — no extra package\n  - One install command per shell (bash, zsh, fish)\n- **Agent-ready schema**\n  - `myapp __schema` emits a full JSON description of commands, options, summaries, and examples\n  - Feed it to an LLM, a docs generator, or diff it in CI to catch breaking changes\n- **Fuzzy matching**\n  - Typos produce actionable errors with the correct qualified path and a `--help` suggestion\n  - No silent no-match\n- **DataAnnotations validation**\n  - Annotate parameters and DTO members with `[Range]`, `[StringLength]`, `[RegularExpression]`, `[AllowedValues]`, and more\n  - `[MinLength]` / `[MaxLength]` on a collection validates item count; on a string validates string length\n  - Constraints appear in `--help`; violations print to stderr and exit with code 2 — no reflection, no runtime dependency\n- **Filesystem path validation** *(for `FileInfo` / `DirectoryInfo`)*\n  - **`[Existing]`** / **`[NonExisting]`** — file vs. directory checks follow the parameter type (`File.Exists` / `Directory.Exists`, or both absent for non-existing)\n  - **`[ExpandUserProfile]`** — resolves `~/` before `FileInfo` / `DirectoryInfo` construction (`Path.GetFullPath` after replacing the profile prefix)\n  - **`[RejectSymbolicLinks]`** — rejects symlink / reparse-point paths (combined with existence checks where needed)\n- **Cancellation on command handlers**\n  - Add `CancellationToken` to a handler signature — it is injected, not a flag; by default it tracks **Ctrl+C** (console cancel)\n- **Zero-dep or ME.* native**\n  - `Nullean.Argh` — no `Microsoft.Extensions.*` dependency\n  - `Nullean.Argh.Hosting` — same registration surface, plugs into `IHost` and DI\n\n## Packages\n\n**Which package do I need?** \n\n* [`Nullean.Argh`](https://www.nuget.org/packages/Nullean.Argh) dependency free version. \n* [`Nullean.Argh.Hosting`](https://www.nuget.org/packages/Nullean.Argh.Hosting) isolated implementation of `.Core` that fully integrates with `Microsoft.Extensions.*` ecosystem.\n\nEverything else is pulled in transitively, you do not reference `.Core` or `.Interfaces` manually for normal apps. \nThe two packages are isolated implementations and both only depend on `.Core`.\n\n* [`Nullean.Argh.Core`](https://www.nuget.org/packages/Nullean.Argh.Core) Shared runtime pulled in by both user-facing packages. Contains `ArghApp`, runtime, help, and the embedded source generator. Not referenced directly in normal apps.\n* [`Nullean.Argh.Interfaces`](https://www.nuget.org/packages/Nullean.Argh.Interfaces) Reference directly only when building a shared library (e.g. reusable middleware or parsers) that other Argh-based apps will consume. Contains attributes, `IArghBuilder`, and middleware/parser contracts. Zero external dependencies.\n\n**`Nullean.Argh.Generator`** is not a separate NuGet package — it ships embedded inside `Nullean.Argh.Core` under `analyzers/dotnet/cs`.\n\n**Console app**\n\n```xml\n\u003cItemGroup\u003e\n  \u003cPackageReference Include=\"Nullean.Argh\" /\u003e\n\u003c/ItemGroup\u003e\n```\n\n**Hosted app**\n\n```xml\n\u003cItemGroup\u003e\n  \u003cPackageReference Include=\"Nullean.Argh.Hosting\" /\u003e\n\u003c/ItemGroup\u003e\n```\n\n**Shared middleware / parser library**\n\n```xml\n\u003cItemGroup\u003e\n  \u003cPackageReference Include=\"Nullean.Argh.Interfaces\" /\u003e\n\u003c/ItemGroup\u003e\n```\n\n## Quick start\n\n### Console app (`Nullean.Argh`)\n\n```csharp\nusing Nullean.Argh;\n\nvar app = new ArghApp();\napp.Map(\"hello\", MyHandlers.SayHello);\n\nreturn await app.RunAsync(args);\n```\n\n[`RunAsync`](src/Nullean.Argh.Core/Runtime/ArghRuntime.cs) dispatches into generated code in your assembly.\n\n### Hosted app (`Nullean.Argh.Hosting`)\n\nUse when the app is already built on `Microsoft.Extensions.Hosting` and you want commands and middleware registered in DI with lifetimes, `CancellationToken` linked to the host, etc.\n\n```csharp\nusing Microsoft.Extensions.Hosting;\nusing Nullean.Argh.Hosting;\n\nvar builder = Host.CreateApplicationBuilder(args);\n\nbuilder.Services.AddArgh(args, b =\u003e\n{\n    b.Map(\"hello\", MyHandlers.SayHello);\n    // b.Map\u003cMyCommandHandlers\u003e(); b.UseGlobalOptions\u003cMyGlobals\u003e(); …\n});\n\nawait builder.Build().RunAsync();\n```\n\nSee [`AddArgh`](src/Nullean.Argh.Hosting/ArghHostingExtensions.cs) for exit behavior and hosted-service ordering.\n\n## Registration model\n\nThree forms, same registration surface — all are fully supported. With class and method-group registration, XML doc comments on your handler methods flow directly into `--help` output. Lambdas skip that path.\n\n```csharp\n// 1. Method group — direct typed dispatch.\napp.Map(\"deploy\", DeployHandlers.Run);\n\n// 2. Lambda — convenient for simple one-liners.\napp.Map(\"greet\", (string name) =\u003e Console.WriteLine($\"Hello, {name}!\"));\n\n// 3. Class — registers every public method on T as a command.\napp.Map\u003cStorageHandlers\u003e();\n```\n\n| API | Purpose |\n|-----|---------|\n| `Map(name, handler)` | Bind a command name to a delegate. |\n| `Map\u003cT\u003e()` | Register every public method on `T` as a command (typically a static class of handlers). |\n| `MapRoot(handler)` | Default handler when no subcommand is given (at app root, or inside a `MapNamespace` callback for that namespace). |\n\nFlat apps route `app \u003ccommand\u003e …`; hierarchical apps route `app \u003cnamespace\u003e … \u003ccommand\u003e …`. The generator emits the switch/dispatch tree accordingly.\n\n## Namespaces\n\nGroup related commands under a shared path, scoped options, and their own help page — the same mental model as ASP.NET's `MapGroup`.\n\nThe idiomatic pattern is to put commands as methods on the class. Nested sub-groups are registered explicitly — there is no auto-discovery of nested types.\n\n```csharp\n/// \u003csummary\u003eCommands under \u003cc\u003estorage\u003c/c\u003e.\u003c/summary\u003e\ninternal sealed class StorageCommands\n{\n    /// \u003csummary\u003eList objects in the bucket.\u003c/summary\u003e\n    public void List() =\u003e Console.WriteLine(\"storage:list\");\n}\n\n/// \u003csummary\u003eCommands under \u003cc\u003estorage blob\u003c/c\u003e.\u003c/summary\u003e\ninternal sealed class BlobCommands\n{\n    /// \u003csummary\u003eUpload a file.\u003c/summary\u003e\n    /// \u003cparam name=\"path\"\u003e-p,--path, Local file path.\u003c/param\u003e\n    public void Upload(string path) =\u003e Console.WriteLine($\"storage:blob:upload:{path}\");\n\n    /// \u003csummary\u003eDownload a file.\u003c/summary\u003e\n    /// \u003cparam name=\"key\"\u003e-k,--key, Object key.\u003c/param\u003e\n    public void Download(string key) =\u003e Console.WriteLine($\"storage:blob:download:{key}\");\n}\n\napp.AddNamespace\u003cStorageCommands\u003e(\"storage\", ns =\u003e\n{\n    ns.AddNamespace\u003cBlobCommands\u003e(\"blob\");\n});\n// Resulting paths:\n//   storage list\n//   storage blob upload --path ./file.txt\n//   storage blob download --key backups/db.sql\n```\n\nThe generator produces separate help printers for the namespace overview and each leaf command. Add `CommandNamespaceOptions\u003cT\u003e()` inside the callback to attach scoped options:\n\n```csharp\napp.AddNamespace\u003cStorageCommands\u003e(\"storage\", ns =\u003e\n{\n    ns.CommandNamespaceOptions\u003cStorageOptions\u003e();\n    ns.AddNamespace\u003cBlobCommands\u003e(\"blob\");\n});\n```\n\n## Parameters and binding\n\nMethod parameters become CLI flags automatically. No attribute boilerplate for the common case.\n\n### Arguments (positional)\n\nMark a parameter with `[Argument]` to make it positional. Indices must start at `0` and be consecutive.\n\n```csharp\npublic static Task\u003cint\u003e Deploy([Argument] string environment) { … }\n// myapp deploy production\n```\n\n**Variadic positional** — combine `[Argument]` with a `T[]` type (or `params T[]`) to collect all remaining tokens into an array. The variadic argument must be the last positional. Because C# requires `params` to be the last method parameter, a variadic positional can appear after flags:\n\n```csharp\n// All remaining tokens become the array — zero items is valid.\npublic static void Copy([Argument] string dest, [Argument] params string[] files) { … }\n// myapp copy ./out/ a.txt b.txt \"path with spaces/c.txt\"\n// → dest=\"./out/\", files=[\"a.txt\", \"b.txt\", \"path with spaces/c.txt\"]\n\n// Flags can appear before or after variadic tokens on the command line.\npublic static void Archive([Argument] params string[] files, bool verbose = false) { … }\n// myapp archive a.zip b.zip --verbose   (verbose parsed as flag, the rest as files)\n\n// Mixed: scalar positional first, then flags, then variadic (params must be last in C#).\npublic static void Tag([Argument] string target, bool force, [Argument] params string[] tags) { … }\n// myapp tag main-branch --force tag1 tag2 tag3\n```\n\nVariadic positionals appear as `\u003cfiles...\u003e` / `[\u003cfiles...\u003e]` in `--help` and include a `[variadic]` annotation. Apply `[MinLength(n)]` or `[MaxLength(n)]` to validate the item count:\n\n```csharp\npublic static void Archive([Argument][MinLength(1)][MaxLength(20)] string[] files) { … }\n// --help: \u003cfiles...\u003e   [variadic] [count: 1–20]\n```\n\n### Flags (named options)\n\nParameters without `[Argument]` become `--kebab-case` long flags. A `bool` flag defaults to `false`; pass `--flag` to set it.\n\n```csharp\npublic static Task\u003cint\u003e Build(string outputDir, bool release = false) { … }\n// myapp build --output-dir ./bin --release\n```\n\n### Long name override\n\nBy default the CLI long name is derived from the C# parameter name (camelCase → kebab-case). Place a `--long-name` token before the description in an XML `\u003cparam\u003e` tag to use a different primary name. Additional `--names` after the first become aliases. The derived name is dropped entirely once an explicit long name is specified.\n\n```csharp\n/// \u003csummary\u003eTag one or more resources.\u003c/summary\u003e\n/// \u003cparam name=\"tags\"\u003e-t, --tag, Tags to apply.\u003c/param\u003e\npublic static void Tag(string[] tags) { … }\n// --tag a --tag b   (NOT --tags — derived name is dropped)\n// -t a              (short opt also works)\n```\n\n```csharp\n/// \u003cparam name=\"outputDir\"\u003e-o, --out, --output, Output directory.\u003c/param\u003e\npublic static void Build(string outputDir) { … }\n// --out ./bin        (primary)\n// --output ./bin     (alias)\n// -o ./bin           (short opt)\n// --output-dir       (not recognized — derived name is dropped)\n```\n\nThis also works on `[AsParameters]` properties, fields, and `[AsParameters]` primary-constructor parameters via their `\u003csummary\u003e` or `\u003cparam\u003e` doc lines.\n\n### Supported types\n\n| Category | Types |\n|----------|-------|\n| Primitives | `string`, `int`, `long`, `double`, `float`, `decimal`, `bool`, `bool?` |\n| System | `enum`, `FileInfo`, `DirectoryInfo`, `Uri` |\n| Collections | `List\u003cT\u003e`, `T[]` — repeated flag, `[CollectionSyntax(Separator=\",\")]` for a single comma-separated value, or `[Argument] T[]` / `[Argument] params T[]` for a variadic positional |\n\nCollections accept the flag multiple times, or a single comma-separated value via `[CollectionSyntax]`:\n\n```csharp\npublic static Task\u003cint\u003e Deploy(string[] targets, [CollectionSyntax(Separator = \",\")] string[] tags) { … }\n// Repeated:   myapp deploy --targets web --targets api\n// Separator:  myapp deploy --targets web,api --tags blue,green\n```\n\n### Nullable bool — `--flag` / `--no-flag` pairs\n\nA `bool?` flag generates **both** `--flag` (sets `true`) and `--no-flag` (sets `false`). Omitting either leaves the value `null`, letting you distinguish \"not specified\" from an explicit false. Help output shows `--flag / --no-flag` for nullable bools.\n\n```csharp\npublic static Task\u003cint\u003e Deploy(string env, bool? dryRun = null) { … }\n// myapp deploy staging               → dryRun is null\n// myapp deploy staging --dry-run     → dryRun is true\n// myapp deploy staging --no-dry-run  → dryRun is false\n```\n\n### DTO binding — `[AsParameters]`\n\nA record or class parameter annotated with `[AsParameters]` expands its members into individual flags or positionals. Works with **records** (constructor parameters) and **classes** (public settable properties). Add a string argument to prefix all long names.\n\n```csharp\n// Record — constructor parameters become flags\npublic record DeployOptions(string Environment, bool DryRun = false);\n\npublic static Task\u003cint\u003e Deploy([AsParameters] DeployOptions opts) { … }\n// myapp deploy --environment staging --dry-run\n\n// Class — public settable properties become flags\npublic class BuildOptions\n{\n    public string OutputDir { get; set; } = \"\";\n    public bool Release { get; set; }\n}\n\npublic static Task\u003cint\u003e Build([AsParameters] BuildOptions opts) { … }\n// myapp build --output-dir ./bin --release\n\n// Prefix — all long names get a common prefix\npublic record AppOptions(string Name, string Version = \"\");\npublic static Task\u003cint\u003e Configure([AsParameters(\"app\")] AppOptions opts) { … }\n// myapp configure --app-name foo --app-version 2\n```\n\n### Custom parsing — `IArgumentParser\u003cT\u003e`\n\nFor types with no built-in support, implement `IArgumentParser\u003cT\u003e` and annotate the parameter:\n\n```csharp\npublic class SemVerParser : IArgumentParser\u003cSemVer\u003e\n{\n    public static bool TryParse(string value, out SemVer result) =\u003e\n        SemVer.TryParse(value, out result);\n}\n\npublic static Task\u003cint\u003e Release([ArgumentParser(typeof(SemVerParser))] SemVer version) { … }\n// myapp release 1.2.3\n```\n\n`IArgumentParser\u003cT\u003e` is in [`Nullean.Argh.Interfaces`](src/Nullean.Argh.Interfaces/Parsing/IArgumentParser.cs).\n\n### CancellationToken (Ctrl+C)\n\nAdd `System.Threading.CancellationToken` as a **parameter of the command handler method** (alongside flags and positionals). It is **not** parsed from the command line and does not appear in `--help` — the source generator **injects** the token the runtime uses for cooperative cancellation.\n\nYou can also add it on an **`[AsParameters]`** type as a **primary constructor parameter** or **`init` property** (same injection rules). Keep **CLI-bound members first** in declaration order: all `[Argument]` positionals must precede flags, and **`CancellationToken` must not appear between a flag and a later positional** (the usual pattern is to put the token **last** on the DTO).\n\n- **Console and `ArghApp`:** the token is cancelled when the user presses **Ctrl+C** (and on Windows, the console **break** signal). The process keeps running after cancel unless your handler exits; Argh only forwards cancellation to your code.\n- **`Nullean.Argh.Hosting` / `AddArgh`:** the same console token is **linked** with `IHostApplicationLifetime.ApplicationStopping`, so the parameter also cancels when the host is shutting down.\n- **`TryParseArgh` / generated `TryParseDto_*`:** injected `CancellationToken` members are set to **`default`** — there is no host or console token in that API, so the value is non-cancellable.\n\n```csharp\npublic static async Task\u003cint\u003e Sync(\n    string source,\n    CancellationToken cancellationToken)\n{\n    await CopyTreeAsync(source, cancellationToken);\n    return 0;\n}\n// myapp sync --source ./data   (CancellationToken is not a CLI option)\n\npublic record RunArgs(string Source, int Port, CancellationToken Ct);\n\npublic static async Task\u003cint\u003e Run([AsParameters] RunArgs args)\n{\n    await Task.Delay(1, args.Ct);\n    return 0;\n}\n```\n\n## Object binding\n\nShare state across commands without repeating parameters on every method signature.\n\n### Global options\n\n```csharp\npublic record GlobalOptions(bool Verbose = false);\n\napp.UseGlobalOptions\u003cGlobalOptions\u003e();\napp.Map(\"build\", (GlobalOptions g) =\u003e { if (g.Verbose) … });\n// myapp build --verbose\n```\n\nGlobals are parsed before routing and available to every command.\n\n### Namespace options\n\nScoped to a namespace and its children. The options type must inherit the parent's options type — `GlobalOptions` at the root, or the enclosing namespace's options further down. The generator reports an error (AGH0004) if the chain is broken.\n\n```csharp\npublic record StorageOptions(string ConnectionString = \"\") : GlobalOptions;\n\napp.MapNamespace\u003cStorageHandlers\u003e(\"storage\", ns =\u003e\n{\n    ns.UseNamespaceOptions\u003cStorageOptions\u003e();\n    ns.Map(\"list\", (StorageOptions o) =\u003e { … });\n});\n// myapp storage list --connection-string \"…\" --verbose\n```\n\nParsing order in generated code: globals → namespace options along the path → command flags and positionals.\n\n### Combining with `[AsParameters]`\n\nA command can extend a global or namespace options type and annotate it with `[AsParameters]` to inherit those flags alongside its own:\n\n```csharp\npublic record DeployOptions(string Environment, bool DryRun = false) : StorageOptions;\n\nns.Map(\"deploy\", ([AsParameters] DeployOptions opts) =\u003e { … });\n// myapp storage deploy --connection-string \"…\" --environment staging --dry-run\n```\n\n\u003e **Note:** commands under a namespace are required to declare the namespace options type as a parameter (enforced by analyzer AGH0021). Annotate the method with `[NoOptionsInjection]` to opt out.\n\n## Fuzzy matching\n\nTypos produce actionable errors with the correct qualified path and a `--help` suggestion:\n\n```\n$ myapp stoarge list\nError: unknown command or namespace 'stoarge'. Did you mean 'storage'?\n\nRun 'myapp storage --help' for usage.\nRun 'myapp --help' for usage.\n```\n\nInside a namespace, the suggestion includes the full path (`storage blob upload`, not just `upload`).\n\n## Help and XML documentation\n\nWrite XML doc once; the generator reads it at build time and bakes the text into `--help` output. No `.xml` doc file is read at runtime — the generator accesses doc comments through the Roslyn compilation model, so **`GenerateDocumentationFile` is not required** for the usual developer inner loop or for routing/parsing/dispatch codegen.\n\n\u003e **Cross-assembly DTO types** — if an `[AsParameters]` DTO (record or class) lives in a **separate project** from the CLI entry point, the generator cannot access its source syntax at analysis time. It falls back to loading the companion `.xml` documentation file from disk. This means the DTO project **must** enable `\u003cGenerateDocumentationFile\u003etrue\u003c/GenerateDocumentationFile\u003e` for short-alias declarations (e.g. `/// \u003csummary\u003e-p, Path to the docs root.\u003c/summary\u003e`) and member descriptions to flow into `--help` output and short-flag parsing. Without it, short aliases are silently ignored and help text is empty for that DTO's members. Handler parameters and `[AsParameters]` types defined in the **same project** as the CLI are always resolved from source and are not affected.\n\n### Test projects referencing Argh apps\n\nIf a **test** assembly uses **`InternalsVisibleTo`** to see **`internal`** members of a referenced CLI project, older stacks sometimes hit **CS0436** because the same generated root type name could appear in more than one compilation. The generator emits a **stable, per-assembly** generated root type name so those collisions should not occur. If you must strip analyzers from a specific project (rare), use **`ExcludeAssets`** on the analyzer package reference or an MSBuild target that removes **`Nullean.Argh.Generator`** analyzers from that project only.\n\n### Commands\n\nDocument handler methods normally:\n\n```csharp\n/// \u003csummary\u003eDeploy the application to the target environment.\u003c/summary\u003e\n/// \u003cremarks\u003e\n/// Runs pre-flight checks before deploying. Pass \u003cc\u003e--dry-run\u003c/c\u003e to\n/// validate without making changes. See also \u003csee cref=\"Rollback\"/\u003e.\n/// \u003c/remarks\u003e\n/// \u003cparam name=\"environment\"\u003eTarget environment (staging, production).\u003c/param\u003e\n/// \u003cparam name=\"dryRun\"\u003eValidate only — make no changes.\u003c/param\u003e\npublic static Task\u003cint\u003e Deploy(string environment, bool dryRun = false) { … }\n```\n\nThe generated `myapp deploy --help` output:\n\n```\nUsage: myapp deploy \u003cenvironment\u003e [options]\n\n   Deploy the application to the target environment.\n\nGlobal options:\n  -h, --help              Show help.\n\nArguments:\n  \u003cenvironment\u003e           Target environment (staging, production).\n\nOptions:\n  --dry-run               Validate only — make no changes.\n\nNotes:\n  Runs pre-flight checks before deploying. Pass --dry-run to validate\n  without making changes. See also: myapp rollback \u003cargs\u003e\n```\n\n### Namespaces\n\nPut the `\u003csummary\u003e` (and optionally `\u003cremarks\u003e`) on the class `T` passed to `MapNamespace\u003cT\u003e`. The generator uses it as the namespace description in `myapp storage --help` and in the root command listing:\n\n```csharp\n/// \u003csummary\u003eManage blob and file storage resources.\u003c/summary\u003e\n/// \u003cremarks\u003e\n/// Requires a storage connection string via \u003cc\u003e--connection-string\u003c/c\u003e\n/// or the \u003cc\u003eSTORAGE_CONN\u003c/c\u003e environment variable.\n/// \u003c/remarks\u003e\ninternal sealed class StorageCommands { … }\n\napp.MapNamespace\u003cStorageCommands\u003e(\"storage\", ns =\u003e { … });\n```\n\n### Root app\n\nThe root `myapp --help` shows a description in two ways:\n\n**`UseCliDescription`** — for apps with no default root command, set a plain one-liner shown beneath the `Usage:` line:\n\n```csharp\napp.UseCliDescription(\"Manage and deploy your application's cloud resources.\");\napp.MapNamespace\u003cStorageCommands\u003e(\"storage\", ns =\u003e { … });\n```\n\n`UseCliDescription` is on `IArghRootBuilder` (not `IArghBuilder`), so it is intentionally unavailable inside `MapNamespace` configure callbacks. It cannot be combined with `MapRoot`; the generator reports `AGH0023` if both are present.\n\n**`MapRoot`** — when you also want a default handler at the root, put the XML doc on that handler method. The summary and remarks become the app-level overview:\n\n```csharp\n/// \u003csummary\u003eManage and deploy your application's cloud resources.\u003c/summary\u003e\n/// \u003cremarks\u003e\n/// Run \u003cc\u003emyapp \u0026lt;command\u0026gt; --help\u003c/c\u003e for details on any command.\n/// \u003c/remarks\u003e\npublic static Task\u003cint\u003e Root() { … }\n\napp.MapRoot(Root);\n```\n\nIn **remarks**, `\u003cparamref\u003e` to a flag becomes `--name`; `\u003csee cref\u003e` to another handler becomes that command's usage synopsis. See [`examples/XmlDocShowcase`](examples/XmlDocShowcase) for the full tag inventory.\n\n## Middleware\n\nCross-cutting logic — auth checks, logging, timing — lives in middleware and stays out of handler methods.\n\n```csharp\npublic class TimingMiddleware : ICommandMiddleware\n{\n    public async Task InvokeAsync(CommandContext ctx, Func\u003cTask\u003e next)\n    {\n        var sw = Stopwatch.StartNew();\n        await next();\n        Console.Error.WriteLine($\"{ctx.CommandPath}: {sw.ElapsedMilliseconds}ms\");\n    }\n}\n\n// Global — runs for every command\napp.UseMiddleware\u003cTimingMiddleware\u003e();\n\n// Per-handler — attribute on the method\n[MiddlewareAttribute\u003cTimingMiddleware\u003e]\npublic static Task\u003cint\u003e Deploy(string environment) { … }\n```\n\n[`ICommandMiddleware`](src/Nullean.Argh.Interfaces/Middleware/CommandMiddleware.cs) receives [`CommandContext`](src/Nullean.Argh.Interfaces/Middleware/CommandMiddleware.cs) with `CommandPath`, `Args`, `ExitCode`, and `CancellationToken`. Middleware does not run for `--help`, `--version`, `__completion`, `__complete`, or `__schema`. The pipeline is wired in generated code — not a runtime delegate chain. Each middleware call is emitted as a direct invocation in the generated dispatch method; there is no runtime list to build or iterate.\n\n## Dependency injection\n\nWhen using `Nullean.Argh.Hosting`, DI integration is fully transparent — register your handler and middleware types in the service collection and the generated code resolves them automatically. No manual `ServiceProvider` wiring needed.\n\nFor advanced use or when not using `Nullean.Argh.Hosting`: [`ArghServices.ServiceProvider`](src/Nullean.Argh.Core/Runtime/ArghHostRuntime.cs) is typed as `System.IServiceProvider` and set when running under a host. For `Map\u003cT\u003e()` instance methods and `UseMiddleware\u003cT\u003e()` / `[MiddlewareAttribute\u003cT\u003e]`, generated code resolves via `GetService(typeof(T))` when a provider is present; otherwise it falls back to `new T()`.\n\n```csharp\n// Handler with an injected service\npublic class DeployCommands(IDeployService deployer)\n{\n    public async Task\u003cint\u003e Run(string environment)\n    {\n        await deployer.DeployAsync(environment);\n        return 0;\n    }\n}\n\n// Registration — service must be in the DI container\nbuilder.Services.AddScoped\u003cIDeployService, DeployService\u003e();\nbuilder.Services.AddArgh(args, b =\u003e b.Map\u003cDeployCommands\u003e());\n```\n\nFor native AOT / trimming, register handler and middleware types explicitly in DI so required constructors are preserved.\n\n## Hosting\n\n`Nullean.Argh.Hosting` plugs the same command registration model into `IHost` and `Microsoft.Extensions.DependencyInjection` — no custom bootstrapping or glue code needed.\n\n`services.AddArgh(args, b =\u003e { … })` ([`AddArgh`](src/Nullean.Argh.Hosting/ArghHostingExtensions.cs)) mirrors the same `Map` / `Map\u003cT\u003e` / `UseGlobalOptions` / `UseNamespaceOptions` / `UseMiddleware` / `MapNamespace` surface as `ArghApp`, and additionally lets you control DI lifetimes:\n\n```csharp\nusing Microsoft.Extensions.DependencyInjection;\n\nbuilder.Services.AddArgh(args, b =\u003e\n{\n    b.MapScoped\u003cDeployCommands\u003e();       // resolved per command invocation\n    b.UseMiddleware\u003cAuditMiddleware\u003e(ServiceLifetime.Singleton);   // single instance for the process\n    b.Map(\"ping\", PingHandlers.Run);    // static method — no DI lifetime needed\n    b.UseGlobalOptions\u003cGlobalOptions\u003e();\n});\n```\n\n| `IArghHostingBuilder` API | Purpose |\n|--------------------------|---------|\n| `Map\u003cT\u003e()` | Register `T` as transient and add all its public methods as commands. |\n| `MapTransient\u003cT\u003e()` / `MapScoped\u003cT\u003e()` / `MapSingleton\u003cT\u003e()` | Same, with an explicit DI lifetime. |\n| `UseGlobalOptions\u003cT\u003e()` | Register `T` as the global options type and add it to DI. |\n| `UseMiddleware\u003cTMiddleware\u003e()` | Register middleware as transient. |\n| `UseMiddleware\u003cTMiddleware\u003e(lifetime)` | Register middleware with an explicit DI lifetime. |\n\n`AddArgh` registers a hosted service that runs `ArghRuntime.RunAsync(args)` and then calls `Environment.Exit` with the exit code — the host does not continue after the CLI completes.\n\n`CancellationToken` on command handlers: see [CancellationToken (Ctrl+C)](#cancellationtoken-ctrlc) — with hosting, the injected token is linked to **Ctrl+C** and **`IHostApplicationLifetime.ApplicationStopping`**.\n\n**Register `AddArgh` before other `IHostedService` registrations** if you want the CLI (including `--help`) to run first and exit without starting later background work. Services registered *before* `AddArgh` still get `StartAsync` on every invocation.\n\n### Intrinsic commands and log suppression\n\nWhen the host starts up, configuration providers, logging infrastructure, and other services initialize before the CLI runs. This means commands like `--help` or `--version` can be preceded by startup noise in the output.\n\n`AddArgh` addresses this automatically: if the invocation is an **intrinsic command** — a built-in (`--help`, `-h`, `--version`, `__schema`, `__completion`, `__complete`) or a user-defined method marked `[CommandIntrinsic]` — it configures logging to suppress entries below `Warning` before the host builds. No configuration needed.\n\n**User-defined intrinsic commands** — mark any handler method `[CommandIntrinsic]` to opt it into the same log suppression. These commands still run through the full host and DI because they may need services:\n\n```csharp\npublic class InfoCommands\n{\n    private readonly IVersionService _version;\n    public InfoCommands(IVersionService version) =\u003e _version = version;\n\n    /// \u003csummary\u003ePrint runtime version and environment info.\u003c/summary\u003e\n    [CommandIntrinsic]\n    [CommandName(\"info\")]\n    public void Info() =\u003e Console.WriteLine(_version.Current);\n}\n\nbuilder.Services.AddArgh(args, b =\u003e b.Map\u003cInfoCommands\u003e());\n// dotnet run -- info  →  no startup log noise\n```\n\n**Override the suppression threshold** — the default minimum level is `Warning` (suppresses `Information` and below). Override it on the hosting builder:\n\n```csharp\nbuilder.Services.AddArgh(args, b =\u003e\n{\n    b.IntrinsicLogLevelMinimum(LogLevel.Trace);   // re-enable all logs for intrinsic commands\n    b.Map\u003cInfoCommands\u003e();\n});\n```\n\n**Expert: pre-host fast path** — if host startup is expensive and you want zero overhead for the built-in intrinsic commands, call `ArghApp.TryArghIntrinsicCommand(args)` *before* `Host.CreateApplicationBuilder`. If the invocation is a built-in, the command runs and the process exits immediately — the host is never constructed. For user-defined `[CommandIntrinsic]` commands (which need DI), log suppression is the right tool instead.\n\n```csharp\n// Built-ins exit here with no host overhead: --help, --version, __schema, etc.\nawait ArghApp.TryArghIntrinsicCommand(args);\n\nvar builder = Host.CreateApplicationBuilder(args);\nbuilder.Services.AddArgh(args, b =\u003e { b.Map\u003cInfoCommands\u003e(); });\nawait builder.Build().RunAsync();\n```\n\nIf `TryArghIntrinsicCommand` is omitted, there is no breakage — the built-ins still work correctly; they are just handled inside the hosted service with log suppression active.\n\n## Routing API\n\n[`ArghParser.Route(args)`](src/Nullean.Argh.Core/Runtime/ArghParser.cs) returns a [`RouteMatch`](src/Nullean.Argh.Core/Runtime/ArghParser.cs) (`CommandPath`, `RemainingArgs`) without invoking handlers — useful for tests and tooling.\n\n## Validation\n\nAnnotate parameters (or `[AsParameters]` members) with standard **`System.ComponentModel.DataAnnotations`** attributes, optionally combined with **`Nullean.Argh`** filesystem attributes where the parameter type is **`FileInfo`**, **`FileInfo?`**, **`DirectoryInfo`**, or **`DirectoryInfo?`**. The source generator reads the attributes at build time and emits inline validation checks — no reflection, no `Validator.ValidateObject` call, AOT-safe. Constraint hints appear in `--help` after the description; failures print to stderr and exit 2.\n\n```csharp\npublic static void Deploy(\n    [Range(1, 65535)]                          int port,\n    [StringLength(64, MinimumLength = 2)]      string name,\n    [AllowedValues(\"dev\", \"staging\", \"prod\")]  string env,\n    [RegularExpression(@\"^[a-z0-9\\-]+$\")]      string slug,\n    [UriScheme(\"https\")]                       Uri endpoint)\n{ … }\n\n// FileInfo / DirectoryInfo — compose DataAnnotations (e.g. [FileExtensions]) with Argh path traits:\npublic static Task\u003cint\u003e Lint(\n    [Existing][FileExtensions(Extensions=\"json\")][RejectSymbolicLinks] FileInfo manifest,\n    [ExpandUserProfile][Existing] DirectoryInfo outDir)\n{ … }\n```\n\n```\n$ myapp deploy --port 99999\nError: --port: value must be between 1 and 65535.\nRun 'myapp deploy --help' for usage.\n\n$ myapp deploy --help\nOptions:\n  --port \u003cint\u003e       [required] [range: 1–65535]\n  --name \u003cstring\u003e    [required] [length: 2–64]\n  --env \u003cstring\u003e     [required] [allowed: dev|staging|prod]\n  --slug \u003cstring\u003e    [required] [pattern: ^[a-z0-9\\-]+$]\n  --endpoint \u003curi\u003e   [required] [schemes: https]\n```\n\n### DataAnnotations\n\n| Attribute | Applies to | Validates | Help token |\n|-----------|------------|-----------|-----------|\n| `[Range(min, max)]` | numeric | numeric value is within bounds | `[range: min–max]` |\n| `[StringLength(max)]` / `[StringLength(max, MinimumLength = min)]` | `string` | string length | `[max-length: n]` / `[length: min–max]` |\n| `[MinLength(n)]` / `[MaxLength(n)]` on `string` | `string` | string length | `[min-length: n]` / `[max-length: n]` |\n| `[MinLength(n)]` / `[MaxLength(n)]` on a collection | `T[]`, `List\u003cT\u003e`, etc. | item count | `[min-count: n]` / `[max-count: n]` |\n| `[Length(min, max)]` (.NET 8) on `string` | `string` | string length range | `[length: min–max]` |\n| `[Length(min, max)]` (.NET 8) on a collection | `T[]`, `List\u003cT\u003e`, etc. | item count range | `[count: min–max]` |\n| `[RegularExpression(pattern)]` | `string` | value matches regex | `[pattern: …]` |\n| `[AllowedValues(v1, v2, …)]` (.NET 8) | any | value is in the set | `[allowed: v1\\|v2\\|…]` |\n| `[DeniedValues(v1, v2, …)]` (.NET 8) | any | value is not in the set | `[denied: v1\\|v2\\|…]` |\n| `[EmailAddress]` | `string` | basic `user@host` shape | `[email]` |\n| `[Url]` on `string` | `string` | absolute URL (http/https/ftp) | `[url]` |\n| `[Url]` on `Uri` | `Uri` | scheme is http or https | `[schemes: http\\|https]` |\n| `[FileExtensions(Extensions=\"json,yaml\")]` | `FileInfo` | `FileInfo` extension | `[extensions: json\\|yaml]` |\n| `[UriScheme(\"https\")]` *(Argh-native)* | `Uri` | `Uri` scheme is in the list | `[schemes: https]` |\n\nWhen `[MinLength]` / `[MaxLength]` / `[Length]` is applied to a **collection** parameter (`T[]`, `List\u003cT\u003e`, `IReadOnlySet\u003cT\u003e`, etc.), it validates the **number of items**, not the length of a string. This applies to both repeatable flags and variadic positionals:\n\n```csharp\n// Flag: must receive --file at least once, at most five times\npublic static void Process([MaxLength(5)] List\u003cstring\u003e files) { … }\n\n// Variadic positional: between 2 and 10 items required\npublic static void Archive([Argument][MinLength(2)][MaxLength(10)] string[] files) { … }\n```\n\nEnum parameters automatically show `[allowed: Member1\\|Member2]` in help — the enum type itself enforces the constraint, no extra attribute needed.\n\n### Filesystem paths (`FileInfo` / `DirectoryInfo`)\n\nThese attributes apply to **`FileInfo`** / **`FileInfo?`** or **`DirectoryInfo`** / **`DirectoryInfo?`** (including on `[AsParameters]` members). Incompatible combinations (such as **`[Existing]`** with **`[NonExisting]`** on the same parameter) are diagnosed at compile time. **`[RejectSymbolicLinks]`** runs before existence checks — a symlink to a real path still fails when symlink rejection is enabled.\n\n| Attribute | Applies to | Validates | Help token |\n|-----------|------------|-----------|------------|\n| `[Existing]` | `FileInfo` / `FileInfo?` | `File.Exists` | `[existing]` |\n| `[Existing]` | `DirectoryInfo` / `DirectoryInfo?` | `Directory.Exists` | `[existing]` |\n| `[NonExisting]` | `FileInfo` / `FileInfo?` **or** `DirectoryInfo` / `DirectoryInfo?` | neither `File.Exists` nor `Directory.Exists` | `[unused path]` |\n| `[RejectSymbolicLinks]` | `FileInfo`/`FileInfo?` **or** `DirectoryInfo`/`DirectoryInfo?` | not a symlink or reparse point | `[no symlinks]` |\n| `[ExpandUserProfile]` | `FileInfo` or `DirectoryInfo` | expands `~/`, `~\\`, or bare `~` before binder constructs `*Info`, then `Path.GetFullPath` | `[expand ~ profile]` |\n\nFailures use stderr messages such as *file does not exist*, *directory does not exist*, *path already exists…*, or *path must not be a symbolic link or reparse point.* (exit code 2).\n\n**Schema (`__schema`):** validations include JSON **`kind`** values such as **`existing`**, **`nonExisting`**, **`rejectSymbolicLinks`**, and **`expandUserProfile`**.\n\nValidation also runs through the `TryParseArgh` static extension emitted for `[AsParameters]` DTOs, so unit tests can assert constraints without spawning a subprocess.\n\n## Shell completions\n\nTab completion for subcommands, namespaces, and flags is included out of the box: the source generator emits **lookup tables** at compile time (same model as routing and `--help`), and a small `__complete` handler answers the shell with one candidate per line. **`--completions` is not reserved** — use `__completion` / `__complete` only for Argh's integration.\n\n| Command | Purpose |\n|--------|---------|\n| `myapp __completion bash\\|zsh\\|fish` | Print an install snippet from [`CompletionScriptTemplates`](src/Nullean.Argh.Core/Help/CompletionScriptTemplates.cs) (substitutes your executable name). |\n| `myapp __complete \u003cshell\u003e -- \u003cwords...\u003e` | Return completion candidates; `words` are argv after the program name (full line context for nested commands). |\n\n**Bash** — `eval \"$(myapp __completion bash)\"` (add to `~/.bashrc` to persist).\n\n**Zsh** — `source \u003c(myapp __completion zsh)` (add to `~/.zshrc` to persist).\n\n**Fish** (3.4+ for `commandline -opc`):\n\n```fish\nmkdir -p ~/.config/fish/completions\nmyapp __completion fish \u003e ~/.config/fish/completions/myapp.fish\n```\n\nDetails: [`CompletionProtocol`](src/Nullean.Argh.Core/Help/CompletionProtocol.cs).\n\n## Schema JSON\n\n`myapp __schema` writes a JSON document to stdout describing your entire CLI — commands, namespaces, global and namespace options, summaries, remarks, usage, and examples. The output is generated at build time from the same source the generator uses for routing and help, so it is always in sync with your code.\n\n```\nmyapp __schema \u003e cli-schema.json\n```\n\nUse cases:\n\n- **LLM / agent tooling** — feed the schema to a language model to give it accurate, structured knowledge of your CLI's commands and options.\n- **Generated documentation** — pipe into a docs generator or templating step to keep reference docs in sync without manual maintenance.\n- **CI validation** — diff `cli-schema.json` across commits to catch unintentional breaking changes to the CLI surface.\n\nThe shape is defined by [`ArghCliSchemaDocument`](src/Nullean.Argh.Core/Schema/ArghCliSchemaDocument.cs) and conforms to the [cli-schema v1 specification](https://github.com/cli-schema/cli-schema). Output is indented camelCase JSON. Reserved meta-commands (`__complete`, `__completion`, `__schema`) appear under `reservedMetaCommands`.\n\n### Schema enrichment attributes\n\nAdd `using Nullean.Argh.Documentation;` to access the attributes below. They have no effect on parsing or validation — they only enrich the `__schema` output for agent tooling and documentation consumers.\n\n**Side-effect profile** (`intent` object):\n\n```csharp\n[CommandIntent(Intent.Destructive | Intent.RequiresConfirmation)]\n[MutationScope(MutationScope.Global)]   // File | Directory | Global\n[RequiresAuth]\npublic static Task Delete([ConfirmationSkip] bool yes = false, ...) { }\n```\n\n`Intent` is a flags enum — combine with `|`. `MutationScope.Global` means the command reaches beyond the local filesystem (cloud resources, databases, registries, etc.). `[RequiresAuth]` signals that an authenticated session is required. `[ConfirmationSkip]` on a flag parameter sets its schema `role` to `\"confirmationSkip\"` so agent consumers know to pass it automatically on destructive commands. `[DryRun]` sets `role` to `\"dryRun\"`.\n\n**Output formats** (`output` object):\n\n```csharp\n// Enum parameter — formats and formatFlag inferred automatically\npublic static void Report([CommandOutput] OutputFormat? format = null) { }\n\n// Explicit format list on a string parameter\npublic static void Export([CommandOutput(\"json\", \"table\")] string? fmt = null) { }\n```\n\nPlace `[CommandOutput]` on the parameter (or `[AsParameters]` DTO property, or GlobalOptions property) that selects the output format. The flag name and format list are derived from the parameter — no extra arguments needed for enum types.\n\n**Deprecated commands and parameters** (`deprecated` object):\n\n```csharp\n[Obsolete(\"Use new-cmd instead.\")]\npublic static void OldCmd(...) { }\n```\n\n`[Obsolete]` on a handler method or an `[AsParameters]` DTO property emits a `deprecated` object in the schema. The message, if provided, appears as `deprecated.message`.\n\n**Environment variables and config files** (`environment` object):\n\n```csharp\nbuilder.DocumentEnvironmentVariables(\n    variables:\n    [\n        new CliEnvVar(\"GITHUB_TOKEN\", Description: \"GitHub API token\", Required: true),\n        new CliEnvVar(\"XDG_CONFIG_HOME\", Description: \"Config directory override\"),\n    ],\n    configFiles:\n    [\n        new CliConfigFile(\"~/.config/myapp/config.json\", Description: \"Main config\"),\n    ]);\n```\n\nArguments must be `new CliEnvVar(...)` / `new CliConfigFile(...)` object creation expressions with string/bool literals so the source generator can extract them statically.\n\n**Native AOT in CI:** The GitHub Actions workflow runs an **`aot-validate`** job that publishes [`examples/ArghAotSmoketest`](examples/ArghAotSmoketest) with Native AOT on Linux, macOS, and Windows and invokes `__schema` on the native binary. That sample uses `Microsoft.Extensions.Hosting` and `AddArgh` so **`Map` / `MapNamespace` DI registration** is included in the AOT publish. The repo uses the SDK unified artifacts layout (output under **`.artifacts/`**, gitignored).\n\n## License and links\n\n- **License**: [MIT](LICENSE)\n- **Repository**: [github.com/nullean/argh](https://github.com/nullean/argh)\n- **Releases**: [GitHub releases](https://github.com/nullean/argh/releases)\n\nThis README is the NuGet package readme for `Nullean.Argh`, `Nullean.Argh.Core`, `Nullean.Argh.Interfaces`, and `Nullean.Argh.Hosting`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnullean%2Fargh","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnullean%2Fargh","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnullean%2Fargh/lists"}