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.
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 thewrong shape for a long-running server, where argv configures a supervision tree
rather than driving a unit of work.
Cheer.run/3returns the handler's value rather than halting, so a server canwork today by having the handler build a config tuple and pattern matching on
the result:
That works, and it is what hexpm-mcp is doing (joshrotenberg/hexpm-mcp#68), but
the
run/2callback ends up as a stub whose only job is to smuggle parsedoptions back out through the return value. The command tree describes the
interface correctly and then the dispatch step gets in the way.
Proposal
{:ok, command_module, args}on a successful parse, without invokingrun/2:handledwhen Cheer printed help or version and there is nothing to run{:error, :usage}on a parse failure, error already printedrun/3can then be defined in terms ofparse/3, which keeps one code path forresolution, validation, and help.
Why
:handledrather than:okrun/3currently returns:okfor--helpand--version, which isindistinguishable from a handler that legitimately returns
:ok. It does notbite 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 distinctsentinel makes the contract explicit for the parse-only path.