themadorg/madmail/add-cli-command

Claude Code and agent skill by themadorg/madmail

Trust tier 2
  • community
LLM review pending
Source check pending

Checking that the SKILL.md file is still in the repository.

Add a new madmail CLI command end-to-end: clap definition, ctl handler with --json output, DB settings keys, dispatch wiring, unit tests, E2E tests, tab-completion verification, and operator documentation. Use when the user asks to add, implement, or create a new madmail/chatmail subcommand, wire a CLI tool, or runs /add-cli-command.

Install

npx vskill@latest install themadorg/madmail/add-cli-command

SKILL.md

Add CLI Command

Implement a new `madmail` operator command following existing patterns in this repo. Production binary: `madmail`; dev crate binary: same name (cargo build -p chatmail → target/debug/madmail).

Do not ship a command without all four layers: clap + handler + tests + docs.


0. Orient — read before coding

Study commands closest to what you are adding:

PatternRead these
DB toggle (enable/disable/status)service_toggle.rs, registration.rs, cli.rs ServiceToggleCommand
DB setting (status/set/reset)message_size.rs, language.rs, proxy.rs
Nested subcommandsport.rs, proxy.rs, federation.rs
Destructive + confirmationdelete_cmd.rs, accounts.rs (-y / --yes)
Read-only statusstatus_cmd.rs, push.rs
Config-file onlyhtml.rs, version.rs

Also read:

FilePurpose
crates/chatmail-config/src/cli.rsCommand tree, global Args, parse tests
crates/chatmail/src/ctl/dispatch.rsRouting, not_implemented list
crates/chatmail/src/ctl/output.rs--json envelope (CtlOut)
crates/chatmail/src/ctl/context.rsCtlContext, open_pool()
crates/chatmail/src/ctl/test_harness.rsUnit-test helpers
crates/chatmail/src/ctl/ops_tests.rsDispatch test examples
tests/ctl_cli_e2e.rsSubprocess + JSON E2E
docs/TDD/14-cli-tools.mdParity matrix

Check Madmail v1 parity if present: context/madmail/docs/chatmail/commands.md.


1. Pick the implementation pattern

TypeWhen to useReuse
Service toggleSingle __FOO_ENABLED__ boolservice_toggle::run() — only if label fits webimap/websmtp pattern; otherwise copy proxy.rs enable/disable
Setting groupstatus / set / reset on one DB keyproxy.rs setting_set / setting_reset / setting_status
Custom handlerAnything elseNew ctl/<name>.rs module

Naming conventions:

ItemRuleExample
CLI namekebab-case, Madmail paritymessage-size, admin-token
Rust modulesnake_casemessage_size.rs
Clap variantPascalCaseMessageSize
Settings key__SCREAMING_SNAKE____SS_ENABLED__ in settings_keys.rs
Doc pagekebab-case pathproxy-cipher-set.md
CtlOut command stringspace-separated CLI path"proxy cipher set"

2. Clap definition (chatmail-config)

File: crates/chatmail-config/src/cli.rs

  1. Add variant to Command enum with doc comment.
  2. Add subcommand enum(s) if nested.
  3. Use existing attributes consistently:
  • #[command(name = "kebab-name")] when Rust variant differs
  • #[command(subcommand_required = false)] + Option<SubCmd> for default status
  • visible_aliases = ["alias"] for short names
  • #[arg(short, long)] yes: bool for destructive ops
  1. Export new types from crates/chatmail-config/src/lib.rs pub use cli::{...} when handlers need them.

Add parse tests in cli.rs #[cfg(test)] or ops_tests.rs:

rust
let cli = parse_cli(dir.path(), &["proxy", "cipher", "set", "aes-256-gcm"]);
assert!(matches!(
cli.command,
Some(Command::Proxy {
cmd: Some(ProxyCommand::Cipher {
cmd: Some(ProxySettingCommand::Set { value })
})
}) if value == "aes-256-gcm"
));

Test aliases (pr → proxy) when added.


3. Settings key (if DB-backed)

File: crates/chatmail-db/src/settings_keys.rs

rust
pub const MY_SETTING: &str = "__MY_SETTING__";

Only add keys stored in the settings table at runtime. Use set_setting, get_setting, get_bool_setting, delete_setting from chatmail_db.

Wire the key into the server reload path if the running daemon must pick it up (see how existing keys are read in the relevant crate).


4. Handler module (chatmail::ctl)

Files:

  • crates/chatmail/src/ctl/<name>.rs — new module
  • crates/chatmail/src/ctl/mod.rs — mod <name>;
  • crates/chatmail/src/ctl/dispatch.rs — import + match arm + command_name() arm

Handler skeleton:

rust
pub async fn my_cmd(args: &Args, cmd: Option<&MyCmdCommand>) -> Result<()> {
let ctx = CtlContext::from_args(args)?;
let pool = ctx.open_pool().await?;
let out = CtlOut::from_args(args, "my-cmd status");
match cmd {
None | Some(MyCmdCommand::Status) => { /* ... */ }
Some(MyCmdCommand::Set { value }) => { /* ... */ }
}
}

Rules:

  • Never println! directly — always CtlOut.
  • Always branch on out.is_json() for status commands with formatted human output.
  • Return Err(ChatmailError::config(...)) for operator errors; main prints JSON error to stderr when --json.
  • Include reload hints in human text and reload_required: true in mutation JSON when madmail reload is needed.
  • Never echo passwords/secrets in JSON (see proxy password status).

5. --json output (mandatory)

Every implemented subcommand must support global --json. Use CtlOut:

MethodUse for
out.emit(data)Status / read-only (JSON only prints; human uses line/blank)
out.done(human, data)Simple mutation without message field
out.done_msg(human, data, message)Mutations — adds "message" to envelope
out.aborted()User declined -y confirmation

Success envelope (stdout):

json
{"ok": true, "command": "proxy status", "data": { ... }}
{"ok": true, "command": "proxy enable", "message": "Shadowsocks proxy enabled", "data": { "enabled": true, "reload_required": true }}

Error envelope (stderr):

json
{"ok": false, "error": "..."}

`command` string is part of the contract — match it exactly in tests and json-output.md. Examples from real handlers:

  • "push status", "proxy cipher set", "webimap" (toggle mutations use service name)

Status command pattern:

rust
if out.is_json() {
return out.emit(json!({ "enabled": on, "reload_required": false }));
}
out.blank();
out.line(format!(" Foo: {}", if on { "enabled" } else { "disabled" }));
out.line(" Apply to a running server: madmail reload");
out.blank();
Ok(())

Mutation pattern:

rust
set_setting(pool, key, &value).await?;
out.done_msg(
format!("✅ Setting updated (madmail reload)"),
json!({ "value": value, "reload_required": true }),
"Setting updated",
)

Destructive ops:

rust
if !confirm(&format!("Delete {u}?"), yes)? {
return out.aborted();
}

6. Dispatch wiring

File: crates/chatmail/src/ctl/dispatch.rs

  1. use super::<module>;
  2. Match arm: Some(Command::MyCmd { cmd }) => my_cmd::my_cmd(&cli.args, cmd.as_ref()).await
  3. command_name() arm for the top-level name
  4. Remove command from not_implemented() message list once implemented
  5. Update docs/TDD/14-cli-tools.md status → done

For simple toggles, wire directly:

rust
Some(Command::Webimap(cmd)) => service_toggle::run(
&cli.args, settings_keys::WEBIMAP_ENABLED, "WebIMAP HTTP API", cmd,
).await,

7. Tests (mandatory)

A. Clap parse tests

Assert Cli::try_parse_from / parse_cli produces correct enum shape. Include alias coverage.

Run: cargo test -p chatmail-config and cargo test -p chatmail ctl

B. Dispatch unit tests (in-process)

Files: crates/chatmail/src/ctl/ops_tests.rs (settings/port/proxy), dispatch_tests.rs (accounts/destructive)

Use test_harness.rs:

rust
#[tokio::test]
async fn dispatch_my_cmd_set_and_reset() {
let (dir, _args, _db, pool) = setup_ctl_env().await;
let cli = parse_cli(dir.path(), &["my-cmd", "set", "value"]);
dispatch(&cli).await.unwrap();
assert_eq!(
get_setting(&pool, settings_keys::MY_SETTING).await.unwrap().as_deref(),
Some("value")
);
let cli = parse_cli(dir.path(), &["my-cmd", "reset"]);
dispatch(&cli).await.unwrap();
assert!(get_setting(&pool, settings_keys::MY_SETTING).await.unwrap().is_none());
}

Assert:

  • DB state (get_setting, get_bool_setting, SQL)
  • Error messages: dispatch(&cli).await.unwrap_err().to_string().contains("expected")
  • Preconditions (e.g. proxy enable fails when not configured)
  • Validation rejects bad input

For config-dependent commands, use write_ss_test_config() pattern or write a similar helper.

Do not rely on stdout in unit tests — assert DB/filesystem side effects.

C. --json unit or E2E tests

At minimum one test per command family verifying JSON envelope shape:

rust
let cli = parse_cli_with_config(dir.path(), &config, &["my-cmd", "status", "--json"]);
// Or E2E:
let envelope: Value = serde_json::from_slice(&stdout).unwrap();
assert_eq!(envelope["ok"], true);
assert_eq!(envelope["command"], "my-cmd status");
assert!(envelope["data"]["enabled"].is_boolean());

E2E pattern in tests/ctl_cli_e2e.rs:

rust
chatmail()
.args(state_argv(&state))
.args(["my-cmd", "status", "--json"])
.assert().success();

Add E2E when the command has subprocess-visible behavior worth guarding (JSON contract, stdout markers).

Run: cargo test -p chatmail-integration --test ctl_cli_e2e --test ctl_ops_e2e

D. Tab completion

Completions are auto-generated from clap. After adding commands/aliases:

bash
cargo build -p chatmail
./target/debug/madmail completion bash | grep my-cmd
cargo test -p chatmail bash_completion

Add assertions in crates/chatmail/src/ctl/docs.rs when the command is top-level or has a non-obvious alias (see bash_completion_includes_proxy_subcommand, bash_completion_prefix_pr_matches_proxy).


8. Documentation (mandatory)

Follow the `update-cli-docs` skill (.grok/skills/update-cli-docs/SKILL.md) for the full doc workflow.

Minimum checklist for a new implemented command:

  • [ ] docs/guide/cli/<command>.md parent page
  • [ ] Leaf pages for each subcommand (<command>-<sub>.md)
  • [ ] docs/guide/cli/json-output.md sections with real data fields from handler
  • [ ] docs/guide/cli/README.md entry in correct category section
  • [ ] docs/TDD/14-cli-tools.md → done, module name, settings keys
  • [ ] If top-level command or alias: docs/man/madmail.1.scd → make man
  • [ ] cd landing && bun run docs:tree (search index)
  • [ ] Cross-link related docs (e.g. feature TDD) when central to the topic

Verify help matches docs:

bash
cargo build -p chatmail
./target/debug/madmail <command> --help
./target/debug/madmail <command> <sub> --help

Never invent flags not present in clap.


9. Final verification

Run before considering the command done:

bash
cargo build -p chatmail
./target/debug/madmail <command> --help
# Unit tests
cargo test -p chatmail ctl
cargo test -p chatmail-config
# E2E (if added)
cargo test -p chatmail-integration --test ctl_cli_e2e --test ctl_ops_e2e
# Completion
cargo test -p chatmail bash_completion
# Quality gate (repo standard)
make lint

Manually spot-check --json on at least one status and one mutation subcommand.


10. Common pitfalls

PitfallFix
Parses but not implementedForgot dispatch.rs match arm
Compile errorForgot mod.rs registration
--json prints human textUsed println! instead of CtlOut
--json prints nothing on statusMissing out.is_json() branch
JSON contract brokenWrong CtlOut::from_args command string
E2E hangsMissing -y on destructive command
Secrets in JSONOmit values; return source / db_override only
Docs out of syncRe-run --help, update json-output.md anchors
not_implemented list staleUpdate message in dispatch.rs
New toggle via service_toggleHardcoded "webimap"/"websmtp" labels — write custom handler if needed

11. File checklist

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewNot runA model review is scheduled for the next rescan.

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
190
Repository forks
20
7-day trend score
26

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/themadorg/madmail/add-cli-command/badge)](https://verified-skill.com/skills/themadorg/madmail/add-cli-command)

Something wrong with this skill or its page? Tell us.