diff --git a/.vscode/settings.shared.json b/.vscode/settings.shared.json
index 747ae06..c6c3cdf 100644
--- a/.vscode/settings.shared.json
+++ b/.vscode/settings.shared.json
@@ -3,8 +3,15 @@
// warrant a line limit
"editor.rulers": [150],
+ // Points the C# extension (OmniSharp or C# Dev Kit, whichever is installed) at the one solution file in the repository; without this, the
+ // extension has to guess which solution to load, and since "CLI.NET Core.slnx" lives in "source/" rather than at the repository root, and
+ // references the unit test project one level up in "tests/unit-tests" (see Architecture), that guess can miss the cross-directory project
+ // reference and report its files as not part of any opened project
+ "dotnet.defaultSolution": "source/CLI.NET Core.slnx",
+
// Maps file names to file types; this is required when Visual Studio Code does not correctly auto-detect file types based on file extension
"files.associations": {
+ ".cspell.json": "jsonc",
"**/.vscode/*.json": "jsonc",
"dprint.json": "jsonc",
"LICENSE": "plaintext",
diff --git a/CLAUDE.md b/CLAUDE.md
index b07ca6c..96f8898 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -22,11 +22,12 @@ Start from [docs/developer-manual/architecture/overview.md](docs/developer-manua
The docs and this file are Markdown and are linted too.
- **Markdown headings are title case** at every level, in every file ([docs/developer-manual/conventions/markdown-style.md](docs/developer-manual/conventions/markdown-style.md)). Preserve the real casing of code spans, brand names (`dprint`), and acronyms.
+- **Wrap code in backticks.** Every type or member name, file or directory path, package name, configuration key, and CLI flag is wrapped in backticks wherever it appears in prose — docs and commit messages alike ([Markdown Style](docs/developer-manual/conventions/markdown-style.md#wrap-code-in-backticks)). XML documentation comments are the one exception: they are XML, not Markdown, so use `` for identifiers and `` for code instead ([C# Style](docs/developer-manual/conventions/csharp-style.md)).
- **Prose uses periods, not semicolons.** In prose (docs, XML documentation comments, commit messages, this file) end each sentence with a period rather than joining two with a semicolon. Plain in-code comments (`//`) do the reverse: sentences are separated by semicolons and the last one takes no terminal punctuation ([docs/developer-manual/conventions/csharp-style.md](docs/developer-manual/conventions/csharp-style.md)).
-- **C# structure**: file-scoped namespaces, an XML documentation comment (``, ``, ``) on every public and internal member, `` for members that implement an interface or override a base member, `sealed` classes by default (composition over inheritance for anything that would otherwise need to extend a sealed framework type), explicit `this.` on member access, expression-bodied members where the implementation is a single expression, and `camelCase` private fields with no underscore. Full detail: [C# Style](docs/developer-manual/conventions/csharp-style.md).
+- **C# structure**: file-scoped namespaces, an XML documentation comment (``, ``, ``) on **every member, including `private` ones** (not just public and internal — this is for readers of the source, not only consumers of the compiled output), `` for members that implement an interface or override a base member, `sealed` classes by default (composition over inheritance for anything that would otherwise need to extend a sealed framework type), explicit `this.` on member access, expression-bodied members where the implementation is a single expression, and `camelCase` private fields with no underscore. Full detail: [C# Style](docs/developer-manual/conventions/csharp-style.md).
- **Nullable reference types and implicit usings are enabled everywhere.** Write genuinely null-safe code rather than silencing the analyzer.
- **Files and directories are kebab-case**, except well-known and tool-mandated names (`README.md`, `LICENSE`, `.editorconfig`, ...). Inside a C# project's own source tree, directories and files switch to PascalCase, one type per file. Full detail: [File Naming Conventions](docs/developer-manual/conventions/file-naming-conventions.md).
-- **Every dependency is pinned to an exact version — never a range.** NuGet packages, dprint plugins, and the linter versions CI installs are all pinned exactly. Every C# project sets `RestorePackagesWithLockFile`, so a `PackageReference` version bump must be followed by `dotnet restore` and the resulting `packages.lock.json` change committed alongside it. Full detail: [Dependency Management](docs/developer-manual/conventions/dependency-management.md).
+- **Every dependency is pinned to an exact version — never a range.** NuGet packages, dprint plugins, the linter versions CI installs, and the .NET SDK (`global.json`) are all pinned exactly. Every C# project sets `RestorePackagesWithLockFile`, so a `PackageReference` version bump must be followed by `dotnet restore` and the resulting `packages.lock.json` change committed alongside it. Full detail: [Dependency Management](docs/developer-manual/conventions/dependency-management.md).
- **Write commit messages by the rules** ([docs/developer-manual/conventions/commit-messages.md](docs/developer-manual/conventions/commit-messages.md)) — the 50/72 rule, a title-cased, past-tense subject, and a prose body. State whether AI was involved and, if it was, what exactly the AI did — this project is developed openly with AI assistance and the commit history is where that is tracked (see the "Use of AI" section of the [root README](README.md)). When you did any of the work, add the trailer this project uses (not a model-specific one):
```text
@@ -49,14 +50,15 @@ Start from [docs/developer-manual/architecture/overview.md](docs/developer-manua
## Conventions in Brief
-Files use file-scoped namespaces and, in larger files, `#region` blocks (`Constructors`, `Private Fields`, `Public Methods`, and so on) to group members — small files skip regions entirely. Every public and internal member is documented with XML comments that explain *why*, not just what. Sealed types compose the framework types they wrap instead of inheriting from them. Markdown headings are title case everywhere, and prose ends sentences with periods; plain code comments do the reverse. Full detail: [docs/developer-manual/conventions/](docs/developer-manual/conventions/csharp-style.md).
+Files use file-scoped namespaces and always group members into `#region` blocks (`Constructors`, `Private Fields`, `Public Methods`, and so on), even in small files, ordered constructors, constants, fields, properties, methods, then interface implementations (no access modifier in an interface-implementation region's name). Every member, including `private` ones, is documented with XML comments that explain *why*, not just what, and marks up keywords like `null` or `sealed` with ``. Sealed types compose the framework types they wrap instead of inheriting from them. Markdown headings are title case everywhere, and prose ends sentences with periods; plain code comments do the reverse. Full detail: [docs/developer-manual/conventions/](docs/developer-manual/conventions/csharp-style.md).
## Keep the Documentation up to Date
**This is important.** Whenever you change, add, remove, or discover something about the project, update the relevant [`docs/`](docs/README.md) article **and** this `CLAUDE.md` in the same change:
- Keep the "When working on X" routing table above accurate.
-- Keep the [docs index](docs/README.md) and the [Developer Manual index](docs/developer-manual/README.md) accurate — every article must be listed.
+- Keep the [docs index](docs/README.md), the [Developer Manual index](docs/developer-manual/README.md), and the [User Manual index](docs/user-manual/README.md) accurate — every article must be listed.
+- A change to the public API updates the User Manual (how consumers use it), not only the Developer Manual (how the codebase itself is built) — the two documents serve different audiences, and the same change often needs a separate edit to each.
- If a change introduces a topic that does not fit an existing article, add a new small, single-topic article, link it from the relevant index, and reference it here if relevant.
- If a change invalidates something a doc says, fix the doc — do not leave it stale.
diff --git a/README.md b/README.md
index 08888dc..ff8a8be 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,4 @@
-# CLI.NET Core
-
- 
+#  
CLI.NET Core is a command line application framework in the style of ASP.NET Core. It allows you to define commands and command line arguments in much the same way you would define actions and arguments for a Web API in ASP.NET Core.
@@ -23,6 +21,10 @@ The project uses a set of linters and a code formatter to (1) find and correct p
- **[MarkdownLint](tests/linters/.markdownlint.yml)**, a linter for Markdown files, which ensures that the Markdown files are consistently formatted and standards are enforced.
- **[dprint](dprint.json)**, an unopinionated, configurable code formatter with plugins for many languages. This code formatter is used to format Markdown, JSON, XML, YAML, and TOML files in the project.
+### Visual Studio Code
+
+The project maintainer uses Visual Studio Code for the development of CLI.NET Core and the configuration is committed to the repository. To allow other developers to change the local configuration when using Visual Studio Code, the [Workspace Config+](https://marketplace.visualstudio.com/items?itemName=swellaby.workspace-config-plus) extension is used to manage the workspace configuration. Instead of having a single set of configuration files (`settings.json`, `tasks.json`, and `launch.json`), the project has shared configuration files (`settings.shared.json`, `tasks.shared.json`, and `launch.shared.json`) that are checked into source control, while allowing individual developers to override specific settings in their local configuration files (`settings.local.json`, `tasks.local.json`, and `launch.local.json`). The extension automatically merges the shared and local configuration files into a single configuration file that is used by Visual Studio Code, giving the local configuration files preference when there are conflicts. The extension is included in the list of recommended extensions (`.vscode/extensions.json`) and will be recommended by Visual Studio Code when opening the repository for the first time.
+
## Use of AI
This project is developed with the help of AI and I want to be fully transparent about that. The heart of the project — its architecture, its public API, and the implementation of its core — was and will continue to be written by hand, by me. AI is a tool that I use for the work around that core, never a substitute for it, and everything an AI produces is reviewed and, where necessary, rewritten by me before it is committed. Concretely, I use AI for the following kinds of tasks:
diff --git a/docs/developer-manual/architecture/overview.md b/docs/developer-manual/architecture/overview.md
index 5b48dd5..f77511c 100644
--- a/docs/developer-manual/architecture/overview.md
+++ b/docs/developer-manual/architecture/overview.md
@@ -11,7 +11,7 @@ The repository is organized into a few top-level directories:
- [`docs/`](../../README.md) — This documentation.
- [`design/`](../../../design/) — Logo and brand assets, and the [design guide](../../../design/DESIGN.md) that governs them.
-Top-level files round out the repository: [`README.md`](../../../README.md) is the project's front door, [`CHANGELOG.md`](../../../CHANGELOG.md) records what changed in each version, [`CONTRIBUTORS.md`](../../../CONTRIBUTORS.md) lists everyone who has contributed, and [`LICENSE`](../../../LICENSE) is the full text of the license (see [Contributing](../contributing.md) for how these are kept up to date). [`CONTRIBUTING.md`](../../../CONTRIBUTING.md), [`CODE_OF_CONDUCT.md`](../../../CODE_OF_CONDUCT.md), and [`SECURITY.md`](../../../SECURITY.md) round out the community-facing files GitHub recognizes by name.
+Top-level files round out the repository: [`README.md`](../../../README.md) is the project's front door, [`CHANGELOG.md`](../../../CHANGELOG.md) records what changed in each version, [`CONTRIBUTORS.md`](../../../CONTRIBUTORS.md) lists everyone who has contributed, and [`LICENSE`](../../../LICENSE) is the full text of the license (see [Contributing](../contributing.md) for how these are kept up to date). [`CONTRIBUTING.md`](../../../CONTRIBUTING.md), [`CODE_OF_CONDUCT.md`](../../../CODE_OF_CONDUCT.md), and [`SECURITY.md`](../../../SECURITY.md) round out the community-facing files GitHub recognizes by name. [`global.json`](../../../global.json) pins the exact .NET SDK version the repository builds with.
## Solution and Projects
diff --git a/docs/developer-manual/conventions/commit-messages.md b/docs/developer-manual/conventions/commit-messages.md
index cc8f96d..5bddfbb 100644
--- a/docs/developer-manual/conventions/commit-messages.md
+++ b/docs/developer-manual/conventions/commit-messages.md
@@ -20,6 +20,8 @@ The subject is **title case** (see [Markdown Style](markdown-style.md)) and **pa
When present, the body explains what changed and why — the diff already shows what changed, so the *why* is what earns its place. It is written as prose paragraphs, bulleted or numbered lists, or a mix of both, freely intermixed (an opening paragraph followed by a list, followed by another paragraph, is fine). Whichever form it takes, it follows the same punctuation rule as the rest of the documentation: periods, not semicolons (see [Markdown Style](markdown-style.md)).
+Code, file names, paths, and identifiers are wrapped in backticks, the same as in the documentation (see [Wrap Code in Backticks](markdown-style.md#wrap-code-in-backticks)). This applies even though most Git tools render commit messages as plain text: the backticks still mark the boundary between prose and literal code for a human reader.
+
## Disclosing AI Involvement
Every commit message states whether AI was involved in producing it and, if it was, what exactly the AI did — this project is developed openly with AI assistance, and the commit history is where that involvement is tracked (see the "Use of AI" section of the [root README](../../../README.md)). A commit written entirely by hand carries no such note. A commit where AI was involved:
diff --git a/docs/developer-manual/conventions/csharp-style.md b/docs/developer-manual/conventions/csharp-style.md
index 4fef384..0727ff7 100644
--- a/docs/developer-manual/conventions/csharp-style.md
+++ b/docs/developer-manual/conventions/csharp-style.md
@@ -4,16 +4,49 @@ This article covers the conventions for C# code in [`source/`](../../../source/)
## File Layout
-Every file starts with a file-scoped namespace declaration (`namespace CliNetCore.Hosting;`), not a namespace block. Beyond the usings that [`ImplicitUsings`](../architecture/overview.md) already brings in, additional `using` directives are wrapped in a `#region Using Directives` block above the namespace declaration.
+Every file starts with a file-scoped namespace declaration (`namespace CliNetCore.Application;`), not a namespace block. Beyond the usings that [`ImplicitUsings`](../architecture/overview.md) already brings in, additional `using` directives are wrapped in a `#region Using Directives` block above the namespace declaration.
-Within a type, members may be grouped into `#region` blocks when the file is large enough to benefit from it, for example `#region Constructors`, `#region Private Fields`, `#region Public Static Methods`, `#region Public Methods`, or `#region Implementation` for the members that implement a specific interface. A small file with only a handful of members does not need regions at all — do not add them just for the sake of having them.
+Every type groups its members into `#region` blocks, regardless of file size — even a type with a single constructor and a single property uses regions. There is no size threshold below which regions are skipped.
+
+Regions appear in this order: constructors, then constants, then fields, then properties, then methods, then interface implementations. Within the constants/fields/properties/methods categories, group first by kind (for example static methods before instance methods) and, within a kind, order by access modifier: `private` before `internal` before `protected` before `public` — so `#region Private Fields` precedes `#region Public Fields` when both exist. Typical region names are `#region Constructors`, `#region Private Fields`, `#region Public Constants` (for `const` members, kept separate from `#region Public Fields`), `#region Public Properties`, `#region Public Static Methods`, `#region Public Methods`, and `#region Protected Methods`. A region for the members that implement a specific interface, inside the *implementing* type, is named `#region Implementation` with **no** access modifier in the name (it is always `#region IHost Implementation`, never `#region Public IHost Implementation`), because an implementing member's accessibility is dictated by the interface, not chosen per member.
+
+Inside an interface declaration itself, access modifiers are meaningful and follow the same rules as a class: an abstract member (one without a body) is implicitly, and must be declared, `public`. A member with a default implementation, however, may instead be declared `protected` or `private`, letting a default `public` method delegate a piece of its behavior to a `protected` method that implementers can override without replacing the whole implementation, or to a `private` helper that only other default implementations in the interface can call. Because the modifier carries real information here, interface regions are named the same way class regions are — `#region Public Methods`, `#region Protected Methods` — instead of dropping the modifier:
+
+```csharp
+public interface ITest
+{
+ #region Public Properties
+
+ public string Name { get; }
+
+ #endregion
+
+ #region Public Methods
+
+ public void Run() => this.OnRunning();
+
+ #endregion
+
+ #region Protected Methods
+
+ protected void OnRunning() { }
+
+ #endregion
+}
+```
## Documentation Comments
-Every public and internal member has an XML documentation comment (`GenerateDocumentationFile` is enabled in [the project files](../architecture/overview.md), so these ship in the compiled output). A `` explains not just what a member does, but why it exists and how it relates to the rest of the design — the reasoning belongs next to the code, not only in a commit message. Parameters and return values are documented with `` and ``. When a member implements an interface member or overrides a base member and has nothing to add beyond what is already documented there, use `` instead of repeating the text.
+Every member has an XML documentation comment — public and internal ones because `GenerateDocumentationFile` is enabled in [the project files](../architecture/overview.md), so those ship in the compiled output and reach a consumer's editor tooltips, but `private` members are documented exactly the same way, for the same reason every other convention on this page exists: someone reading the source, not just the compiled output, needs it. A `` explains not just what a member does, but why it exists and how it relates to the rest of the design — the reasoning belongs next to the code, not only in a commit message. Parameters and return values are documented with `` and ``. When a member implements an interface member or overrides a base member and has nothing to add beyond what is already documented there, use `` instead of repeating the text.
Prose inside documentation comments follows the same punctuation rule as the rest of the documentation: periods, not semicolons (see [Markdown Style](markdown-style.md)).
+A `` element always starts with "Returns", for example `Returns the exit code produced by the executed command.`.
+
+C# keywords mentioned in documentation-comment prose — `null`, `true`, `false`, `default`, `sealed`, `internal`, and so on — are marked up with `` rather than written as plain text or in a code span, for example `` or ``.
+
+A documentation comment is XML, not Markdown, so backticks stay literal instead of rendering as a code span (see [Wrap Code in Backticks](markdown-style.md#wrap-code-in-backticks) for the general rule). Reference a type or member with `` (for example ``) so that it becomes a clickable link in IDE tooltips, and wrap anything else that is code — a file name, a literal value, a short snippet — in `...`.
+
## Plain Comments
A plain `//` comment inside a method body is not prose — it explains a non-obvious *why* (a hidden constraint, an ordering requirement, a workaround) that the code next to it does not already make clear. It follows the opposite punctuation rule from documentation comments and the rest of the documentation: sentences are separated by semicolons, and the last one takes no terminal punctuation.
@@ -33,6 +66,7 @@ Do not write a comment that only restates what the code already says. If removin
- **Async methods are suffixed `Async`** and return `Task` or `Task`. They accept a `CancellationToken` (typically the last parameter) whenever the work they do can meaningfully be canceled.
- **Prefer factory methods over public constructors** when a type has invariants to establish or work to do beyond simple field assignment. An `internal` constructor paired with a `public static` creation method (or a dedicated builder type) keeps construction explicit and controllable.
- **Interfaces are prefixed with `I`** (`IDisposable`), following the standard .NET convention.
+- **Interface members declare an explicit `public` modifier** (`public string Name { get; }`), even though it is the only modifier C# permits on them by default, matching the explicit style used elsewhere in this codebase (`this.`, `sealed`).
- **Private fields are `camelCase`** with no leading underscore and no `_` prefix.
## Nullability and Implicit Usings
diff --git a/docs/developer-manual/conventions/dependency-management.md b/docs/developer-manual/conventions/dependency-management.md
index 008977c..e4f2105 100644
--- a/docs/developer-manual/conventions/dependency-management.md
+++ b/docs/developer-manual/conventions/dependency-management.md
@@ -10,7 +10,7 @@ Every dependency is pinned to an exact version — never a floating range (`*`),
```
-not `Version="10.*"` or `Version="[10.0.0,)"`. The same rule applies outside of NuGet: the dprint plugins in [`dprint.json`](../../../dprint.json) are pinned to an exact release URL, and [Continuous Integration](../tooling/continuous-integration.md) installs `dprint`, `cspell`, and `markdownlint-cli2` at exact, explicit versions. An upgrade is always a deliberate, visible change to a version string, never something that happens silently on the next restore.
+not `Version="10.*"` or `Version="[10.0.0,)"`. The same rule applies outside of NuGet: the dprint plugins in [`dprint.json`](../../../dprint.json) are pinned to an exact release URL, [Continuous Integration](../tooling/continuous-integration.md) installs `dprint`, `cspell`, and `markdownlint-cli2` at exact, explicit versions, and [`global.json`](../../../global.json) pins the exact .NET SDK version (with `"rollForward": "disable"`, so a newer SDK on a machine or runner is never silently substituted) that every `dotnet` command in this repository builds with. An upgrade is always a deliberate, visible change to a version string, never something that happens silently on the next restore.
## Lock Files
diff --git a/docs/developer-manual/conventions/file-naming-conventions.md b/docs/developer-manual/conventions/file-naming-conventions.md
index 2e7eb86..5e504f6 100644
--- a/docs/developer-manual/conventions/file-naming-conventions.md
+++ b/docs/developer-manual/conventions/file-naming-conventions.md
@@ -15,7 +15,7 @@ Files with a conventional, tool- or ecosystem-mandated name keep that name inste
## PascalCase Inside C# Projects
-Inside a C# project's own source tree, directories and files switch to **PascalCase** — for example `source/clinet-core/Hosting/CliApplication.cs`. This follows the standard .NET convention of naming a file after the single type it contains, and grouping related types into a PascalCase namespace folder. The project's own top-level folder (`clinet-core`, `sample-app`) still follows the repository's kebab-case rule — the switch to PascalCase only happens once you are inside the project, looking at its namespaces and types.
+Inside a C# project's own source tree, directories and files switch to **PascalCase** — for example `source/clinet-core/Application/CliApplication.cs`. This follows the standard .NET convention of naming a file after the single type it contains, and grouping related types into a PascalCase namespace folder. The project's own top-level folder (`clinet-core`, `sample-app`) still follows the repository's kebab-case rule — the switch to PascalCase only happens once you are inside the project, looking at its namespaces and types.
The project and solution files themselves (`CLI.NET Core.csproj`, `CLI.NET Core Sample App.csproj`, `CLI.NET Core.slnx`) are named after the product, spaces included, rather than either convention — see [Architecture](../architecture/overview.md) for where they live.
diff --git a/docs/developer-manual/conventions/markdown-style.md b/docs/developer-manual/conventions/markdown-style.md
index 5b4476e..deb9741 100644
--- a/docs/developer-manual/conventions/markdown-style.md
+++ b/docs/developer-manual/conventions/markdown-style.md
@@ -22,6 +22,10 @@ Title case never overrides the real casing of a name. Leave these exactly as the
When a name is intrinsically lowercase, it stays lowercase even as the first word: `## dprint Owns Formatting`.
+## Wrap Code in Backticks
+
+Anywhere prose mentions code — a type or member name, a file or directory path, a package name, a configuration key, a CLI flag — wrap it in backticks: `CliApplication`, `global.json`, `source/clinet-core/`, `RestorePackagesWithLockFile`. This applies in this documentation, in XML documentation comments, and in commit messages (see [Commit Messages](commit-messages.md)), not only in Markdown proper.
+
## Punctuation
- **Prefer a period over a semicolon.** Where a semicolon joins two full sentences, split them into two sentences with a period instead. This applies to all prose — the documentation, `README.md`, etc. In-code comments are the one exception and follow their own rule (see [C# Style](csharp-style.md)).
diff --git a/docs/developer-manual/tooling/vscode-integration.md b/docs/developer-manual/tooling/vscode-integration.md
index bf09c00..7ae0ee7 100644
--- a/docs/developer-manual/tooling/vscode-integration.md
+++ b/docs/developer-manual/tooling/vscode-integration.md
@@ -20,7 +20,7 @@ The `settings.local.json` layer wins over `settings.shared.json`, so each develo
## Settings
-[`settings.shared.json`](../../../.vscode/settings.shared.json) sets a 150-character editor ruler to match [dprint's line width](formatting-dprint.md), maps a few file names to the correct language for syntax highlighting, hides build output and OS clutter from the explorer and file watcher, protects the `main` and `development` branches from direct commits, imports the [CSpell](spell-checking-cspell.md) configuration and raises its diagnostic severity to error, points [MarkdownLint](linting-markdownlint.md) at the project configuration, and enables format-on-save with [dprint](formatting-dprint.md).
+[`settings.shared.json`](../../../.vscode/settings.shared.json) sets a 150-character editor ruler to match [dprint's line width](formatting-dprint.md), maps a few file names to the correct language for syntax highlighting, hides build output and OS clutter from the explorer and file watcher, protects the `main` and `development` branches from direct commits, imports the [CSpell](spell-checking-cspell.md) configuration and raises its diagnostic severity to error, points [MarkdownLint](linting-markdownlint.md) at the project configuration, enables format-on-save with [dprint](formatting-dprint.md), and points `dotnet.defaultSolution` at [`CLI.NET Core.slnx`](../../../source/CLI.NET%20Core.slnx) so a C# extension (OmniSharp or C# Dev Kit) does not have to guess which solution to load — without it, the solution's non-standard location (in `source/`, not the repository root) can lead the extension to miss part of the solution and report a file as not belonging to any project.
## Extensions
diff --git a/docs/user-manual/README.md b/docs/user-manual/README.md
index 2ff0b2a..009fc1b 100644
--- a/docs/user-manual/README.md
+++ b/docs/user-manual/README.md
@@ -4,4 +4,6 @@ This manual is for people building a command line application with CLI.NET Core,
CLI.NET Core is a .NET framework for command line applications, built in the style of ASP.NET Core: it lets you define commands and their arguments much the same way you would define actions and parameters for a Web API. The package is distributed on NuGet as `CliNetCore` (see [Architecture](../developer-manual/architecture/overview.md) for how the project and its packages are laid out).
-This part of the documentation is still growing alongside the framework's public API. Until it does, the [root README](../../README.md) is the best starting point for what the framework is for.
+This part of the documentation is still growing alongside the framework's public API — the [root README](../../README.md) is the best starting point for anything not yet covered here.
+
+1. [Building Applications](building-applications.md)
diff --git a/docs/user-manual/building-applications.md b/docs/user-manual/building-applications.md
new file mode 100644
index 0000000..07b0228
--- /dev/null
+++ b/docs/user-manual/building-applications.md
@@ -0,0 +1,106 @@
+# Building Applications
+
+This article walks through building a command line application with CLI.NET Core, from creating the application to handling its exit code. It follows the same steps a new console project would take, in order.
+
+## Create the Application
+
+Start from a plain .NET console project (`dotnet new console`) that references the `CliNetCore` NuGet package. In `Program.cs`, create a builder from the command-line arguments your `Main` method (or top-level statements) received:
+
+```csharp
+using CliNetCore.Application;
+
+CliApplicationBuilder builder = CliApplication.CreateBuilder(args);
+```
+
+`CliApplication.CreateBuilder(args)` is the CLI.NET Core analogue of ASP.NET Core's `WebApplication.CreateBuilder(args)`. Pass it the same `args` array your program was started with. If you don't have arguments to pass (for example, in a test), call the parameterless `CliApplication.CreateBuilder()` instead — it behaves the same way but with no command-line arguments.
+
+## Configure Services, Configuration, and Logging
+
+Before building the application, `builder` gives you the same configuration surface an ASP.NET Core host gives you, because `CliApplicationBuilder` implements `IHostApplicationBuilder`:
+
+```csharp
+builder.Services.AddSingleton();
+builder.Logging.AddConsole();
+string? greeting = builder.Configuration["Greeting"];
+```
+
+Use `builder.Services` to register your own services with dependency injection, `builder.Configuration` to read configuration, and `builder.Logging` to configure logging, exactly as you would in an ASP.NET Core application. Anything you register here is available once the application starts.
+
+## Read the Command-Line Arguments
+
+If a service you register needs the raw command-line arguments, depend on `CliCommandLineArguments` instead of calling `Environment.GetCommandLineArgs()` directly:
+
+```csharp
+public sealed class Greeter : IGreeter
+{
+ public Greeter(CliCommandLineArguments commandLineArguments) => this.commandLineArguments = commandLineArguments;
+
+ private readonly CliCommandLineArguments commandLineArguments;
+
+ public void Greet()
+ {
+ foreach (string argument in this.commandLineArguments.Arguments)
+ {
+ Console.WriteLine(argument);
+ }
+ }
+}
+```
+
+`CliCommandLineArguments` is always registered for you, so you never need to register it yourself and never need to guard against it being missing. Its `Arguments` property holds the arguments in the order they were given, and is an empty list — never `null` — when the application was created without arguments.
+
+## Build and Run the Application
+
+Once you are done configuring the builder, call `Build()` to produce the application, then run it:
+
+```csharp
+CliApplication application = builder.Build();
+await application.RunAsync();
+```
+
+`Build()` freezes configuration: after it returns, the service provider has been built and no more services can be registered. `RunAsync()` (or its blocking counterpart, `Run()`) starts the application and does not return until it shuts down, which today happens automatically as soon as the current command has finished executing.
+
+Put together, a minimal `Program.cs` looks like this:
+
+```csharp
+using CliNetCore.Application;
+
+CliApplicationBuilder builder = CliApplication.CreateBuilder(args);
+builder.Services.AddSingleton();
+
+CliApplication application = builder.Build();
+await application.RunAsync();
+```
+
+## Handle the Exit Code
+
+When the application shuts down, the process exits with a code from `CliNetCore.ExitCode`:
+
+| Constant | Value | Meaning |
+| -------------------------------- | ----- | ---------------------------------------------------------------------------------- |
+| `ExitCode.Success` | `0` | The command completed successfully. |
+| `ExitCode.GeneralError` | `1` | An unspecified error occurred while executing the command. |
+| `ExitCode.UsageError` | `64` | The command line was used incorrectly, for example missing or malformed arguments. |
+| `ExitCode.DataError` | `65` | The command's input data was incorrect or malformed. |
+| `ExitCode.NoInput` | `66` | An input file named on the command line does not exist or cannot be read. |
+| `ExitCode.InternalSoftwareError` | `70` | An internal error was detected within the command's own logic. |
+| `ExitCode.CannotCreateOutput` | `73` | An output file the command needed to create could not be created. |
+| `ExitCode.IoError` | `74` | An I/O error occurred while the command was executing. |
+| `ExitCode.ConfigError` | `78` | A configuration file the command depends on was missing, unreadable, or invalid. |
+| `ExitCode.CommandNotExecutable` | `126` | The specified command was found but could not be executed. |
+| `ExitCode.CommandNotFound` | `127` | The specified command could not be found. |
+| `ExitCode.Cancelled` | `130` | The command was cancelled by the user, for example by pressing `Ctrl+C`. |
+| `ExitCode.Killed` | `137` | The command's process was terminated by an unhandled `SIGKILL` signal. |
+| `ExitCode.Terminated` | `143` | The command's process was terminated by an unhandled `SIGTERM` signal. |
+| `ExitCode.OutOfRangeExitStatus` | `255` | The command exited with a status outside the range a calling shell can interpret. |
+
+None of these are reserved for a particular layer of the framework — they're ordinary `int` constants, and any command or application code can return any of them. Today, the framework itself only sets three automatically: cancelling the application (for example with `Ctrl+C`) reports `ExitCode.Cancelled`, an unhandled exception reports `ExitCode.GeneralError`, and otherwise the application reports `ExitCode.Success`. The rest, including `ExitCode.UsageError` and `ExitCode.CommandNotFound`, are there for you to return yourself; some of them will simply read more naturally once the command model described below exists to produce them on your behalf.
+
+## What's Missing Today
+
+CLI.NET Core does not yet have a command model: there is no way to define individual commands or bind their arguments to strongly typed parameters, so an application built today only reaches the bootstrap-and-exit behavior described above. Once the command model exists, this article will grow with the articles that describe it.
+
+## Related
+
+- How the framework's projects and packages are laid out: [Architecture](../developer-manual/architecture/overview.md).
+- Back to the [User Manual](README.md).
diff --git a/dprint.json b/dprint.json
index 87718a2..92ca539 100644
--- a/dprint.json
+++ b/dprint.json
@@ -13,6 +13,17 @@
// languages, like Markdown have separate settings to disable line breaking, so the character limit does not apply to them
"lineWidth": 150,
+ // Configuration for C# files; the Roslyn plugin does not define its own option names, it reflects them straight off Microsoft's
+ // "Microsoft.CodeAnalysis.CSharp.Formatting.CSharpFormattingOptions" class (the same type the Roslyn formatter itself uses), converting
+ // "csharp.somePropertyName" into the "SomePropertyName" property; unknown keys are rejected with a configuration diagnostic; the plugin only
+ // drives whitespace/newline/indentation (Formatter.Format), never style-analyzer preferences (there is no "csharp_style_*" or "dotnet_*"
+ // support)
+ "roslyn": {
+ // The line ending written to formatted files; it defaults to Carriage Return + Line Feed (CRLF), since ".gitattributes" normalizes "*.cs"
+ // files to Line Feed (LF) on commit anyway, we can use LF here to reduce friction
+ "newLineKind": "lf",
+ },
+
// Configuration for Markdown files
"markdown": {
"textWrap": "never",
@@ -81,6 +92,7 @@
"https://plugins.dprint.dev/g-plane/pretty_yaml-v0.6.0.wasm",
"https://plugins.dprint.dev/json-0.22.0.wasm",
"https://plugins.dprint.dev/markdown-0.22.1.wasm",
+ "https://plugins.dprint.dev/roslyn-0.21.0.json@c1429e04de13cff489025e8739f76a476fce6513128366f30feecef2bec0848d",
"https://plugins.dprint.dev/toml-0.7.0.wasm",
],
}
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..daa8bf3
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "sdk": {
+ "version": "10.0.300",
+ "rollForward": "disable"
+ }
+}
diff --git a/source/clinet-core/Application/CliApplication.cs b/source/clinet-core/Application/CliApplication.cs
new file mode 100644
index 0000000..f6141ee
--- /dev/null
+++ b/source/clinet-core/Application/CliApplication.cs
@@ -0,0 +1,122 @@
+
+#region Using Directives
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+#endregion
+
+namespace CliNetCore.Application;
+
+///
+/// A built CLI application, ready to be run. It is the CLI.NET Core analogue of ASP.NET Core's WebApplication: It implements
+/// so that it interoperates with the whole generic-host ecosystem, but it does not reimplement hosting and it does not sub-class
+/// the concrete host (which is and ). Instead it composes a real and forwards
+/// every member to it. The convenience properties are not additional state, they are simply resolved from the one real
+/// the inner host owns.
+///
+public sealed class CliApplication : IHost, IAsyncDisposable
+{
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class. This constructor is because
+ /// instances can only be created through the class.
+ ///
+ /// The built generic host to wrap. This is produced by .
+ internal CliApplication(IHost host) => this.host = host;
+
+ #endregion
+
+ #region Private Fields
+
+ ///
+ /// The concrete generic host that this application wraps. All hosting behavior, i.e., starting and stopping hosted services, owning the service
+ /// provider, and coordinating shutdown, is delegated to it.
+ ///
+ private readonly IHost host;
+
+ #endregion
+
+ #region Public Properties
+
+ ///
+ /// Gets the configuration of the application, resolved from the application's service provider.
+ ///
+ public IConfiguration Configuration => this.host.Services.GetRequiredService();
+
+ ///
+ /// Gets the hosting environment of the application (its name, application name, and content-root path), resolved from the service provider.
+ ///
+ public IHostEnvironment Environment => this.host.Services.GetRequiredService();
+
+ ///
+ /// Gets the application lifetime, which signals application start and shutdown and can be used to request that the application stop.
+ ///
+ public IHostApplicationLifetime Lifetime => this.host.Services.GetRequiredService();
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Creates a with the host defaults and no command-line arguments.
+ ///
+ /// Returns the newly created builder.
+ public static CliApplicationBuilder CreateBuilder() => CliApplicationBuilder.CreateBuilder();
+
+ ///
+ /// Creates a with the host defaults and the given command-line arguments. This is the usual entry point for a
+ /// CLI application. The are the same array that is passed to the program's Main method.
+ ///
+ /// The command-line arguments the application was started with.
+ /// Returns the newly created builder.
+ public static CliApplicationBuilder CreateBuilder(string[] args) => CliApplicationBuilder.CreateBuilder(args);
+
+ ///
+ /// Runs the application and blocks the calling thread until the host shuts down. For a CLI application this typically means until the command has
+ /// finished executing, because the dispatcher hosted service requests shutdown as soon as the command completes. This delegates to the standard
+ /// hosting run helper, so it behaves exactly like running any other .
+ ///
+ public void Run() => this.host.Run();
+
+ ///
+ /// Runs the application and returns a task that completes when the host shuts down. For a CLI application this typically means when the command
+ /// has finished executing. This delegates to the standard hosting run helper, so it behaves exactly like running any other .
+ ///
+ /// A token that, when signaled, requests that the application shut down.
+ /// Returns a task that completes when the application has shut down.
+ public Task RunAsync(CancellationToken cancellationToken = default) => this.host.RunAsync(cancellationToken);
+
+ #endregion
+
+ #region IHost Implementation
+
+ ///
+ public IServiceProvider Services => this.host.Services;
+
+ ///
+ public Task StartAsync(CancellationToken cancellationToken = default) => this.host.StartAsync(cancellationToken);
+
+ ///
+ public Task StopAsync(CancellationToken cancellationToken = default) => this.host.StopAsync(cancellationToken);
+
+ #endregion
+
+ #region IAsyncDisposable Implementation
+
+ ///
+ public void Dispose() => this.host.Dispose();
+
+ ///
+ public ValueTask DisposeAsync()
+ {
+ if (this.host is IAsyncDisposable asyncDisposableHost)
+ return asyncDisposableHost.DisposeAsync();
+ this.host.Dispose();
+ return ValueTask.CompletedTask;
+ }
+
+ #endregion
+}
diff --git a/source/clinet-core/Application/CliApplicationBuilder.cs b/source/clinet-core/Application/CliApplicationBuilder.cs
new file mode 100644
index 0000000..6380da3
--- /dev/null
+++ b/source/clinet-core/Application/CliApplicationBuilder.cs
@@ -0,0 +1,121 @@
+
+#region Using Directives
+
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.Metrics;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+#endregion
+
+namespace CliNetCore.Application;
+
+///
+/// A builder for configuring and creating a . It is the CLI.NET Core analogue of ASP.NET Core's
+/// WebApplicationBuilder: It implements so that the whole generic-host ecosystem (hosted-service
+/// registration, configuration, logging, dependency injection, and tooling such as dotnet run) works against it unchanged, while internally it
+/// merely wraps a concrete and forwards to it. The concrete builder is and therefore
+/// cannot be sub-classed, so composition, not inheritance, is used, which also lets this type expose a small, CLI-shaped surface instead of the full
+/// host builder surface.
+///
+public sealed class CliApplicationBuilder : IHostApplicationBuilder
+{
+ #region Constructors
+
+ ///
+ /// Initializes a new instance of the class. The constructor is , because
+ /// instances can only be created through the or
+ /// factory methods, or, even better, through the or
+ /// factory methods.
+ ///
+ ///
+ /// The settings used to initialize the underlying , most notably the command-line arguments. May be
+ /// , in which case the host defaults are used.
+ ///
+ internal CliApplicationBuilder(HostApplicationBuilderSettings? settings)
+ {
+ this.hostApplicationBuilder = Host.CreateApplicationBuilder(settings);
+
+ // Registered unconditionally, defaulting to an empty list, so that command logic can always depend on CliCommandLineArguments instead of
+ // reading Environment.GetCommandLineArgs() directly, regardless of whether the application was created with arguments
+ this.hostApplicationBuilder.Services.AddSingleton(new CliCommandLineArguments(settings?.Args ?? []));
+ }
+
+ #endregion
+
+ #region Private Fields
+
+ ///
+ /// The concrete generic-host builder that does the actual work. Every member of this wrapper delegates to it, and ultimately
+ /// calls its to produce the real that the returned
+ /// wraps.
+ ///
+ private readonly HostApplicationBuilder hostApplicationBuilder;
+
+ #endregion
+
+ #region Public Static Methods
+
+ ///
+ /// Creates a pre-configured with the host defaults and no command-line arguments.
+ ///
+ /// Returns the newly created builder.
+ public static CliApplicationBuilder CreateBuilder() => new(new HostApplicationBuilderSettings());
+
+ ///
+ /// Creates a pre-configured with the host defaults and the given command-line arguments. This is the usual
+ /// entry point for a CLI application; the are the same array that is passed to the program's Main method.
+ ///
+ /// The command-line arguments the application was started with.
+ /// Returns the newly created builder.
+ public static CliApplicationBuilder CreateBuilder(string[] args) => new(new HostApplicationBuilderSettings { Args = args });
+
+ #endregion
+
+ #region Public Methods
+
+ ///
+ /// Builds the . This registers the infrastructure the CLI needs, builds the underlying generic host, and wraps it in
+ /// a . After this method returns, the underlying host is frozen: its service provider has been built and no further
+ /// configuration is possible, which mirrors the "frozen after Build()" invariant of the generic host.
+ ///
+ /// Returns the built , ready to be run.
+ public CliApplication Build()
+ {
+ // The dispatcher hosted service is registered last (during Build, after all of the consumer's ConfigureServices calls have run) so that it
+ // starts after — and therefore stops before — any hosted services the consumer registered themselves.
+ this.hostApplicationBuilder.Services.AddHostedService();
+
+ return new CliApplication(this.hostApplicationBuilder.Build());
+ }
+
+ #endregion
+
+ #region IHostApplicationBuilder Implementation
+
+ ///
+ public IDictionary