mirror of
https://github.com/PerpetualSoftware/pad.git
synced 2026-09-11 21:39:01 +00:00
eecc683ab0
* feat(cmdhelp): implement --format json emitter (TASK-934) Adds internal/cmdhelp package that walks the cobra command tree and emits a cmdhelp v0.1 Document conforming to schema/cmdhelp.schema.json. Wires it into cmd/pad/help_cmdhelp.go so `pad help --format json` is no longer a stub. Files: - internal/cmdhelp/types.go — Document/Command/Arg/Flag/Stdin/Stdout/ ExitCode/Example structs mirroring the schema. ExitCode implements custom MarshalJSON for the string-or-object union (spec §5.2). - internal/cmdhelp/json.go — Build() walks target's subtree; EmitJSON() serializes to indented JSON. Type mapping covers pflag's full type space, including slice/array→repeatable. Hidden commands and flags filtered. Cobra's auto-installed --help flag suppressed. Zero-default values suppressed to keep output compact. argRE parses positional arg placeholders from cobra Use strings, filtering [flags]/[options]/ [command] cobra conventions. parseExamples splits cmd.Example by newline, drops blanks and # comments. MaxDepth maps to spec §4 semantics: 0 = subcommand list, 1 = + grandchildren, -1 = unlimited. - cmd/pad/help_cmdhelp.go — replaces emitCmdhelpJSON stub with a call into the package; threads --depth and --all through MaxDepth (--all overrides --depth). Verification: - 17 emitter tests in internal/cmdhelp/json_test.go covering envelope, global-flag emission, hidden-thing exclusion, positional arg parsing, pflag type mapping, zero-default suppression, example parsing, description-vs-summary, command-path key shape, MaxDepth semantics, target-subtree scoping, JSON validity, version pattern, ExitCode union marshaling, parseExamples filtering. - 11 cmd/pad routing tests still pass; TestHelpCmd_FormatJSONStubError replaced by TestHelpCmd_FormatJSONEmits which validates the structure. - End-to-end: `pad help --format json` on the real binary emits 100 commands across the full tree; output validates against schema/cmdhelp.schema.json (verified with python jsonschema). - `pad help item --format json` correctly limits output to 29 commands in the item subtree (homepage and other top-level metadata still populated from root). - `pad help --format json --depth 0` correctly emits 15 immediate children of root, no grandchildren. - make check clean (lint + go test + web build). Out of scope (deferred): - Examples: pad's existing commands embed examples in Long rather than using cobra's Example field. The emitter correctly reads Example; TASK-939 will normalize the pad-side commands to populate it. - Dynamic enum injection (workspace-aware enums): TASK-936. - --capabilities discovery flag: TASK-937. - Schema-validation of live output in CI: TASK-938. Parent: PLAN-930. * fix(cmdhelp): handle alternation, variadic, and ValidArgs in Use parser per Codex review (round 1) Codex round 1 on PR #327 flagged that parseArgs missed two real cobra Use-string idioms in pad's command tree: 1. `completion [bash|zsh|fish|powershell]` — alternation in brackets. The old regex only allowed `[a-zA-Z0-9_./-]+` inside brackets, so the `|` made the whole token unmatched and the shell arg disappeared from the emitted JSON. Consumers asking "what does completion take?" got nothing. 2. `item bulk-update [--status X] <ref>...` — variadic ellipsis. Old regex didn't capture trailing `...`, so the arg was emitted but without `repeatable: true`. Consumers couldn't tell that <ref> may be passed multiple times. Fixes: - argRE now allows full-bracket content (`[^<>]+` / `[^\[\]]+`) and captures an optional trailing `...` group. - parseArgs takes *cobra.Command (not just Use string) so it can read cmd.ValidArgs and attach those values as the first arg's enum when set. This covers `Use: "completion [shell]"` + `ValidArgs: [...]` where the allowed values only live on the cobra struct. - Alternation `<a|b|c>` / `[a|b|c]` produces an enum-typed arg with the values as `Enum`. When Use carries no semantic name (only the alternation), the arg name is synthesized as "value". - New validArgName check rejects embedded flag-like fragments such as `[--status X]` and prose with whitespace/punctuation that the broader regex would otherwise capture from idiosyncratic Use strings. - ValidArgs entries strip cobra's tab-separated completion descriptions before becoming enum values. New tests: - TestBuild_VariadicArgsRepeatable — `<ref>...` → repeatable=true. - TestBuild_AlternationProducesEnum — `[bash|zsh|fish|powershell]` → enum with values in source order. - TestBuild_ValidArgsFillsEnumOnNamedArg — Use says `[shell]`, ValidArgs carries the values → enum-typed arg named `shell`. - TestBuild_EmbeddedFlagFragmentsFiltered — `[--status X]` does not leak as a positional arg. Verified on the real binary: - `pad help completion --format json` now emits the shell enum. - `pad help item bulk-update --format json` now marks <ref> repeatable. - `pad help --format json` still validates against the schema (100 cmds). - `make check` clean.
131 lines
5.2 KiB
Go
131 lines
5.2 KiB
Go
// Package cmdhelp implements the cmdhelp v0.1 wire format
|
|
// (https://getpad.dev/cmdhelp; IDEA-927) for the `pad` CLI.
|
|
//
|
|
// The package walks a cobra command tree and emits a Document conforming
|
|
// to schema/cmdhelp.schema.json. JSON serialization lives in json.go;
|
|
// markdown serialization arrives in TASK-935.
|
|
package cmdhelp
|
|
|
|
import "encoding/json"
|
|
|
|
// Version is the cmdhelp wire-format version this package emits.
|
|
// MAJOR.MINOR only — never includes a PATCH component (spec §9).
|
|
const Version = "0.1"
|
|
|
|
// Document is the top-level cmdhelp envelope. JSON-serializable; field
|
|
// tags match schema/cmdhelp.schema.json exactly. Optional fields use
|
|
// `omitempty` so the emitter does not produce noisy null/empty keys.
|
|
type Document struct {
|
|
CmdhelpVersion string `json:"cmdhelp_version"`
|
|
Binary string `json:"binary"`
|
|
Version string `json:"version,omitempty"`
|
|
Summary string `json:"summary,omitempty"`
|
|
Homepage string `json:"homepage,omitempty"`
|
|
GlobalFlags map[string]Flag `json:"global_flags,omitempty"`
|
|
Commands map[string]Command `json:"commands"`
|
|
Schemas map[string]json.RawMessage `json:"schemas,omitempty"`
|
|
Context *Context `json:"context,omitempty"`
|
|
}
|
|
|
|
// Context holds dynamic facts spliced in by CLIs with session state
|
|
// (spec §7). Populated by TASK-936; left nil for static emission.
|
|
type Context struct {
|
|
Workspace string `json:"workspace,omitempty"`
|
|
Profile string `json:"profile,omitempty"`
|
|
Auth string `json:"auth,omitempty"`
|
|
}
|
|
|
|
// Command describes a single command in the tree.
|
|
type Command struct {
|
|
Summary string `json:"summary"`
|
|
Description string `json:"description,omitempty"`
|
|
Args []Arg `json:"args,omitempty"`
|
|
Flags map[string]Flag `json:"flags,omitempty"`
|
|
Stdin *Stdin `json:"stdin,omitempty"`
|
|
Stdout *Stdout `json:"stdout,omitempty"`
|
|
ExitCodes map[string]ExitCode `json:"exit_codes,omitempty"`
|
|
Examples []Example `json:"examples,omitempty"`
|
|
SeeAlso []string `json:"see_also,omitempty"`
|
|
Since string `json:"since,omitempty"`
|
|
Stability string `json:"stability,omitempty"`
|
|
}
|
|
|
|
// Arg is a positional argument.
|
|
type Arg struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
Required bool `json:"required,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Default interface{} `json:"default,omitempty"`
|
|
Format string `json:"format,omitempty"`
|
|
Repeatable bool `json:"repeatable,omitempty"`
|
|
Enum []interface{} `json:"enum,omitempty"`
|
|
EnumSource string `json:"enum_source,omitempty"`
|
|
}
|
|
|
|
// Flag is a flag (option) on a command.
|
|
type Flag struct {
|
|
Type string `json:"type"`
|
|
Required bool `json:"required,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Default interface{} `json:"default,omitempty"`
|
|
Format string `json:"format,omitempty"`
|
|
Repeatable bool `json:"repeatable,omitempty"`
|
|
Enum []interface{} `json:"enum,omitempty"`
|
|
EnumSource string `json:"enum_source,omitempty"`
|
|
NegateFlag string `json:"negate_flag,omitempty"`
|
|
}
|
|
|
|
// Stdin describes whether the command accepts stdin.
|
|
type Stdin struct {
|
|
Accepted bool `json:"accepted"`
|
|
Format string `json:"format,omitempty"`
|
|
}
|
|
|
|
// Stdout describes the command's success output.
|
|
type Stdout struct {
|
|
TextTemplate string `json:"text_template,omitempty"`
|
|
JSONSchemaRef string `json:"json_schema_ref,omitempty"`
|
|
}
|
|
|
|
// ExitCode is a string-or-object union (spec §5.2). When only Description
|
|
// is set, it marshals as a bare string ("terse" form). When any of When,
|
|
// Recovery, or MessageTemplate is set, it marshals as an object ("rich"
|
|
// form). The schema's `oneOf: [string, object]` accepts both shapes.
|
|
type ExitCode struct {
|
|
// Description is the terse-form text. Mutually exclusive with the
|
|
// rich-form fields below — set Description OR (When [+ Recovery +
|
|
// MessageTemplate]), not both.
|
|
Description string `json:"-"`
|
|
|
|
When string `json:"-"`
|
|
Recovery string `json:"-"`
|
|
MessageTemplate string `json:"-"`
|
|
}
|
|
|
|
// MarshalJSON serializes the union per spec §5.2.
|
|
func (e ExitCode) MarshalJSON() ([]byte, error) {
|
|
hasRich := e.When != "" || e.Recovery != "" || e.MessageTemplate != ""
|
|
if e.Description != "" && !hasRich {
|
|
return json.Marshal(e.Description)
|
|
}
|
|
type rich struct {
|
|
When string `json:"when,omitempty"`
|
|
Recovery string `json:"recovery,omitempty"`
|
|
MessageTemplate string `json:"message_template,omitempty"`
|
|
}
|
|
return json.Marshal(rich{
|
|
When: e.When,
|
|
Recovery: e.Recovery,
|
|
MessageTemplate: e.MessageTemplate,
|
|
})
|
|
}
|
|
|
|
// Example is a canonical invocation. Same source feeds both --format json
|
|
// and --format md (spec §6); MD rendering wraps cmd in a fenced bash block
|
|
// and renders note as accompanying prose.
|
|
type Example struct {
|
|
Cmd string `json:"cmd"`
|
|
Note string `json:"note,omitempty"`
|
|
}
|