Compare commits
15 Commits
integratio
...
integratio
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ed947bd26 | |||
| 1613ff95ca | |||
| 68288ed684 | |||
| 56e3b013f9 | |||
| 96c96bb793 | |||
| d914295ced | |||
| a54ff03aef | |||
| 7aff296c86 | |||
| 3271c18b77 | |||
| 7dd30f7df3 | |||
| 5e5f61a306 | |||
| 6727d22ce6 | |||
| f23af30234 | |||
| ec0e58efff | |||
| 69257066e2 |
@@ -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
|
||||
// "true"; explicit values are converted with strconv.ParseBool. Set distinguishes
|
||||
// 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] {
|
||||
binding := flagBinding[I]{identity: newBindingIdentity(), meta: meta, defaultText: fmt.Sprint(defaultValue), boolean: true}
|
||||
if set == nil {
|
||||
|
||||
@@ -95,7 +95,8 @@ type FlagMeta struct {
|
||||
// Name is a nonempty long name without leading dashes. It consists of
|
||||
// Unicode lowercase letters or digits in segments separated by single
|
||||
// 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
|
||||
// Short is an optional one-byte spelling other than '-' or the reserved 'h'.
|
||||
Short string
|
||||
@@ -498,7 +499,7 @@ func validateBindings[I any](flags []Flag[I], arguments []Argument[I]) error {
|
||||
if binding.meta.Name == "help" {
|
||||
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)
|
||||
}
|
||||
if _, exists := flagNames[binding.meta.Name]; exists {
|
||||
|
||||
@@ -35,8 +35,7 @@ type completionFlagState struct {
|
||||
func completionFlags(context completionContext) []commandmeta.Flag {
|
||||
flags := make([]commandmeta.Flag, 0, len(context.command.Flags)+len(context.command.CoreFlags)+1)
|
||||
flags = append(flags, commandmeta.HelpFlag())
|
||||
flags = append(flags, context.command.CoreFlags...)
|
||||
flags = append(flags, context.command.Flags...)
|
||||
flags = append(flags, commandmeta.EffectiveFlags(context.command.Flags, context.command.CoreFlags)...)
|
||||
return flags
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
arguments := []commandmeta.Argument{
|
||||
{Name: "optional", Min: 0, Max: 1, Completion: commandmeta.CompletionHint{Kind: commandmeta.CompletionFiles}},
|
||||
|
||||
@@ -120,3 +120,51 @@ func TestCompletionQueryDiscovery(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
The reserved long spellings are `--quiet`, `--verbose`, `--debug`,
|
||||
`--progress`, and `--format`; application declarations cannot reuse them. There
|
||||
are no built-in short spellings. Applications may define aliases such as `-q → --quiet`,
|
||||
`--progress`, and `--format`. An application may attach its own boolean input
|
||||
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
|
||||
the same ordering. Thus `example export --verbose -q` selects quiet mode.
|
||||
|
||||
|
||||
@@ -18,20 +18,21 @@ The following lists the current release set and earlier module combinations:
|
||||
|
||||
| Selection | Core | ckconfig | ckprompt | Upstream libraries and status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Published core | v0.5.0 | — | — | Usable independently. |
|
||||
| ConfigKit integration | v0.5.0 | v0.5.0 | — | ConfigKit v1.1.0. |
|
||||
| Published core | v0.6.0 | — | — | Usable independently. |
|
||||
| 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. |
|
||||
| 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:
|
||||
|
||||
```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
|
||||
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
|
||||
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.
|
||||
@@ -154,6 +155,35 @@ Deferred submissions are accepted until finalization begins; later submissions
|
||||
return `ErrReporterClosed`. The [CLI reference](../cli.md#reporting) owns flag
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
[maintainer release procedure](release.md) owns current release operations.
|
||||
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),
|
||||
[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),
|
||||
@@ -117,7 +118,8 @@ go work init . ./integrations/ckconfig ./integrations/ckprompt
|
||||
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
|
||||
`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
|
||||
support independent builds. The aligned workspace exercises the local sources
|
||||
together.
|
||||
|
||||
@@ -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
|
||||
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
|
||||
report rendering, delivery observations, and finalization. It presents queued
|
||||
messages after the handler returns, then a failed outcome's explanation, and
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
**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)
|
||||
|
||||
@@ -96,6 +96,22 @@ Additional compatibility promises and deliberate exceptions belong in the
|
||||
adopted policy or canonical contract documentation. Release notes explain
|
||||
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
|
||||
|
||||
Distinguish the minimum toolchain supported for consumers from the toolchain
|
||||
|
||||
@@ -9,11 +9,28 @@ Gitea release page; there are no binaries,
|
||||
checksums, or other delivery assets. CI, module proxy availability, and consumer
|
||||
installation are not completion gates.
|
||||
|
||||
Publish in dependency order: core, then `ckconfig`, then `ckprompt`. The
|
||||
integration modules require the preceding CommandKit releases, so prepare and
|
||||
publish each module only after its CommandKit dependencies are available. A root
|
||||
release runs only the root module's default suite; it need not run suites for
|
||||
independently released integrations.
|
||||
Plan the initiating release and all downstream integration releases required by
|
||||
the [dependency-alignment policy](policy/release.md#commandkit-downstream-dependency-alignment).
|
||||
That policy sets the minimum version bump for affected integrations; select
|
||||
each module's version independently according to its compatibility impact.
|
||||
|
||||
| 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
|
||||
`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 dependencies in the selected module before the candidate is finalized.
|
||||
Do this after the preceding CommandKit module has been published and before the
|
||||
selected candidate suite is run. For `core`, run the command at the repository
|
||||
root; for integrations, run it from that module directory.
|
||||
Do this after any CommandKit dependency releases required by the coordinated
|
||||
update have been published and before the selected candidate suite is run.
|
||||
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
|
||||
CommandKit versions. `go mod download` downloads the versions already selected;
|
||||
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
|
||||
# After publishing core, prepare ckconfig from its module directory.
|
||||
|
||||
25
docs/releases/v0.6.0.md
Normal file
25
docs/releases/v0.6.0.md
Normal 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)
|
||||
@@ -1,7 +1,6 @@
|
||||
# Future work
|
||||
|
||||
Status: pending ckprompt v0.5.0 publication, optional shell validation, and
|
||||
future capability proposals.
|
||||
Status: optional shell validation and future capability proposals.
|
||||
No delivery dates are selected here.
|
||||
|
||||
## Purpose and authority
|
||||
@@ -9,6 +8,8 @@ No delivery dates are selected here.
|
||||
Earlier completed roadmaps, implementation plans, and review findings have been retired.
|
||||
This document preserves unfinished work and unselected ideas under the
|
||||
[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 audits, consolidated remediation, implementation plan, and
|
||||
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
|
||||
an idea here does not select its syntax, API, priority, or delivery date.
|
||||
|
||||
## Pending release and integration work
|
||||
|
||||
### 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 verification
|
||||
|
||||
### Optional shell completion validation
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package commandkit
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ type FlagValues[I any] struct {
|
||||
// each adapted flag stores its prepared value in target, associated with the
|
||||
// original inner binding identity. Copies of an original handle remain
|
||||
// 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] {
|
||||
captured := make([]Flag[Outer], len(flags))
|
||||
for index, original := range flags {
|
||||
|
||||
15
identity.go
15
identity.go
@@ -59,6 +59,21 @@ type Invocation struct {
|
||||
// manually constructed Invocation has no 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.
|
||||
// It returns an operational failure when the invocation has no help renderer.
|
||||
func (inv Invocation) Help() Outcome {
|
||||
|
||||
16
integrations/ckconfig/docs/releases/v0.6.0.md
Normal file
16
integrations/ckconfig/docs/releases/v0.6.0.md
Normal 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)
|
||||
@@ -3,7 +3,7 @@ module go.maximumdirect.net/ckconfig
|
||||
go 1.27.0
|
||||
|
||||
require (
|
||||
go.maximumdirect.net/commandkit v0.5.0
|
||||
go.maximumdirect.net/commandkit v0.6.0
|
||||
go.maximumdirect.net/configkit v1.1.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
)
|
||||
|
||||
@@ -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.5.0 h1:Ln+bCp+FwCg4tZD3NaMKQXUDB3mPusgwsnRZmb0+3cM=
|
||||
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/go.mod h1:s1zLJ9GGB/1Q4a+aOHMLISonJxWFgNWpA6SpPlOafeA=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 h1:1h7H1ohdUh93/FyE4YaDa1Zh64K6VVbjF4K6WUxMtH4=
|
||||
|
||||
@@ -77,9 +77,7 @@ func (session *Session) Parse(argv []string) commandmeta.Result {
|
||||
}
|
||||
|
||||
remaining := append(commandpath.CloneTokens(unconsumed), tail...)
|
||||
localFlags := make([]commandmeta.Flag, 0, len(selectedNode.Flags)+len(selectedNode.CoreFlags))
|
||||
localFlags = append(localFlags, selectedNode.Flags...)
|
||||
localFlags = append(localFlags, selectedNode.CoreFlags...)
|
||||
localFlags := commandmeta.EffectiveFlags(selectedNode.Flags, selectedNode.CoreFlags)
|
||||
aliases := session.flagAliases
|
||||
if session.flagAliasesForPath != nil {
|
||||
aliases = session.flagAliasesForPath(result.Path)
|
||||
@@ -254,7 +252,7 @@ func displayCommandPath(rootName string, path []string) string {
|
||||
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) {
|
||||
if session == nil || session.root == 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() {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -316,7 +330,7 @@ func buildTree(root *commandmeta.Command) (*spfcobra.Command, map[*spfcobra.Comm
|
||||
helpFlag := backend.Flags().Lookup("help")
|
||||
helpFlag.NoOptDefVal = "true"
|
||||
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}
|
||||
backend.Flags().VarP(value, declarationFlag.Name, declarationFlag.Short, flagUsage(declarationFlag))
|
||||
backendFlag := backend.Flags().Lookup(declarationFlag.Name)
|
||||
|
||||
@@ -281,3 +281,14 @@ func TestMissingSessionTreeReturnsNeutralFailures(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// its command-line backend.
|
||||
package commandmeta
|
||||
|
||||
import "strings"
|
||||
|
||||
// Key identifies one command inside one routing snapshot.
|
||||
type Key uint64
|
||||
|
||||
@@ -100,6 +102,35 @@ func ReportingFlags() []Flag {
|
||||
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.
|
||||
func IsReportingFlag(name string) bool {
|
||||
for _, flag := range reportingFlags {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package commandmeta
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReportingFlagsAreBooleanAndReturnedIndependently(t *testing.T) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
40
reporter.go
40
reporter.go
@@ -10,13 +10,19 @@ import (
|
||||
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 (
|
||||
modeOrdinary reportingMode = iota
|
||||
modeQuiet
|
||||
modeVerbose
|
||||
modeDebug
|
||||
// ReportingModeOrdinary uses ordinary reporting visibility.
|
||||
ReportingModeOrdinary ReportingMode = iota
|
||||
// ReportingModeQuiet suppresses ordinary output and messages.
|
||||
ReportingModeQuiet
|
||||
// ReportingModeVerbose enables verbose reporting.
|
||||
ReportingModeVerbose
|
||||
// ReportingModeDebug enables debug reporting.
|
||||
ReportingModeDebug
|
||||
)
|
||||
|
||||
type reportingDestination uint8
|
||||
@@ -41,7 +47,7 @@ type reportingState struct {
|
||||
diagnostics destinationState
|
||||
closed bool
|
||||
deferredClosed bool
|
||||
mode reportingMode
|
||||
mode ReportingMode
|
||||
format Format
|
||||
progress bool
|
||||
progressSet bool
|
||||
@@ -109,7 +115,7 @@ func newReporter(streams Streams) *Reporter {
|
||||
return &Reporter{state: &reportingState{
|
||||
output: destinationState{writer: streams.Out},
|
||||
diagnostics: destinationState{writer: streams.Err},
|
||||
mode: modeOrdinary,
|
||||
mode: ReportingModeOrdinary,
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -125,13 +131,13 @@ func (r *Reporter) applySelection(selection commandmeta.ReportingSelection) {
|
||||
}
|
||||
switch selection.Mode {
|
||||
case commandmeta.ReportingModeQuiet:
|
||||
state.mode = modeQuiet
|
||||
state.mode = ReportingModeQuiet
|
||||
case commandmeta.ReportingModeVerbose:
|
||||
state.mode = modeVerbose
|
||||
state.mode = ReportingModeVerbose
|
||||
case commandmeta.ReportingModeDebug:
|
||||
state.mode = modeDebug
|
||||
state.mode = ReportingModeDebug
|
||||
default:
|
||||
state.mode = modeOrdinary
|
||||
state.mode = ReportingModeOrdinary
|
||||
}
|
||||
state.progress = selection.Progress
|
||||
state.progressSet = selection.ProgressSet
|
||||
@@ -189,7 +195,7 @@ func (r *Reporter) RenderOutput(write func(io.Writer) error) error {
|
||||
destination.submitted = true
|
||||
writer := &coordinatedWriter{state: state, destination: destinationOutput}
|
||||
renderErr := write(writer)
|
||||
if state.mode == modeQuiet {
|
||||
if state.mode == ReportingModeQuiet {
|
||||
destination.suppressed = true
|
||||
} else if renderErr == nil && writer.writeError == nil {
|
||||
destination.delivered = true
|
||||
@@ -489,17 +495,17 @@ func renderDetailsLocked(state *reportingState, details []Detail, visible bool)
|
||||
func (state *reportingState) visible(category messageCategory) bool {
|
||||
switch category {
|
||||
case categoryInfo, categoryWarning:
|
||||
return state.mode == modeVerbose || state.mode == modeDebug
|
||||
return state.mode == ReportingModeVerbose || state.mode == ReportingModeDebug
|
||||
case categoryDebug:
|
||||
return state.mode == modeDebug
|
||||
return state.mode == ReportingModeDebug
|
||||
case categoryProgress:
|
||||
if state.mode == modeQuiet {
|
||||
if state.mode == ReportingModeQuiet {
|
||||
return false
|
||||
}
|
||||
if state.progressSet {
|
||||
return state.progress
|
||||
}
|
||||
return state.mode == modeDebug
|
||||
return state.mode == ReportingModeDebug
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -617,7 +623,7 @@ func (state *reportingState) destination(destination reportingDestination) *dest
|
||||
|
||||
func (state *reportingState) suppressed(destination reportingDestination) bool {
|
||||
if destination == destinationOutput {
|
||||
return state.mode == modeQuiet
|
||||
return state.mode == ReportingModeQuiet
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
)
|
||||
|
||||
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) {
|
||||
reporter := newReporter(Streams{})
|
||||
setReporterVisibility(reporter, mode, false, false)
|
||||
@@ -45,7 +45,7 @@ func TestHiddenSimpleDeferredMessagesDoNotAccumulate(t *testing.T) {
|
||||
func TestHiddenDeferredDetailsRemainDeferredAndCaptured(t *testing.T) {
|
||||
var diagnostics bytes.Buffer
|
||||
reporter := newReporter(Streams{Err: &diagnostics})
|
||||
setReporterVisibility(reporter, modeQuiet, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeQuiet, false, false)
|
||||
failure := errors.New("hidden rendering failed")
|
||||
var order []string
|
||||
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{})}
|
||||
entered := writer.entered
|
||||
reporter := newReporter(Streams{Err: writer})
|
||||
setReporterVisibility(reporter, modeVerbose, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeVerbose, false, false)
|
||||
deferred := reporter.AtEnd()
|
||||
if err := deferred.Warn("accepted"); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestReporterExplainsItsOwnFailuresWithoutExposingCauses(t *testing.T) {
|
||||
var diagnostics bytes.Buffer
|
||||
reporter := newReporter(Streams{Err: &diagnostics})
|
||||
if quiet {
|
||||
setReporterVisibility(reporter, modeQuiet, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeQuiet, false, false)
|
||||
}
|
||||
failure := errors.New("private rendering detail")
|
||||
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"))
|
||||
destination := &reporterTestWriter{err: writeErr}
|
||||
reporter := newReporter(Streams{Out: destination, Err: destination})
|
||||
setReporterVisibility(reporter, modeVerbose, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeVerbose, false, false)
|
||||
render := func(w io.Writer) error {
|
||||
_, err := io.WriteString(w, "report")
|
||||
return test.combine(renderErr, err)
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
func TestReporterReportQuietAndClosed(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
reporter := newReporter(Streams{Out: &out})
|
||||
reporter.state.mode = modeQuiet
|
||||
reporter.state.mode = ReportingModeQuiet
|
||||
err := reporter.Report(Report{Value: make(chan int)})
|
||||
if err == nil {
|
||||
t.Fatal("expected serialization error")
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestReporterLeavesCallerWriterOpen(t *testing.T) {
|
||||
func TestReporterQuietOutputDiscardsSuccessfully(t *testing.T) {
|
||||
var destination bytes.Buffer
|
||||
reporter := newReporter(Streams{Out: &destination})
|
||||
setReporterVisibility(reporter, modeQuiet, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeQuiet, false, false)
|
||||
writer := reporter.Output()
|
||||
|
||||
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
|
||||
reporter := newReporter(Streams{Out: &destination})
|
||||
writer := reporter.Output()
|
||||
setReporterVisibility(reporter, modeQuiet, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeQuiet, false, false)
|
||||
_ = reporter.finalize(Success(), nil)
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
setReporterVisibility(reporter, modeDebug, true, true)
|
||||
if reporter.state.mode != modeDebug || !reporter.state.progress || !reporter.state.progressSet {
|
||||
setReporterVisibility(reporter, ReportingModeDebug, true, true)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -205,17 +205,17 @@ func TestNewReporterNormalizesNilDestinationsAndStartsOrdinary(t *testing.T) {
|
||||
func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mode reportingMode
|
||||
mode ReportingMode
|
||||
progress bool
|
||||
progressSet bool
|
||||
want string
|
||||
}{
|
||||
{name: "ordinary", mode: modeOrdinary},
|
||||
{name: "verbose", mode: modeVerbose, want: "not used\nWARN: not used\n"},
|
||||
{name: "debug", mode: modeDebug, want: "not used\nWARN: not used\nDEBUG: DEBUG\nPROGRESS\n"},
|
||||
{name: "quiet overrides progress", mode: modeQuiet, progress: true, progressSet: true},
|
||||
{name: "explicit ordinary progress", mode: modeOrdinary, 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: "ordinary", mode: ReportingModeOrdinary},
|
||||
{name: "verbose", mode: ReportingModeVerbose, want: "not used\nWARN: not used\n"},
|
||||
{name: "debug", mode: ReportingModeDebug, want: "not used\nWARN: not used\nDEBUG: DEBUG\nPROGRESS\n"},
|
||||
{name: "quiet overrides progress", mode: ReportingModeQuiet, progress: true, progressSet: true},
|
||||
{name: "explicit ordinary progress", mode: ReportingModeOrdinary, progress: true, progressSet: true, want: "PROGRESS\n"},
|
||||
{name: "explicit debug disable", mode: ReportingModeDebug, progressSet: true, want: "not used\nWARN: not used\nDEBUG: DEBUG\n"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@@ -237,7 +237,7 @@ func TestReporterMessageVisibilityAndProgressPrecedence(t *testing.T) {
|
||||
func TestReporterFormatsMessagesAndPreservesDetailOrder(t *testing.T) {
|
||||
var diagnostics bytes.Buffer
|
||||
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,
|
||||
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 {
|
||||
@@ -258,7 +258,7 @@ func TestReporterFormatsMessagesAndPreservesDetailOrder(t *testing.T) {
|
||||
func TestReporterHiddenRenderersRunAgainstDiscardAndRetainErrors(t *testing.T) {
|
||||
var diagnostics bytes.Buffer
|
||||
reporter := newReporter(Streams{Err: &diagnostics})
|
||||
setReporterVisibility(reporter, modeQuiet, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeQuiet, false, false)
|
||||
renderErr := errors.New("native renderer failed")
|
||||
calls := 0
|
||||
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")
|
||||
diagnosticErr := errors.New("diagnostic renderer failed")
|
||||
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.Warn("warning", func(io.Writer) error { return diagnosticErr })
|
||||
|
||||
@@ -297,7 +297,7 @@ func TestReporterRecordsIgnoredRenderErrors(t *testing.T) {
|
||||
func TestReporterRenderOutputRunsInQuietAndRetainsFailure(t *testing.T) {
|
||||
destination := &reporterTestWriter{}
|
||||
reporter := newReporter(Streams{Out: destination})
|
||||
setReporterVisibility(reporter, modeQuiet, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeQuiet, false, false)
|
||||
renderErr := errors.New("quiet encoder failed")
|
||||
calls := 0
|
||||
_ = 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 {
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -409,7 +409,7 @@ func TestReporterConcurrentOperationsRemainWhole(t *testing.T) {
|
||||
t.Run("render callback and message", func(t *testing.T) {
|
||||
var output bytes.Buffer
|
||||
reporter := newReporter(Streams{Out: &output, Err: &output})
|
||||
setReporterVisibility(reporter, modeVerbose, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeVerbose, false, false)
|
||||
start := make(chan struct{})
|
||||
ready := 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) {
|
||||
var output bytes.Buffer
|
||||
reporter := newReporter(Streams{Out: &output, Err: &output})
|
||||
setReporterVisibility(reporter, modeVerbose, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeVerbose, false, false)
|
||||
start := make(chan struct{})
|
||||
ready := make(chan struct{}, 2)
|
||||
done := make(chan struct{}, 2)
|
||||
@@ -481,7 +481,7 @@ func TestReporterConcurrentOperationsRemainWhole(t *testing.T) {
|
||||
func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) {
|
||||
var diagnostics bytes.Buffer
|
||||
reporter := newReporter(Streams{Err: &diagnostics})
|
||||
setReporterVisibility(reporter, modeVerbose, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeVerbose, false, false)
|
||||
if err := reporter.Warn("", func(w io.Writer) error {
|
||||
_, err := io.WriteString(w, "native multiline\nsecond line\n")
|
||||
return err
|
||||
@@ -499,7 +499,7 @@ func TestReporterEmptyHeadingDoesNotAddPrefixOrBlankLine(t *testing.T) {
|
||||
func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *testing.T) {
|
||||
var output, diagnostics bytes.Buffer
|
||||
reporter := newReporter(Streams{Out: &output, Err: &diagnostics})
|
||||
setReporterVisibility(reporter, modeDebug, false, false)
|
||||
setReporterVisibility(reporter, ReportingModeDebug, false, false)
|
||||
if err := reporter.Info("started"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -525,7 +525,7 @@ func TestReporterFinalizesDeferredMessagesBeforeCompletionAndExplanation(t *test
|
||||
func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *testing.T) {
|
||||
var diagnostics bytes.Buffer
|
||||
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 })
|
||||
writeSecond := Detail(func(w io.Writer) error { _, err := io.WriteString(w, "second detail\n"); return err })
|
||||
details := []Detail{writeFirst}
|
||||
@@ -545,7 +545,7 @@ func TestReporterDeferredSubmissionCopiesDetailsAndRetainsDeferredFailures(t *te
|
||||
|
||||
renderErr := errors.New("deferred renderer failed")
|
||||
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 {
|
||||
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}
|
||||
switch mode {
|
||||
case modeQuiet:
|
||||
case ReportingModeQuiet:
|
||||
selection.Mode = commandmeta.ReportingModeQuiet
|
||||
case modeVerbose:
|
||||
case ReportingModeVerbose:
|
||||
selection.Mode = commandmeta.ReportingModeVerbose
|
||||
case modeDebug:
|
||||
case ReportingModeDebug:
|
||||
selection.Mode = commandmeta.ReportingModeDebug
|
||||
}
|
||||
reporter.applySelection(selection)
|
||||
|
||||
205
shared_debug_external_test.go
Normal file
205
shared_debug_external_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user