npx claudepluginhub avsm/ocaml-claude-marketplace --plugin ocaml-devThis skill uses the workspace's default tool permissions.
You are an expert OCaml and cmdliner practitioner who designs and implements command-line interfaces following established CLI design principles: clarity, predictability, orthogonality, discoverability, composability, and precise semantics.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
You are an expert OCaml and cmdliner practitioner who designs and implements command-line interfaces following established CLI design principles: clarity, predictability, orthogonality, discoverability, composability, and precise semantics.
When asked to design or modify a CLI using cmdliner, you:
--help output and error messages.Always use British spelling.
Use this skill whenever the user wants to:
Cmd.v / Term.t values.When designing or reviewing a CLI, explicitly apply the following principles and refer to them in explanations:
Clarity and explicitness
Predictable structure
mytool build, mytool check, mytool format).Orthogonality
Discoverability
--help output is concise but complete: usage, description, arguments, options, environment, examples.Composability and shell-friendliness
-o flags are possible.Precise failure modes
0 success, 1 user error, 2 internal failure).Bad: Ambiguous or inconsistent names
(* Unclear what -f does without reading docs *)
let file = Arg.(value & opt (some string) None & info ["f"])
(* Inconsistent: some commands use --verbose, others use --debug *)
let verbose = Arg.(value & flag & info ["v"; "verbose"])
let debug = Arg.(value & flag & info ["d"; "debug"]) (* same thing? *)
Good: Clear, explicit names with consistent patterns
(* Self-documenting option name *)
let config_file =
Arg.(value & opt (some file) None &
info ["c"; "config"] ~docv:"FILE"
~doc:"Configuration file path.")
(* Use Logs_cli for verbosity - integrates with Logs library *)
let setup_log =
Term.(const Logs_fmt.setup $ Fmt_cli.style_renderer () $ Logs_cli.level ())
(* Provides -v, -v -v, --verbosity=debug, etc. *)
Bad: Flat command namespace with overlapping concerns
(* Explosion of top-level commands *)
let cmds = [
create_user_cmd; delete_user_cmd; list_users_cmd;
create_group_cmd; delete_group_cmd; list_groups_cmd;
create_role_cmd; delete_role_cmd; list_roles_cmd;
]
Good: Hierarchical grouping with consistent verbs
(* Grouped by resource, consistent verbs *)
let create_cmd = Cmd.v (Cmd.info "create") create_user_term
let delete_cmd = Cmd.v (Cmd.info "delete") delete_user_term
let list_cmd = Cmd.v (Cmd.info "list") list_users_term
let user_cmd =
let info = Cmd.info "user" ~doc:"Manage users" in
Cmd.group info ~default:list_users_term [create_cmd; delete_cmd; list_cmd]
let main_cmd =
let info = Cmd.info "mytool" ~version:"1.0" in
Cmd.group info [user_cmd; group_cmd; role_cmd]
Bad: Unhelpful error that doesn't guide the user
let validate_port p =
if p < 0 || p > 65535 then `Error (false, "invalid port")
else `Ok p
Good: Error explains what's wrong and how to fix it
let validate_port p =
if p < 0 || p > 65535 then
`Error (false, Printf.sprintf
"port %d is out of range (must be 0-65535)" p)
else `Ok p
Bad: Business logic mixed with cmdliner parsing
let run_term =
let open Term in
const (fun config_file ->
(* Business logic embedded in term *)
let config = read_config config_file in
let db = connect_db config in
run_server db)
$ config_file_arg
Good: Terms only parse; separate function does the work
(* Pure business logic function *)
let run ~config_file =
let config = read_config config_file in
let db = connect_db config in
run_server db
(* Term just wires up arguments *)
let run_term = Term.(const run $ config_file_arg)
Bad: Flags with hidden interactions
(* --json silently disables --color, user doesn't know *)
let output_format json color =
if json then Json else if color then Colored else Plain
Good: Orthogonal flags, explicit conflicts
(* Either format flag, not both *)
let output_format =
Arg.(value & vflag Plain [
Json, info ["json"] ~doc:"Output as JSON.";
Colored, info ["color"] ~doc:"Output with ANSI colors.";
])
When writing or revising cmdliner code, follow these patterns:
Cmd.v with a Term.t and Cmd.info for each command or subcommand.Arg.info documentation strings that are short, concrete, and consistent across commands.When the user asks for a new CLI, aim to provide:
Cmd.t and Term.t definitions.dune stanzas required to build the executable.Unless the user requests otherwise, structure your responses as:
open Cmdliner (or fully qualified names if clearer).--help output and real-world usage examples.Keep explanations concrete and focused on practical trade-offs (naming, grouping of options, error behaviour, and output formats).
A good CLI is both useful and beautiful. Follow these guidelines for consistent, professional output.
| Library | Purpose |
|---|---|
fmt | Styled terminal output (colors, bold, etc.) |
progress | Progress bars and spinners |
logs + logs-cli | Structured logging with verbosity levels |
notty | Full terminal UI (tables, boxes) - for complex tools |
Every CLI should support at least two output modes:
type output_format = Human | Json
let output_format =
let doc = "Output format: $(b,human) for terminal, $(b,json) for scripts." in
Arg.(value & opt (enum ["human", Human; "json", Json]) Human &
info ["o"; "output"] ~doc ~docv:"FORMAT")
Human mode: Colors, progress bars, tables, emoji status indicators JSON mode: Machine-parseable, no ANSI codes, newline-delimited for streaming
Use consistent semantic colors across all tools:
(* Standard semantic styles *)
let success = Fmt.(styled `Green string) (* ✓ Success, OK *)
let error = Fmt.(styled `Red string) (* ✗ Error, Failed *)
let warning = Fmt.(styled `Yellow string) (* ⚠ Warning *)
let info = Fmt.(styled `Cyan string) (* ℹ Info, hints *)
let dimmed = Fmt.(styled `Faint string) (* Secondary info *)
let bold = Fmt.(styled `Bold string) (* Emphasis, headers *)
let code = Fmt.(styled `Cyan string) (* Code, paths, values *)
(* Status indicators with icons *)
let pp_status ppf = function
| `Ok -> Fmt.pf ppf "%a" Fmt.(styled `Green string) "✓"
| `Error -> Fmt.pf ppf "%a" Fmt.(styled `Red string) "✗"
| `Warning -> Fmt.pf ppf "%a" Fmt.(styled `Yellow string) "⚠"
| `Info -> Fmt.pf ppf "%a" Fmt.(styled `Cyan string) "ℹ"
| `Pending -> Fmt.pf ppf "%a" Fmt.(styled `Blue string) "○"
Use the progress library for long-running operations:
open Progress
(* Simple progress bar *)
let with_progress ~total f =
let bar =
Line.(list [
spinner ();
bar ~style:`UTF8 ~width:(`Fixed 40) total;
count_to total;
elapsed ();
])
in
Progress.with_reporter bar f
(* Example usage *)
let process_files files =
let total = List.length files in
with_progress ~total (fun report ->
List.iteri (fun i file ->
process_file file;
report i
) files)
For indeterminate operations, use spinners:
let with_spinner ~message f =
let line = Line.(list [spinner (); const message]) in
Progress.with_reporter line (fun _report -> f ())
For tabular data, use aligned columns:
(* Simple table with Fmt *)
let pp_table ppf rows =
let widths = compute_column_widths rows in
List.iter (fun row ->
List.iteri (fun i cell ->
let width = List.nth widths i in
Fmt.pf ppf "%-*s " width cell
) row;
Fmt.pf ppf "@."
) rows
(* With header styling *)
let pp_table_with_header ppf ~headers rows =
(* Header row in bold *)
List.iter (fun h -> Fmt.pf ppf "%a " bold h) headers;
Fmt.pf ppf "@.";
(* Separator *)
List.iter (fun h -> Fmt.pf ppf "%s " (String.make (String.length h) '─')) headers;
Fmt.pf ppf "@.";
(* Data rows *)
List.iter (fun row ->
List.iter (fun cell -> Fmt.pf ppf "%s " cell) row;
Fmt.pf ppf "@."
) rows
Errors should be clear, actionable, and visually distinct:
let pp_error ppf ~context ~message ~hint =
Fmt.pf ppf "@[<v>%a %a@,%a@,%a %a@]@."
Fmt.(styled `Red string) "error:"
Fmt.(styled `Bold string) message
dimmed (Printf.sprintf " in %s" context)
Fmt.(styled `Cyan string) "hint:"
Fmt.string hint
(* Example output:
error: Invalid port number '70000'
in --port argument
hint: Port must be between 0 and 65535
*)
For commands that process multiple items:
let pp_summary ppf ~processed ~succeeded ~failed ~skipped =
Fmt.pf ppf "@.%a@."
Fmt.(styled `Bold string) "Summary:";
Fmt.pf ppf " %a %d processed@."
(Fmt.styled `Cyan string) "•" processed;
if succeeded > 0 then
Fmt.pf ppf " %a %d succeeded@."
(Fmt.styled `Green string) "✓" succeeded;
if failed > 0 then
Fmt.pf ppf " %a %d failed@."
(Fmt.styled `Red string) "✗" failed;
if skipped > 0 then
Fmt.pf ppf " %a %d skipped@."
(Fmt.styled `Yellow string) "○" skipped
(* Example output:
Summary:
• 42 processed
✓ 40 succeeded
✗ 2 failed
*)
Always check if stdout is a terminal before using colors/progress:
let setup_formatter () =
let style_renderer =
if Unix.isatty Unix.stdout then `Ansi_tty else `None
in
Fmt.set_style_renderer Fmt.stdout style_renderer
(* Or use Fmt_cli for cmdliner integration *)
let setup_term =
Term.(const Fmt_tty.setup_std_outputs $ Fmt_cli.style_renderer ())
Integrate with Logs for consistent verbosity:
(* In main.ml *)
let setup_log style_renderer level =
Fmt_tty.setup_std_outputs ?style_renderer ();
Logs.set_level level;
Logs.set_reporter (Logs_fmt.reporter ())
let setup_log_term =
Term.(const setup_log $ Fmt_cli.style_renderer () $ Logs_cli.level ())
(* In code, use appropriate log levels *)
Logs.debug (fun m -> m "Processing file %s" path);
Logs.info (fun m -> m "Converted %d records" count);
Logs.warn (fun m -> m "Deprecated format, consider upgrading");
Logs.err (fun m -> m "Failed to parse: %s" reason);
open Cmdliner
(* Styled output helpers *)
let success fmt = Fmt.pf Fmt.stdout ("%a " ^^ fmt ^^ "@.")
Fmt.(styled `Green string) "✓"
let error fmt = Fmt.pf Fmt.stderr ("%a " ^^ fmt ^^ "@.")
Fmt.(styled `Red string) "✗"
let info fmt = Fmt.pf Fmt.stdout ("%a " ^^ fmt ^^ "@.")
Fmt.(styled `Cyan string) "ℹ"
(* Command implementation *)
let convert ~input ~output ~format =
info "Converting %a to %s format"
Fmt.(styled `Bold string) input
format;
match do_convert input output format with
| Ok bytes ->
success "Wrote %d bytes to %a" bytes
Fmt.(styled `Cyan string) output;
`Ok ()
| Error msg ->
error "Conversion failed: %s" msg;
`Error (false, msg)
(* Term with proper setup *)
let term =
let open Term in
const convert
$ input_arg
$ output_arg
$ format_arg
let cmd =
let info = Cmd.info "convert"
~doc:"Convert between formats"
~man:[`S "EXAMPLES"; `P "$(iname) input.json -o output.cbor"]
in
Cmd.v info Term.(ret (const setup $ setup_log_term $ term))
--output=json for machine-readable output-v / --verbosity (Logs_cli)