Files
emcli/internal/cli/help.go
T
steve d023df1b4a feat(cli): folders command, search --all-folders, empty-search hint
Fixes from testing email search (docs/enhancements-2026-07-07.md):

- New `folders` agent command lists the account's mailboxes (name,
  delimiter, selectable), INBOX first, so agents can discover archived
  mail outside INBOX.
- `search --all-folders` sweeps every selectable mailbox; each hit
  carries a `folder` field, `skipped_folders` reports mailboxes the
  server refused, and --limit caps visible results across the sweep.
  The sweep deliberately skips EnsureFolderBaseline so a read-only
  search never mutates list --new state.
- Empty search results include a generic `data.hint` with next steps.
  The hint is a fixed constant per mode, so the invisibility invariant
  holds: absent and policy-filtered mail produce byte-identical
  envelopes (codified in TestSearchEmptyHintIndistinguishableFromFiltered).
- Skill and user docs: document `--text` full-text search as
  best-effort (server-dependent); recommend --subject-contains/--from.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 14:28:38 +01:00

87 lines
3.8 KiB
Go

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 | --all-folders] [--from A] [--subject-contains S] [--text S] [--since-date D] [--before-date D] [--limit N]", "Server-side IMAP search."},
{"folders", "folders --account <name>", "List the account's mailboxes/folders."},
{"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|show|list> [name] [flags]", "Manage accounts. `add`/`edit` take a positional name + field flags, or run with none for an interactive form."},
{"whitelist", "whitelist <add|remove|list|enable|disable> <account> [address…] --in|--out", "Manage inbound/outbound whitelists. Direction (--in/--out) is required."},
{"config", "config <list|get|set> [key] [value]", "List, get, or set global settings (e.g. audit_retention_days)."},
{"audit", "audit list [account] [--limit N]", "Show recent audit-log entries."},
{"doctor", "doctor [account]", "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, "\nAliases: rm/del = remove, ls = list. Admin commands take positional\n")
fmt.Fprint(w, "operands (account/address/key); agent commands use flags (--account …).\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()
}
}