15 Commits

Author SHA1 Message Date
1613ff95ca Require downstream integration releases for dependency updates 2026-11-08 15:13:39 +00:00
68288ed684 Retire completed shared debug roadmap 2026-11-08 15:11:40 +00:00
56e3b013f9 Verify shared debug binding implementation 2026-11-08 15:11:15 +00:00
96c96bb793 Document shared debug input and reporting mode 2026-11-08 15:10:00 +00:00
d914295ced Align shared debug help and completion 2026-11-08 15:08:50 +00:00
a54ff03aef Cover shared debug binding composition 2026-11-08 15:03:24 +00:00
7aff296c86 Parse shared debug input once 2026-11-08 15:02:00 +00:00
3271c18b77 Accept shared debug bindings 2026-11-08 15:00:34 +00:00
7dd30f7df3 Expose invocation reporting mode 2026-11-08 14:59:37 +00:00
5e5f61a306 Plan shared debug bindings and reporting mode access 2026-11-08 14:51:21 +00:00
6727d22ce6 Adopt ordinary application settings for shared debug bindings 2026-11-08 13:59:34 +00:00
f23af30234 Select ordinary BoolFlag registration for shared debug input 2026-11-08 13:55:37 +00:00
ec0e58efff Plan shared debug binding and reporting mode access 2026-11-08 13:52:48 +00:00
69257066e2 Record completion of the integration releases 2026-11-08 00:34:41 +00:00
88ac6e76e4 Prepare ckprompt v0.5.0 with aligned release dependencies 2026-11-08 00:33:46 +00:00
32 changed files with 641 additions and 393 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
// "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 {

View File

@@ -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 {

View File

@@ -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
}

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) {
arguments := []commandmeta.Argument{
{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())
}
}
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.
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.

View File

@@ -14,14 +14,14 @@ applications.
| `go.maximumdirect.net/ckconfig` | You want operators to view, validate, explain, and compare ConfigKit configuration. |
| `go.maximumdirect.net/ckprompt` | You want operators to inspect PromptKit resources, check and preview requests, or run smoke tests. |
The following distinguishes published combinations from the current checkout:
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. |
| Published ConfigKit integration | v0.4.0 | v0.4.0 | — | ConfigKit v1.1.0. |
| ConfigKit integration | v0.5.0 | v0.5.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. |
| Current source | Local | Local | Local | ConfigKit v1.1.0 and PromptKit v0.20.0; use an aligned workspace until release dependencies are updated. |
Install the published core with:
@@ -29,13 +29,12 @@ Install the published core with:
go get go.maximumdirect.net/commandkit@v0.5.0
```
The integration releases being prepared are ckconfig v0.5.0 and ckprompt v0.5.0. Current
ckprompt source calls `ckconfig.AdaptDiagnostics`, which is absent from its
selected released ckconfig v0.4.0 dependency. It therefore needs local
ckconfig source in the workspace until [release
alignment](../roadmap/future.md#pending-release-and-integration-work) is
complete. Published ckconfig v0.3.0 also uses ConfigKit v1.1.0 and does not
compose with ckprompt v0.2.0's earlier ConfigKit dependency.
Select either integration with `go get go.maximumdirect.net/ckconfig@v0.5.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`.
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.
Importing a module does not install its commands. Pass commands to
`commandkit.New` or `App.Register` explicitly. ckprompt depends on ckconfig's
@@ -155,6 +154,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.

View File

@@ -62,7 +62,7 @@ capability proposals in [future work](roadmap/future.md).
| Public facade or adapter boundaries | [Internal overview](internal/overview.md), [ADR 0002](adr/0002-public-facade-and-cli-backend.md), [ADR 0004](adr/0004-optional-integration-modules.md), and the architecture policy |
| Module ownership and public contracts | [Consumer guide](consumers/guide.md), [internal overview](internal/overview.md), [core GoDoc](../doc.go), [ckconfig GoDoc](../integrations/ckconfig/doc.go), [ckprompt GoDoc](../integrations/ckprompt/doc.go), and applicable ADRs |
| Unfinished work and capability proposals | [Future work](roadmap/future.md) |
| Documentation refresh record | [Documentation roadmap](roadmap/documentation-refresh.md) |
| Completed documentation refresh | [Retired refresh record](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/5c960532552037d38119941389e558f78bf73aad/docs/roadmap/documentation-refresh.md) |
| Completed planning and implementation history | [Completed dependency upgrades, PromptKit validation, discovery, smoke testing, and remediation](https://gitea.maximumdirect.net/eric/CommandKit/src/commit/a6278a6d9f940948ec7745955d745523aaff0734/docs/roadmap), [earlier ConfigKit and PromptKit adoption and remediation](https://gitea.maximumdirect.net/eric/CommandKit/src/commit/8a82620dfcd34c0ae56bbecb0d014eb49786bc92/docs/roadmap), [earlier retired records](https://gitea.maximumdirect.net/eric/CommandKit/src/commit/6bc37e7ba64ca0920b4d0cc77039a18d4006b24e/docs/roadmap), and [completed module implementation and remediation](https://gitea.maximumdirect.net/eric/CommandKit/src/commit/bb2528532b78d30e52f5082e491a5e1fd9b0fb99/docs/roadmap) in Git history |
| Release tooling | [Maintainer release procedure](release.md) |
| Performance maintenance | [Internal overview](internal/overview.md), existing benchmarks, and [possible further efficiency work](roadmap/future.md#possible-further-efficiency-work) |
@@ -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,11 +118,10 @@ 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. The integrations select released core v0.4.0;
ckprompt also selects released ckconfig v0.4.0.
Current ckprompt source requires a ckconfig helper absent from that release, so
its independent suite remains pending release and dependency alignment; the
aligned workspace exercises the current source together.
and independent module suites. Both integrations select released 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.
Run `GOWORK=off GOTOOLCHAIN=local go test ./...` separately from each module
directory. From the repository root, a workspace containing all three modules can
also run:

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
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

View File

@@ -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

View File

@@ -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.
@@ -117,11 +137,11 @@ GOWORK=off GOTOOLCHAIN=local go get \
go.maximumdirect.net/ckconfig@vCKCONFIG_VERSION
```
Run only the command for the integration being prepared. In the current release
sequence, ckprompt needs a ckconfig release containing `AdaptDiagnostics`;
ckconfig v0.4.0 does not contain it. Validate the integration with the workspace
disabled against its updated requirements, using the selected module's release
validation below. An aligned workspace can hide an outdated dependency.
Run only the command for the integration being prepared. ckprompt needs a
ckconfig release containing `AdaptDiagnostics`; v0.5.0 contains it, while v0.4.0
does not. Validate the integration with the workspace disabled against its
updated requirements, using the selected module's release validation below.
An aligned workspace can hide an outdated dependency.
Review and commit the resulting `go.mod` and `go.sum` changes with the release
candidate, and make its release note agree with the selected versions. The sum

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,253 +0,0 @@
# Documentation refresh
Status: implemented and verified. Release/dependency alignment remains separate
work in [future.md](future.md#pending-release-and-integration-work).
## Purpose
Refresh CommandKit's documentation for the next minor release. Operators should
be able to find a command, understand what it does, and interpret its results
without learning the implementation. Application developers should have a short
path from choosing a module to using its public API. Maintainer documentation
and exact API contracts can retain technical detail where it is useful.
This roadmap captures the accepted documentation review recommendations. The
[documentation policy](../policy/documentation.md) remains authoritative for
audience, canonical ownership, examples, and document lifecycle. No new policy,
documentation framework, or ADR is needed.
## Scope and boundaries
The sprint covers current READMEs, the CLI reference, consumer guidance,
maintained example instructions, generated help descriptions, package
introductions, and release notes being prepared for publication.
Preserve API and CLI behavior: command and flag names, defaults, format
selection, report fields, status semantics, availability, and ownership rules.
Source edits for help text and GoDoc are in scope; runtime refactoring and new
features are not. If review exposes a behavioral defect or an unresolved
contract, record it separately rather than silently changing behavior through
an editorial revision.
Use existing documentation owners. Move useful detail to its appropriate owner
or link to the existing explanation; do not duplicate contracts. Keep exact
ownership, callback lifetime, error identity, and concurrency rules in GoDoc.
Internal documentation and maintainer procedures need only changes justified by
clarity or affected navigation, not a wholesale simplification.
Preserve accepted ADR decisions, historical published release records, license
texts, and attribution. Apply release-note editing to unpublished candidates
after checking their publication status under the
[release policy](../policy/release.md). Dependency alignment and publication
remain separate work under the [release procedure](../release.md).
## Audience and writing approach
| Audience | Starting points | Approach |
| --- | --- | --- |
| Prospective users | Root and integration READMEs | Explain purpose, useful capabilities, and where to start. |
| CLI operators | CLI reference and generated help | Lead with tasks, commands, results, and relevant limitations. |
| Go application developers | Consumer guide, examples, package introductions | Show a small useful integration first; explain choices and responsibilities next. |
| API users and maintainers | Symbol GoDoc, internal docs, development and release guides | Retain precise technical detail needed for correct use and maintenance. |
Use short, direct sentences and familiar terms. Define necessary domain terms
where first used; avoid a separate glossary unless one proves useful. For
example, explain a configuration scope as a named configuration such as `app`
or `promptkit`.
Where meaning permits, prefer “load configuration” to “acquire configuration,”
“command output” to “substantive results,” and “files and other sources used to
load configuration” to “source inventory captured during acquisition.” These
are editorial examples, not a mechanical replacement list. Keep technical names
when referring to an API symbol or a distinction that matters to the reader.
## Work areas
### 1. Product and module introductions
Update the [root README](../../README.md), [ckconfig README](../../integrations/ckconfig/README.md),
and [ckprompt README](../../integrations/ckprompt/README.md).
- Explain what the library helps applications and operators do before describing
declarations, adapters, or reporting internals.
- Keep the module overview, explicit installation distinction, and navigation.
Link to the consumer guide for compatibility and integration details.
- Provide the shortest useful starting example or a direct link to it, rather
than a catalog of callback counts or implementation responsibilities.
An illustrative root opening is: “CommandKit helps Go applications build
consistent command-line interfaces. It provides typed flags and arguments,
help, output formatting, and error reporting. Optional integrations add commands
for inspecting configuration and working with PromptKit.” Final wording can
evolve during implementation.
### 2. CLI tasks and reference detail
Refresh the [CLI reference](../cli.md), particularly reporting, configuration,
prepared-request preview, requirements, and smoke testing.
- Begin each operation with its purpose and a small command example. Explain
results and important limitations before detailed report fields.
- Replace installation and callback introductions with an operator explanation
of command availability; link to the consumer guide for application wiring.
- Move API-only material such as callback construction, writer ownership, and
`Outcome.Presentation` access to its existing developer-facing owner where
appropriate. Retain observable output, failure, and status behavior here.
- Organize detailed report shapes and edge cases into reference subsections so
they remain available without interrupting the ordinary task.
- Make application-defined flags and optional convenience aliases identifiable;
examples must not imply every application installs them.
Retain distinctions operators need: omission versus an explicit empty value,
concise versus detailed reports, independent comparison sides, redaction,
partial evidence on failure, and which operations can contact providers.
Explain that requirements discovery, request validation, prepared preview, and
smoke testing answer different questions. Successful preparation does not
guarantee later execution, and a smoke command's successful status does not
mean every reported observation is positive.
### 3. Guided examples
Refresh the [core example](../../examples/core/README.md),
[ConfigKit example](../../integrations/ckconfig/examples/simple/README.md), and
[composed example](../../integrations/ckprompt/examples/composed/README.md).
- State the working directory, required Go version, and applicable workspace or
independent-module setup before commands.
- Start with a few useful commands and explain expected results beside them.
Group additional examples by task rather than presenting one long command block.
- Label intentional failures beside the relevant command, including how to
interpret the result. Distinguish application status from `go run`'s own exit
behavior when explaining shell results.
- Explain early that the composed example uses local fixtures and requires no
model service or credentials; do not imply this about every consuming app.
- Put alias wiring, shared state, and cache explanations after the walkthrough,
linking to API contracts rather than repeating them.
### 4. Consumer guide and public API orientation
Update the [consumer guide](../consumers/guide.md) and the package introductions
in [core](../../doc.go), [ckconfig](../../integrations/ckconfig/doc.go), and
[ckprompt](../../integrations/ckprompt/doc.go).
- Demonstrate `Reporter.Report` with a small value so the recommended path to
shared `--format` support is visible. Explain raw output as a separate choice.
- Make the initial greeting example's name, description, and output agree; it
currently prints the count rather than a greeting.
- Present module compatibility in a compact table that distinguishes published
releases from current source requirements. Keep exact version guidance in
this owner and link from module introductions.
- Lead package introductions with purpose and primary entry points. Keep exact
symbol contracts in GoDoc, including factory timing, supplied values, error
handling, and object ownership.
Recheck compatibility statements when implementing the refresh. Current source
and released dependencies can differ; the
[pending alignment work](future.md#pending-release-and-integration-work) owns
the outstanding release work. Do not present workspace success as proof that
the selected published dependencies support an independent build.
### 5. Generated help
Review command summaries, descriptions, flag usage, and argument usage across
core and both integrations. These strings are operator documentation even when
stored in Go source.
Use action-oriented descriptions and explain choices where useful. In
particular, the [smoke command](../../integrations/ckprompt/smoke_command.go)
should explain that it can run one test or all tests and identify accepted kinds;
“Run one PromptKit smoke test” and “Native smoke-test kind” are insufficient.
The [request flag descriptions](../../integrations/ckprompt/request_flags.go)
can use direct wording such as “Use this model.” Preserve flag names, grammar,
defaults, and supported operations.
### 6. Release notes
Review the [core v0.5.0 note](../releases/v0.5.0.md) and applicable unpublished
integration candidates for the upcoming releases. Explain user benefits before
implementation mechanisms: accepted deferred messages are no longer lost,
YAML values retain their intended types, and installation and help avoid repeated
work. Remove unnecessary discussion of scratch paths, chain suffixes, and batch
detachment from the release summary.
Retain compatibility guidance, required consumer actions, meaningful API names,
and license/attribution links. Verify candidate dependencies against the versions
actually selected for publication. Do not invent release dates or performance
guarantees, or revise published historical notes as part of this refresh.
## Suggested sprint sequence
1. Refresh README introductions and the CLI reference together to establish
vocabulary and the operator/developer split.
2. Update example walkthroughs and generated help to match that language.
3. Polish consumer guidance, package introductions, and candidate release notes.
4. Review the changed documentation together for consistency and validate the
affected examples, help, and navigation.
No unresolved product decision blocks implementation planning. Exact wording,
section placement, and illustrative examples are ordinary editorial choices.
## Validation and completion checklist
Use the existing [documentation checks](../development.md#documentation-validation)
and [testing policy](../policy/testing.md). Review prose and local links; run
affected maintained examples and inspect representative help when those change.
For Go example or help-source edits, run the relevant existing package tests.
Do not add wording snapshots, readability gates, or a new checker framework.
Record workspace and independent-module results separately, including any
dependency limitation. This work adds no release gates.
- [x] README openings explain purpose and give a clear starting path.
- [x] CLI sections explain operator tasks before reference detail; necessary
behavior, report fields, status rules, and limitations remain discoverable.
- [x] Example setup is explicit, commands have nearby explanations, and intended
failures are labeled.
- [x] Consumer guidance demonstrates formatted reporting, has a coherent initial
example, and distinguishes published compatibility from current source needs.
- [x] Package introductions and generated help use clear, accurate language;
precise API contracts remain intact.
- [x] Unpublished release notes explain benefits and consumer impact while
historical records and required notices remain preserved.
- [x] Changed commands, snippets, and help have appropriate verification; local
links and Markdown whitespace checks pass.
- [x] Final review confirms consistent terminology, canonical ownership, and no
unintended behavior or contract changes.
## Implementation and verification
Completed the six work areas above. READMEs and package introductions now lead
with purpose and starting points; the CLI reference separates operator tasks
from report detail and developer wiring. Examples are grouped by task with
setup, expected results, and intentional failures explained nearby. The consumer
guide demonstrates shared report formatting and distinguishes published modules
from current source. Help strings and unpublished candidate notes use the same
plain language. Exact symbol contracts and runtime behavior were preserved.
Verification used Go 1.27.0 and existing cached dependencies:
- The aligned workspace suites passed for core, ckconfig, and ckprompt, including
their maintained examples.
- Independent core and ckconfig suites passed with `GOWORK=off`.
- Independent ckprompt compilation confirmed the existing dependency limitation:
released ckconfig v0.4.0 lacks `AdaptDiagnostics`. No module requirements,
checksums, or workspace files were changed to hide it. Its README, example,
consumer guidance, and candidate note identify the pending alignment.
- All 48 `go run` invocations in the three example READMEs were exercised with
binaries built from the aligned workspace, from the documented directories.
Results matched the walkthroughs, including two failed-load examples, missing
request input, and comparison status 3. Shared help and completion behavior
also passed the existing suites; representative rendered help was inspected.
- Both new consumer-guide report snippets compiled in a temporary consumer using
local core. Six checks confirmed greeting counts, empty-list output, and YAML
and JSON formatting.
- Local Markdown links, including incoming links to renamed sections, and
whitespace were checked. Go source changes are limited to help strings,
package comments, and formatting; no new tests or validation framework were
added.
- The authoritative remote had neither the core v0.5.0 nor ckprompt v0.3.0 tag
when their candidate notes were revised. Published notes, accepted ADRs,
policies, and license/attribution records were left intact.
The updated ckprompt candidate still requires dependency guidance to be finalized
when release alignment occurs. This is release preparation, not unfinished
editorial work or evidence of an independently buildable candidate.

View File

@@ -1,7 +1,6 @@
# Future work
Status: pending release/dependency alignment, 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,9 @@ 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
[audit remediation retirement snapshot](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/aac0e11ae67c78a35745ed9a25688aa51c15e793/docs/roadmap).
@@ -28,25 +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
### Audit remediation release alignment
The audit remediation is complete in source. The released ckconfig v0.4.0 does
not include `AdaptDiagnostics`, which current ckprompt source uses. Select and
publish appropriate releases in dependency order, align module requirements and
checksums, and rerun independent module suites under the
[release procedure](../release.md). No publication is implied by source completion.
### ckprompt release
The PromptKit v0.20.0 adoption is complete. The
[ckprompt v0.3.0 candidate](../../integrations/ckprompt/docs/releases/v0.3.0.md)
remains unpublished and needs revalidation against the current source and aligned
dependencies. Publication and hosted delivery remain separate operations
under the [release procedure](../release.md).
The published ckprompt v0.2.0 retains its earlier dependencies; see the
[consumer guide](../consumers/guide.md#module-selection) for module selection.
## Optional verification
### Optional shell completion validation

View File

@@ -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)
}
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -12,8 +12,7 @@ does not install commands.
The module path is `go.maximumdirect.net/ckprompt`. See the
[consumer guide](https://forge.maximumdirect.net/base-layer/commandkit/src/branch/main/docs/consumers/guide.md#module-selection)
for compatible releases, Go requirements, and the current source's dependency
alignment requirement.
for compatible releases and Go requirements.
## Example

View File

@@ -1,8 +1,8 @@
# CommandKit ckprompt v0.3.0
# CommandKit ckprompt v0.5.0
This unpublished candidate adds PromptKit v0.20.0 support, including request
preview, namespace selection, and shared text, YAML, and JSON output. It uses
ConfigKit v1.1.0 and requires Go 1.27.0.
CommandKit ckprompt v0.5.0 adds PromptKit v0.20.0 support, including request
preview, namespace selection, and shared text, YAML, and JSON output. It requires
CommandKit core v0.5.0, ckconfig v0.5.0, ConfigKit v1.1.0, and Go 1.27.0.
## Operator features
@@ -20,22 +20,33 @@ let application commands offer the same request flags. `ckprompt.Declarations`
adds the standard commands with their shorter flag aliases. Literal and file
inputs keep their command-line order.
## Compatibility and preparation
## Correctness improvements
This candidate changes resource report shapes, input flags, formatting, and
configuration comparison behavior. Review the current
Aggregate smoke testing retains cancellation when it stops remaining tests,
including previously returned errors and reports. A prepared-request preview
fails if the provider returns no prepared request without an error. Parsed
provider-parameter objects are copied between independent request destinations,
so mutations in one callback do not change later requests. Configuration
inspection shares ckconfig's native diagnostic adapter.
## Compatibility and upgrade
This pre-1.0 minor release changes resource report shapes, input flags,
formatting, and configuration comparison behavior. Review the current
[CLI reference](../../../../docs/cli.md#promptkit-definition-inspection) and
[consumer guide](../../../../docs/consumers/guide.md#promptkit-configuration-definitions-and-requirements)
when updating declarations or code that reads reports. Applications continue to
own engine construction, credentials, and execution.
The current source requires `ckconfig.AdaptDiagnostics`, which is absent from
its selected ckconfig v0.4.0 dependency. Before publication, complete
[dependency alignment](../../../../docs/roadmap/future.md#pending-release-and-integration-work)
and update this candidate's dependency guidance to the selected releases.
Upgrade all three CommandKit modules to v0.5.0 together. This release follows
published ckprompt v0.2.0 and adopts the vanity imports
`go.maximumdirect.net/configkit` and `go.maximumdirect.net/promptkit`.
Replace earlier upstream import paths consistently; types from different
module identities cannot be mixed. Review callers of the former prompt
version-report API and use native prompt, profile, and backend inspections.
The [module selection table](../../../../docs/consumers/guide.md#module-selection)
distinguishes published combinations from the current workspace.
records the release dependencies.
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckprompt/v0.3.0/integrations/ckprompt/COPYRIGHT) |
[LICENSE](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckprompt/v0.3.0/integrations/ckprompt/LICENSE) |
[ATTRIBUTION](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckprompt/v0.3.0/integrations/ckprompt/docs/oss/attribution.md)
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckprompt/v0.5.0/integrations/ckprompt/COPYRIGHT) |
[LICENSE](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckprompt/v0.5.0/integrations/ckprompt/LICENSE) |
[ATTRIBUTION](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckprompt/v0.5.0/integrations/ckprompt/docs/oss/attribution.md)

View File

@@ -4,11 +4,10 @@ This example combines application and PromptKit configuration, resource
inspection, request checks, and smoke tests. It uses local fixtures and an
in-memory model client. **No model service or credentials are required.**
Use Go 1.27.0 on `PATH` and an aligned workspace containing all three local
modules. Follow the [workspace setup](../../../../docs/development.md#documentation-validation)
from the repository root first. Current ckprompt source needs a helper in local
ckconfig source that its released dependency does not yet contain; independent
builds remain pending [release alignment](../../../../docs/roadmap/future.md#pending-release-and-integration-work).
Use Go 1.27.0 on `PATH`. An aligned workspace containing all three local modules
exercises their current sources; follow the [workspace setup](../../../../docs/development.md#documentation-validation)
from the repository root. The example also builds independently with
`GOWORK=off` against its module's released dependencies.
The [consumer guide](../../../../docs/consumers/guide.md#module-selection) owns
version and compatibility details.
@@ -168,5 +167,5 @@ for suite composition, builders, and ownership, and the [source](main.go) for
the complete application.
Run `GOWORK=auto GOTOOLCHAIN=local go test ./...` from this example directory
for an aligned workspace check. Once dependency alignment is complete, also use
for an aligned workspace check. Use
`GOWORK=off GOTOOLCHAIN=local go test ./...` to check the declared releases.

View File

@@ -3,8 +3,8 @@ module go.maximumdirect.net/ckprompt
go 1.27.0
require (
go.maximumdirect.net/ckconfig v0.4.0
go.maximumdirect.net/commandkit v0.4.0
go.maximumdirect.net/ckconfig v0.5.0
go.maximumdirect.net/commandkit v0.5.0
go.maximumdirect.net/configkit v1.1.0
go.maximumdirect.net/promptkit v0.20.0
go.yaml.in/yaml/v4 v4.0.0-rc.6

View File

@@ -12,8 +12,12 @@ github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.maximumdirect.net/ckconfig v0.4.0 h1:7MpI/jIy+3BZrVME5rhKQibh5U9IA3s97BUJ2EOG41s=
go.maximumdirect.net/ckconfig v0.4.0/go.mod h1:Mk6QOfHrFy00uE0/gw9h6/Y9LcdTShVEzM/oI/HGDns=
go.maximumdirect.net/ckconfig v0.5.0 h1:5YCPFySTFIFR+gzd0FjqkTPEVB7zkGa8kOEYh4aBvWc=
go.maximumdirect.net/ckconfig v0.5.0/go.mod h1:BONXRSQlHdJvYVw9mN6lMV4G4ldlV/5fRO9EEApA99s=
go.maximumdirect.net/commandkit v0.4.0 h1:bWqflDWDn98FUCgh+0e+w6LGmvWIkaeJK519H7PjD2Y=
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/configkit v1.1.0 h1:UzXN2ZMnEkcL0O4YSLqD9qd9M1gb/NWCHQR/H8AaQzA=
go.maximumdirect.net/configkit v1.1.0/go.mod h1:s1zLJ9GGB/1Q4a+aOHMLISonJxWFgNWpA6SpPlOafeA=
go.maximumdirect.net/promptkit v0.20.0 h1:ZpwbjiCZWXdnnZ26RuWKoOco0jFnaAX8m2tQAX7ZlnM=

View File

@@ -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)

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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")
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)

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)
}
}