Command Man is an interactive CLI for creating, browsing, and managing your shell
aliases and functions. Entries you add become real shell definitions, so a stored
gitsw works exactly like an alias you wrote by hand: you type gitsw, not
cmd-man gitsw.
- Full-screen keyboard TUI: browse categories, page through entries, fuzzy search, and filter by kind (alias or function) or category.
- Live shell integration for zsh and bash, so aliases and functions behave natively.
- Aliases and functions are both first-class, tagged by kind so you can parse your catalog by type.
- Function bodies are checked before they are saved. One your shell could not read is refused with the reason, so a broken entry never reaches your shell.
- Directory scopes: an entry can belong to a project directory, so that project's
deployis live only inside it and shadows any global entry of the same name. - Categories, descriptions, usage, options, examples, and tags on every entry.
- Description, usage, options, and examples auto-populate from the command's
--help/manoutput when possible, and are always editable by hand. - Safe example-output capture: commands run sandboxed and hard-timeboxed, and the output is sanitized before it is stored.
- Automatic background sync, plus manual backup and restore, to a private GitHub
cmd-man-backuprepository.
- macOS or Linux.
gitfor backups. Optionally the GitHub CLI (gh) for the simplest backup path.
Install directly from GitHub with Cargo (no clone needed):
cargo install --git https://github.com/brennacodes/cmd-man.gitOr build from a local clone:
cargo install --path .Then wire cmd-man into your shells:
cmd-man initOpen a new shell, or run the source command that init prints, to load your
entries into the current shell. After that, every change takes effect in new
shells automatically, or in the current one with cmd-man reload.
Launch the interactive TUI:
cmd-man| Key | Action |
|---|---|
/ |
fuzzy search |
j / k, arrows |
move selection |
PgUp / PgDn |
page through the list |
Tab |
cycle kind filter (all / alias / function) |
g |
cycle category filter |
s |
cycle scope filter (all / live here / one scope) |
a |
add a new entry |
e |
edit the selected entry |
d |
delete the selected entry |
c |
capture example output |
Ctrl-F (in the add/edit form) |
fetch help and fill empty fields |
r |
regenerate shell files |
b |
back up to GitHub |
? |
help |
q / Esc |
quit |
cmd-man add gitsw --command "git switch" --desc "switch branches" --category gitcmd-man add mkcd --kind function --command 'mkdir -p "$1" && cd "$1"' --desc "make and enter a dir"cmd-man newcmd-man add gitsw --command "git switch" --desc "switch branches" --no-helpcmd-man search switch
cmd-man list --kind function
cmd-man edit gitsw --desc "switch or create branches"
cmd-man rm gitswcmd-man add dep --command "make deploy" --desc "deploy" --herecmd-man edit dep --move-here
cmd-man edit dep --make-globalcmd-man rm dep --in api
cmd-man list --all
cmd-man list --in globalcmd-man scope list
cmd-man scope relocate api ~/work/api
cmd-man scope rm api --promote-to-globalcmd-man capture <alias>cmd-man importcmd-man reloadcmd-man backup
cmd-man restoreA scope is a short id attached to one directory, for example api for
~/work/api. An entry placed in a scope is defined only while your shell's
working directory is inside that directory, and it shadows a global entry of the
same name while it is live. Leave the directory and the global definition comes
back. If scopes are nested, the deepest one containing your working directory
wins.
New entries are global unless you say otherwise. --here scopes an entry to the
enclosing git repository, or to the current directory when there is no repository,
creating a scope named after that directory the first time. --scope <id> places
it in a scope that already exists, and --global forces a global entry. Every add
tells you where the entry landed:
$ cmd-man add dep --command "make deploy" --desc "deploy" --here
Added 'dep' scoped to api ($HOME/work/api).cmd-man scope list shows every scope, its directory, how many entries it holds,
and which one you are standing in. cmd-man list shows your globals plus the
entries of the scopes you are inside, so a global and the scoped entry shadowing
it both appear; cmd-man list --all shows every entry, and cmd-man list --in api
or --in global shows one scope's worth.
When a name exists in more than one place, edit, rm, and capture pick the
same entry your shell would run: the innermost scope you are inside, otherwise the
global one. If neither applies, cmd-man lists the candidates and asks you to choose
with --in <id|global>.
- A scoped entry is not available on the same line as the
cdthat entered its scope, socd ~/work/api && depdoes not work. Use two lines, orcdfirst. In zsh a scoped function is the one exception. - On bash, scoped entries are interactive-only and do not work in scripts.
- A scope matches one exact directory spelling, so reaching the directory through
a differently-spelled symlink does not activate it, and on a case-insensitive
macOS volume
cd /users/...does not activate a scope stored as/Users/....cmd-man scope listsays so when your working directory reaches a scope only once symlinks are resolved. - A scope whose directory does not exist on the machine you are on never
activates.
cmd-man scope listmarks it(missing), andcmd-man scope relocate <id> <dir>points it at the right place. - A hand-typed alias of yours that collides with a cmd-man name is cleared when your shell loads cmd-man.
cmd-man capture <name> runs an entry and stores its output as an example. Safe
commands run automatically; anything destructive or with outward side effects (for
example rm, reset, push) needs confirmation first. Every capture is
sandboxed, hard-timeboxed, and sanitized, so home paths, usernames, IP addresses,
emails, and token-shaped strings never reach your store.
When you add or edit an entry, cmd-man reads the command's --help output and
fills any empty description, usage, options, or examples fields. It never
overwrites text you already entered. Press Ctrl-F in the TUI form to fetch on
demand, or pass --no-help to add/edit to skip the lookup.
Once a backup remote is reachable, cmd-man keeps your entries synced
automatically, in the background, without ever blocking you. A remote is reachable
when backup.remote_url is set, when gh is installed and authenticated, or when
a GitHub OAuth token is already stored. On a new machine, the first run clones your
existing backup and sets up your shell files, so your aliases travel with you.
Sync is on by default. Turn it off with auto_sync = false under [backup] in
your config, or set CMD_MAN_DISABLE_SYNC=1 for a single run.
cmd-man backup and cmd-man restore are the manual equivalents. Both use a
private cmd-man-backup repository and pick the best available method for you;
you can also choose one explicitly in your config.
~/.config/cmd-man/config.toml controls enabled shells, capture timeout and
sandboxing, backup method and opt-outs, and default entry placement. The built-in
defaults apply until you create the file yourself, and any key you leave out keeps
its default. The file is part of your backup.
scope.default decides where cmd-man add puts a new entry when you pass no
placement flag. It is global by default:
[scope]
default = "repo"In repo mode a new entry is scoped to the git repository you are in, and stays
global when there is no repository, or when the repository root would be your home
directory or the filesystem root.
Run cmd-man against a throwaway store instead of your real ~/.config/cmd-man:
CMD_MAN_HOME=$(mktemp -d) cargo run -- listThere is no CI, so run fmt, clippy, and the tests before opening a pull request.
cargo build # debug build
cargo test # unit tests and the end-to-end suites
cargo fmt # format
cargo clippy --all-targets # lint