15 Commits

29 changed files with 610 additions and 90 deletions

View File

@@ -84,6 +84,12 @@ func RenameFlag[I any](flag Flag[I], name string) Flag[I] {
// BoolFlag declares a boolean flag. A bare flag is supplied by the parser as // BoolFlag declares a boolean flag. A bare flag is supplied by the parser as
// "true"; explicit values are converted with strconv.ParseBool. Set distinguishes // "true"; explicit values are converted with strconv.ParseBool. Set distinguishes
// an omitted flag from an explicit false value. // an omitted flag from an explicit false value.
//
// A BoolFlag named debug receives the shared reporting flag's input. Its
// default and requiredness apply to application input, not reporting mode;
// explicit false satisfies requiredness. Its optional short spelling and
// completion metadata follow ordinary flag rules. The final input value can
// differ from [Invocation.ReportingMode] when other mode flags are supplied.
func BoolFlag[I any](meta FlagMeta, defaultValue bool, set func(*I, Supplied[bool])) Flag[I] { func BoolFlag[I any](meta FlagMeta, defaultValue bool, set func(*I, Supplied[bool])) Flag[I] {
binding := flagBinding[I]{identity: newBindingIdentity(), meta: meta, defaultText: fmt.Sprint(defaultValue), boolean: true} binding := flagBinding[I]{identity: newBindingIdentity(), meta: meta, defaultText: fmt.Sprint(defaultValue), boolean: true}
if set == nil { if set == nil {

View File

@@ -95,7 +95,8 @@ type FlagMeta struct {
// Name is a nonempty long name without leading dashes. It consists of // Name is a nonempty long name without leading dashes. It consists of
// Unicode lowercase letters or digits in segments separated by single // Unicode lowercase letters or digits in segments separated by single
// hyphens; digit-leading and numeric-only names are valid. The names help, // hyphens; digit-leading and numeric-only names are valid. The names help,
// quiet, verbose, debug, progress and format are reserved. // quiet, verbose, debug, progress and format are reserved, except that a
// BoolFlag named debug attaches to the shared reporting flag.
Name string Name string
// Short is an optional one-byte spelling other than '-' or the reserved 'h'. // Short is an optional one-byte spelling other than '-' or the reserved 'h'.
Short string Short string
@@ -498,7 +499,7 @@ func validateBindings[I any](flags []Flag[I], arguments []Argument[I]) error {
if binding.meta.Name == "help" { if binding.meta.Name == "help" {
return fmt.Errorf("flag name %q is reserved for help", binding.meta.Name) return fmt.Errorf("flag name %q is reserved for help", binding.meta.Name)
} }
if commandmeta.IsReportingFlag(binding.meta.Name) { if commandmeta.IsReportingFlag(binding.meta.Name) && !(binding.meta.Name == "debug" && binding.boolean) {
return fmt.Errorf("flag name %q is reserved for reporting", binding.meta.Name) return fmt.Errorf("flag name %q is reserved for reporting", binding.meta.Name)
} }
if _, exists := flagNames[binding.meta.Name]; exists { if _, exists := flagNames[binding.meta.Name]; exists {

View File

@@ -35,8 +35,7 @@ type completionFlagState struct {
func completionFlags(context completionContext) []commandmeta.Flag { func completionFlags(context completionContext) []commandmeta.Flag {
flags := make([]commandmeta.Flag, 0, len(context.command.Flags)+len(context.command.CoreFlags)+1) flags := make([]commandmeta.Flag, 0, len(context.command.Flags)+len(context.command.CoreFlags)+1)
flags = append(flags, commandmeta.HelpFlag()) flags = append(flags, commandmeta.HelpFlag())
flags = append(flags, context.command.CoreFlags...) flags = append(flags, commandmeta.EffectiveFlags(context.command.Flags, context.command.CoreFlags)...)
flags = append(flags, context.command.Flags...)
return flags return flags
} }

View File

@@ -70,6 +70,34 @@ func TestCompletionFlagAndValuePositions(t *testing.T) {
} }
} }
func TestSharedDebugCompletionHints(t *testing.T) {
for _, tc := range []struct {
name string
hint CompletionHint
want commandmeta.CompletionKind
}{
{"none", NoCompletion(), commandmeta.CompletionNone},
{"choices", CompleteChoices(CompletionChoice{Value: "true"}), commandmeta.CompletionChoices},
{"files", CompleteFiles(), commandmeta.CompletionFiles},
{"directories", CompleteDirectories(), commandmeta.CompletionDirectories},
} {
t.Run(tc.name, func(t *testing.T) {
command := NewCommand(CommandSpec[struct{}]{Name: "run", Flags: []Flag[struct{}]{BoolFlag(FlagMeta{Name: "debug", Short: "d", Completion: tc.hint}, false, func(*struct{}, Supplied[bool]) {})}, Run: func(Invocation, struct{}) Outcome { return Success() }})
app, err := New(Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
snapshot := app.routeSnapshot()
for _, current := range []string{"--debug=", "-d="} {
target := completionTargetFor(snapshot, resolveCompletionContext(snapshot, []string{"run"}, current))
if target.kind != completionTargetValue || target.hint.Kind != tc.want {
t.Fatalf("%s: %#v", current, target)
}
}
})
}
}
func TestCompletionOperandAllocation(t *testing.T) { func TestCompletionOperandAllocation(t *testing.T) {
arguments := []commandmeta.Argument{ arguments := []commandmeta.Argument{
{Name: "optional", Min: 0, Max: 1, Completion: commandmeta.CompletionHint{Kind: commandmeta.CompletionFiles}}, {Name: "optional", Min: 0, Max: 1, Completion: commandmeta.CompletionHint{Kind: commandmeta.CompletionFiles}},

View File

@@ -120,3 +120,51 @@ func TestCompletionQueryDiscovery(t *testing.T) {
t.Fatalf("discovery outcome=%#v reply=%q", outcome, output.String()) t.Fatalf("discovery outcome=%#v reply=%q", outcome, output.String())
} }
} }
func TestSharedDebugHelpAndCompletion(t *testing.T) {
var factories, setters, checks, handlers int
command := NewCommand(CommandSpec[struct{}]{
Name: "run",
NewInput: func() struct{} { factories++; return struct{}{} },
Flags: []Flag[struct{}]{BoolFlag(FlagMeta{Name: "debug", Short: "d", Usage: "application tracing", Required: true, Completion: CompleteChoices(CompletionChoice{Value: "true"}, CompletionChoice{Value: "trace"})}, true, func(*struct{}, Supplied[bool]) { setters++ })},
CheckInput: func(struct{}) error { checks++; return nil },
Run: func(Invocation, struct{}) Outcome { handlers++; return Success() },
})
app, err := New(Identity{Name: "example"}, command, Completion("example"), Alias("shortcut", "run"))
if err != nil {
t.Fatal(err)
}
for _, args := range [][]string{{"run", "--help"}, {"help", "run"}, {"shortcut", "--help"}, {"help", "shortcut"}, {"run", "--unknown"}, {"shortcut", "--unknown"}} {
var output, diagnostic bytes.Buffer
result := app.Execute(context.Background(), args, Streams{Out: &output, Err: &diagnostic})
text := output.String() + diagnostic.String()
if strings.Count(text, "--debug") != 1 || !strings.Contains(text, "-d") || !strings.Contains(text, "application tracing") || !strings.Contains(text, "application input defaults to true") || !strings.Contains(text, "required") || !strings.Contains(text, "default: false") {
t.Fatalf("%v: outcome=%#v text=%q", args, result, text)
}
}
for _, tc := range []struct {
args []string
want string
count int
}{
{[]string{"__complete", "run", "--deb"}, "--debug", 1},
{[]string{"__complete", "run", "-"}, "\n-d\t", 1},
{[]string{"__complete", "run", "--debug=tr"}, "trace", 1},
{[]string{"__complete", "run", "-d=tr"}, "trace", 1},
} {
var output bytes.Buffer
result := app.Execute(context.Background(), tc.args, Streams{Out: &output, Err: io.Discard})
if result.Kind != OutcomeSuccess || strings.Count(output.String(), tc.want) != tc.count {
t.Fatalf("%v: %#v %q", tc.args, result, output.String())
}
}
if factories != 0 || setters != 0 || checks != 0 || handlers != 0 {
t.Fatalf("callbacks: %d %d %d %d", factories, setters, checks, handlers)
}
if result := app.Execute(context.Background(), []string{"run", "--debug=invalid", "--help"}, Streams{Out: io.Discard, Err: io.Discard}); result.Kind != OutcomeUsage {
t.Fatalf("help skipped boolean validation: %#v", result)
}
if result := app.Execute(context.Background(), []string{"run", "--debug=trace"}, Streams{Out: io.Discard, Err: io.Discard}); result.Kind != OutcomeUsage {
t.Fatalf("suggestion bypassed boolean parsing: %#v", result)
}
}

View File

@@ -178,8 +178,11 @@ Reporting flags do not go before or between command-path components. Tokens
consumed as values and tokens after `--` retain their normal meaning. consumed as values and tokens after `--` retain their normal meaning.
The reserved long spellings are `--quiet`, `--verbose`, `--debug`, The reserved long spellings are `--quiet`, `--verbose`, `--debug`,
`--progress`, and `--format`; application declarations cannot reuse them. There `--progress`, and `--format`. An application may attach its own boolean input
are no built-in short spellings. Applications may define aliases such as `-q → --quiet`, to the existing `--debug` option. Command help shows both purposes in one row,
including an application short spelling, requiredness, and any different
application default. Application input defaults do not select reporting mode.
There are no built-in short spellings. Applications may define aliases such as `-q → --quiet`,
`-v → --verbose`, and `-d → --debug`; canonical and aliased occurrences share `-v → --verbose`, and `-d → --debug`; canonical and aliased occurrences share
the same ordering. Thus `example export --verbose -q` selects quiet mode. the same ordering. Thus `example export --verbose -q` selects quiet mode.

View File

@@ -18,20 +18,21 @@ The following lists the current release set and earlier module combinations:
| Selection | Core | ckconfig | ckprompt | Upstream libraries and status | | Selection | Core | ckconfig | ckprompt | Upstream libraries and status |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| Published core | v0.5.0 | — | — | Usable independently. | | Published core | v0.6.0 | — | — | Usable independently. |
| ConfigKit integration | v0.5.0 | v0.5.0 | — | ConfigKit v1.1.0. | | ConfigKit integration | v0.6.0 | v0.6.0 | — | ConfigKit v1.1.0. |
| PromptKit integration | v0.5.0 | v0.5.0 | v0.5.0 | ConfigKit v1.1.0 and PromptKit v0.20.0. | | PromptKit integration | v0.5.0 | v0.5.0 | v0.5.0 | ConfigKit v1.1.0 and PromptKit v0.20.0. |
| Earlier matched integration set | v0.3.0 | v0.2.0 | v0.2.0 | ConfigKit v0.8.0 and PromptKit v0.16.0, using their earlier import paths. | | Earlier matched integration set | v0.3.0 | v0.2.0 | v0.2.0 | ConfigKit v0.8.0 and PromptKit v0.16.0, using their earlier import paths. |
Install the published core with: Install the published core with:
```sh ```sh
go get go.maximumdirect.net/commandkit@v0.5.0 go get go.maximumdirect.net/commandkit@v0.6.0
``` ```
Select either integration with `go get go.maximumdirect.net/ckconfig@v0.5.0` Select either integration with `go get go.maximumdirect.net/ckconfig@v0.6.0`
or `go get go.maximumdirect.net/ckprompt@v0.5.0`. The integrations select core or `go get go.maximumdirect.net/ckprompt@v0.5.0`. The integrations select core
v0.5.0, and ckprompt selects ckconfig v0.5.0, which includes `AdaptDiagnostics`. v0.6.0 for ckconfig and v0.5.0 for ckprompt; ckprompt selects ckconfig v0.5.0,
which includes `AdaptDiagnostics`.
They build independently against these released dependencies; a local workspace They build independently against these released dependencies; a local workspace
is optional for development. Published ckconfig v0.3.0 also uses ConfigKit v1.1.0 is optional for development. Published ckconfig v0.3.0 also uses ConfigKit v1.1.0
and does not compose with ckprompt v0.2.0's earlier ConfigKit dependency. and does not compose with ckprompt v0.2.0's earlier ConfigKit dependency.
@@ -154,6 +155,35 @@ Deferred submissions are accepted until finalization begins; later submissions
return `ErrReporterClosed`. The [CLI reference](../cli.md#reporting) owns flag return `ErrReporterClosed`. The [CLI reference](../cli.md#reporting) owns flag
grammar, visibility, prefixes, and status behavior. grammar, visibility, prefixes, and status behavior.
An application can attach a boolean input to the shared `--debug` option and
inspect the reporting selection separately. For example:
```go
type input struct { debug commandkit.Supplied[bool] }
command := commandkit.NewCommand(commandkit.CommandSpec[input]{
Name: "run",
Flags: []commandkit.Flag[input]{
commandkit.BoolFlag(commandkit.FlagMeta{Name: "debug", Short: "d"}, false,
func(in *input, value commandkit.Supplied[bool]) { in.debug = value }),
},
Run: func(inv commandkit.Invocation, in input) commandkit.Outcome {
captureArtifacts := in.debug.Value
quiet := inv.ReportingMode() == commandkit.ReportingModeQuiet
_, _ = captureArtifacts, quiet // Apply application policy here.
return commandkit.Success()
},
})
_ = command
```
With `run --debug --quiet`, the input is true and explicitly supplied, while
the selected reporting mode is quiet. Application defaults and requiredness
affect input, not mode selection; explicit false counts as supplied. Captured
bindings retain the usual selection and application behavior. Applications own
any artifact creation and other effects. The mode reports selection rather than
successful delivery; see [Invocation.ReportingMode](../../identity.go).
Reports use JSON serialization to establish the common value representation Reports use JSON serialization to establish the common value representation
for YAML and JSON. A text template receives the original value; without one, for YAML and JSON. A text template receives the original value; without one,
text uses readable scalar or string-list output and YAML for structured values. text uses readable scalar or string-list output and YAML for structured values.

View File

@@ -73,7 +73,8 @@ Read all applicable rows for cross-cutting work. Go declarations and GoDoc own
current exported contracts; the CLI reference owns operator behavior. The current exported contracts; the CLI reference owns operator behavior. The
[maintainer release procedure](release.md) owns current release operations. [maintainer release procedure](release.md) owns current release operations.
Completed roadmaps and implementation evidence remain available in Git history, Completed roadmaps and implementation evidence remain available in Git history,
including [audit remediation and its declaration-path follow-up](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/aac0e11ae67c78a35745ed9a25688aa51c15e793/docs/roadmap), including [shared debug binding and reporting-mode implementation](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/56e3b013f9f83c9c9494f11602e802ce5ed8814e/docs/roadmap),
[audit remediation and its declaration-path follow-up](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/aac0e11ae67c78a35745ed9a25688aa51c15e793/docs/roadmap),
[shared reporting and PromptKit v0.20.0 implementation](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/2f43f78f76439f5d11ef313cd379bec036543504/docs/roadmap), [shared reporting and PromptKit v0.20.0 implementation](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/2f43f78f76439f5d11ef313cd379bec036543504/docs/roadmap),
[PromptKit v0.18.0 adoption and implementation](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/540e2359d66d604a563df0be1c9fc961588f5b9c/docs/roadmap), [PromptKit v0.18.0 adoption and implementation](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/540e2359d66d604a563df0be1c9fc961588f5b9c/docs/roadmap),
[ConfigKit adoption, reporting, release-tooling, and remediation records](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/260256d722f52bc947103d078cb67a129f4cc9f1/docs/roadmap), [ConfigKit adoption, reporting, release-tooling, and remediation records](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/260256d722f52bc947103d078cb67a129f4cc9f1/docs/roadmap),
@@ -117,7 +118,8 @@ go work init . ./integrations/ckconfig ./integrations/ckprompt
If a workspace already exists, ensure it includes these three modules. Preserve If a workspace already exists, ensure it includes these three modules. Preserve
the ignored `go.work` and `go.work.sum`; do not commit them. Use the ignored `go.work` and `go.work.sum`; do not commit them. Use
`GOWORK=off GOTOOLCHAIN=local` for dependency preparation `GOWORK=off GOTOOLCHAIN=local` for dependency preparation
and independent module suites. Both integrations select released core v0.5.0; and independent module suites. ckconfig selects released core v0.6.0 and
ckprompt selects core v0.5.0;
ckprompt also selects released ckconfig v0.5.0. Their declared dependencies ckprompt also selects released ckconfig v0.5.0. Their declared dependencies
support independent builds. The aligned workspace exercises the local sources support independent builds. The aligned workspace exercises the local sources
together. together.

View File

@@ -38,6 +38,12 @@ collected parser events through that selection. The selected mode therefore
applies to command execution, help, and version; routing and parsing failures applies to command execution, help, and version; routing and parsing failures
remain visible in every mode. remain visible in every mode.
An effective flag view combines a shared application debug binding with the
core declaration for parsing, help, and completion, while preserving the
application binding's position. The same occurrence stream selects reporting
mode independently of the prepared application value. The invocation's mode
accessor reads the reporter's selection under its existing state mutex.
The reporter owns invocation-scoped visibility, message formatting, managed The reporter owns invocation-scoped visibility, message formatting, managed
report rendering, delivery observations, and finalization. It presents queued report rendering, delivery observations, and finalization. It presents queued
messages after the handler returns, then a failed outcome's explanation, and messages after the handler returns, then a failed outcome's explanation, and

View File

@@ -2,7 +2,7 @@
**Base Policy Version:** 1.6 **Base Policy Version:** 1.6
**Last Updated**: 2026-10-11 **Last Updated**: 2026-11-08
**Upstream Source:** [release.md](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348/docs/policy/release.md) **Upstream Source:** [release.md](https://gitea.maximumdirect.net/eric/policy/src/commit/e833faddf120c789a9b45348edaf75720de16348/docs/policy/release.md)
@@ -96,6 +96,22 @@ Additional compatibility promises and deliberate exceptions belong in the
adopted policy or canonical contract documentation. Release notes explain adopted policy or canonical contract documentation. Release notes explain
changes; they do not redefine current compatibility contracts. changes; they do not redefine current compatibility contracts.
### CommandKit downstream dependency alignment
Each new core or integration release requires dependency updates and new releases
of all downstream CommandKit-owned integration modules, following direct and
transitive dependencies. Each affected integration advances at least its minor
version to incorporate the dependency updates, even when the initiating release
is a patch. Use a major release where the compatibility rules above require it.
Versions remain independent and need not match across modules.
A module with no downstream CommandKit integrations may release independently;
its release does not require new versions of its dependencies. This policy does
not require releases of external consuming applications. The
[runbook](../release.md#scope-and-release-order) owns dependency-order sequencing.
Downstream releases are required follow-up work, not additional completion gates
for an already published source release.
## Consumer Minimum And Release Toolchain ## Consumer Minimum And Release Toolchain
Distinguish the minimum toolchain supported for consumers from the toolchain Distinguish the minimum toolchain supported for consumers from the toolchain

View File

@@ -9,11 +9,28 @@ Gitea release page; there are no binaries,
checksums, or other delivery assets. CI, module proxy availability, and consumer checksums, or other delivery assets. CI, module proxy availability, and consumer
installation are not completion gates. installation are not completion gates.
Publish in dependency order: core, then `ckconfig`, then `ckprompt`. The Plan the initiating release and all downstream integration releases required by
integration modules require the preceding CommandKit releases, so prepare and the [dependency-alignment policy](policy/release.md#commandkit-downstream-dependency-alignment).
publish each module only after its CommandKit dependencies are available. A root That policy sets the minimum version bump for affected integrations; select
release runs only the root module's default suite; it need not run suites for each module's version independently according to its compatibility impact.
independently released integrations.
| Initiating release | Required downstream releases, in order |
| --- | --- |
| Core | `ckconfig`, then `ckprompt` |
| `ckconfig` | `ckprompt` |
| `ckprompt` | None with the current dependency graph |
As integrations are added, follow their actual dependencies to include every
affected downstream integration. Publish dependencies before their consumers.
Prepare each integration after its updated CommandKit dependencies are published,
incorporating all of those versions in one candidate. For a core update, release
`ckprompt` once after both core and `ckconfig`; it does not need separate releases
for each dependency bump. Unaffected dependencies do not need new releases.
Track any remaining downstream releases as pending coordination work. Each
successful source-tag push still completes that module's release; a downstream
failure does not undo it or authorize replacing its tag. A root release runs
only the root module's default suite; each downstream release runs its own suite.
Use branch `main`, remote `origin`, and expected effective push endpoint Use branch `main`, remote `origin`, and expected effective push endpoint
`ssh://git@forge.maximumdirect.net:22222/base-layer/commandkit.git`. The selected `ssh://git@forge.maximumdirect.net:22222/base-layer/commandkit.git`. The selected
@@ -98,14 +115,17 @@ standard offline suite and ReleaseKit’s candidate/note and publication guards.
## Prepare and publish one candidate ## Prepare and publish one candidate
Prepare dependencies in the selected module before the candidate is finalized. Prepare dependencies in the selected module before the candidate is finalized.
Do this after the preceding CommandKit module has been published and before the Do this after any CommandKit dependency releases required by the coordinated
selected candidate suite is run. For `core`, run the command at the repository update have been published and before the selected candidate suite is run.
root; for integrations, run it from that module directory. For `core`, run the command at the repository root; for integrations, run it
from that module directory.
Before downloading dependencies, update each integration to the newly published Before downloading dependencies, update each integration to the newly published
CommandKit versions. `go mod download` downloads the versions already selected; CommandKit versions. `go mod download` downloads the versions already selected;
it does not upgrade requirements. Replace the version placeholders below with it does not upgrade requirements. Replace the version placeholders below with
the actual published versions, which may differ between modules: the actual published versions, which may differ between modules. When a
dependency is not being released in this update, use its current selected
release; a `ckconfig`-initiated update does not require a new core release:
```sh ```sh
# After publishing core, prepare ckconfig from its module directory. # After publishing core, prepare ckconfig from its module directory.

25
docs/releases/v0.6.0.md Normal file
View File

@@ -0,0 +1,25 @@
# CommandKit v0.6.0
CommandKit v0.6.0 lets applications bind the shared debug option to typed
input and inspect the selected reporting mode during and after an invocation.
## Changes
- A `BoolFlag` named `debug` attaches to the existing reporting option. Its
application default, requiredness, short spelling, and completion hints are
retained; help describes the combined option.
- `Invocation.ReportingMode` returns the resolved visibility selection, which
can differ from the bound debug value when other mode flags are supplied.
## Compatibility and upgrade
Other reporting flag names remain reserved. Applications choosing a required
debug binding require an explicit value; an explicit false satisfies it. A
true application default does not enable debug reporting. See the
[consumer guide](../consumers/guide.md#invocation-reporting) and
[CLI reference](../cli.md#reporting). The core consumer minimum remains Go
1.26.0 and the maintainer release toolchain remains Go 1.27.0.
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/v0.6.0/COPYRIGHT) |
[LICENSE](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/v0.6.0/LICENSE) |
[ATTRIBUTION](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/v0.6.0/docs/oss/attribution.md)

View File

@@ -1,7 +1,6 @@
# Future work # Future work
Status: pending ckprompt v0.5.0 publication, optional shell validation, and Status: optional shell validation and future capability proposals.
future capability proposals.
No delivery dates are selected here. No delivery dates are selected here.
## Purpose and authority ## Purpose and authority
@@ -9,6 +8,8 @@ No delivery dates are selected here.
Earlier completed roadmaps, implementation plans, and review findings have been retired. Earlier completed roadmaps, implementation plans, and review findings have been retired.
This document preserves unfinished work and unselected ideas under the This document preserves unfinished work and unselected ideas under the
[release policy](../policy/release.md#release-documentation-and-preparation). [release policy](../policy/release.md#release-documentation-and-preparation).
The completed shared debug binding and reporting-mode implementation is recorded
in its [retirement snapshot](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/56e3b013f9f83c9c9494f11602e802ce5ed8814e/docs/roadmap).
The completed [documentation refresh](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/5c960532552037d38119941389e558f78bf73aad/docs/roadmap/documentation-refresh.md) remains available in Git history. The completed [documentation refresh](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/5c960532552037d38119941389e558f78bf73aad/docs/roadmap/documentation-refresh.md) remains available in Git history.
The completed audits, consolidated remediation, implementation plan, and The completed audits, consolidated remediation, implementation plan, and
declaration-path follow-up remain available in the declaration-path follow-up remain available in the
@@ -29,14 +30,7 @@ govern any future implementation. The [CLI reference](../cli.md), public GoDoc,
and [consumer guide](../consumers/guide.md) own implemented behavior. Retaining and [consumer guide](../consumers/guide.md) own implemented behavior. Retaining
an idea here does not select its syntax, API, priority, or delivery date. an idea here does not select its syntax, API, priority, or delivery date.
## Pending release and integration work ## Optional verification
### ckprompt release
Core and ckconfig v0.5.0 are published. The ckprompt v0.5.0 candidate now selects
both releases and replaces the unpublished v0.3.0 draft. Complete validation and
publication using the [release procedure](../release.md). Hosted delivery is a
separate operation.
### Optional shell completion validation ### Optional shell completion validation

View File

@@ -1,6 +1,7 @@
package commandkit package commandkit
import ( import (
"strconv"
"strings" "strings"
"testing" "testing"
@@ -313,3 +314,27 @@ func TestFlagAliasSharedSuffixAndUnavailableDestination(t *testing.T) {
} }
} }
} }
func TestSharedDebugDeclarationValidation(t *testing.T) {
valid := BoolFlag(FlagMeta{Name: "debug", Short: "d"}, true, func(*struct{}, Supplied[bool]) {})
if _, err := New(Identity{Name: "Example"}, flagAliasCommand("run", valid)); err != nil {
t.Fatalf("valid debug: %v", err)
}
for _, flags := range [][]Flag[struct{}]{
{valid, valid},
{BoolFlag[struct{}](FlagMeta{Name: "debug"}, false, nil)},
{BoolFlag(FlagMeta{Name: "debug", Short: "h"}, false, func(*struct{}, Supplied[bool]) {})},
{ValueFlag(FlagMeta{Name: "debug"}, (*bool)(nil), strconv.ParseBool, func(*struct{}, Supplied[bool]) {})},
{OccurrenceFlag(FlagMeta{Name: "debug"}, strconv.ParseBool, func(*struct{}, Supplied[bool]) {})},
} {
if _, err := New(Identity{Name: "Example"}, flagAliasCommand("bad", flags...)); err == nil {
t.Fatalf("invalid binding accepted: %#v", flags)
}
}
for _, name := range []string{"quiet", "verbose", "progress", "format"} {
flag := BoolFlag(FlagMeta{Name: name}, false, func(*struct{}, Supplied[bool]) {})
if _, err := New(Identity{Name: "Example"}, flagAliasCommand("bad", flag)); err == nil {
t.Errorf("reserved boolean --%s was accepted", name)
}
}
}

View File

@@ -24,6 +24,8 @@ type FlagValues[I any] struct {
// each adapted flag stores its prepared value in target, associated with the // each adapted flag stores its prepared value in target, associated with the
// original inner binding identity. Copies of an original handle remain // original inner binding identity. Copies of an original handle remain
// selectable; an independently constructed lookalike does not. // selectable; an independently constructed lookalike does not.
// A captured debug BoolFlag shares reporting input as described by [BoolFlag];
// its requiredness is checked when selected for application.
func CaptureFlags[Outer, I any](flags []Flag[I], target func(*Outer) *FlagValues[I]) []Flag[Outer] { func CaptureFlags[Outer, I any](flags []Flag[I], target func(*Outer) *FlagValues[I]) []Flag[Outer] {
captured := make([]Flag[Outer], len(flags)) captured := make([]Flag[Outer], len(flags))
for index, original := range flags { for index, original := range flags {

View File

@@ -59,6 +59,21 @@ type Invocation struct {
// manually constructed Invocation has no reporter. // manually constructed Invocation has no reporter.
func (inv Invocation) Reporter() *Reporter { return inv.reporter } func (inv Invocation) Reporter() *Reporter { return inv.reporter }
// ReportingMode returns the selected reporting mode, independently of delivery
// success. The selection is stable during the handler and remains available
// after finalization. A zero Invocation returns ReportingModeOrdinary. Reads
// are safe alongside concurrent reporting; reporting callbacks must not reenter
// the same reporter.
func (inv Invocation) ReportingMode() ReportingMode {
if inv.reporter == nil || inv.reporter.state == nil {
return ReportingModeOrdinary
}
state := inv.reporter.state
state.mu.Lock()
defer state.mu.Unlock()
return state.mode
}
// Help renders the selected command's help through this invocation's output. // Help renders the selected command's help through this invocation's output.
// It returns an operational failure when the invocation has no help renderer. // It returns an operational failure when the invocation has no help renderer.
func (inv Invocation) Help() Outcome { func (inv Invocation) Help() Outcome {

View File

@@ -0,0 +1,16 @@
# CommandKit ckconfig v0.6.0
CommandKit ckconfig v0.6.0 selects CommandKit core v0.6.0, which supports
shared debug input bindings and access to the selected reporting mode.
ConfigKit remains at v1.1.0.
## Compatibility and upgrade
Select `go.maximumdirect.net/ckconfig v0.6.0` with CommandKit core v0.6.0.
The consumer minimum remains Go 1.27.0. See the
[consumer guide](../../../../docs/consumers/guide.md#invocation-reporting)
for the new core reporting and debug input APIs.
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckconfig/v0.6.0/integrations/ckconfig/COPYRIGHT) |
[LICENSE](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckconfig/v0.6.0/integrations/ckconfig/LICENSE) |
[ATTRIBUTION](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckconfig/v0.6.0/integrations/ckconfig/docs/oss/attribution.md)

View File

@@ -3,7 +3,7 @@ module go.maximumdirect.net/ckconfig
go 1.27.0 go 1.27.0
require ( require (
go.maximumdirect.net/commandkit v0.5.0 go.maximumdirect.net/commandkit v0.6.0
go.maximumdirect.net/configkit v1.1.0 go.maximumdirect.net/configkit v1.1.0
go.yaml.in/yaml/v4 v4.0.0-rc.6 go.yaml.in/yaml/v4 v4.0.0-rc.6
) )

View File

@@ -10,6 +10,8 @@ go.maximumdirect.net/commandkit v0.4.0 h1:bWqflDWDn98FUCgh+0e+w6LGmvWIkaeJK519H7
go.maximumdirect.net/commandkit v0.4.0/go.mod h1:ZS98DewKNVBygKVDoh5MFD09CnPlP37dS9pVsID5Eoc= go.maximumdirect.net/commandkit v0.4.0/go.mod h1:ZS98DewKNVBygKVDoh5MFD09CnPlP37dS9pVsID5Eoc=
go.maximumdirect.net/commandkit v0.5.0 h1:Ln+bCp+FwCg4tZD3NaMKQXUDB3mPusgwsnRZmb0+3cM= go.maximumdirect.net/commandkit v0.5.0 h1:Ln+bCp+FwCg4tZD3NaMKQXUDB3mPusgwsnRZmb0+3cM=
go.maximumdirect.net/commandkit v0.5.0/go.mod h1:ZS98DewKNVBygKVDoh5MFD09CnPlP37dS9pVsID5Eoc= go.maximumdirect.net/commandkit v0.5.0/go.mod h1:ZS98DewKNVBygKVDoh5MFD09CnPlP37dS9pVsID5Eoc=
go.maximumdirect.net/commandkit v0.6.0 h1:4PHhMIlZuuvreIxZhUazeweg6zoYixEvhM9LJnkbGNA=
go.maximumdirect.net/commandkit v0.6.0/go.mod h1:ZS98DewKNVBygKVDoh5MFD09CnPlP37dS9pVsID5Eoc=
go.maximumdirect.net/configkit v1.1.0 h1:UzXN2ZMnEkcL0O4YSLqD9qd9M1gb/NWCHQR/H8AaQzA= go.maximumdirect.net/configkit v1.1.0 h1:UzXN2ZMnEkcL0O4YSLqD9qd9M1gb/NWCHQR/H8AaQzA=
go.maximumdirect.net/configkit v1.1.0/go.mod h1:s1zLJ9GGB/1Q4a+aOHMLISonJxWFgNWpA6SpPlOafeA= go.maximumdirect.net/configkit v1.1.0/go.mod h1:s1zLJ9GGB/1Q4a+aOHMLISonJxWFgNWpA6SpPlOafeA=
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4= go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=

View File

@@ -77,9 +77,7 @@ func (session *Session) Parse(argv []string) commandmeta.Result {
} }
remaining := append(commandpath.CloneTokens(unconsumed), tail...) remaining := append(commandpath.CloneTokens(unconsumed), tail...)
localFlags := make([]commandmeta.Flag, 0, len(selectedNode.Flags)+len(selectedNode.CoreFlags)) localFlags := commandmeta.EffectiveFlags(selectedNode.Flags, selectedNode.CoreFlags)
localFlags = append(localFlags, selectedNode.Flags...)
localFlags = append(localFlags, selectedNode.CoreFlags...)
aliases := session.flagAliases aliases := session.flagAliases
if session.flagAliasesForPath != nil { if session.flagAliasesForPath != nil {
aliases = session.flagAliasesForPath(result.Path) aliases = session.flagAliasesForPath(result.Path)
@@ -254,7 +252,7 @@ func displayCommandPath(rootName string, path []string) string {
return strings.Join(parts, " ") return strings.Join(parts, " ")
} }
// UsageLine returns Cobra's usage line for an exact path on this session. // UsageLine returns usage for an exact path on this session.
func (session *Session) UsageLine(path []string) (string, *commandmeta.Failure) { func (session *Session) UsageLine(path []string) (string, *commandmeta.Failure) {
if session == nil || session.root == nil { if session == nil || session.root == nil {
return "", &commandmeta.Failure{Message: "command tree is nil"} return "", &commandmeta.Failure{Message: "command tree is nil"}
@@ -269,6 +267,22 @@ func (session *Session) UsageLine(path []string) (string, *commandmeta.Failure)
if selected.HasSubCommands() { if selected.HasSubCommands() {
line += " [command]" line += " [command]"
} }
for _, flag := range declaration.Flags {
if flag.Name != "debug" || !flag.Boolean {
continue
}
for _, effective := range commandmeta.EffectiveFlags(declaration.Flags, declaration.CoreFlags) {
if effective.Name == "debug" {
spelling := "--debug"
if effective.Short != "" {
spelling = "-" + effective.Short + ", " + spelling
}
line += "\n " + spelling + " " + flagUsage(effective)
break
}
}
break
}
return line, nil return line, nil
} }
@@ -316,7 +330,7 @@ func buildTree(root *commandmeta.Command) (*spfcobra.Command, map[*spfcobra.Comm
helpFlag := backend.Flags().Lookup("help") helpFlag := backend.Flags().Lookup("help")
helpFlag.NoOptDefVal = "true" helpFlag.NoOptDefVal = "true"
helpFlag.Usage = "help for this command (default: false)" helpFlag.Usage = "help for this command (default: false)"
for _, declarationFlag := range append(append([]commandmeta.Flag(nil), declaration.Flags...), declaration.CoreFlags...) { for _, declarationFlag := range commandmeta.EffectiveFlags(declaration.Flags, declaration.CoreFlags) {
value := &rawValue{boolean: declarationFlag.Boolean} value := &rawValue{boolean: declarationFlag.Boolean}
backend.Flags().VarP(value, declarationFlag.Name, declarationFlag.Short, flagUsage(declarationFlag)) backend.Flags().VarP(value, declarationFlag.Name, declarationFlag.Short, flagUsage(declarationFlag))
backendFlag := backend.Flags().Lookup(declarationFlag.Name) backendFlag := backend.Flags().Lookup(declarationFlag.Name)

View File

@@ -281,3 +281,14 @@ func TestMissingSessionTreeReturnsNeutralFailures(t *testing.T) {
t.Fatalf("missing-tree usage failure = %#v", failure) t.Fatalf("missing-tree usage failure = %#v", failure)
} }
} }
func TestSharedDebugExtractionPreservesConsumerIndexes(t *testing.T) {
tree := &Command{Name: "app", CoreFlags: commandmeta.ReportingFlags(), Children: []*Command{{Name: "run", Runnable: true, CoreFlags: commandmeta.ReportingFlags(), Flags: []Flag{{Name: "before"}, {Name: "debug", Short: "d", Boolean: true}, {Name: "after"}}}}}
result := Parse(tree, []string{"run", "--before=one", "-d", "--after=two", "-x=false"}, ParseOptions{FlagAliases: []commandmeta.FlagAlias{{Source: "-x", Target: "--debug"}}})
if result.Failure != nil {
t.Fatal(result.Failure)
}
if !reflect.DeepEqual(result.FlagValues, [][]string{{"one"}, {"true", "false"}, {"two"}}) || !reflect.DeepEqual(result.FlagSpellings[1], []string{"-d", "-x"}) || len(result.FlagOccurrences) != 4 {
t.Fatalf("parse = %#v", result)
}
}

View File

@@ -2,6 +2,8 @@
// its command-line backend. // its command-line backend.
package commandmeta package commandmeta
import "strings"
// Key identifies one command inside one routing snapshot. // Key identifies one command inside one routing snapshot.
type Key uint64 type Key uint64
@@ -100,6 +102,35 @@ func ReportingFlags() []Flag {
return flags return flags
} }
// EffectiveFlags combines the shared debug option while preserving consumer order.
func EffectiveFlags(flags, core []Flag) []Flag {
result := append([]Flag(nil), flags...)
shared := -1
for i, flag := range result {
if flag.Name == "debug" && flag.Boolean {
shared = i
break
}
}
for _, flag := range core {
if flag.Name == "debug" && shared >= 0 {
app := result[shared]
usage := flag.Usage
if app.Usage != "" {
usage += "; " + app.Usage
}
if strings.EqualFold(app.Default, "true") {
usage += "; application input defaults to true (reporting defaults to ordinary)"
}
flag.Usage, flag.Short, flag.Required, flag.Completion = usage, app.Short, app.Required, app.Completion
result[shared] = flag
continue
}
result = append(result, flag)
}
return result
}
// IsReportingFlag reports whether name is reserved for a shared reporting flag. // IsReportingFlag reports whether name is reserved for a shared reporting flag.
func IsReportingFlag(name string) bool { func IsReportingFlag(name string) bool {
for _, flag := range reportingFlags { for _, flag := range reportingFlags {

View File

@@ -1,6 +1,9 @@
package commandmeta package commandmeta
import "testing" import (
"strings"
"testing"
)
func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) { func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) {
flags := ReportingFlags() flags := ReportingFlags()
@@ -33,3 +36,15 @@ func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) {
} }
} }
} }
func TestEffectiveFlagsCombinesDebugMetadata(t *testing.T) {
flags := []Flag{{Name: "before"}, {Name: "debug", Short: "d", Boolean: true, Default: "true", Required: true, Usage: "application diagnostics", Completion: CompletionHint{Kind: CompletionFiles}}, {Name: "after"}}
core := ReportingFlags()
got := EffectiveFlags(flags, core)
if len(got) != len(flags)+len(core)-1 || got[1].Name != "debug" || got[1].Default != "false" || got[1].Short != "d" || !got[1].Required || got[1].Completion.Kind != CompletionFiles || !strings.Contains(got[1].Usage, "application diagnostics") || !strings.Contains(got[1].Usage, "application input defaults to true") {
t.Fatalf("effective debug = %#v", got)
}
if flags[1].Default != "true" || core[2].Short != "" {
t.Fatal("input metadata changed")
}
}

View File

@@ -10,13 +10,19 @@ import (
commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta" commandmeta "go.maximumdirect.net/commandkit/internal/commandmeta"
) )
type reportingMode uint8 // ReportingMode identifies the invocation's selected reporting visibility.
// Its zero value is ReportingModeOrdinary.
type ReportingMode uint8
const ( const (
modeOrdinary reportingMode = iota // ReportingModeOrdinary uses ordinary reporting visibility.
modeQuiet ReportingModeOrdinary ReportingMode = iota
modeVerbose // ReportingModeQuiet suppresses ordinary output and messages.
modeDebug ReportingModeQuiet
// ReportingModeVerbose enables verbose reporting.
ReportingModeVerbose
// ReportingModeDebug enables debug reporting.
ReportingModeDebug
) )
type reportingDestination uint8 type reportingDestination uint8
@@ -41,7 +47,7 @@ type reportingState struct {
diagnostics destinationState diagnostics destinationState
closed bool closed bool
deferredClosed bool deferredClosed bool
mode reportingMode mode ReportingMode
format Format format Format
progress bool progress bool
progressSet bool progressSet bool
@@ -109,7 +115,7 @@ func newReporter(streams Streams) *Reporter {
return &Reporter{state: &reportingState{ return &Reporter{state: &reportingState{
output: destinationState{writer: streams.Out}, output: destinationState{writer: streams.Out},
diagnostics: destinationState{writer: streams.Err}, diagnostics: destinationState{writer: streams.Err},
mode: modeOrdinary, mode: ReportingModeOrdinary,
}} }}
} }
@@ -125,13 +131,13 @@ func (r *Reporter) applySelection(selection commandmeta.ReportingSelection) {
} }
switch selection.Mode { switch selection.Mode {
case commandmeta.ReportingModeQuiet: case commandmeta.ReportingModeQuiet:
state.mode = modeQuiet state.mode = ReportingModeQuiet
case commandmeta.ReportingModeVerbose: case commandmeta.ReportingModeVerbose:
state.mode = modeVerbose state.mode = ReportingModeVerbose
case commandmeta.ReportingModeDebug: case commandmeta.ReportingModeDebug:
state.mode = modeDebug state.mode = ReportingModeDebug
default: default:
state.mode = modeOrdinary state.mode = ReportingModeOrdinary
} }
state.progress = selection.Progress state.progress = selection.Progress
state.progressSet = selection.ProgressSet state.progressSet = selection.ProgressSet
@@ -189,7 +195,7 @@ func (r *Reporter) RenderOutput(write func(io.Writer) error) error {
destination.submitted = true destination.submitted = true
writer := &coordinatedWriter{state: state, destination: destinationOutput} writer := &coordinatedWriter{state: state, destination: destinationOutput}
renderErr := write(writer) renderErr := write(writer)
if state.mode == modeQuiet { if state.mode == ReportingModeQuiet {
destination.suppressed = true destination.suppressed = true
} else if renderErr == nil && writer.writeError == nil { } else if renderErr == nil && writer.writeError == nil {
destination.delivered = true destination.delivered = true
@@ -489,17 +495,17 @@ func renderDetailsLocked(state *reportingState, details []Detail, visible bool)
func (state *reportingState) visible(category messageCategory) bool { func (state *reportingState) visible(category messageCategory) bool {
switch category { switch category {
case categoryInfo, categoryWarning: case categoryInfo, categoryWarning:
return state.mode == modeVerbose || state.mode == modeDebug return state.mode == ReportingModeVerbose || state.mode == ReportingModeDebug
case categoryDebug: case categoryDebug:
return state.mode == modeDebug return state.mode == ReportingModeDebug
case categoryProgress: case categoryProgress:
if state.mode == modeQuiet { if state.mode == ReportingModeQuiet {
return false return false
} }
if state.progressSet { if state.progressSet {
return state.progress return state.progress
} }
return state.mode == modeDebug return state.mode == ReportingModeDebug
default: default:
return false return false
} }
@@ -617,7 +623,7 @@ func (state *reportingState) destination(destination reportingDestination) *dest
func (state *reportingState) suppressed(destination reportingDestination) bool { func (state *reportingState) suppressed(destination reportingDestination) bool {
if destination == destinationOutput { if destination == destinationOutput {
return state.mode == modeQuiet return state.mode == ReportingModeQuiet
} }
return false return false
} }

View File

@@ -10,7 +10,7 @@ import (
) )
func TestHiddenSimpleDeferredMessagesDoNotAccumulate(t *testing.T) { func TestHiddenSimpleDeferredMessagesDoNotAccumulate(t *testing.T) {
for _, mode := range []reportingMode{modeOrdinary, modeQuiet} { for _, mode := range []ReportingMode{ReportingModeOrdinary, ReportingModeQuiet} {
t.Run(fmt.Sprint(mode), func(t *testing.T) { t.Run(fmt.Sprint(mode), func(t *testing.T) {
reporter := newReporter(Streams{}) reporter := newReporter(Streams{})
setReporterVisibility(reporter, mode, false, false) setReporterVisibility(reporter, mode, false, false)
@@ -45,7 +45,7 @@ func TestHiddenSimpleDeferredMessagesDoNotAccumulate(t *testing.T) {
func TestHiddenDeferredDetailsRemainDeferredAndCaptured(t *testing.T) { func TestHiddenDeferredDetailsRemainDeferredAndCaptured(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) reporter := newReporter(Streams{Err: &diagnostics})
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, false, false)
failure := errors.New("hidden rendering failed") failure := errors.New("hidden rendering failed")
var order []string var order []string
details := []Detail{func(w io.Writer) error { details := []Detail{func(w io.Writer) error {
@@ -87,7 +87,7 @@ func TestReporterDeferredAcceptanceEndsBeforeDelivery(t *testing.T) {
writer := &gatedDiagnosticWriter{entered: make(chan struct{}), release: make(chan struct{})} writer := &gatedDiagnosticWriter{entered: make(chan struct{}), release: make(chan struct{})}
entered := writer.entered entered := writer.entered
reporter := newReporter(Streams{Err: writer}) reporter := newReporter(Streams{Err: writer})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, false, false)
deferred := reporter.AtEnd() deferred := reporter.AtEnd()
if err := deferred.Warn("accepted"); err != nil { if err := deferred.Warn("accepted"); err != nil {
t.Fatal(err) t.Fatal(err)

View File

@@ -16,7 +16,7 @@ func TestReporterExplainsItsOwnFailuresWithoutExposingCauses(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) reporter := newReporter(Streams{Err: &diagnostics})
if quiet { if quiet {
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, false, false)
} }
failure := errors.New("private rendering detail") failure := errors.New("private rendering detail")
render := func(io.Writer) error { return failure } render := func(io.Writer) error { return failure }
@@ -84,7 +84,7 @@ func TestReporterPreservesJoinedRendererEvidence(t *testing.T) {
writeErr := errors.Join(errors.New("sink failure"), errors.New("sink detail")) writeErr := errors.Join(errors.New("sink failure"), errors.New("sink detail"))
destination := &reporterTestWriter{err: writeErr} destination := &reporterTestWriter{err: writeErr}
reporter := newReporter(Streams{Out: destination, Err: destination}) reporter := newReporter(Streams{Out: destination, Err: destination})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, false, false)
render := func(w io.Writer) error { render := func(w io.Writer) error {
_, err := io.WriteString(w, "report") _, err := io.WriteString(w, "report")
return test.combine(renderErr, err) return test.combine(renderErr, err)

View File

@@ -12,7 +12,7 @@ import (
func TestReporterReportQuietAndClosed(t *testing.T) { func TestReporterReportQuietAndClosed(t *testing.T) {
var out bytes.Buffer var out bytes.Buffer
reporter := newReporter(Streams{Out: &out}) reporter := newReporter(Streams{Out: &out})
reporter.state.mode = modeQuiet reporter.state.mode = ReportingModeQuiet
err := reporter.Report(Report{Value: make(chan int)}) err := reporter.Report(Report{Value: make(chan int)})
if err == nil { if err == nil {
t.Fatal("expected serialization error") t.Fatal("expected serialization error")

View File

@@ -80,7 +80,7 @@ func TestReporterLeavesCallerWriterOpen(t *testing.T) {
func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) { func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) {
var destination bytes.Buffer var destination bytes.Buffer
reporter := newReporter(Streams{Out: &destination}) reporter := newReporter(Streams{Out: &destination})
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, false, false)
writer := reporter.Output() writer := reporter.Output()
if n, err := writer.Write([]byte("hidden")); err != nil || n != len("hidden") { if n, err := writer.Write([]byte("hidden")); err != nil || n != len("hidden") {
@@ -177,7 +177,7 @@ func TestReporterClosedHandlesReturnClosedErrorIncludingQuiet(t *testing.T) {
var destination bytes.Buffer var destination bytes.Buffer
reporter := newReporter(Streams{Out: &destination}) reporter := newReporter(Streams{Out: &destination})
writer := reporter.Output() writer := reporter.Output()
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, false, false)
_ = reporter.finalize(Success(), nil) _ = reporter.finalize(Success(), nil)
if n, err := writer.Write([]byte("late")); n != 0 || !errors.Is(err, ErrReporterClosed) { if n, err := writer.Write([]byte("late")); n != 0 || !errors.Is(err, ErrReporterClosed) {
@@ -193,11 +193,11 @@ func TestNewReporterNormalizesNilDestinationsAndStartsOrdinary(t *testing.T) {
if reporter.state.output.writer != io.Discard || reporter.state.diagnostics.writer != io.Discard { if reporter.state.output.writer != io.Discard || reporter.state.diagnostics.writer != io.Discard {
t.Fatalf("nil destinations were not normalized: %#v", reporter.state) t.Fatalf("nil destinations were not normalized: %#v", reporter.state)
} }
if reporter.state.mode != modeOrdinary || reporter.state.progress || reporter.state.progressSet { if reporter.state.mode != ReportingModeOrdinary || reporter.state.progress || reporter.state.progressSet {
t.Fatalf("initial visibility = mode %v, progress %v (set %v)", reporter.state.mode, reporter.state.progress, reporter.state.progressSet) t.Fatalf("initial visibility = mode %v, progress %v (set %v)", reporter.state.mode, reporter.state.progress, reporter.state.progressSet)
} }
setReporterVisibility(reporter, modeDebug, true, true) setReporterVisibility(reporter, ReportingModeDebug, true, true)
if reporter.state.mode != modeDebug || !reporter.state.progress || !reporter.state.progressSet { if reporter.state.mode != ReportingModeDebug || !reporter.state.progress || !reporter.state.progressSet {
t.Fatalf("visibility was not retained: mode %v, progress %v (set %v)", reporter.state.mode, reporter.state.progress, reporter.state.progressSet) t.Fatalf("visibility was not retained: mode %v, progress %v (set %v)", reporter.state.mode, reporter.state.progress, reporter.state.progressSet)
} }
} }
@@ -205,17 +205,17 @@ func TestNewReporterNormalizesNilDestinationsAndStartsOrdinary(t *testing.T) {
func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) { func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
mode reportingMode mode ReportingMode
progress bool progress bool
progressSet bool progressSet bool
want string want string
}{ }{
{name: "ordinary", mode: modeOrdinary}, {name: "ordinary", mode: ReportingModeOrdinary},
{name: "verbose", mode: modeVerbose, want: "not used\nWARN: not used\n"}, {name: "verbose", mode: ReportingModeVerbose, want: "not used\nWARN: not used\n"},
{name: "debug", mode: modeDebug, want: "not used\nWARN: not used\nDEBUG: DEBUG\nPROGRESS\n"}, {name: "debug", mode: ReportingModeDebug, want: "not used\nWARN: not used\nDEBUG: DEBUG\nPROGRESS\n"},
{name: "quiet overrides progress", mode: modeQuiet, progress: true, progressSet: true}, {name: "quiet overrides progress", mode: ReportingModeQuiet, progress: true, progressSet: true},
{name: "explicit ordinary progress", mode: modeOrdinary, progress: true, progressSet: true, want: "PROGRESS\n"}, {name: "explicit ordinary progress", mode: ReportingModeOrdinary, progress: true, progressSet: true, want: "PROGRESS\n"},
{name: "explicit debug disable", mode: modeDebug, progressSet: true, want: "not used\nWARN: not used\nDEBUG: DEBUG\n"}, {name: "explicit debug disable", mode: ReportingModeDebug, progressSet: true, want: "not used\nWARN: not used\nDEBUG: DEBUG\n"},
} }
for _, test := range tests { for _, test := range tests {
@@ -237,7 +237,7 @@ func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) {
func TestReporterFormatsMessagesAndPreservesDetailOrder(t *testing.T) { func TestReporterFormatsMessagesAndPreservesDetailOrder(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) reporter := newReporter(Streams{Err: &diagnostics})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, false, false)
if err := reporter.Warn("Native warning\nwith second line\n", nil, if err := reporter.Warn("Native warning\nwith second line\n", nil,
func(w io.Writer) error { _, err := io.WriteString(w, "detail one\n"); return err }, func(w io.Writer) error { _, err := io.WriteString(w, "detail one\n"); return err },
func(w io.Writer) error { _, err := io.WriteString(w, "detail two\n"); return err }); err != nil { func(w io.Writer) error { _, err := io.WriteString(w, "detail two\n"); return err }); err != nil {
@@ -258,7 +258,7 @@ func TestReporterFormatsMessagesAndPreservesDetailOrder(t *testing.T) {
func TestReporterHiddenRenderersRunAgainstDiscardAndRetainErrors(t *testing.T) { func TestReporterHiddenRenderersRunAgainstDiscardAndRetainErrors(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) reporter := newReporter(Streams{Err: &diagnostics})
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, false, false)
renderErr := errors.New("native renderer failed") renderErr := errors.New("native renderer failed")
calls := 0 calls := 0
err := reporter.Warn("hidden warning", func(w io.Writer) error { err := reporter.Warn("hidden warning", func(w io.Writer) error {
@@ -284,7 +284,7 @@ func TestReporterRecordsIgnoredRenderErrors(t *testing.T) {
renderErr := errors.New("output renderer failed") renderErr := errors.New("output renderer failed")
diagnosticErr := errors.New("diagnostic renderer failed") diagnosticErr := errors.New("diagnostic renderer failed")
reporter := newReporter(Streams{Out: io.Discard, Err: io.Discard}) reporter := newReporter(Streams{Out: io.Discard, Err: io.Discard})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, false, false)
_ = reporter.RenderOutput(func(io.Writer) error { return renderErr }) _ = reporter.RenderOutput(func(io.Writer) error { return renderErr })
_ = reporter.Warn("warning", func(io.Writer) error { return diagnosticErr }) _ = reporter.Warn("warning", func(io.Writer) error { return diagnosticErr })
@@ -297,7 +297,7 @@ func TestReporterRecordsIgnoredRenderErrors(t *testing.T) {
func TestReporterRenderOutputRunsInQuietAndRetainsFailure(t *testing.T) { func TestReporterRenderOutputRunsInQuietAndRetainsFailure(t *testing.T) {
destination := &reporterTestWriter{} destination := &reporterTestWriter{}
reporter := newReporter(Streams{Out: destination}) reporter := newReporter(Streams{Out: destination})
setReporterVisibility(reporter, modeQuiet, false, false) setReporterVisibility(reporter, ReportingModeQuiet, false, false)
renderErr := errors.New("quiet encoder failed") renderErr := errors.New("quiet encoder failed")
calls := 0 calls := 0
_ = reporter.RenderOutput(func(w io.Writer) error { _ = reporter.RenderOutput(func(w io.Writer) error {
@@ -321,7 +321,7 @@ func TestReporterCountsSuccessfulZeroByteRenderAsSubmitted(t *testing.T) {
if err := reporter.RenderOutput(func(io.Writer) error { return nil }); err != nil { if err := reporter.RenderOutput(func(io.Writer) error { return nil }); err != nil {
t.Fatal(err) t.Fatal(err)
} }
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, false, false)
if err := reporter.Warn("", func(io.Writer) error { return nil }); err != nil { if err := reporter.Warn("", func(io.Writer) error { return nil }); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -409,7 +409,7 @@ func TestReporterConcurrentOperationsRemainWhole(t *testing.T) {
t.Run("render callback and message", func(t *testing.T) { t.Run("render callback and message", func(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
reporter := newReporter(Streams{Out: &output, Err: &output}) reporter := newReporter(Streams{Out: &output, Err: &output})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, false, false)
start := make(chan struct{}) start := make(chan struct{})
ready := make(chan struct{}, 2) ready := make(chan struct{}, 2)
done := make(chan struct{}, 2) done := make(chan struct{}, 2)
@@ -443,7 +443,7 @@ func TestReporterConcurrentOperationsRemainWhole(t *testing.T) {
t.Run("message details and render callback", func(t *testing.T) { t.Run("message details and render callback", func(t *testing.T) {
var output bytes.Buffer var output bytes.Buffer
reporter := newReporter(Streams{Out: &output, Err: &output}) reporter := newReporter(Streams{Out: &output, Err: &output})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, false, false)
start := make(chan struct{}) start := make(chan struct{})
ready := make(chan struct{}, 2) ready := make(chan struct{}, 2)
done := make(chan struct{}, 2) done := make(chan struct{}, 2)
@@ -481,7 +481,7 @@ func TestReporterConcurrentOperationsRemainWhole(t *testing.T) {
func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) { func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) reporter := newReporter(Streams{Err: &diagnostics})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, false, false)
if err := reporter.Warn("", func(w io.Writer) error { if err := reporter.Warn("", func(w io.Writer) error {
_, err := io.WriteString(w, "native multiline\nsecond line\n") _, err := io.WriteString(w, "native multiline\nsecond line\n")
return err return err
@@ -499,7 +499,7 @@ func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) {
func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *testing.T) { func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *testing.T) {
var output, diagnostics bytes.Buffer var output, diagnostics bytes.Buffer
reporter := newReporter(Streams{Out: &output, Err: &diagnostics}) reporter := newReporter(Streams{Out: &output, Err: &diagnostics})
setReporterVisibility(reporter, modeDebug, false, false) setReporterVisibility(reporter, ReportingModeDebug, false, false)
if err := reporter.Info("started"); err != nil { if err := reporter.Info("started"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -525,7 +525,7 @@ func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *test
func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *testing.T) { func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *testing.T) {
var diagnostics bytes.Buffer var diagnostics bytes.Buffer
reporter := newReporter(Streams{Err: &diagnostics}) reporter := newReporter(Streams{Err: &diagnostics})
setReporterVisibility(reporter, modeVerbose, false, false) setReporterVisibility(reporter, ReportingModeVerbose, false, false)
writeFirst := Detail(func(w io.Writer) error { _, err := io.WriteString(w, "first detail\n"); return err }) writeFirst := Detail(func(w io.Writer) error { _, err := io.WriteString(w, "first detail\n"); return err })
writeSecond := Detail(func(w io.Writer) error { _, err := io.WriteString(w, "second detail\n"); return err }) writeSecond := Detail(func(w io.Writer) error { _, err := io.WriteString(w, "second detail\n"); return err })
details := []Detail{writeFirst} details := []Detail{writeFirst}
@@ -545,7 +545,7 @@ func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *te
renderErr := errors.New("deferred renderer failed") renderErr := errors.New("deferred renderer failed")
failedReporter := newReporter(Streams{Err: io.Discard}) failedReporter := newReporter(Streams{Err: io.Discard})
setReporterVisibility(failedReporter, modeQuiet, false, false) setReporterVisibility(failedReporter, ReportingModeQuiet, false, false)
if err := failedReporter.AtEnd().Warn("hidden", func(io.Writer) error { return renderErr }); err != nil { if err := failedReporter.AtEnd().Warn("hidden", func(io.Writer) error { return renderErr }); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -699,14 +699,14 @@ func TestReporterFinalizationClosesOutputAndDeferredHandles(t *testing.T) {
} }
} }
func setReporterVisibility(reporter *Reporter, mode reportingMode, progress, progressSet bool) { func setReporterVisibility(reporter *Reporter, mode ReportingMode, progress, progressSet bool) {
selection := commandmeta.ReportingSelection{Progress: progress, ProgressSet: progressSet} selection := commandmeta.ReportingSelection{Progress: progress, ProgressSet: progressSet}
switch mode { switch mode {
case modeQuiet: case ReportingModeQuiet:
selection.Mode = commandmeta.ReportingModeQuiet selection.Mode = commandmeta.ReportingModeQuiet
case modeVerbose: case ReportingModeVerbose:
selection.Mode = commandmeta.ReportingModeVerbose selection.Mode = commandmeta.ReportingModeVerbose
case modeDebug: case ReportingModeDebug:
selection.Mode = commandmeta.ReportingModeDebug selection.Mode = commandmeta.ReportingModeDebug
} }
reporter.applySelection(selection) reporter.applySelection(selection)

View File

@@ -0,0 +1,205 @@
package commandkit_test
import (
"context"
"errors"
"io"
"sync"
"testing"
commandkit "go.maximumdirect.net/commandkit"
)
func TestInvocationReportingMode(t *testing.T) {
if got := (commandkit.Invocation{}).ReportingMode(); got != commandkit.ReportingModeOrdinary {
t.Fatalf("zero invocation mode = %v", got)
}
var retained commandkit.Invocation
var observed commandkit.ReportingMode
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "run", Run: func(inv commandkit.Invocation, _ struct{}) commandkit.Outcome {
retained = inv
observed = inv.ReportingMode()
return commandkit.Success()
}})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
flag string
want commandkit.ReportingMode
}{{"", commandkit.ReportingModeOrdinary}, {"--quiet", commandkit.ReportingModeQuiet}, {"--verbose", commandkit.ReportingModeVerbose}, {"--debug", commandkit.ReportingModeDebug}, {"", commandkit.ReportingModeOrdinary}} {
args := []string{"run"}
if tc.flag != "" {
args = append(args, tc.flag)
}
result := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if result.Kind != commandkit.OutcomeSuccess || observed != tc.want || retained.ReportingMode() != tc.want {
t.Fatalf("%q: outcome=%#v mode=%v", tc.flag, result, retained.ReportingMode())
}
}
}
func TestInvocationReportingModeConcurrentReads(t *testing.T) {
command := commandkit.NewCommand(commandkit.CommandSpec[struct{}]{Name: "run", Run: func(inv commandkit.Invocation, _ struct{}) commandkit.Outcome {
start := make(chan struct{})
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
<-start
for j := 0; j < 100; j++ {
if inv.ReportingMode() != commandkit.ReportingModeDebug {
t.Error("mode changed")
}
_ = inv.Reporter().Debug("diagnostic")
}
}()
}
close(start)
wg.Wait()
return commandkit.Success()
}})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, command)
if err != nil {
t.Fatal(err)
}
if outcome := app.Execute(context.Background(), []string{"run", "--debug"}, commandkit.Streams{Out: io.Discard, Err: io.Discard}); outcome.Kind != commandkit.OutcomeSuccess {
t.Fatal(outcome)
}
}
func TestSharedDebugBindingAndReportingMode(t *testing.T) {
type input struct {
debug commandkit.Supplied[bool]
before, after commandkit.Supplied[string]
operand commandkit.Supplied[string]
}
var received input
var mode commandkit.ReportingMode
factories, calls := 0, 0
parse := func(s string) (string, error) { return s, nil }
cmd := commandkit.NewCommand(commandkit.CommandSpec[input]{Name: "run", NewInput: func() input { factories++; return input{} }, Flags: []commandkit.Flag[input]{
commandkit.ValueFlag(commandkit.FlagMeta{Name: "before"}, (*string)(nil), parse, func(i *input, v commandkit.Supplied[string]) { i.before = v }),
commandkit.BoolFlag(commandkit.FlagMeta{Name: "debug", Short: "d"}, false, func(i *input, v commandkit.Supplied[bool]) { i.debug = v }),
commandkit.ValueFlag(commandkit.FlagMeta{Name: "after"}, (*string)(nil), parse, func(i *input, v commandkit.Supplied[string]) { i.after = v }),
}, Arguments: []commandkit.Argument[input]{commandkit.ValueArgument(commandkit.ArgumentMeta{Name: "operand"}, false, parse, func(i *input, v commandkit.Supplied[string]) { i.operand = v })}, Run: func(inv commandkit.Invocation, i input) commandkit.Outcome {
calls++
received = i
mode = inv.ReportingMode()
return commandkit.Success()
}})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, cmd, commandkit.FlagAlias("-x", "--debug"))
if err != nil {
t.Fatal(err)
}
for _, tc := range []struct {
args []string
value, set bool
mode commandkit.ReportingMode
}{
{nil, false, false, commandkit.ReportingModeOrdinary}, {[]string{"--debug"}, true, true, commandkit.ReportingModeDebug}, {[]string{"--debug", "--quiet"}, true, true, commandkit.ReportingModeQuiet}, {[]string{"--quiet", "--debug"}, true, true, commandkit.ReportingModeDebug}, {[]string{"--debug", "--verbose"}, true, true, commandkit.ReportingModeVerbose}, {[]string{"--debug", "--debug=false"}, false, true, commandkit.ReportingModeOrdinary}, {[]string{"--verbose", "--debug", "--debug=false"}, false, true, commandkit.ReportingModeOrdinary}, {[]string{"--debug", "--quiet=false"}, true, true, commandkit.ReportingModeDebug}, {[]string{"--debug", "--progress=false"}, true, true, commandkit.ReportingModeDebug}, {[]string{"-x"}, true, true, commandkit.ReportingModeDebug}, {[]string{"-d=false"}, false, true, commandkit.ReportingModeOrdinary},
} {
args := append([]string{"run", "--before=one", "--after=two"}, tc.args...)
got := app.Execute(context.Background(), args, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if got.Kind != commandkit.OutcomeSuccess || received.debug.Value != tc.value || received.debug.Set != tc.set || mode != tc.mode || received.before.Value != "one" || received.after.Value != "two" {
t.Fatalf("%v: result=%#v input=%#v mode=%v", tc.args, got, received, mode)
}
}
for _, value := range []string{"--debug=", "--debug=invalid"} {
beforeFactories, beforeCalls := factories, calls
got := app.Execute(context.Background(), []string{"run", value}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if got.Kind != commandkit.OutcomeUsage || factories != beforeFactories || calls != beforeCalls {
t.Fatalf("%s result=%#v factories=%d calls=%d", value, got, factories, calls)
}
}
got := app.Execute(context.Background(), []string{"run", "--before", "--debug"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if got.Kind != commandkit.OutcomeSuccess || received.before.Value != "--debug" || received.debug.Set || mode != commandkit.ReportingModeOrdinary {
t.Fatalf("option-looking value: %#v %#v mode=%v", got, received, mode)
}
got = app.Execute(context.Background(), []string{"run", "--", "--debug"}, commandkit.Streams{Out: io.Discard, Err: io.Discard})
if got.Kind != commandkit.OutcomeSuccess || received.operand.Value != "--debug" || received.debug.Set || mode != commandkit.ReportingModeOrdinary {
t.Fatalf("operand after terminator: %#v %#v mode=%v", got, received, mode)
}
}
func TestSharedDebugCapturedDefaultsAndRequiredness(t *testing.T) {
type inner struct{ debug commandkit.Supplied[bool] }
type outer struct{ flags commandkit.FlagValues[inner] }
for _, required := range []bool{false, true} {
flag := commandkit.BoolFlag(commandkit.FlagMeta{Name: "debug", Short: "d", Required: required}, true, func(i *inner, v commandkit.Supplied[bool]) { i.debug = v })
captured := commandkit.CaptureFlags([]commandkit.Flag[inner]{flag}, func(i *outer) *commandkit.FlagValues[inner] { return &i.flags })
var applied, supplied inner
var applyErr error
var mode commandkit.ReportingMode
cmd := commandkit.NewCommand(commandkit.CommandSpec[outer]{Name: "run", Flags: captured, Run: func(inv commandkit.Invocation, i outer) commandkit.Outcome {
mode = inv.ReportingMode()
applied = inner{}
supplied = inner{debug: commandkit.Supplied[bool]{Value: true, Set: true}}
applyErr = i.flags.ApplyTo(&applied, flag)
if err := i.flags.ApplySuppliedTo(&supplied, flag); err != nil && applyErr == nil {
applyErr = err
}
return commandkit.Success()
}})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, cmd, commandkit.FlagAliasFor("run", "-x", "-d"))
if err != nil {
t.Fatal(err)
}
result := app.Execute(context.Background(), []string{"run"}, commandkit.Streams{})
if result.Kind != commandkit.OutcomeSuccess || mode != commandkit.ReportingModeOrdinary {
t.Fatalf("omission=%#v mode=%v", result, mode)
}
if required {
if !errors.Is(applyErr, commandkit.ErrFlagInput) {
t.Fatalf("missing required: %v", applyErr)
}
} else if applyErr != nil || !applied.debug.Value || applied.debug.Set || !supplied.debug.Value || !supplied.debug.Set {
t.Fatalf("defaults=%#v %#v %v", applied, supplied, applyErr)
}
result = app.Execute(context.Background(), []string{"run", "-x=false"}, commandkit.Streams{})
if result.Kind != commandkit.OutcomeSuccess || applyErr != nil || applied.debug.Value || !applied.debug.Set || supplied.debug.Value || !supplied.debug.Set || mode != commandkit.ReportingModeOrdinary {
t.Fatalf("false=%#v %#v %#v mode=%v err=%v", result, applied, supplied, mode, applyErr)
}
}
}
func TestSharedDebugDirectRequiredAndRenamedBinding(t *testing.T) {
type input struct{ debug commandkit.Supplied[bool] }
var got input
var mode commandkit.ReportingMode
original := commandkit.BoolFlag(commandkit.FlagMeta{Name: "debug", Required: true}, true, func(i *input, v commandkit.Supplied[bool]) { i.debug = v })
required := commandkit.NewCommand(commandkit.CommandSpec[input]{Name: "required", Flags: []commandkit.Flag[input]{original}, Run: func(inv commandkit.Invocation, i input) commandkit.Outcome {
got = i
mode = inv.ReportingMode()
return commandkit.Success()
}})
renamed := commandkit.NewCommand(commandkit.CommandSpec[input]{Name: "renamed", Flags: []commandkit.Flag[input]{commandkit.RenameFlag(original, "trace")}, Run: func(inv commandkit.Invocation, i input) commandkit.Outcome {
got = i
mode = inv.ReportingMode()
return commandkit.Success()
}})
ordinary := commandkit.BoolFlag(commandkit.FlagMeta{Name: "local"}, false, func(i *input, v commandkit.Supplied[bool]) { i.debug = v })
attached := commandkit.NewCommand(commandkit.CommandSpec[input]{Name: "attached", Flags: []commandkit.Flag[input]{commandkit.RenameFlag(ordinary, "debug")}, Run: func(inv commandkit.Invocation, i input) commandkit.Outcome {
got = i
mode = inv.ReportingMode()
return commandkit.Success()
}})
app, err := commandkit.New(commandkit.Identity{Name: "example"}, required, renamed, attached)
if err != nil {
t.Fatal(err)
}
if r := app.Execute(context.Background(), []string{"required"}, commandkit.Streams{}); r.Kind != commandkit.OutcomeUsage {
t.Fatalf("required omission=%#v", r)
}
if r := app.Execute(context.Background(), []string{"required", "--debug=false"}, commandkit.Streams{}); r.Kind != commandkit.OutcomeSuccess || got.debug.Value || !got.debug.Set || mode != commandkit.ReportingModeOrdinary {
t.Fatalf("explicit false=%#v %#v", r, got)
}
if r := app.Execute(context.Background(), []string{"renamed", "--trace=false", "--debug"}, commandkit.Streams{}); r.Kind != commandkit.OutcomeSuccess || got.debug.Value || !got.debug.Set || mode != commandkit.ReportingModeDebug {
t.Fatalf("renamed=%#v %#v mode=%v", r, got, mode)
}
if r := app.Execute(context.Background(), []string{"attached", "--debug"}, commandkit.Streams{}); r.Kind != commandkit.OutcomeSuccess || !got.debug.Value || !got.debug.Set || mode != commandkit.ReportingModeDebug {
t.Fatalf("renamed to debug=%#v %#v mode=%v", r, got, mode)
}
}