Files
pad/internal/cmdhelp/types.go
T
xarmian eecc683ab0 feat(cmdhelp): implement --format json emitter (TASK-934) (#327)
* 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.
2026-05-01 00:56:50 -04:00

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"`
}