290 lines
17 KiB
Markdown
290 lines
17 KiB
Markdown
# Shell completion implementation plan
|
||
|
||
Status: Stages 1–4 complete. Stage 5 is next.
|
||
|
||
Implement the [shell completion roadmap](completion.md) from baseline
|
||
`2301872`. Follow the stages in order in one sprint. Intermediate stages may
|
||
leave unfinished wiring or a noncompiling tree; validate the assembled result
|
||
before calling the sprint complete. Use the existing ownership boundaries in
|
||
[ADR 0013](../adr/0013-application-alias-routing.md),
|
||
[ADR 0014](../adr/0014-invocation-reporting.md), and the
|
||
[architecture policy](../policy/architecture.md). Apply the
|
||
[testing policy](../policy/testing.md) to integration assertions. Keep protocol
|
||
names, candidate ordering, and backend mechanics private.
|
||
|
||
## Stage 1: Prove the shell transport
|
||
|
||
Status: complete.
|
||
|
||
1. Use the Cobra version pinned in the root module. In a disposable prototype,
|
||
generate scripts with `GenBashCompletionV2(w, true)`,
|
||
`GenZshCompletion(w)`, and `GenFishCompletion(w, true)`. Give the generator
|
||
a minimal root whose name is the installed executable basename. Do not
|
||
install Cobra's completion command or execute its completion parser.
|
||
2. Supply a fixture executable that handles the generated scripts' hidden
|
||
request (`__complete` with these generator settings). Verify the actual
|
||
arguments and replies using each script in its shell. The request carries
|
||
the words through the cursor, including the partial word as its last
|
||
argument; preserve an explicitly empty last argument. Check what each
|
||
script does with the prefix of an attached value before deciding how to
|
||
encode its replacement. Cover `--config=./par`, separated path values,
|
||
spaces, an empty no-file reply, directory-only completion, and a partial
|
||
short cluster. Verify the edited line, not only the printed candidate list.
|
||
3. Use Cobra's line protocol: candidate lines with optional tab-separated
|
||
descriptions, followed by the colon-prefixed directive integer and newline.
|
||
Use the adapter's Cobra directive constants, not hardcoded numbers in core.
|
||
Map command/flag/static choices and no candidates to NoFileComp, file hints
|
||
to native file completion, and directory hints to FilterDirs. If a generated
|
||
script cannot express the selected behavior as emitted, make the smallest
|
||
adapter-owned script adjustment that preserves it and add a shell regression.
|
||
In particular, verify attached-value prefix reconstruction and Fish's
|
||
filesystem handling. Do not narrow the supported contexts to fit a script.
|
||
4. Record the observed argument and replacement conventions in the sprint
|
||
evidence. Carry forward only the adapter and fixtures needed for the final
|
||
implementation; do not commit disposable prototype machinery.
|
||
|
||
Completion: generated scripts using Cobra v1.10.1 and exercised them through
|
||
interactive Bash 5.3.9, Zsh 5.9, and Fish 4.2.1 with a fixture that records the
|
||
query and the executed edited line. In the tested attached-value cases the
|
||
query includes the entered flag and value; a static candidate contains the value
|
||
alone, and the shell script preserves the flag prefix. Explicit empty words,
|
||
quoted prior arguments, short clusters, file and directory hints, and no-file
|
||
replies were exercised. Zsh's generated word splitting lost the attached flag
|
||
context when a path contained an escaped space. Fish split quoted prior words,
|
||
passed escapes in current words, and treated directory-only replies as ordinary
|
||
file completion. The adapter adjusts those scripts and preserves the first `=`
|
||
as the flag/value separator. The opt-in `TestCompletionShells` regression checks
|
||
actual edited lines for these cases. Run it with Bash, Zsh, Fish, Python pexpect,
|
||
and Bash completion installed using `COMMANDKIT_COMPLETION_SHELLS=1
|
||
GOWORK=off GOTOOLCHAIN=local go test -count=1 -run '^TestCompletionShells$'
|
||
./internal/cobra`. Zsh and Fish were extracted into a disposable directory for
|
||
this run; the focused adapter suite and the full interactive matrix pass.
|
||
|
||
## Stage 2: Capture completion hints
|
||
|
||
Status: complete.
|
||
|
||
1. Add the following public API in the root package. Add
|
||
`Completion CompletionHint` to both `FlagMeta` and `ArgumentMeta`;
|
||
existing binding constructors and their signatures stay in place.
|
||
|
||
```go
|
||
type CompletionChoice struct {
|
||
Value string
|
||
Description string
|
||
}
|
||
|
||
type CompletionHint struct { /* private representation */ }
|
||
|
||
func CompleteChoices(choices ...CompletionChoice) CompletionHint
|
||
func CompleteFiles() CompletionHint
|
||
func CompleteDirectories() CompletionHint
|
||
func NoCompletion() CompletionHint
|
||
```
|
||
|
||
2. Represent the four strategies as a private discriminator and choice list.
|
||
The zero hint and `NoCompletion()` are equivalent. Copy choice slices
|
||
when constructing the hint. Capture the hint through ordinary bindings,
|
||
repeatable bindings, and `CaptureFlags`; bridge it through
|
||
`bridgeFlags` and `bridgeArguments` to neutral metadata. Use neutral
|
||
strategy and choice types in `internal/commandmeta`; backend types do not
|
||
cross the adapter boundary.
|
||
3. Keep hints independent of binding identity, requiredness, defaults,
|
||
repetition, and converters. Static choices are literal prefix matches and
|
||
do not constrain accepted values. Empty choices mean no suggestions, not
|
||
file fallback. Use ordinary boolean literals `true` and `false` for
|
||
the built-in boolean assignment case; an application boolean binding
|
||
without a hint has no value suggestions.
|
||
4. Verify caller mutation of the original choice slice cannot change captured
|
||
suggestions, repeated/captured bindings keep hints, and values absent from
|
||
a suggestion list still reach the existing converter.
|
||
|
||
Completion: added the public choice and hint constructors, carried hints through
|
||
flag and operand bindings into neutral metadata, and supplied explicit boolean
|
||
choices for built-in help and reporting flags. Focused tests cover copied
|
||
choices, repeated and captured bindings, the empty-choice strategy, independent
|
||
bridge copies, and execution of values outside the suggested choices. Root,
|
||
internal adapter, and neutral metadata package tests pass with `GOWORK=off` and
|
||
`GOTOOLCHAIN=local`.
|
||
|
||
## Stage 3: Install completion as an ordinary declaration
|
||
|
||
Status: complete.
|
||
|
||
1. Add `func Completion(executable string) Command`. The parameter is the
|
||
executable basename the operator types, independent of the human-readable
|
||
`Identity.Name`. Use a root-only `completion` group with three runnable
|
||
children, `bash`, `zsh`, and `fish`. Use the existing declaration and
|
||
registration API (`New(..., Completion("app"))` or
|
||
`app.Register(Completion("app"))`). Keep script-specific name handling
|
||
inside completion; do not change validation of ordinary application identity.
|
||
2. Carry a private completion-installation marker with the declaration and
|
||
derive query availability from the committed routing snapshot. Use the
|
||
existing registration candidate to validate collisions in either order,
|
||
including aliases and same-batch declarations. Reserve the query's first
|
||
token only while installed: a canonical command or alias beginning with
|
||
that token would be shadowed by raw query dispatch. Reuse existing
|
||
command/alias collision rules for `completion`. Do not represent the
|
||
query as a canonical node, alias target, or public help entry.
|
||
3. Generate scripts in the internal adapter using the transport established
|
||
in Stage 1. Surface generator errors through ordinary presentation.
|
||
Use `Reporter.RenderOutput` for script generation and delivery, so
|
||
`app completion bash --quiet` follows normal reporting. Document the
|
||
basename expected by the generated scripts; handle unsupported generator
|
||
names at the completion boundary.
|
||
4. Verify opt-in behavior, root-only placement, help visibility, batch
|
||
rollback, late installation, and absence of new reserved names for an app
|
||
that does not install completion.
|
||
|
||
Completion: added the root-only completion declaration and three script
|
||
commands, with generator output delivered through the invocation reporter.
|
||
Registration validates hidden-query collisions in either order, and the routing
|
||
snapshot reflects committed installation. Tests cover help, quiet output,
|
||
delivery failure, invalid basenames, opt-in behavior, collisions, rollback,
|
||
nesting, and late installation. Fish variable names are adjusted to support
|
||
dotted executable basenames; interactive checks of those names pass in Bash,
|
||
Zsh, and Fish. Root and adapter package tests pass with `GOWORK=off` and
|
||
`GOTOOLCHAIN=local`.
|
||
|
||
## Stage 4: Resolve the entered command context
|
||
|
||
Status: complete.
|
||
|
||
1. Add a private core completion request that separates completed words from
|
||
the current word. Call `resolveEnteredRoute` on completed words only;
|
||
use the snapshot's canonical metadata, resolved flag-alias context, and
|
||
discovery context. Keep entered words unchanged. For canonical routes
|
||
with a runnable operand owner, use the existing canonical prefix length
|
||
to distinguish its trailing operands/options from command words.
|
||
2. Suggest canonical child names matching the partial word while the input
|
||
is still in command-path position. At a node with both children and
|
||
operands, matching children take precedence; if there are no matching
|
||
children, use the operand context when available. Check insertion against
|
||
normal routing: resolve completed entered words plus the candidate and
|
||
retain it only if it reaches the intended canonical child. Do not treat a
|
||
partial alias as already entered. After a flag or operand has ended the
|
||
command path, do not suggest children.
|
||
3. An alias-only discovery prefix provides no command continuations, but can
|
||
supply applicable canonical flags using discovery metadata and alias
|
||
mappings. Unknown routes with no valid context yield no candidates.
|
||
At root, use the installed visible commands, including built-ins and
|
||
integrations. Exclude the protocol endpoint everywhere.
|
||
4. Add context tests for root/group/runnable paths, a partial canonical
|
||
child, a complete alias and alias chain, an alias-only prefix, a canonical
|
||
child shadowed in the entered route, and flags beginning before the
|
||
complete command path. Use existing routing rules as the oracle.
|
||
|
||
Completion: added private request context resolution using completed words and
|
||
the committed routing snapshot. Canonical child candidates are checked by
|
||
resolving the entered route with the candidate inserted; discovery-only and
|
||
unknown contexts do not invent command continuations. Tests cover canonical
|
||
and aliased routes, chains, scoped and discovery flag aliases, shadowed
|
||
children, runnable operand metadata, and paths ended by flags or operands.
|
||
The root package suite passes with `GOWORK=off` and `GOTOOLCHAIN=local`.
|
||
|
||
## Stage 5: Interpret flags and locate value positions
|
||
|
||
1. Walk the completed tail using the same canonical and alias spellings and
|
||
flag token rules as normal parsing. Factor shared token classification
|
||
from `internal/cobra/flags.go` into neutral helpers where needed; keep
|
||
backend parsing and failure wording at their current owner. The completion
|
||
walk records supplied canonical flags, consumed operands, whether options
|
||
ended, and any pending value flag. It does not run Cobra dispatch,
|
||
converters, required checks, input factories, or providers.
|
||
2. A pending value takes precedence even if the current word starts with
|
||
a dash. For long equals forms, explicit boolean assignments, and short
|
||
value flags in clusters, separate the value prefix from the entered
|
||
spelling and separator. Preserve that spelling, including an alias.
|
||
For a partially typed short cluster still accepting flag members, use its
|
||
canonical short spellings for continuations; do not rewrite earlier members.
|
||
A standalone `--` ends flags unless consumed as a value. After it,
|
||
dash-looking words are operands. An unknown completed option that makes
|
||
further interpretation ambiguous yields no suggestions.
|
||
3. For flag-name suggestions, match canonical spellings and suppress already
|
||
supplied singletons by canonical identity, including occurrences through
|
||
aliases. Keep repeatable flags and the four reporting flags eligible.
|
||
When a repeated singleton is typed anyway, still complete its value.
|
||
4. For an operand, use the same cardinality allocation as `allocateOperands`
|
||
in `binding.go` without required validation: include the current word
|
||
as a placeholder, determine which binding receives that final position,
|
||
and use its hint. Extract the cardinality calculation if necessary to avoid
|
||
a second allocation policy. Excess operands receive no suggestions.
|
||
When both operand and flag positions are available, an empty word uses
|
||
the operand hint and an option-looking partial word requests flags.
|
||
5. Test pending/attached/empty values, short clusters, scoped aliases and
|
||
unavailable targets, repeats, booleans, option-looking values, interspersed
|
||
operands, terminators, and optional/repeated operands before a required
|
||
suffix. Keep existing normal execution tests green at the sprint boundary.
|
||
|
||
## Stage 6: Assemble candidates and deliver query replies
|
||
|
||
1. Produce a neutral result of candidate replacements, optional descriptions,
|
||
or a filesystem strategy with value context. Keep prefix preservation
|
||
distinct from matching; the adapter translates the neutral replacement
|
||
according to the shell conventions verified in Stage 1. Do not interpret
|
||
choice values as shell source. Normalize description line breaks and omit
|
||
candidates the line protocol cannot represent rather than changing what
|
||
an inserted value means. Do not add execution-time validation for these
|
||
presentation limitations.
|
||
2. In `Execute`, after normal receiver/context checks and before ordinary
|
||
routing, recognize the exact raw query token only on an installed snapshot.
|
||
Treat all following tokens as data. Split off the final word; a missing
|
||
current-word argument is a malformed transport request, distinct from an
|
||
empty string. Use the current snapshot on each query, including after
|
||
later registration. Do not enter normal parsing, reporting selection,
|
||
input preparation, or handler execution for this path.
|
||
3. Write the adapter response only to normalized `Streams.Out`, with
|
||
managed output delivery in ordinary mode. A valid query with no inferable
|
||
context returns success and a NoFileComp reply. For a malformed request
|
||
or query construction failure, return an operational outcome and, if the
|
||
output is still usable, emit an error directive with no candidates.
|
||
Explanations go to diagnostics, never into the reply. Preserve write
|
||
failure evidence through normal presentation bookkeeping; do not retry
|
||
a broken output stream. No generated script should consume ordinary
|
||
usage guidance or debug output as a candidate.
|
||
4. Test query dispatch via `App.Execute`, including explicit empty last word,
|
||
no-context reply, malformed request, writer failure, hidden discovery,
|
||
later registration, and target words containing `--quiet` and `--debug`.
|
||
Use counter fixtures to prove target handlers, converters, factories,
|
||
and providers are untouched. Verify no special dispatch without installation.
|
||
|
||
## Stage 7: Annotate maintained commands and examples
|
||
|
||
1. Annotate static format/scope flags where their choices already come from
|
||
fixed command metadata or literals. Use the same values as the existing
|
||
version, config, and PromptKit command implementations; omit scope hints
|
||
that would advertise values not supported by that installed declaration.
|
||
Mark existing config/overlay filename flags as files where the binding
|
||
takes a filename. Leave prompt IDs, dynamic selectors, and composite
|
||
assignments unannotated. Do not infer domain semantics or load providers.
|
||
2. Add explicit installation to the maintained core and composed examples.
|
||
Demonstrate a static choice, a path hint, and an already-entered alias
|
||
with no new runtime dependency. Include an integration-level check
|
||
that installed hints reach completion without acquisition.
|
||
3. Update GoDoc, the CLI reference, and consumer/example instructions with
|
||
actual syntax, basename selection, manual script loading for each shell,
|
||
and examples of hints. Keep the wire protocol private. Once delivered,
|
||
update the feature status and keep alias discovery and dynamic discovery
|
||
in deferred work.
|
||
|
||
## Stage 8: Validate the assembled sprint
|
||
|
||
1. Exercise the generated scripts in Bash, Zsh, and Fish using a built fixture
|
||
executable, a disposable directory with filenames and directories containing
|
||
spaces, and isolated shell configuration. Verify actual completion output
|
||
and replacement behavior. Cover canonical commands, an entered alias,
|
||
static choices and descriptions, no fallback, file/directory hints in
|
||
separated and attached values, and the empty operand/leading-dash rule.
|
||
Use a PTY or a shell's native completion test interface where appropriate.
|
||
Syntax checking alone is insufficient. Record shell versions and the
|
||
commands or harness used so the evidence is reproducible.
|
||
2. Run the root and both optional module suites under the local workspace
|
||
procedure in [development.md](../development.md), plus the root suite
|
||
with `GOWORK=off`. Keep shell-dependent checks outside the default Go
|
||
suite unless the existing environment provides those shells; an absent
|
||
shell is missing acceptance evidence, not a passing check.
|
||
3. Verify no backend types in public declarations, no reverse optional-module
|
||
dependency, and no changes to ordinary routing, flag acceptance, or report
|
||
behavior outside the selected completion scope. Review documentation links,
|
||
formatting, and the final diff. Record the actual validation results.
|