feat(cli): add help for all commands

emcli had only raw flag usage and no command listing; `--help` on agent commands
even emitted a JSON error envelope and exited 2. Add real help:

- Top-level `emcli` / `help` / `-h` / `--help` prints a grouped command catalogue
  (agent vs admin) with one-line summaries and the EMCLI_KEY/EMCLI_DB env vars.
- `emcli help <command>` prints that command's synopsis + summary.
- `emcli <command> --help` prints synopsis + summary + flags and exits 0. Agent
  commands keep stdout JSON-free (usage goes to stderr); admin commands print to
  stdout. Help works without EMCLI_KEY (no DB access).
- help.go holds the command catalogue; flag.ErrHelp is handled as success, and
  admin handlers short-circuit help before opening the store.

Unknown commands still error (exit 2). Full suite passes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-22 21:11:40 +01:00
parent 7087533644
commit 1b2fe99055
6 changed files with 209 additions and 5 deletions
+83
View File
@@ -0,0 +1,83 @@
package cli
import (
"flag"
"fmt"
"io"
)
type cmdHelp struct {
name string
synopsis string
summary string
}
// agentCmds emit machine-readable JSON; adminCmds are human-readable.
var agentCmds = []cmdHelp{
{"list", "list --account <name> [--folder F] [--new] [--limit N] [--before U] [--since U]", "List message headers, newest first."},
{"get", "get --account <name> [--folder F] --uid <uid>", "Fetch one full message (body + attachments)."},
{"search", "search --account <name> [--folder F] [--from A] [--subject-contains S] [--text S] [--since-date D] [--before-date D] [--limit N]", "Server-side IMAP search."},
{"ack", "ack --account <name> [--folder F] --uid-list U1,U2,…", "Mark message(s) processed."},
{"send", "send --account <name> --to A… [--cc A…] [--bcc A…] --subject S --body B [--attach P]… [--reply-to U [--folder F]]", "Send or reply (RW accounts only)."},
}
var adminCmds = []cmdHelp{
{"init", "init", "Create the database and add the first account (interactive)."},
{"account", "account <add|edit|remove|list> [flags]", "Manage accounts (add/edit accept flags, or run with none for an interactive form)."},
{"whitelist", "whitelist <in|out> <add|remove|list> --account <name> [--address A]", "Manage inbound/outbound whitelists."},
{"config", "config <set|get> <key> [value]", "Get or set global settings (e.g. audit_retention_days)."},
{"audit", "audit list [--account <name>] [--limit N]", "Show recent audit-log entries."},
{"doctor", "doctor [--account <name>]", "Check each account's IMAP/SMTP connectivity and auth."},
{"version", "version", "Print the emcli version."},
{"help", "help [command]", "Show this help, or detailed usage for one command."},
}
func helpIndex() map[string]cmdHelp {
m := make(map[string]cmdHelp, len(agentCmds)+len(adminCmds))
for _, c := range append(append([]cmdHelp{}, agentCmds...), adminCmds...) {
m[c.name] = c
}
return m
}
// helpRequested reports whether an argument is a help flag/word.
func helpRequested(s string) bool {
return s == "help" || s == "-h" || s == "--help"
}
// printMainHelp writes the top-level command catalogue.
func printMainHelp(w io.Writer) {
fmt.Fprint(w, "emcli — guard-railed email gateway for agents\n\n")
fmt.Fprint(w, "Usage:\n emcli <command> [flags]\n\n")
fmt.Fprint(w, "Agent commands (machine-readable JSON on stdout):\n")
for _, c := range agentCmds {
fmt.Fprintf(w, " %-10s %s\n", c.name, c.summary)
}
fmt.Fprint(w, "\nAdmin commands (human-readable):\n")
for _, c := range adminCmds {
fmt.Fprintf(w, " %-10s %s\n", c.name, c.summary)
}
fmt.Fprint(w, "\nRun \"emcli <command> --help\" for a command's flags.\n")
fmt.Fprint(w, "\nEnvironment:\n")
fmt.Fprint(w, " EMCLI_KEY base64-encoded 32-byte AES key; required for any command that uses the database\n")
fmt.Fprint(w, " EMCLI_DB database path (default ~/.config/emcli/emcli.db; %AppData%\\emcli\\emcli.db on Windows)\n")
}
// printCmdUsage writes "Usage: emcli <synopsis>" and the summary for one command.
func printCmdUsage(w io.Writer, name string) {
if h, ok := helpIndex()[name]; ok {
fmt.Fprintf(w, "Usage: emcli %s\n\n%s\n", h.synopsis, h.summary)
return
}
fmt.Fprintf(w, "Usage: emcli %s\n", name)
}
// usageFlags makes a flag set print the command's synopsis/summary followed by
// its flags whenever flag prints usage (on -h/--help or a flag error).
func usageFlags(fs *flag.FlagSet, name string, w io.Writer) {
fs.Usage = func() {
printCmdUsage(w, name)
fmt.Fprintln(w, "\nFlags:")
fs.PrintDefaults()
}
}