Skip to content

feat(spectre)!: Ploch.CommandLine.Spectre initial release-ready implementation - #11

Merged
kploch merged 77 commits into
mainfrom
#3-spectre-console-initial
Aug 28, 2026
Merged

feat(spectre)!: Ploch.CommandLine.Spectre initial release-ready implementation#11
kploch merged 77 commits into
mainfrom
#3-spectre-console-initial

Conversation

@kploch

@kploch kploch commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Describe your changes

Initial implementation of Ploch.CommandLine.Spectre — adopting Spectre.Console as the framework this library builds on — together with everything needed to make the repository releasable: the legacy projects retired, CI made functional, packaging modernised, a sample application, published documentation, and a test suite.

+35,652 / −107,469 across 422 files. The large deletion is the retired McMaster-based library and 4.2 MB of committed analyser output.

This began as the library alone. It grew because the repository could not actually ship: it did not compile, its CI had never once executed, it had no licence file, no release path, and 3.33% test coverage. Each of those was tracked as its own issue and delivered as its own reviewed pull request, then merged here.

Why this is one large PR

The sub-PRs (#15, #16, #17, #18, #21) were each reviewed independently before merging in. They had to converge here rather than land separately, for a structural reason: SonarCloud measures new code, and this branch introduces the entire library, so every file in the repository counts as new. Any finding anywhere therefore fails this PR's gate. #16's deletion of DocumentationSite/Dockerfile was literally required to clear the security rating. Splitting the work further would not have produced independently mergeable PRs.

The cost is real and should be stated: at 422 files this PR is past CodeRabbit's 100-file review limit, so it has had no CodeRabbit review. SonarCloud, Qodana, CodeAnt and Codex all reviewed the constituent PRs.


Changes

The framework

  • AppBuilder, wrapping Microsoft.Extensions.Hosting with Spectre.Console.Cli, plus CommandAppExecutor / ICommandAppExecutor.
  • AppCommand<TSettings> and AsyncAppCommand<TSettings> base classes with validation, exception handling and cancellation.
  • Settings pipeline: ICommandSettingsProcessor, CommandArgumentsRootProcessor, CommandSettingsPropertyTypeProcessor<T>, and TokensArgumentsProcessor for {date} / {datetime} substitution via [SupportsTokens].
  • Output pipeline: IOutput / AnsiConsoleMarkupOutput, IMessageFormatterProcessor, type-based formatters and writers.
  • UseCaseAsyncCommand, bridging commands to IResultUseCase (Ardalis.Result).
  • Sub-packages for Serilog and FluentValidation integration.

Runtime defects fixed

The branch did not compile when this work started (3 errors, 116 warnings). External review then surfaced seven defects on consumer-visible paths:

# Defect Impact
1 ConvertibleMessageFormatter was DI-registered but threw NotImplementedException Any output.Write(42) crashed — int, DateTime, bool all implement IConvertible
2 CommandInfoFactory.CreateFromType threw on its primary path Public API that only threw
3 CancellationToken accepted everywhere, honoured nowhere Ctrl+C cancelled a token nothing observed
4 AnsiConsoleMarkupOutput.Write double-wrote Every writer-handled message printed twice
5 DefaultExceptionHandler markup-unsafe Win32 exception text containing [ could throw inside the exception handler
6 Startup banner rendered FigletText twice Duplicated application name on every run
7 TreatWarningsAsErrors=false in Release Published builds silently dropped the zero-warning bar

Plus the review findings (#14): EnvironmentSettings.Current check-then-act lazy init replaced with a synchronised one; EnvironmentSettingsLoader no longer retains the entire environment block, which routinely carries secrets; the Serilog error sink sat outside its filtered sub-logger so the "errors" file received every event; AddSerilog registered Serilog twice.

Legacy retired (#10)

src/CommandLine{,.Autofac,.Hosting,.Serilog}, src/DemoApp, their tests and samples, plus the old solutions. Nothing here was ever published to NuGet, so no consumer is affected and no deprecation shims were added.

Repository clutter removed with evidence for each: azure-pipelines.yml built another repository's solution (Ploch.Common.sln / ploch_common); run-sonar-build-test.ps1 scanned mrploch_ploch-data; build.cmd/.ps1/.sh were NUKE bootstraps for a build/_build.csproj that does not exist; qodana.sarif.json was 4.2 MB of generated output.

Security — action required outside this PR. publish-nugetorg.ps1, deleted here, contained a plaintext nuget.org API key, committed in 37ae9bc (2024-10-16) to a public repository. Deleting the file does not purge it from history. That key must be revoked and regenerated on nuget.org.

CI made functional (#12)

CI had never run. build-dotnet.yml filtered on branch master while the default branch is main.

Once enabled, a chain of failures surfaced, each hidden by the previous one:

  1. Restore failed MSB3202 for every ../ploch-common project — the solution references sibling repositories by relative path, and CI checked out only this one. Siblings are now cloned to reproduce the workspace layout.
  2. NBGV cannot compute version height from a shallow clone → fetch-depth: 0.
  3. SonarCloud failed Not authorized on every run. Root cause was not permissions, despite the message naming the token, key, organisation and permissions. The shared mrploch/ploch-github-actions/build-test-sonar action passes the token only as sonar.login, which is deprecated and no longer honoured by the current scanner engine — so it authenticated anonymously. Proved by re-running the old workflow and the new one minutes apart against the same token, key and organisation: old fails, new passes. The inline workflow sets SONAR_TOKEN as an environment variable, which the scanner reads natively. Filed upstream as build-test-sonar passes the Sonar token only via the removed sonar.login property, so every analysis fails 'Not authorized' ploch-github-actions#1, since every repository still using that action is silently unanalysed.
  4. dotnet nuget add source failed on a duplicate URL — NuGet.Config already registers that feed as github, and the command rejects duplicate URLs as well as duplicate names. Packages are now pushed to the feed URL directly.

Also: pull_request was filtered to branches: [main], which matches the PR's base branch, so stacked PRs got no build at all; the filter is gone. code_quality.yml and test_report.yml were stale duplicates and are removed. Third-party actions are pinned to commit SHAs. The Pages deploy and all package publishing are gated so they cannot fire from an arbitrary branch.

Packaging and versioning (#7)

Legacy VersionPrefix 0.0.1 + RELEASEVERSION replaced with Nerdbank.GitVersioning (version.json at 1.0-prerelease), .config/dotnet-tools.json pinning nbgv and docfx, and a global.json pinning the SDK — the build previously worked only because the runner image happened to ship a .NET 10 SDK and nothing pinned it, while $(TargetFrameworkVersion) resolves to net10.0.

PlochCommandLine.Spectre.FluentValidation was renamed to Ploch.CommandLine.Spectre.FluentValidation — the package id was missing a dot. The C# namespace was renamed with it, since shipping a Ploch.* package containing a PlochCommandLine.* namespace would have been permanent.

SourceLink, .snupkg symbols and portable PDBs added. Four packages now pack cleanly.

Licence, documentation and release (#6)

  • LICENSE added. Directory.Build.props has asserted PackageLicenseExpression = Apache-2.0 since the repository was created, with no licence file present — every package would have shipped claiming a licence the repository did not carry.
  • Documentation site rewritten. index.md documented the retired McMaster API, and toc.yml linked to ploch-data. Rewritten against the current public surface, verified against the source. Builds with 0 errors and 2 warnings, both benign cross-repository references.
  • publish-docs.yml deploys to GitHub Pages via the official Pages actions.
  • release.yml — manually dispatched, tags before publishing (orphaned NuGet packages cannot be deleted, only unlisted), publishes to NuGet.org, creates the GitHub Release from change-log/ entries, then bumps to the next development version.
  • RELEASE_NOTES.md and the change-log/ convention.

Sample application (#9)

samples/SampleApp/ — a complete multi-level CLI (config, file, project, user branches) demonstrating DI, configuration, FluentValidation, token expansion, Serilog, use cases and exit codes, with 28 tests of its own and a docs/GETTING_STARTED.md walkthrough. It consumes the libraries as PackageReference with a -p:UsePlochProjectReferences=true switch for in-repo validation. CI builds it, so it cannot silently rot against a library change again.

Running it found five further defects, four of which only manual execution could surface — including config show printing the entire process environment, API keys included, because it enumerated configuration.GetChildren() and the host adds an environment-variable provider.

Tests and coverage (#13)

Before After
Tests 8 195
Test projects 2 4
SonarCloud new_coverage 3.33% 98.0%

Two packages had no test project at all (Serilog, UseCases). Regression cover was added for every defect above — including a test that writes real events through a real Serilog pipeline and asserts that only Warning and above reach the errors file.


Post-review fixes

Codacy raised 16 threads on this pull request. All are resolved: two were already fixed by #28, eight were fixed in code across #35, #36, #37 and #39, three were declined with evidence, and three were deferred to new issues.

The substantive one was a release blocker. output.WriteError("Value [archive] is invalid") threw InvalidOperationException: Could not find color or style 'archive' — arbitrary caller data was going through Spectre's markup parser, in the one method whose purpose is printing exception text. Fixed in #35 and #37 by escaping content where this library adds the tag, while leaving markup the caller writes untouched. The same change recovered format specifiers: $"total: {1234.5:N2}" had been rendering as total: 1234.5.

Two defects were also found in the fixes themselves, both caught by review before merge: a trailing blank line after writer-handled collections, and a first attempt at that fix which contradicted its own contract by suppressing the line break for inline writers. IMessageWriter.WritesLineTerminator settles it.

Design decisions

  • SonarCloud left blocking, deliberately deviating from ploch-common, which marks its Sonar steps continue-on-error. Recorded in a comment in the workflow so it is not "aligned" away later.
  • Sibling repositories cloned in CI rather than switching to PackageReference, preserving the documented workspace model and requiring no change to the eight cross-repository project references.
  • Tag before publish in the release workflow: tags can be deleted and recreated, NuGet packages cannot.
  • IDE0058 disabled repo-wide in .editorconfig, per .claude/rules/code-quality.md, which prescribes this exact remedy and forbids the _ = discard alternative. Matches ploch-common and ploch-data. This cleared 357 INFO findings.
  • VSTHRD200 disabled for tests, because this repository mandates <TestedMember>_should_<behaviour> names which cannot also carry the Async suffix the rule wants.

Breaking changes

Pre-release; nothing published depends on these yet.

  • AppCommand<TSettings>.DoExecute and AsyncAppCommand<TSettings>.DoExecuteAsync now accept a CancellationToken.
  • IMessageFormatterProcessor.WriteMessage returns bool instead of void.
  • Package and namespace PlochCommandLine.Spectre.FluentValidationPloch.CommandLine.Spectre.FluentValidation.
  • The Ploch.Common.CommandLine packages and their Autofac, Hosting and Serilog companions are removed.
  • ConsoleAppInfo.AppNameColorSys, AppNameInfoColorSys, AppDescriptionColorSys and ConsoleAppInfoExtensions.FromSysColor removed — use the Spectre Color properties.
  • AnsiConsoleMarkupOutput.WriteMarkupLineInterpolated removed — use MarkupLineInterpolated.
  • EnvironmentSettings.Initialize throws if called after Current has been read; DevRuntimeVariables contains only DEV_RUNTIME-prefixed variables; PauseBeforeExit defaults to false.

Testing

Local: dotnet build -c Release0 errors, 0 warnings. dotnet test195 tests, 0 failures. Sample builds and runs; real console output captured in #18.

CI on the head commit: build pass, Test Results pass, qodana pass, SonarCloud Code Analysis pass.

SonarCloud quality gate OK — every condition green:

Condition Value Threshold
new_coverage 98.0% ≥ 80
new_reliability_rating 1 1
new_security_rating 1 1
new_maintainability_rating 1 1
new_duplicated_lines_density 0.0% ≤ 3
new_security_hotspots_reviewed 100% 100

Zero security hotspots to review; zero unresolved review threads. 45 platform findings remain, all pre-existing INFO plus two MAJOR code smells that predate this work — enumerated and triaged in #24.

Related

Follow-ups filed: #19 (sample tests built but not run), #20 (IOutput.Write(exception) throws InvalidCastException), #22 (AppBuilder.ConfigureServices discards all but the last delegate), #23 (Codacy coverage upload unauthenticated), #24 (remaining analyser findings), mrploch/ploch-github-actions#1 (shared Sonar action broken org-wide).

Checklist before requesting a review

  • I have performed a self-review of my code
  • If it is a core feature, I have added thorough tests. — 8 → 195 tests, 98.0% coverage on new code
  • Do we need to implement analytics? — No
  • Will this be part of a product update? — Yes: the first supported release of Ploch.CommandLine.Spectre

Release-readiness review pass (2026-08-26)

Twelve findings from the Copilot reviewer were each verified against source, fixed, and
re-reviewed by an external panel (Codex, Gemini, GitHub Copilot CLI/Grok 4.6). Commits
06864bb and 46c2884.

Fixed

Finding Resolution
Markup injection in the start-up banner PrintAppInfo escapes consumer name/description. Reproduced the Could not find color or style 'Dev' crash before fixing.
Win32ExceptionMessageFormatter unreachable Formatter/writer selection is now most-derived-wins, not first-registered-wins — which also unshadows any formatter a consumer registers for their own exception type.
IFormatProvider ignored for scalars Honoured for any IFormattable; null results still coalesce to empty. Default behaviour unchanged.
Non-volatile double-checked lock EnvironmentSettings._current is volatile.
Captive dependency in FluentValidation wiring Validator resolved per validation from a scope; consumer validators keep their scoped lifetime and may depend on a DbContext.
Duplicate open-generic registration AddCommandLineSettingsFluentValidation registers the mapping once.
Settings echo disclosed secrets Opt-in via EchoSettings, default off. Breaking.
Sample printed nested secrets Leaf values on sensitive configuration paths render as <redacted>.
Quick-start did not compile Corrected; verified by extracting the sample verbatim from the markdown and building it.
Docs overstated the settings pipeline Corrected — only the async bases run it.
Stale AddSerilog remarks Rewritten to match the implementation.
Unrelated .claude/local-marketplace/ content 35 files removed — also why CodeRabbit skipped this PR (426 files > 100 limit).

Breaking changes

  • UseCaseAsyncCommand<...> no longer echoes settings; override EchoSettings to restore.
  • FluentCommandSettingsValidator<TSettings> takes IServiceScopeFactory instead of an
    optional IValidator<TSettings> (resolved from DI, so direct construction only).

Verification — build clean in Debug and Release, 0 warnings under
TreatWarningsAsErrors. 210 tests in the Spectre suite (was 202), 8 in FluentValidation
(was 4), 9 in UseCases (was 7). Every fix has a regression test, and each was
mutation-verified by reverting the fix and confirming the test fails.

Known blockers

@codeant-ai

codeant-ai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Skipping CodeAnt AI review — this PR changes more than 100 files, which usually means a migration, codemod, or vendored drop. Line-level review on diffs this large produces duplicate findings on the same rewrite pattern and drowns out anything that actually matters.

If you still want a review, comment @codeant-ai : review. For better signal, consider splitting the PR into smaller chunks.

@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@bito-code-review

bito-code-review Bot commented Aug 19, 2026

Copy link
Copy Markdown

Bito Automatic Review Skipped - Large PR

Bito didn't auto-review this change because the pull request exceeded the line limit. No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.

@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 400 files, which is 300 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5c70ad31-4113-4b21-8e1a-cf1902a324ed

📥 Commits

Reviewing files that changed from the base of the PR and between e9517a8 and 1f9461e.

📒 Files selected for processing (400)
  • .agents/commit/SKILL.md
  • .agents/dotnet-dev-finishing-touches/SKILL.md
  • .agents/dotnet-dev-practical/SKILL.md
  • .agents/dotnet-dev-practical/analyzer-reference.md
  • .agents/dotnet-dev-practical/related-skills.md
  • .agents/implement-issue/SKILL.md
  • .agents/pr/SKILL.md
  • .agents/prompt-lookup/.skillfish.json
  • .agents/prompt-lookup/SKILL.md
  • .agents/qa-explore/SKILL.md
  • .agents/review-pr-comments/SKILL.md
  • .agents/review-pr/SKILL.md
  • .checkov.yaml
  • .claude/mrploch-dev/.claude-plugin/plugin.json
  • .claude/mrploch-dev/agents/implement-issue.md
  • .claude/rules/agent.md
  • .claude/rules/branch-naming.md
  • .claude/rules/code-quality.md
  • .claude/rules/commits.md
  • .claude/rules/data-access.md
  • .claude/rules/data-project.md
  • .claude/rules/data-provider-project.md
  • .claude/rules/dependencies.md
  • .claude/rules/documentation.md
  • .claude/rules/domain-model.md
  • .claude/rules/external-ai-review.md
  • .claude/rules/local-setup-scripts.md
  • .claude/rules/naming.md
  • .claude/rules/notes-keeping.md
  • .claude/rules/pr-checks-completion-gate.md
  • .claude/rules/pr-descriptions.md
  • .claude/rules/project-structure.md
  • .claude/rules/qa.md
  • .claude/rules/rules.md
  • .claude/rules/sample-apps.md
  • .claude/rules/summaries.md
  • .claude/rules/todo-tasks-execution.md
  • .claude/rules/traceability.md
  • .claude/rules/work-traceability.md
  • .claude/rules/writing-dotnet-tests.md
  • .claude/skills/dev-finishing-touches/SKILL.md
  • .claude/skills/docfx-api-docs/SKILL.md
  • .claude/skills/dotnet-dev-finishing-touches/SKILL.md
  • .claude/skills/dotnet-dev-practical/SKILL.md
  • .claude/skills/dotnet-dev-practical/analyzer-reference.md
  • .claude/skills/dotnet-dev-practical/related-skills.md
  • .claude/skills/implement-issue/SKILL.md
  • .claude/skills/prompt-lookup/.skillfish.json
  • .claude/skills/prompt-lookup/SKILL.md
  • .codacy.yml
  • .config/dotnet-tools.json
  • .contextstream/config.json
  • .cursor/mcp.json
  • .cursor/rules/agent.mdc
  • .cursor/rules/branch-naming.mdc
  • .cursor/rules/code-quality.mdc
  • .cursor/rules/commits.mdc
  • .cursor/rules/data-access.mdc
  • .cursor/rules/data-project.mdc
  • .cursor/rules/data-provider-project.mdc
  • .cursor/rules/dependencies.mdc
  • .cursor/rules/documentation.mdc
  • .cursor/rules/domain-model.mdc
  • .cursor/rules/local-setup-scripts.mdc
  • .cursor/rules/naming.mdc
  • .cursor/rules/notes-keeping.mdc
  • .cursor/rules/pr-checks-completion-gate.mdc
  • .cursor/rules/pr-descriptions.mdc
  • .cursor/rules/project-structure.mdc
  • .cursor/rules/qa.mdc
  • .cursor/rules/rules.mdc
  • .cursor/rules/sample-apps.mdc
  • .cursor/rules/summaries.mdc
  • .cursor/rules/todo-tasks-execution.mdc
  • .cursor/rules/traceability.mdc
  • .cursor/rules/work-traceability.mdc
  • .cursor/rules/writing-dotnet-tests.mdc
  • .cursor/skills/docfx-api-docs/SKILL.md
  • .cursor/skills/dotnet-dev-finishing-touches/SKILL.md
  • .cursor/skills/dotnet-dev-practical/SKILL.md
  • .cursor/skills/dotnet-dev-practical/analyzer-reference.md
  • .cursor/skills/dotnet-dev-practical/related-skills.md
  • .cursor/skills/implement-issue/SKILL.md
  • .cursor/skills/prompt-lookup/.skillfish.json
  • .cursor/skills/prompt-lookup/SKILL.md
  • .cursor/skills/winui3-controls-layouts/SKILL.md
  • .cursor/skills/winui3-controls-layouts/custom-controls.md
  • .cursor/skills/winui3-data-binding/SKILL.md
  • .cursor/skills/winui3-dependency-injection/SKILL.md
  • .cursor/skills/winui3-efcore-desktop/SKILL.md
  • .cursor/skills/winui3-mrploch-app/SKILL.md
  • .cursor/skills/winui3-mvvm-toolkit/SKILL.md
  • .cursor/skills/winui3-mvvm-toolkit/source-generators.md
  • .cursor/skills/winui3-mvvm-toolkit/validation-and-messaging.md
  • .cursor/skills/winui3-navigation/SKILL.md
  • .cursor/skills/winui3-performance-pitfalls/SKILL.md
  • .cursor/skills/winui3-project-setup/SKILL.md
  • .cursor/skills/winui3-project-setup/deployment-models.md
  • .cursor/skills/winui3-testing/SKILL.md
  • .cursor/skills/winui3-theming-windowing/SKILL.md
  • .cursorrules
  • .editorconfig
  • .github/dependabot.yml
  • .github/pull_request_template.md
  • .github/workflows/build-dotnet.yml
  • .github/workflows/codeql.yml
  • .github/workflows/publish-docs.yml
  • .github/workflows/qodana_code_quality.yml
  • .github/workflows/release.yml
  • .gitignore
  • .sonarlint/connectedMode.json
  • .syncignore
  • AGENTS.md
  • CLAUDE.md
  • Clean-DocFx.ps1
  • Directory.Build.props
  • Directory.Packages.props
  • DocumentationSite/.gitignore
  • DocumentationSite/Clean-DocFx-Common.ps1
  • DocumentationSite/api/.gitignore
  • DocumentationSite/api/index.md
  • DocumentationSite/articles/intro.md
  • DocumentationSite/articles/toc.yml
  • DocumentationSite/docfx.json
  • DocumentationSite/index.md
  • DocumentationSite/toc.yml
  • GEMINI.md
  • LICENSE
  • Ploch.CommandLine.LocalDev.sln
  • Ploch.CommandLine.Spectre.sln.DotSettings
  • Ploch.CommandLine.Spectre.slnx
  • Ploch.CommandLine.sln
  • Ploch.CommandLine.sln.DotSettings
  • README.md
  • RELEASE_NOTES.md
  • SonarLint.xml
  • azure-pipelines.yml
  • build.cmd
  • build.ps1
  • build.sh
  • change-log/11-release-readiness-review-fixes.md
  • change-log/20-output-write-message-dispatch.md
  • change-log/22-appbuilder-additive-configuration.md
  • change-log/31-markup-injection-in-output-helpers.md
  • change-log/README.md
  • directory.build.targets
  • docker-compose.yml
  • docs/GETTING_STARTED.md
  • global.json
  • nuget.config
  • ploch-commandline.code-workspace
  • publish-nugetorg.ps1
  • qodana.sarif.json
  • qodana.yaml
  • run-sonar-build-test.ps1
  • samples/HelloWorldConsoleApp/Dockerfile
  • samples/HelloWorldConsoleApp/HelloWorldConsoleApp.csproj
  • samples/HelloWorldConsoleApp/HellowWorldCommand.cs
  • samples/HelloWorldConsoleApp/Program.cs
  • samples/HostingSample/Dockerfile
  • samples/HostingSample/HelloRootCommand.cs
  • samples/HostingSample/HostingSample.csproj
  • samples/HostingSample/Program.cs
  • samples/HostingSample/Properties/launchSettings.json
  • samples/HostingSample/Worker.cs
  • samples/HostingSample/appsettings.Development.json
  • samples/HostingSample/appsettings.json
  • samples/README.md
  • samples/SampleApp/Directory.Build.props
  • samples/SampleApp/Directory.Build.targets
  • samples/SampleApp/Directory.Packages.props
  • samples/SampleApp/Ploch.CommandLine.Spectre.SampleApp.slnx
  • samples/SampleApp/ProjectReferences.props
  • samples/SampleApp/README.md
  • samples/SampleApp/src/SampleApp/Commands/Common/GlobalSettings.cs
  • samples/SampleApp/src/SampleApp/Commands/Common/InfoCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Config/ConfigGetCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Config/ConfigSetCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Config/ConfigSettings.cs
  • samples/SampleApp/src/SampleApp/Commands/Config/ConfigShowCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Config/ConfigurationDisclosurePolicy.cs
  • samples/SampleApp/src/SampleApp/Commands/Files/FileProcessCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Files/FileProcessSettings.cs
  • samples/SampleApp/src/SampleApp/Commands/Files/FileReportCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Projects/ProjectCreateCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Projects/ProjectExportCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Projects/ProjectSettings.cs
  • samples/SampleApp/src/SampleApp/Commands/Projects/UseCases/CreateProjectRequest.cs
  • samples/SampleApp/src/SampleApp/Commands/Projects/UseCases/CreateProjectUseCase.cs
  • samples/SampleApp/src/SampleApp/Commands/Projects/UseCases/ExportProjectRequest.cs
  • samples/SampleApp/src/SampleApp/Commands/Projects/UseCases/ExportProjectUseCase.cs
  • samples/SampleApp/src/SampleApp/Commands/Users/UserAddCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Users/UserDeleteCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Users/UserListCommand.cs
  • samples/SampleApp/src/SampleApp/Commands/Users/UserSettings.cs
  • samples/SampleApp/src/SampleApp/Commands/Users/Validators/UserAddCommandSettingsValidator.cs
  • samples/SampleApp/src/SampleApp/Ploch.CommandLine.Spectre.SampleApp.csproj
  • samples/SampleApp/src/SampleApp/Program.cs
  • samples/SampleApp/src/SampleApp/Services/IProjectRepository.cs
  • samples/SampleApp/src/SampleApp/Services/IUserService.cs
  • samples/SampleApp/src/SampleApp/Services/InMemoryProjectRepository.cs
  • samples/SampleApp/src/SampleApp/Services/Models/ProjectItem.cs
  • samples/SampleApp/src/SampleApp/Services/Models/UserProfile.cs
  • samples/SampleApp/src/SampleApp/Services/UserService.cs
  • samples/SampleApp/src/SampleApp/appsettings.json
  • samples/SampleApp/tests/SampleApp.Tests/Commands/ConfigGetCommandTests.cs
  • samples/SampleApp/tests/SampleApp.Tests/Commands/ConfigSetCommandTests.cs
  • samples/SampleApp/tests/SampleApp.Tests/Commands/ConfigShowCommandTests.cs
  • samples/SampleApp/tests/SampleApp.Tests/Commands/FileProcessCommandTests.cs
  • samples/SampleApp/tests/SampleApp.Tests/Commands/FileReportCommandTests.cs
  • samples/SampleApp/tests/SampleApp.Tests/Commands/ProjectCreateCommandTests.cs
  • samples/SampleApp/tests/SampleApp.Tests/Commands/UserAddCommandTests.cs
  • samples/SampleApp/tests/SampleApp.Tests/Commands/UserDeleteCommandTests.cs
  • samples/SampleApp/tests/SampleApp.Tests/Commands/UserListCommandTests.cs
  • samples/SampleApp/tests/SampleApp.Tests/Ploch.CommandLine.Spectre.SampleApp.Tests.csproj
  • samples/SampleApp/tests/SampleApp.Tests/UseCases/ExportProjectUseCaseTests.cs
  • samples/SampleApp/tests/SampleApp.Tests/Validation/UserAddCommandSettingsValidatorTests.cs
  • src/.dockerignore
  • src/.idea/.idea.Ploch.CommandLine.LocalDev/.idea/.gitignore
  • src/.idea/.idea.Ploch.CommandLine.LocalDev/.idea/.name
  • src/.idea/.idea.Ploch.CommandLine.LocalDev/.idea/GitLink.xml
  • src/.idea/.idea.Ploch.CommandLine.LocalDev/.idea/aws.xml
  • src/.idea/.idea.Ploch.CommandLine.LocalDev/.idea/encodings.xml
  • src/.idea/.idea.Ploch.CommandLine.LocalDev/.idea/indexLayout.xml
  • src/.idea/.idea.Ploch.CommandLine.LocalDev/.idea/vcs.xml
  • src/.idea/.idea.Ploch.CommandLine/.idea/.gitignore
  • src/.idea/.idea.Ploch.CommandLine/.idea/.name
  • src/.idea/.idea.Ploch.CommandLine/.idea/GitLink.xml
  • src/.idea/.idea.Ploch.CommandLine/.idea/aws.xml
  • src/.idea/.idea.Ploch.CommandLine/.idea/encodings.xml
  • src/.idea/.idea.Ploch.CommandLine/.idea/git_toolbox_blame.xml
  • src/.idea/.idea.Ploch.CommandLine/.idea/indexLayout.xml
  • src/.idea/.idea.Ploch.CommandLine/.idea/vcs.xml
  • src/.idea/.idea.src/.idea/.gitignore
  • src/.idea/.idea.src/.idea/GitLink.xml
  • src/.idea/.idea.src/.idea/aws.xml
  • src/.idea/.idea.src/.idea/encodings.xml
  • src/.idea/.idea.src/.idea/indexLayout.xml
  • src/.idea/.idea.src/.idea/vcs.xml
  • src/CommandLine.Autofac/AutofacAppConfiguration.cs
  • src/CommandLine.Autofac/Ploch.Common.CommandLine.Autofac.csproj
  • src/CommandLine.Hosting/IUnhandledExceptionHandler.cs
  • src/CommandLine.Hosting/Internal/CommandLineLifetime.cs
  • src/CommandLine.Hosting/Internal/CommandLineService.cs
  • src/CommandLine.Hosting/Internal/CommandLineState.cs
  • src/CommandLine.Hosting/Internal/ICommandLineService.cs
  • src/CommandLine.Hosting/Internal/StoreExceptionHandler.cs
  • src/CommandLine.Hosting/Ploch.CommandLine.Hosting.csproj
  • src/CommandLine.Hosting/ServiceCollectionRegistrations.cs
  • src/CommandLine.Serilog/LoggingSetup.cs
  • src/CommandLine.Serilog/Ploch.Common.CommandLine.Serilog.csproj
  • src/CommandLine/AppBuilder.cs
  • src/CommandLine/AppConstructionContainer.cs
  • src/CommandLine/CommandAppProperties.cs
  • src/CommandLine/CommandLineApplicationConfigurationExtensions.cs
  • src/CommandLine/CommandLineApplicationRuntimeExtensions.cs
  • src/CommandLine/CommandValidations.cs
  • src/CommandLine/ConfigurationSetup.cs
  • src/CommandLine/DelegatedCommandValidator.cs
  • src/CommandLine/HelpOnlyCommand.cs
  • src/CommandLine/IAsyncApp.cs
  • src/CommandLine/IAsyncCommand.cs
  • src/CommandLine/ICommand.cs
  • src/CommandLine/ICommandAppPropertiesConfigurator.cs
  • src/CommandLine/Ploch.Common.CommandLine.csproj
  • src/DemoApp/AdvancedFeaturesSample/AdvancedFeaturesSample.csproj
  • src/DemoApp/AdvancedFeaturesSample/ISampleService.cs
  • src/DemoApp/AdvancedFeaturesSample/Program.cs
  • src/DemoApp/AdvancedFeaturesSample/README.md
  • src/DemoApp/AdvancedFeaturesSample/SampleChildCommand.cs
  • src/DemoApp/AdvancedFeaturesSample/SampleHelloWorldService.cs
  • src/DemoApp/AdvancedFeaturesSample/SampleRootCommand.cs
  • src/DemoApp/AdvancedFeaturesSample/appsettings.json
  • src/Spectre/CommandLine.Spectre.FluentValidation/CommandLineFluentValidationServicesBundle.cs
  • src/Spectre/CommandLine.Spectre.FluentValidation/CommandLineFluentValidationServicesBundleRegistration.cs
  • src/Spectre/CommandLine.Spectre.FluentValidation/FluentCommandSettingsValidator.cs
  • src/Spectre/CommandLine.Spectre.FluentValidation/GlobalUsings.cs
  • src/Spectre/CommandLine.Spectre.FluentValidation/Ploch.CommandLine.Spectre.FluentValidation.csproj
  • src/Spectre/CommandLine.Spectre.FluentValidation/README.md
  • src/Spectre/CommandLine.Spectre.Serilog/GlobalUsings.cs
  • src/Spectre/CommandLine.Spectre.Serilog/LoggerConfigurationExtensions.cs
  • src/Spectre/CommandLine.Spectre.Serilog/Ploch.CommandLine.Spectre.Serilog.csproj
  • src/Spectre/CommandLine.Spectre.Serilog/README.md
  • src/Spectre/CommandLine.Spectre.Serilog/SerilogConfigurationBundle.cs
  • src/Spectre/CommandLine.Spectre.Serilog/SerilogLoggingConfigurator.cs
  • src/Spectre/CommandLine.Spectre/AppBuilder.cs
  • src/Spectre/CommandLine.Spectre/CommandAppConfigurator.cs
  • src/Spectre/CommandLine.Spectre/CommandAppExecutor.cs
  • src/Spectre/CommandLine.Spectre/Commands/AppCommand.cs
  • src/Spectre/CommandLine.Spectre/Commands/AsyncAppCommand.cs
  • src/Spectre/CommandLine.Spectre/Commands/CommandArgumentsRootProcessor.cs
  • src/Spectre/CommandLine.Spectre/Commands/CommandAttribute.cs
  • src/Spectre/CommandLine.Spectre/Commands/CommandInfo.cs
  • src/Spectre/CommandLine.Spectre/Commands/CommandInfoFactory.cs
  • src/Spectre/CommandLine.Spectre/Commands/CommandSettingsPropertyTypeProcessor.cs
  • src/Spectre/CommandLine.Spectre/Commands/CommandSettingsValidator.cs
  • src/Spectre/CommandLine.Spectre/Commands/DefaultExceptionHandler.cs
  • src/Spectre/CommandLine.Spectre/Commands/ExitCode.cs
  • src/Spectre/CommandLine.Spectre/Commands/ICommandSettingsProcessor.cs
  • src/Spectre/CommandLine.Spectre/Commands/ICommandSettingsValidator.cs
  • src/Spectre/CommandLine.Spectre/Commands/IExceptionHandler.cs
  • src/Spectre/CommandLine.Spectre/Commands/SupportsTokensAttribute.cs
  • src/Spectre/CommandLine.Spectre/Commands/TokenInfo.cs
  • src/Spectre/CommandLine.Spectre/Commands/TokensArgumentsProcessor.cs
  • src/Spectre/CommandLine.Spectre/Configuration/AppServicesBundle.cs
  • src/Spectre/CommandLine.Spectre/Configuration/OutputServicesBundle.cs
  • src/Spectre/CommandLine.Spectre/ConsoleAppInfo.cs
  • src/Spectre/CommandLine.Spectre/ConsoleAppInfoExtensions.cs
  • src/Spectre/CommandLine.Spectre/DependencyInjection/DependencyInjectionTypeRegistrar.cs
  • src/Spectre/CommandLine.Spectre/DependencyInjection/DependencyInjectionTypeResolver.cs
  • src/Spectre/CommandLine.Spectre/EnvironmentSettings.cs
  • src/Spectre/CommandLine.Spectre/EnvironmentSettingsLoader.cs
  • src/Spectre/CommandLine.Spectre/EnvironmentVariableNames.cs
  • src/Spectre/CommandLine.Spectre/GlobalUsings.cs
  • src/Spectre/CommandLine.Spectre/ICommandAppConfigurator.cs
  • src/Spectre/CommandLine.Spectre/ICommandAppExecutor.cs
  • src/Spectre/CommandLine.Spectre/IEnvironmentSettingsLoader.cs
  • src/Spectre/CommandLine.Spectre/Output/AnsiConsoleMarkupOutput.cs
  • src/Spectre/CommandLine.Spectre/Output/BaseExceptionMessageFormatter.cs
  • src/Spectre/CommandLine.Spectre/Output/ConvertibleMessageFormatter.cs
  • src/Spectre/CommandLine.Spectre/Output/EnumerableMessageFormatter.cs
  • src/Spectre/CommandLine.Spectre/Output/EnumerableMessageWriter.cs
  • src/Spectre/CommandLine.Spectre/Output/ExceptionMessageFormatter.cs
  • src/Spectre/CommandLine.Spectre/Output/ExceptionMessageWriter.cs
  • src/Spectre/CommandLine.Spectre/Output/FormattableStringMessageWriter.cs
  • src/Spectre/CommandLine.Spectre/Output/FormattedText.cs
  • src/Spectre/CommandLine.Spectre/Output/IMessageFormatter.cs
  • src/Spectre/CommandLine.Spectre/Output/IMessageFormatterProcessor.cs
  • src/Spectre/CommandLine.Spectre/Output/IMessageHandler.cs
  • src/Spectre/CommandLine.Spectre/Output/IMessageWriter.cs
  • src/Spectre/CommandLine.Spectre/Output/IOutput.cs
  • src/Spectre/CommandLine.Spectre/Output/MessageFormatterProcessor.cs
  • src/Spectre/CommandLine.Spectre/Output/ServiceCollectionOutputExtensions.cs
  • src/Spectre/CommandLine.Spectre/Output/StringMessageFormatter.cs
  • src/Spectre/CommandLine.Spectre/Output/StringMessageWriter.cs
  • src/Spectre/CommandLine.Spectre/Output/TypeMessageFormatter.cs
  • src/Spectre/CommandLine.Spectre/Output/TypeMessageHandler.cs
  • src/Spectre/CommandLine.Spectre/Output/TypeMessageWriter.cs
  • src/Spectre/CommandLine.Spectre/Output/Win32ExceptionMessageFormatter.cs
  • src/Spectre/CommandLine.Spectre/Ploch.CommandLine.Spectre.csproj
  • src/Spectre/CommandLine.Spectre/README.md
  • src/Spectre/CommandLine.UseCases/GlobalUsings.cs
  • src/Spectre/CommandLine.UseCases/IResultUseCase.cs
  • src/Spectre/CommandLine.UseCases/IUseCase.cs
  • src/Spectre/CommandLine.UseCases/Ploch.CommandLine.UseCases.csproj
  • src/Spectre/CommandLine.UseCases/README.md
  • src/Spectre/CommandLine.UseCases/UseCaseAsyncCommand.cs
  • tests/.editorconfig
  • tests/CommandLine.IntegrationTests/CommandExecutionIntegrationTests.cs
  • tests/CommandLine.IntegrationTests/Ploch.Common.CommandLine.IntegrationTests.csproj
  • tests/CommandLine.IntegrationTests/TestCallback.cs
  • tests/CommandLine.IntegrationTests/TestCommand.cs
  • tests/CommandLine.IntegrationTests/TestCommandLineApp.cs
  • tests/CommandLine.IntegrationTests/appsettings.json
  • tests/Spectre/CommandLine.Spectre.FluentValidation.Tests/CommandLineFluentValidationServicesBundleTests.cs
  • tests/Spectre/CommandLine.Spectre.FluentValidation.Tests/FluentCommandSettingsValidatorTests.cs
  • tests/Spectre/CommandLine.Spectre.FluentValidation.Tests/GlobalUsings.cs
  • tests/Spectre/CommandLine.Spectre.FluentValidation.Tests/Ploch.CommandLine.Spectre.FluentValidation.Tests.csproj
  • tests/Spectre/CommandLine.Spectre.FluentValidation.Tests/TestAsyncCommand.cs
  • tests/Spectre/CommandLine.Spectre.FluentValidation.Tests/TestCommand.cs
  • tests/Spectre/CommandLine.Spectre.FluentValidation.Tests/TestCommandSettings.cs
  • tests/Spectre/CommandLine.Spectre.FluentValidation.Tests/TestCommandSettingsValidator.cs
  • tests/Spectre/CommandLine.Spectre.Serilog.Tests/GlobalUsings.cs
  • tests/Spectre/CommandLine.Spectre.Serilog.Tests/LoggerConfigurationExtensionsTests.cs
  • tests/Spectre/CommandLine.Spectre.Serilog.Tests/Ploch.CommandLine.Spectre.Serilog.Tests.csproj
  • tests/Spectre/CommandLine.Spectre.Serilog.Tests/SerilogLoggingConfiguratorTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/AppBuilderTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/CommandAppConfiguratorTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/CommandAppExecutorTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Commands/AppCommandTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Commands/AsyncAppCommandTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Commands/CommandInfoFactoryTests.Factory.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Commands/CommandInfoFactoryTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Commands/CommandSettingsPropertyTypeProcessorTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Commands/CommandSettingsValidatorTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Commands/DefaultExceptionHandlerTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Commands/TokensArgumentsProcessorTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Configuration/OutputServicesBundleTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/ConsoleAppInfoBannerTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/ConsoleAppInfoExtensionsTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/DependencyInjection/DependencyInjectionTypeRegistrarTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/EnvironmentSettingsLoaderTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/EnvironmentSettingsTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/GlobalUsings.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Output/AnsiConsoleMarkupOutputTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Output/ConvertibleMessageFormatterTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Output/EnumerableMessageFormatterTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Output/ExceptionMessageFormatterTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Output/MessageFormatterProcessorTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Output/MessageWriterTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Output/TypeMessageHandlerTest.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Output/TypedMessageHandlerDefaultsTests.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Ploch.CommandLine.Spectre.Tests.csproj
  • tests/Spectre/CommandLine.Spectre.Tests/Testing/GlobalConsoleState.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Testing/NoOpOutput.cs
  • tests/Spectre/CommandLine.Spectre.Tests/Testing/RecordingConsole.cs
  • tests/Spectre/CommandLine.UseCases.Tests/GlobalUsings.cs
  • tests/Spectre/CommandLine.UseCases.Tests/Ploch.CommandLine.UseCases.Tests.csproj
  • tests/Spectre/CommandLine.UseCases.Tests/UseCaseAsyncCommandTests.cs
  • version.json

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cbf6cc142

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/build-dotnet.yml Outdated
Comment thread src/Spectre/CommandLine.Spectre/Configuration/AppServicesBundle.cs Outdated
Comment thread src/Spectre/CommandLine.Spectre/AppBuilder.cs Outdated
Comment thread src/Spectre/CommandLine.Spectre/EnvironmentSettingsLoader.cs Outdated
Comment thread src/Spectre/CommandLine.Spectre/Output/AnsiConsoleMarkupOutput.cs Outdated
Comment thread src/Spectre/CommandLine.Spectre/ConsoleAppInfoExtensions.cs Outdated
kploch added 2 commits August 19, 2026 15:19
…warnings

Finishing-touches pass over the Spectre.Console initial implementation.

Build fixes (branch did not compile):
- Stale IExceptionHandler<T> type arguments in the FluentValidation test
  commands, and a CommandContext? / CommandContext nullability mismatch between
  AppCommand and its AsyncAppCommand sibling. Both base classes guarantee a
  non-null context via NotNull(), so the nullable annotation was incorrect.

Runtime defects identified by external review (Grok 4.6, Kimi K3):
- ConvertibleMessageFormatter was registered in DI but threw
  NotImplementedException, crashing any output write of an IConvertible value
  (int, DateTime, bool).
- CommandInfoFactory.CreateFromType threw NotImplementedException on its primary
  path; it now maps CommandAttribute metadata onto CommandInfo.
- CancellationToken was accepted throughout but never honoured. It is now
  forwarded from Execute/ExecuteAsync into DoExecute/DoExecuteAsync and on to
  IUseCase.ExecuteAsync.
- AnsiConsoleMarkupOutput.Write emitted every writer-handled message twice.
- DefaultExceptionHandler printed Win32 exceptions twice and routed them through
  a markup-parsing path, so exception text containing '[' could fail inside the
  exception handler itself.
- The startup banner rendered the FigletText application name twice.
- The Release configuration set TreatWarningsAsErrors=false, dropping this
  repository's zero-warning bar in the published build.

Quality:
- 78 XML documentation warnings resolved across 26 files. Several existing doc
  comments described intended rather than actual behaviour and were corrected.
- Remaining analyser warnings fixed rather than suppressed; removed a
  #pragma warning disable CS1591 and a block of commented-out code.
- .editorconfig: dotnet_diagnostic.IDE0002.severity was "true:error", which is
  not a valid severity value and disabled the rule across all six projects.
- qodana.yaml targeted Ploch.Common.sln, a different repository's solution,
  which was the cause of the failing Qodana check.

Build is clean (0 errors, 0 warnings) and all tests pass.

Known gap: the library has 9 tests covering 3 of 63 public types, and the
repository has no coverage tooling. Tracked separately.

BREAKING CHANGE: AppCommand<TSettings>.DoExecute and
AsyncAppCommand<TSettings>.DoExecuteAsync now accept a CancellationToken
parameter; implementations must update their signatures.
IMessageFormatterProcessor.WriteMessage now returns bool, indicating whether a
registered writer handled the message.
Refs: #3
…I solution path

Resolves the three unresolved review threads raised on PR #11.

- AppBuilder: the single-argument AddJsonFile overload made appsettings.json
  mandatory, overriding the optional behaviour Host.CreateDefaultBuilder already
  provides. Consumers without an appsettings.json got a file-not-found exception
  when the host was built, even when their command needs no JSON configuration.
  Now registered as optional with reloadOnChange.

- EnvironmentSettingsLoader: PauseBeforeExit defaulted to true when
  DEV_RUNTIME_CONSOLE_EXIT_PAUSE was unset, so CommandAppExecutor.RunAsync called
  Console.ReadLine() after every command. An ordinary interactive invocation
  appeared to hang until Enter was pressed. This is a development-oriented
  setting and now defaults to false.

- build-dotnet.yml: the build action was passed ./Ploch.Common.sln, another
  repository's solution, which does not exist here. Points at
  ./Ploch.CommandLine.Spectre.slnx instead. Note the workflow still filters on
  branch 'master' while this repository's default branch is 'main', so it does
  not currently trigger; raised separately.

Build remains clean (0 errors, 0 warnings) and all tests pass.

Refs: #3
@kploch

kploch commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Quality pass complete — summary

Automated finishing-touches pass on this branch. Full detail is in the updated PR description.

Build: was 3 errors + 116 warnings → now 0 errors, 0 warnings. All tests pass.

Seven runtime defects fixed (found by external review, all pre-existing): two public members that threw NotImplementedException on DI-registered paths, a CancellationToken accepted but never honoured, a double-writing output path, a markup-unsafe exception handler that could throw on its own error text, a duplicated startup banner, and TreatWarningsAsErrors=false in the Release configuration.

78 XML documentation warnings resolved across 26 files. All analyser warnings fixed rather than suppressed — a #pragma warning disable CS1591 was removed, not added.

Two config bugs corrected: .editorconfig had dotnet_diagnostic.IDE0002.severity = true:error (not a valid severity — silently disabled the rule in all six projects), and both qodana.yaml and the Build action pointed at Ploch.Common.sln, a different repository's solution.

All three review threads addressed and resolved.

⚠️ This PR has no functioning CI

The only check reporting is CodeRabbit, and it returns pass while skipping the review (263 files > its 100-file limit). codeant-ai, bito and qodo all skipped for the same reason. There are zero check-runs on the head commit, because the Build workflow filters on branch master while this repository's default branch is main. Tracked in #12 — please treat the green tick above as unverified.

Follow-ups filed

Resolves a conflict in qodana.yaml: main added `profile` and `include`
(CheckDependencyLicenses) while this branch corrected `dotnet.solution` from
Ploch.Common.sln — another repository's solution — to
Ploch.CommandLine.Spectre.slnx and scoped `exclude` to tests/**. Both sides are
additive and are combined.

Refs: #3
@bito-code-review

bito-code-review Bot commented Aug 19, 2026

Copy link
Copy Markdown

Bito Automatic Review Skipped - Large PR

Bito didn't auto-review this change because the pull request exceeded the line limit. No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.

kploch added 2 commits August 19, 2026 16:32
…w findings

Addresses #12 and #14, and the tooling half of #13.

CI (#12) — this repository's CI did not run at all:
- build-dotnet.yml filtered on branch 'master' while the default branch is
  'main', so Build, tests and SonarCloud never executed on any pull request.
- Removed code_quality.yml, a stale duplicate of qodana_code_quality.yml whose
  pull_request and push triggers were commented out. The version already on main
  has correct triggers.
- Bumped actions/checkout and actions/upload-artifact from v3 to v4;
  upload-artifact@v3 is retired and now fails outright.
- Gated the GitHub Pages deploy and all three NuGet publish steps to pushes on
  main. Previously every pull request build ran `dotnet nuget push`, publishing
  packages from unmerged branches.

Coverage tooling (#13):
- Injected coverlet.msbuild into every test project via Directory.Build.props,
  following the ploch-common pattern. Without it the CI coverage flag was a
  silent no-op and the Codacy coverage step had no report to upload.
- Scoped collection to Ploch.CommandLine.* so the figure describes this
  repository rather than referenced sibling repos.

Review findings (#14):
- EnvironmentSettings.Current: check-then-act lazy init replaced with a
  synchronised one; Initialize now throws if Current has already been
  materialised instead of silently doing nothing; added Reset for test isolation.
- EnvironmentSettingsLoader: removed the unreachable `result.Keys is null`
  guard, switched to an ordinal-ignore-case dictionary and indexer assignment so
  environment names differing only in case no longer throw, and filtered to the
  DEV_RUNTIME prefix the property name and docs describe. The full environment
  block, which routinely carries secrets, is no longer retained.
- CommandAppExecutor: Run now honours PauseBeforeExit identically to RunAsync.
- AppCommand/AsyncAppCommand: OperationCanceledException is no longer reported
  to the exception handler as a generic failure; it returns the new
  ExitCode.Cancelled (130, the conventional SIGINT code).
- Serilog: the error log sink sat outside its filtered sub-logger, so the
  "errors" file received every event. AddSerilog registered Serilog twice and
  the second registration dropped the output template. Also removed a duplicated
  Enrich.FromLogContext, a second console sink that doubled every log line, a
  stale CS8604 pragma whose justification referenced a parameter the call no
  longer has, and replaced duplicated literals with the existing constants.
- AppServicesBundle: dropped a duplicate AnsiConsole.Console singleton, an
  unused keyed TokensArgumentsProcessor registration, and AddConsole() alongside
  the Serilog console sink.
- CommandSettingsPropertyTypeProcessor: Properties is cleared per invocation;
  it previously accumulated and would throw on a repeated property name.
- ConsoleAppInfo: removed the parallel SysColor properties and the FromSysColor
  extension - three properties plus an extension method that added no capability
  and had no callers.
- AnsiConsoleMarkupOutput: removed WriteMarkupLineInterpolated, which was absent
  from IOutput and silently discarded any non-FormattableString message.
- CommandInfo: dropped the redundant IEqualityOperators interface.
- Documentation corrected: the Validate message now says "null or empty" to
  match the check, and the token docs state that {date}/{datetime} are UTC.

Build is clean (0 errors, 0 warnings) and all tests pass.

BREAKING CHANGE: ConsoleAppInfo.AppNameColorSys, AppNameInfoColorSys and
AppDescriptionColorSys are removed, along with the ConsoleAppInfoExtensions
FromSysColor extension method. Use the Spectre Color properties instead.
AnsiConsoleMarkupOutput.WriteMarkupLineInterpolated is removed; use
MarkupLineInterpolated. EnvironmentSettings.Initialize now throws
InvalidOperationException if called after Current has been read.
EnvironmentSettings.DevRuntimeVariables now contains only DEV_RUNTIME-prefixed
variables rather than the entire environment block.
Refs: #3
Grows the Ploch.CommandLine.Spectre suite from 8 tests to 74 and raises line
coverage on the library from 3.33% to 42.25%.

Regression cover for the defects fixed earlier on this branch — each of these
would have caught the corresponding bug before it shipped:
- ConvertibleMessageFormatterTests: the formatter is registered in DI for every
  IConvertible, and previously threw NotImplementedException, so writing an int
  crashed the application.
- MessageFormatterProcessorTests: WriteMessage now reports whether a writer
  handled the message, which is what stops the output path writing twice.
- OutputServicesBundleTests: resolves the bundle through a real ServiceProvider
  and exercises the formatter pipeline end to end. This is the test that covers
  both consumer-visible output defects at once.
- CommandInfoFactory tests: the attribute-carrying path — the entire purpose of
  the factory — previously threw and the existing test file contained helper
  types but no test methods at all.
- AppCommandTests / AsyncAppCommandTests: the cancellation token reaches the
  implementation, cancellation returns ExitCode.Cancelled instead of being
  reported to the exception handler as a fault, and exceptions still route to
  the handler.

New behavioural cover:
- EnvironmentSettingsTests: lazy initialisation loads exactly once under
  concurrent access, Initialize throws once Current has been materialised, and
  Reset restores a clean slate.
- EnvironmentSettingsLoaderTests: only DEV_RUNTIME-prefixed variables are
  captured, prefix matching and lookup are case-insensitive, and PauseBeforeExit
  defaults to false.
- TokensArgumentsProcessorTests: {date}/{datetime} substitution, case-insensitive
  token matching, path-safe values, untagged properties left alone, null values
  tolerated, and repeated invocations on one instance.
- CommandSettingsPropertyTypeProcessorTests: required-attribute filtering and the
  per-invocation reset.
- ConsoleAppInfoExtensionsTests: banner validation.

The validation test found a real gap: Validate used IsNullOrEmpty, so a
whitespace-only application name passed and then rendered a blank FigletText
banner. It now rejects whitespace, and the exception message describes all three
rejected states rather than only "null".

VSTHRD200 is disabled for test projects in tests/.editorconfig with a documented
rationale: the repository mandates `<TestedMember>_should_<behaviour>` test names,
which cannot also carry the "Async" suffix the rule requires.

Build is clean (0 errors, 0 warnings) and all 75 tests pass.

Refs: #3
Copilot AI review requested due to automatic review settings August 27, 2026 12:23
@bito-code-review

bito-code-review Bot commented Aug 27, 2026

Copy link
Copy Markdown

Bito Automatic Review Skipped - Large PR

Bito didn't auto-review this change because the pull request exceeded the line limit. No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 399 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

.cursor/rules/naming.mdc:8

  • This always-applied rule conflicts with the C# codebase's PascalCase public members, such as AppBuilder.Create, ConfigureCommandApp, and ConsoleAppInfo.AppNameColor. Cursor will be instructed to generate names that violate the repository's actual convention and analyzer expectations.
    .claude/rules/naming.md:3
  • This repository is C#, where the established public-member convention is PascalCase (AppBuilder.Create, ConfigureCommandApp, and ConsoleAppInfo.AppNameColor). Declaring camelCase for methods and properties will make Claude generate code that conflicts with the existing API and analyzers. Scope the rule by language and use PascalCase for C# members.
    .cursor/skills/winui3-controls-layouts/SKILL.md:3
  • This PR adds an entire WinUI-specific skill set under .cursor/skills/winui3-*, but the repository contains a console library and sample with no WinUI project or dependency. These unrelated files expand an already oversized release PR and leave contributor guidance for a technology that cannot be used here. Remove the WinUI skill files from this change, consistent with the PR's cleanup of unrelated marketplace content.

.cursor/mcp.json:5

  • This commits an API credential in plaintext. Anyone with repository access can reuse it, and removing the file later will not remove it from history. Remove this machine-specific config, ignore it or commit a credential-free example, and revoke/rotate the exposed key.

BuildFullLogPath combined logPath with a file name built from logName,
a public parameter of AddSerilog. Path.Combine discards everything before
a rooted later segment, so logName: "C:\app" produced "C:\app.log" and
the configured logPath was silently dropped - the library ignoring the
very parameter documented to control where logs are written.

Confirmed by mutation rather than inference: reverting this line to
Path.Combine makes the new test fail with the log file actually present
at the rooted location, so Serilog really did write outside the
configured directory.

Path.Join concatenates unconditionally, so the file stays under logPath.
For an ordinary logName the two produce an identical string, verified
against .NET, so nothing changes for callers passing a bare name - only
the case that was already violating the documented contract behaves
differently now.

The six Path.Combine calls in the Serilog tests are converted as well.
Those were genuine false positives - the second segment is
Path.GetTempPath(), a Guid or a literal, none of which can be rooted -
but the analyser re-fires on every push and re-blocks the merge each
cycle, so removing the construct settles the class instead of
re-arguing it.

Refs: #48
Copilot AI review requested due to automatic review settings August 27, 2026 12:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 399 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

.cursor/mcp.json:5

  • This commits an apparent live ContextStream API key to the public repository. Anyone can reuse it with the workspace/project identifiers in this file; remove it from the tracked config, rotate/revoke it immediately, and load the replacement from a local secret or environment variable. Removing the line alone is insufficient because the value remains in Git history.

Comment thread tests/Spectre/CommandLine.Spectre.Tests/EnvironmentSettingsTests.cs Outdated
ConfigGetCommand and ConfigSetCommand had regression tests for their
disclosure paths; ConfigShowCommand did not, so its recursive redaction
could have regressed to printing a nested secret without any test
failing.

Both halves of its policy are now covered - the section allow-list and
the per-leaf redaction - plus a control proving the redaction is driven
by the key rather than applied to every value. Each is mutation-verified:
replacing the IsSensitive check with `false`, and swapping the allow-list
for configuration.GetChildren(), each fail exactly one test.

Testing this command needed a different approach from its siblings. They
render strings, so mocking MarkupLineInterpolated is enough; this one
builds a Tree and hands it over, so the renderable is captured and
rendered to plain text through Spectre's own console. That is deliberate
rather than incidental: asserting on what a user would actually see, and
without adding a Spectre.Console.Testing dependency to a sample that
consumers copy.

Worth recording, because the first version of these tests passed while
verifying nothing: `output.Write(tree)` binds to the generic
Write<TMessage> overload, not Write(IRenderable) - the generic is an
exact match for Tree while the IRenderable overload needs a conversion.
Mocking IRenderable captured nothing, and the assertions then ran against
an empty string.

Also converts the last Path.Combine, which the previous commit's own new
test had introduced.

Refs: #48
Copilot AI review requested due to automatic review settings August 27, 2026 12:44

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 400 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

.cursor/skills/winui3-mrploch-app/SKILL.md:3

  • The release-readiness notes say the unrelated WinUI/agent content was removed, but this file and the other 14 newly added .cursor/skills/winui3-* files are still present. They are unrelated to the Spectre CLI library, sample, documentation, or release path and continue to inflate an already review-limit-sized PR. Remove this subtree or move it to a separately described tooling change.

Comment thread src/Spectre/CommandLine.Spectre.Serilog/LoggerConfigurationExtensions.cs Outdated
The previous commit stopped a rooted logName from replacing logPath, but
Path.Join preserves ".." segments - so logName: "../outside" still
produced logs/../outside.log, which the operating system resolves to a
sibling of the configured directory. Half the hole was closed.

logName names a file, not a path, so the directory portion is now
stripped rather than assumed absent. That closes both escapes at once and
leaves an ordinary name untouched. A value consisting only of a directory
part ("sub/") leaves nothing behind, so the process name stands in rather
than producing a file called ".log".

Covered by a Theory over both separators, mutation-verified: reverting to
the previous fix fails both cases with the log file present outside the
directory.

The two theory rows deliberately build a unique destination each. Without
that they resolve to the same sibling file on Windows, where '/' and '\'
are equivalent, and race for one path - the first version failed only one
of the two under mutation for exactly that reason.

Also puts EnvironmentSettingsTests in the GlobalConsoleState collection.
It had a private collection name while mutating process-wide
EnvironmentSettings.Current, and xUnit runs distinct collections in
parallel - so it could reset that state underneath AppBuilderTests,
CommandAppExecutorTests and CommandAppConfiguratorTests, all of which
read it. GlobalConsoleState sets DisableParallelization.

Refs: #48
Copilot AI review requested due to automatic review settings August 27, 2026 13:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 400 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

Previously missed (1) — in code that hasn't changed since the last review.

.cursor/skills/winui3-controls-layouts/SKILL.md:3

  • This is one of 15 newly committed WinUI-specific skill files, but the repository contains no WinUI source or project references and this guidance is not mirrored in the other agent skill directories. These files are unrelated to the command-line release and add substantial repository/tooling clutter; remove the winui3-* skill set from this PR.

.cursor/mcp.json:5

  • This commits a live-looking ContextStream API credential to the public repository. Anyone can reuse it, and removing the file later will not remove it from Git history. Remove the credential from version control, load it from a local environment/secret store, and revoke/rotate this key before merging.

Comment thread .github/workflows/build-dotnet.yml
CI failed on the row that hard-coded a backslash. On Linux a backslash is
an ordinary file-name character, not a separator, so "..\name" is a single
valid file name that lands inside the log directory - the assertion that
it escaped was asserting something untrue, and the path the test computed
as a "sibling" was that same in-directory file.

The theory now takes its rows from Path.DirectorySeparatorChar and
Path.AltDirectorySeparatorChar, deduplicated: two rows on Windows, one on
Unix where both are '/'. Each row therefore tests a character that really
does delimit directories on the platform running it.

Mutation-verified on Windows: reverting the production fix fails both
rows.

The underlying mistake was verifying platform-dependent behaviour only on
the platform I was sitting on. Every Path API in this fix was checked
against .NET on Windows; CI runs ubuntu-latest, where GetInvalidFileNameChars,
the separator set and GetFileName all behave differently.

Refs: #48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 400 changed files in this pull request and generated no new comments.

Suppressed comments (1)

.cursor/mcp.json:5

  • This commits an API credential in a tracked editor configuration file. Anyone who can read the repository can reuse it, and removing it later will not remove it from history. Revoke/rotate the key immediately, remove it from the repository, and load it from an ignored local configuration or secret/environment injection; the current /.mcp.json ignore rule does not cover .cursor/mcp.json.

GitHub withholds repository secrets from workflow runs triggered by a
pull request opened from a fork, so SONAR_TOKEN arrives empty and the
scanner cannot authenticate. Because SonarScanner Begin precedes Build
and Test, an external contributor's pull request would fail before a
single line was compiled - with an error about a token they have no way
of supplying.

The step now runs for pushes and for same-repository pull requests only.
That keeps it blocking everywhere it can actually run, which is the
deliberate choice recorded above it, rather than weakening the gate.

SonarScanner End needs no change: it already runs only when
steps.sonar-begin.outcome == 'success', and a skipped step reports
'skipped', so it stands down with Begin.

This repository is public, so fork pull requests are possible even though
none has arrived yet.

Refs: #48
@bito-code-review

bito-code-review Bot commented Aug 27, 2026

Copy link
Copy Markdown

Bito Automatic Review Skipped - Large PR

Bito didn't auto-review this change because the pull request exceeded the line limit. No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 400 changed files in this pull request and generated no new comments.

Suppressed comments (4)

Previously missed (3) — in code that hasn't changed since the last review.

.claude/rules/naming.md:5

  • This rule conflicts with the repository's established C# naming convention: public methods such as AppBuilder.ConfigureCommandApp (src/Spectre/CommandLine.Spectre/AppBuilder.cs:52) and properties such as EnvironmentSettings.Current (EnvironmentSettings.cs:25) use PascalCase. Applying this always-on instruction would make generated code violate the codebase convention.
    .cursor/rules/naming.mdc:10
  • This always-applied Cursor rule also prescribes camelCase C# members, contradicting AppBuilder.ConfigureCommandApp (src/Spectre/CommandLine.Spectre/AppBuilder.cs:52) and EnvironmentSettings.Current (EnvironmentSettings.cs:25). Keep the mirrored rule aligned with the repository's PascalCase member convention.
    .cursor/skills/winui3-controls-layouts/SKILL.md:4
  • This WinUI skill set is unrelated to a console-command-line library and is not part of the PR's stated release work. The PR adds multiple WinUI packs and reference files under .cursor/skills, increasing an already review-limited change set and creating unrelated maintenance surface. Remove these files from this PR or move them to the repository where WinUI guidance is actually used.

.cursor/mcp.json:5

  • This file commits a live API credential in plaintext. Anyone with repository access can reuse it, and deleting the file later will not remove it from Git history. Remove the credential from tracked configuration, load it from a local environment/secret store, and revoke/rotate the exposed key.

AppBuilder.Create attached an anonymous handler to the static
Console.CancelKeyPress event and created a CancellationTokenSource, and
released neither. The event is process-wide and the lambda captures the
source, so every Create call left one more handler subscribed for the
life of the process, pinning the source and its closure. GitHub Code
Quality flags the undisposed source at warning severity, which is what
blocks the merge under this repository's code_quality ruleset rule.

AppBuilder now implements IDisposable and tears down exactly what it
created. A source passed to the public constructor belongs to the caller
and is deliberately left alone, so that constructor keeps its current
semantics and only Create takes ownership. Configuring a disposed
builder throws ObjectDisposedException instead of publishing a released
source to the application's services, and a Ctrl+C that races Dispose is
answered on the console thread rather than throwing there.

The token has to outlive construction - the application runs after
ConfigureCommandApp returns - so the documented pattern is a using
declaration that falls out of scope once the run has returned, not one
wrapped around the builder chain. The documentation and the getting
started guide are updated to that form.

The sample keeps its current shape: it builds against the published
package, where AppBuilder is not yet disposable, so a using declaration
there does not compile. It adopts one when the package ships.

Issue #32 also records that e.Cancel = true leaves Ctrl+C unable to
force-terminate the process. That half is unchanged and stays tracked
there.

Refs: #32

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KECFm7givf8zoQi2hJmCiN

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 400 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

.cursor/mcp.json:5

  • This commits an API credential in a tracked editor configuration file. Anyone with repository access can reuse it against the ContextStream endpoint, and deleting it later will not remove it from history. Remove the credential from the repository, load it from local secret/environment configuration, add the local config path to .gitignore, and revoke/rotate the exposed key before merging.

Comment thread samples/SampleApp/src/SampleApp/Program.cs
SonarCloud raised IDE0039 against the lambda assigned to the handler
variable - the one new finding the previous commit introduced. A named
local function states what the handler is at its declaration and drops
the deep indentation the lambda forced.

Behaviour is unchanged. The delegate is still captured in a variable and
that same instance is what Dispose unsubscribes. Converting the local
function a second time would yield a second delegate instance and -=
would still match it on method plus target, but holding the instance
keeps the subscribe/unsubscribe pairing explicit rather than resting on
structural equality.

Refs: #32

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KECFm7givf8zoQi2hJmCiN
@sonarqubecloud

Copy link
Copy Markdown

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 104 out of 400 changed files in this pull request and generated no new comments.

Suppressed comments (1)

.cursor/mcp.json:5

  • This commits a live-looking ContextStream API key to the public repository. Anyone with read access can reuse it, and removing it later will not remove it from history. Revoke/rotate this key immediately and keep the editor-specific MCP configuration untracked or source the credential from an environment variable/secret store.

@bito-code-review

bito-code-review Bot commented Aug 27, 2026

Copy link
Copy Markdown

Bito Automatic Review Skipped - Large PR

Bito didn't auto-review this change because the pull request exceeded the line limit. No action is needed if you didn't intend for the agent to review it. Otherwise, to manually trigger a review, type /review in a comment and save.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants