17 Commits

Author SHA1 Message Date
1ca660e35f Prepare ckprompt v0.6.0 with released dependencies 2026-11-08 15:23:50 +00:00
5ed947bd26 Prepare ckconfig v0.6.0 with released core 2026-11-08 15:22:33 +00:00
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
36 changed files with 682 additions and 396 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -14,28 +14,27 @@ applications.
| `go.maximumdirect.net/ckconfig` | You want operators to view, validate, explain, and compare ConfigKit configuration. | | `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. | | `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 | | Selection | Core | ckconfig | ckprompt | Upstream libraries and status |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| Published core | v0.5.0 | — | — | Usable independently. | | Published core | v0.6.0 | — | — | Usable independently. |
| Published ConfigKit integration | v0.4.0 | v0.4.0 | — | ConfigKit v1.1.0. | | ConfigKit integration | v0.6.0 | v0.6.0 | — | ConfigKit v1.1.0. |
| PromptKit integration | v0.6.0 | v0.6.0 | v0.6.0 | ConfigKit v1.1.0 and PromptKit v0.20.0. |
| Earlier matched integration set | v0.3.0 | v0.2.0 | v0.2.0 | ConfigKit v0.8.0 and PromptKit v0.16.0, using their earlier import paths. | | Earlier matched integration set | v0.3.0 | v0.2.0 | v0.2.0 | ConfigKit v0.8.0 and PromptKit v0.16.0, using their earlier import paths. |
| 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: Install the published core with:
```sh ```sh
go get go.maximumdirect.net/commandkit@v0.5.0 go get go.maximumdirect.net/commandkit@v0.6.0
``` ```
The integration releases being prepared are ckconfig v0.5.0 and ckprompt v0.5.0. Current Select either integration with `go get go.maximumdirect.net/ckconfig@v0.6.0`
ckprompt source calls `ckconfig.AdaptDiagnostics`, which is absent from its or `go get go.maximumdirect.net/ckprompt@v0.6.0`. The integrations select core
selected released ckconfig v0.4.0 dependency. It therefore needs local v0.6.0, and ckprompt selects ckconfig v0.6.0, which includes `AdaptDiagnostics`.
ckconfig source in the workspace until [release They build independently against these released dependencies; a local workspace
alignment](../roadmap/future.md#pending-release-and-integration-work) is is optional for development. Published ckconfig v0.3.0 also uses ConfigKit v1.1.0
complete. Published ckconfig v0.3.0 also uses ConfigKit v1.1.0 and does not and does not compose with ckprompt v0.2.0's earlier ConfigKit dependency.
compose with ckprompt v0.2.0's earlier ConfigKit dependency.
Importing a module does not install its commands. Pass commands to Importing a module does not install its commands. Pass commands to
`commandkit.New` or `App.Register` explicitly. ckprompt depends on ckconfig's `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 return `ErrReporterClosed`. The [CLI reference](../cli.md#reporting) owns flag
grammar, visibility, prefixes, and status behavior. grammar, visibility, prefixes, and status behavior.
An application can attach a boolean input to the shared `--debug` option and
inspect the reporting selection separately. For example:
```go
type input struct { debug commandkit.Supplied[bool] }
command := commandkit.NewCommand(commandkit.CommandSpec[input]{
Name: "run",
Flags: []commandkit.Flag[input]{
commandkit.BoolFlag(commandkit.FlagMeta{Name: "debug", Short: "d"}, false,
func(in *input, value commandkit.Supplied[bool]) { in.debug = value }),
},
Run: func(inv commandkit.Invocation, in input) commandkit.Outcome {
captureArtifacts := in.debug.Value
quiet := inv.ReportingMode() == commandkit.ReportingModeQuiet
_, _ = captureArtifacts, quiet // Apply application policy here.
return commandkit.Success()
},
})
_ = command
```
With `run --debug --quiet`, the input is true and explicitly supplied, while
the selected reporting mode is quiet. Application defaults and requiredness
affect input, not mode selection; explicit false counts as supplied. Captured
bindings retain the usual selection and application behavior. Applications own
any artifact creation and other effects. The mode reports selection rather than
successful delivery; see [Invocation.ReportingMode](../../identity.go).
Reports use JSON serialization to establish the common value representation Reports use JSON serialization to establish the common value representation
for YAML and JSON. A text template receives the original value; without one, for YAML and JSON. A text template receives the original value; without one,
text uses readable scalar or string-list output and YAML for structured values. text uses readable scalar or string-list output and YAML for structured values.

View File

@@ -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 | | 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 | | 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) | | 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 | | 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) | | 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) | | 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 current exported contracts; the CLI reference owns operator behavior. The
[maintainer release procedure](release.md) owns current release operations. [maintainer release procedure](release.md) owns current release operations.
Completed roadmaps and implementation evidence remain available in Git history, Completed roadmaps and implementation evidence remain available in Git history,
including [audit remediation and its declaration-path follow-up](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/aac0e11ae67c78a35745ed9a25688aa51c15e793/docs/roadmap), including [shared debug binding and reporting-mode implementation](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/56e3b013f9f83c9c9494f11602e802ce5ed8814e/docs/roadmap),
[audit remediation and its declaration-path follow-up](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/aac0e11ae67c78a35745ed9a25688aa51c15e793/docs/roadmap),
[shared reporting and PromptKit v0.20.0 implementation](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/2f43f78f76439f5d11ef313cd379bec036543504/docs/roadmap), [shared reporting and PromptKit v0.20.0 implementation](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/2f43f78f76439f5d11ef313cd379bec036543504/docs/roadmap),
[PromptKit v0.18.0 adoption and implementation](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/540e2359d66d604a563df0be1c9fc961588f5b9c/docs/roadmap), [PromptKit v0.18.0 adoption and implementation](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/540e2359d66d604a563df0be1c9fc961588f5b9c/docs/roadmap),
[ConfigKit adoption, reporting, release-tooling, and remediation records](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/260256d722f52bc947103d078cb67a129f4cc9f1/docs/roadmap), [ConfigKit adoption, reporting, release-tooling, and remediation records](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/260256d722f52bc947103d078cb67a129f4cc9f1/docs/roadmap),
@@ -117,11 +118,10 @@ go work init . ./integrations/ckconfig ./integrations/ckprompt
If a workspace already exists, ensure it includes these three modules. Preserve If a workspace already exists, ensure it includes these three modules. Preserve
the ignored `go.work` and `go.work.sum`; do not commit them. Use the ignored `go.work` and `go.work.sum`; do not commit them. Use
`GOWORK=off GOTOOLCHAIN=local` for dependency preparation `GOWORK=off GOTOOLCHAIN=local` for dependency preparation
and independent module suites. The integrations select released core v0.4.0; and independent module suites. Both integrations select released core v0.6.0;
ckprompt also selects released ckconfig v0.4.0. ckprompt also selects released ckconfig v0.6.0. Their declared dependencies
Current ckprompt source requires a ckconfig helper absent from that release, so support independent builds. The aligned workspace exercises the local sources
its independent suite remains pending release and dependency alignment; the together.
aligned workspace exercises the current source together.
Run `GOWORK=off GOTOOLCHAIN=local go test ./...` separately from each module Run `GOWORK=off GOTOOLCHAIN=local go test ./...` separately from each module
directory. From the repository root, a workspace containing all three modules can directory. From the repository root, a workspace containing all three modules can
also run: 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 applies to command execution, help, and version; routing and parsing failures
remain visible in every mode. remain visible in every mode.
An effective flag view combines a shared application debug binding with the
core declaration for parsing, help, and completion, while preserving the
application binding's position. The same occurrence stream selects reporting
mode independently of the prepared application value. The invocation's mode
accessor reads the reporter's selection under its existing state mutex.
The reporter owns invocation-scoped visibility, message formatting, managed The reporter owns invocation-scoped visibility, message formatting, managed
report rendering, delivery observations, and finalization. It presents queued report rendering, delivery observations, and finalization. It presents queued
messages after the handler returns, then a failed outcome's explanation, and messages after the handler returns, then a failed outcome's explanation, and

View File

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

View File

@@ -9,11 +9,28 @@ Gitea release page; there are no binaries,
checksums, or other delivery assets. CI, module proxy availability, and consumer checksums, or other delivery assets. CI, module proxy availability, and consumer
installation are not completion gates. installation are not completion gates.
Publish in dependency order: core, then `ckconfig`, then `ckprompt`. The Plan the initiating release and all downstream integration releases required by
integration modules require the preceding CommandKit releases, so prepare and the [dependency-alignment policy](policy/release.md#commandkit-downstream-dependency-alignment).
publish each module only after its CommandKit dependencies are available. A root That policy sets the minimum version bump for affected integrations; select
release runs only the root module's default suite; it need not run suites for each module's version independently according to its compatibility impact.
independently released integrations.
| Initiating release | Required downstream releases, in order |
| --- | --- |
| Core | `ckconfig`, then `ckprompt` |
| `ckconfig` | `ckprompt` |
| `ckprompt` | None with the current dependency graph |
As integrations are added, follow their actual dependencies to include every
affected downstream integration. Publish dependencies before their consumers.
Prepare each integration after its updated CommandKit dependencies are published,
incorporating all of those versions in one candidate. For a core update, release
`ckprompt` once after both core and `ckconfig`; it does not need separate releases
for each dependency bump. Unaffected dependencies do not need new releases.
Track any remaining downstream releases as pending coordination work. Each
successful source-tag push still completes that module's release; a downstream
failure does not undo it or authorize replacing its tag. A root release runs
only the root module's default suite; each downstream release runs its own suite.
Use branch `main`, remote `origin`, and expected effective push endpoint Use branch `main`, remote `origin`, and expected effective push endpoint
`ssh://git@forge.maximumdirect.net:22222/base-layer/commandkit.git`. The selected `ssh://git@forge.maximumdirect.net:22222/base-layer/commandkit.git`. The selected
@@ -98,14 +115,17 @@ standard offline suite and ReleaseKit’s candidate/note and publication guards.
## Prepare and publish one candidate ## Prepare and publish one candidate
Prepare dependencies in the selected module before the candidate is finalized. Prepare dependencies in the selected module before the candidate is finalized.
Do this after the preceding CommandKit module has been published and before the Do this after any CommandKit dependency releases required by the coordinated
selected candidate suite is run. For `core`, run the command at the repository update have been published and before the selected candidate suite is run.
root; for integrations, run it from that module directory. For `core`, run the command at the repository root; for integrations, run it
from that module directory.
Before downloading dependencies, update each integration to the newly published Before downloading dependencies, update each integration to the newly published
CommandKit versions. `go mod download` downloads the versions already selected; CommandKit versions. `go mod download` downloads the versions already selected;
it does not upgrade requirements. Replace the version placeholders below with it does not upgrade requirements. Replace the version placeholders below with
the actual published versions, which may differ between modules: the actual published versions, which may differ between modules. When a
dependency is not being released in this update, use its current selected
release; a `ckconfig`-initiated update does not require a new core release:
```sh ```sh
# After publishing core, prepare ckconfig from its module directory. # After publishing core, prepare ckconfig from its module directory.
@@ -117,11 +137,11 @@ GOWORK=off GOTOOLCHAIN=local go get \
go.maximumdirect.net/ckconfig@vCKCONFIG_VERSION go.maximumdirect.net/ckconfig@vCKCONFIG_VERSION
``` ```
Run only the command for the integration being prepared. In the current release Run only the command for the integration being prepared. ckprompt needs a
sequence, ckprompt needs a ckconfig release containing `AdaptDiagnostics`; ckconfig release containing `AdaptDiagnostics`; v0.5.0 contains it, while v0.4.0
ckconfig v0.4.0 does not contain it. Validate the integration with the workspace does not. Validate the integration with the workspace disabled against its
disabled against its updated requirements, using the selected module's release updated requirements, using the selected module's release validation below.
validation below. An aligned workspace can hide an outdated dependency. An aligned workspace can hide an outdated dependency.
Review and commit the resulting `go.mod` and `go.sum` changes with the release 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 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 # Future work
Status: pending release/dependency alignment, optional shell validation, and Status: optional shell validation and future capability proposals.
future capability proposals.
No delivery dates are selected here. No delivery dates are selected here.
## Purpose and authority ## Purpose and authority
@@ -9,6 +8,9 @@ No delivery dates are selected here.
Earlier completed roadmaps, implementation plans, and review findings have been retired. Earlier completed roadmaps, implementation plans, and review findings have been retired.
This document preserves unfinished work and unselected ideas under the This document preserves unfinished work and unselected ideas under the
[release policy](../policy/release.md#release-documentation-and-preparation). [release policy](../policy/release.md#release-documentation-and-preparation).
The completed shared debug binding and reporting-mode implementation is recorded
in its [retirement snapshot](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/56e3b013f9f83c9c9494f11602e802ce5ed8814e/docs/roadmap).
The completed [documentation refresh](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/5c960532552037d38119941389e558f78bf73aad/docs/roadmap/documentation-refresh.md) remains available in Git history.
The completed audits, consolidated remediation, implementation plan, and The completed audits, consolidated remediation, implementation plan, and
declaration-path follow-up remain available in the declaration-path follow-up remain available in the
[audit remediation retirement snapshot](https://forge.maximumdirect.net/base-layer/commandkit/src/commit/aac0e11ae67c78a35745ed9a25688aa51c15e793/docs/roadmap). [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 and [consumer guide](../consumers/guide.md) own implemented behavior. Retaining
an idea here does not select its syntax, API, priority, or delivery date. an idea here does not select its syntax, API, priority, or delivery date.
## Pending release and integration work ## Optional verification
### 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 shell completion validation ### Optional shell completion validation

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,8 +12,7 @@ does not install commands.
The module path is `go.maximumdirect.net/ckprompt`. See the 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) [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 for compatible releases and Go requirements.
alignment requirement.
## Example ## 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 CommandKit ckprompt v0.5.0 adds PromptKit v0.20.0 support, including request
preview, namespace selection, and shared text, YAML, and JSON output. It uses preview, namespace selection, and shared text, YAML, and JSON output. It requires
ConfigKit v1.1.0 and requires Go 1.27.0. CommandKit core v0.5.0, ckconfig v0.5.0, ConfigKit v1.1.0, and Go 1.27.0.
## Operator features ## 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 adds the standard commands with their shorter flag aliases. Literal and file
inputs keep their command-line order. inputs keep their command-line order.
## Compatibility and preparation ## Correctness improvements
This candidate changes resource report shapes, input flags, formatting, and Aggregate smoke testing retains cancellation when it stops remaining tests,
configuration comparison behavior. Review the current 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 [CLI reference](../../../../docs/cli.md#promptkit-definition-inspection) and
[consumer guide](../../../../docs/consumers/guide.md#promptkit-configuration-definitions-and-requirements) [consumer guide](../../../../docs/consumers/guide.md#promptkit-configuration-definitions-and-requirements)
when updating declarations or code that reads reports. Applications continue to when updating declarations or code that reads reports. Applications continue to
own engine construction, credentials, and execution. own engine construction, credentials, and execution.
The current source requires `ckconfig.AdaptDiagnostics`, which is absent from Upgrade all three CommandKit modules to v0.5.0 together. This release follows
its selected ckconfig v0.4.0 dependency. Before publication, complete published ckprompt v0.2.0 and adopts the vanity imports
[dependency alignment](../../../../docs/roadmap/future.md#pending-release-and-integration-work) `go.maximumdirect.net/configkit` and `go.maximumdirect.net/promptkit`.
and update this candidate's dependency guidance to the selected releases. 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) 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) | [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.3.0/integrations/ckprompt/LICENSE) | [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.3.0/integrations/ckprompt/docs/oss/attribution.md) [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckprompt/v0.5.0/integrations/ckprompt/docs/oss/attribution.md)

View File

@@ -0,0 +1,16 @@
# CommandKit ckprompt v0.6.0
CommandKit ckprompt v0.6.0 selects CommandKit core and ckconfig v0.6.0.
Core adds shared debug input bindings and access to the selected reporting mode.
ConfigKit remains at v1.1.0 and PromptKit at v0.20.0.
## Compatibility and upgrade
Select `go.maximumdirect.net/ckprompt v0.6.0` with CommandKit core and ckconfig
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/ckprompt/v0.6.0/integrations/ckprompt/COPYRIGHT) |
[LICENSE](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckprompt/v0.6.0/integrations/ckprompt/LICENSE) |
[ATTRIBUTION](https://forge.maximumdirect.net/base-layer/commandkit/src/tag/integrations/ckprompt/v0.6.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 inspection, request checks, and smoke tests. It uses local fixtures and an
in-memory model client. **No model service or credentials are required.** 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 Use Go 1.27.0 on `PATH`. An aligned workspace containing all three local modules
modules. Follow the [workspace setup](../../../../docs/development.md#documentation-validation) exercises their current sources; follow the [workspace setup](../../../../docs/development.md#documentation-validation)
from the repository root first. Current ckprompt source needs a helper in local from the repository root. The example also builds independently with
ckconfig source that its released dependency does not yet contain; independent `GOWORK=off` against its module's released dependencies.
builds remain pending [release alignment](../../../../docs/roadmap/future.md#pending-release-and-integration-work).
The [consumer guide](../../../../docs/consumers/guide.md#module-selection) owns The [consumer guide](../../../../docs/consumers/guide.md#module-selection) owns
version and compatibility details. version and compatibility details.
@@ -168,5 +167,5 @@ for suite composition, builders, and ownership, and the [source](main.go) for
the complete application. the complete application.
Run `GOWORK=auto GOTOOLCHAIN=local go test ./...` from this example directory 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. `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 go 1.27.0
require ( require (
go.maximumdirect.net/ckconfig v0.4.0 go.maximumdirect.net/ckconfig v0.6.0
go.maximumdirect.net/commandkit v0.4.0 go.maximumdirect.net/commandkit v0.6.0
go.maximumdirect.net/configkit v1.1.0 go.maximumdirect.net/configkit v1.1.0
go.maximumdirect.net/promptkit v0.20.0 go.maximumdirect.net/promptkit v0.20.0
go.yaml.in/yaml/v4 v4.0.0-rc.6 go.yaml.in/yaml/v4 v4.0.0-rc.6

View File

@@ -12,8 +12,16 @@ github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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 h1:7MpI/jIy+3BZrVME5rhKQibh5U9IA3s97BUJ2EOG41s=
go.maximumdirect.net/ckconfig v0.4.0/go.mod h1:Mk6QOfHrFy00uE0/gw9h6/Y9LcdTShVEzM/oI/HGDns= 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/ckconfig v0.6.0 h1:N7MJKJiXhv+tT4HA7dnINUxC0Uq8hzYmBTFjrpvkbvQ=
go.maximumdirect.net/ckconfig v0.6.0/go.mod h1:69HOkR2Jbq7rJz3jlFAL6Oo5qorEiVrxQVLXoqav98s=
go.maximumdirect.net/commandkit v0.4.0 h1:bWqflDWDn98FUCgh+0e+w6LGmvWIkaeJK519H7PjD2Y= 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.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 h1:UzXN2ZMnEkcL0O4YSLqD9qd9M1gb/NWCHQR/H8AaQzA=
go.maximumdirect.net/configkit v1.1.0/go.mod h1:s1zLJ9GGB/1Q4a+aOHMLISonJxWFgNWpA6SpPlOafeA= go.maximumdirect.net/configkit v1.1.0/go.mod h1:s1zLJ9GGB/1Q4a+aOHMLISonJxWFgNWpA6SpPlOafeA=
go.maximumdirect.net/promptkit v0.20.0 h1:ZpwbjiCZWXdnnZ26RuWKoOco0jFnaAX8m2tQAX7ZlnM= 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...) remaining := append(commandpath.CloneTokens(unconsumed), tail...)
localFlags := make([]commandmeta.Flag, 0, len(selectedNode.Flags)+len(selectedNode.CoreFlags)) localFlags := commandmeta.EffectiveFlags(selectedNode.Flags, selectedNode.CoreFlags)
localFlags = append(localFlags, selectedNode.Flags...)
localFlags = append(localFlags, selectedNode.CoreFlags...)
aliases := session.flagAliases aliases := session.flagAliases
if session.flagAliasesForPath != nil { if session.flagAliasesForPath != nil {
aliases = session.flagAliasesForPath(result.Path) aliases = session.flagAliasesForPath(result.Path)
@@ -254,7 +252,7 @@ func displayCommandPath(rootName string, path []string) string {
return strings.Join(parts, " ") return strings.Join(parts, " ")
} }
// UsageLine returns Cobra's usage line for an exact path on this session. // UsageLine returns usage for an exact path on this session.
func (session *Session) UsageLine(path []string) (string, *commandmeta.Failure) { func (session *Session) UsageLine(path []string) (string, *commandmeta.Failure) {
if session == nil || session.root == nil { if session == nil || session.root == nil {
return "", &commandmeta.Failure{Message: "command tree is nil"} return "", &commandmeta.Failure{Message: "command tree is nil"}
@@ -269,6 +267,22 @@ func (session *Session) UsageLine(path []string) (string, *commandmeta.Failure)
if selected.HasSubCommands() { if selected.HasSubCommands() {
line += " [command]" line += " [command]"
} }
for _, flag := range declaration.Flags {
if flag.Name != "debug" || !flag.Boolean {
continue
}
for _, effective := range commandmeta.EffectiveFlags(declaration.Flags, declaration.CoreFlags) {
if effective.Name == "debug" {
spelling := "--debug"
if effective.Short != "" {
spelling = "-" + effective.Short + ", " + spelling
}
line += "\n " + spelling + " " + flagUsage(effective)
break
}
}
break
}
return line, nil return line, nil
} }
@@ -316,7 +330,7 @@ func buildTree(root *commandmeta.Command) (*spfcobra.Command, map[*spfcobra.Comm
helpFlag := backend.Flags().Lookup("help") helpFlag := backend.Flags().Lookup("help")
helpFlag.NoOptDefVal = "true" helpFlag.NoOptDefVal = "true"
helpFlag.Usage = "help for this command (default: false)" helpFlag.Usage = "help for this command (default: false)"
for _, declarationFlag := range append(append([]commandmeta.Flag(nil), declaration.Flags...), declaration.CoreFlags...) { for _, declarationFlag := range commandmeta.EffectiveFlags(declaration.Flags, declaration.CoreFlags) {
value := &rawValue{boolean: declarationFlag.Boolean} value := &rawValue{boolean: declarationFlag.Boolean}
backend.Flags().VarP(value, declarationFlag.Name, declarationFlag.Short, flagUsage(declarationFlag)) backend.Flags().VarP(value, declarationFlag.Name, declarationFlag.Short, flagUsage(declarationFlag))
backendFlag := backend.Flags().Lookup(declarationFlag.Name) backendFlag := backend.Flags().Lookup(declarationFlag.Name)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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