Skip to content

feat: add Cheer.parse/3 to parse without dispatching #132

Description

@joshrotenberg

Problem

Cheer assumes run-to-completion CLI semantics: parse argv, invoke the matched
command's run/2, return, exit. That is the right model for an escript and the
wrong shape for a long-running server, where argv configures a supervision tree
rather than driving a unit of work.

Cheer.run/3 returns the handler's value rather than halting, so a server can
work today by having the handler build a config tuple and pattern matching on
the result:

def start(_type, _args) do
  case Cheer.run(HexpmMcp.CLI.Root, argv(), prog: "hexpm_mcp") do
    {:serve, opts}   -> Supervisor.start_link(children(opts), strategy: :one_for_one)
    :ok              -> System.halt(0)
    {:error, :usage} -> System.halt(2)
  end
end

That works, and it is what hexpm-mcp is doing (joshrotenberg/hexpm-mcp#68), but
the run/2 callback ends up as a stub whose only job is to smuggle parsed
options back out through the return value. The command tree describes the
interface correctly and then the dispatch step gets in the way.

Proposal

@spec parse(module(), [String.t()], keyword()) ::
        {:ok, module(), map()} | :handled | {:error, :usage}
def parse(root_command, argv, opts \\ [])
  • {:ok, command_module, args} on a successful parse, without invoking run/2
  • :handled when Cheer printed help or version and there is nothing to run
  • {:error, :usage} on a parse failure, error already printed

run/3 can then be defined in terms of parse/3, which keeps one code path for
resolution, validation, and help.

Why :handled rather than :ok

run/3 currently returns :ok for --help and --version, which is
indistinguishable from a handler that legitimately returns :ok. It does not
bite hexpm-mcp because its handler returns a tuple, but a caller that wants to
be correct has to know that its own handlers must never return :ok. A distinct
sentinel makes the contract explicit for the parse-only path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions