Skip to content

Repository files navigation

clift

Command Line Interface Framework in Task

A batteries-included, language-agnostic framework for building custom CLIs using go-task.
You write your command logic in any language — clift provides the UX.

CI Coverage

clift in action

More demos

Help at every level

help output

Rich --help with groups, choices, deprecated markers, and completion install hint.

Flag parsing

flag parsing

String, bool, int, list, and repeated flags all export as CLIFT_FLAG_* env vars.

Validation — choices and patterns

choices and pattern validation

choices: and pattern: fail fast before the user script runs.

Mutex and required-together groups

group constraints

Flat group schema: mutually exclusive (--json vs --yaml) and required-together (--user + --token).

Persistent flags

persistent flags

Declared on the root Taskfile, accepted either before or after the command token.

Aliases and deprecated flags

aliases and deprecation

Command aliases (depdeploy), flag aliases (--env--target), and one-shot deprecation warnings.

Error UX

error messages

Typos get did-you-mean suggestions; wrong types and missing flags get clear, actionable errors.


Table of Contents

Quick Start

# Prerequisites: bash 4+, task (go-task), jq, yq
# Optional: gum (for pretty prompts)

# Clone the framework
git clone <repo-url> ~/.clift
export PATH="$HOME/.clift/bin:$PATH"

# Create a new CLI
clift init ~/mycli

# Source your shell and start using it
source ~/.bashrc
mycli                         # see available commands
mycli new cmd                 # scaffold your first command
mycli greet --name world      # run it with flags
mycli greet --help            # per-command help
mycli help greet              # equivalent form — `help <cmd>` is an alias

All clift init options are available as flags for non-interactive use:

clift init ~/mycli --name myapp --version 1.0.0 --theme brackets --mode standard

Opt-in integrations are gated behind their own flags (nothing is generated by default):

clift init ~/mycli --cfgd   # also generate module.yaml for cfgd versioning
clift init ~/mycli --ci     # also generate .github/workflows/ci.yml (shellcheck)

See clift help for the full list. You can also use task directly:

task --taskfile ~/.clift/Taskfile.yaml setup:cli -- ~/mycli

Features

  • Cobra-style help system with grouped commands
  • Themed logging (7 built-in themes + custom color schemes, honors NO_COLOR)
  • Typed flag parsing (bool, string, int, list) with defaults, required flags, and short aliases
  • Rich flag schema — long-name aliases, deprecated markers, hidden flags, mutually-exclusive and required-together groups, and value validation via choices: / pattern:
  • Persistent (CLI-wide) flags that work before or after the command token
  • Hidden commands (vars.HIDDEN: true) — executable but omitted from help + completion
  • Command aliases (aliases: [...] on tasks) — dispatch, help, completion, and did-you-mean all honor them
  • Override system — per-command and CLI-global tiers for help_detail, version_print, log, and pre/post command hooks (top-level help_list is CLI-global only)
  • Cache control — --no-cache flag and CLIFT_CACHE=rebuild|bypass environment variable (see docs/cache.md)
  • go-task runner flags as first-class UX — mycli watch, --task:dry, --task:interval, --task:parallel, …
  • Did-you-mean error suggestions (Levenshtein)
  • Interactive prompts (gum with read fallback)
  • Config management (get/set/show/edit/theme)
  • Command scaffolding with new cmd
  • Global flags: --verbose, --quiet, --no-color, --help, --version
  • Shell completions (bash, zsh) — static + dynamic flag-value completers, completion-install hint in mycli --help, setup-time installer
  • Framework self-update; optional versioning and distribution via cfgd

Requirements

Dependency Required Purpose
bash 4.2+ Yes Associative arrays, declare -g, ${var^^}, used throughout
Task v3.0+ Yes Task runner that powers the CLI
jq Yes JSON processing for help and config
yq Yes YAML processing for metadata
gum No Enhanced interactive prompts (falls back to read)

macOS note: macOS ships bash 3.2. Install bash 4.2+ via brew install bash.

How It Works

clift is a framework repo that provides shared libraries (lib/) for help, logging, routing, argument parsing, config, and more. When you bootstrap a CLI with setup:cli, it creates a project directory with:

~/.config/mycli/
  .env                       # CLI_NAME, CLI_VERSION, FRAMEWORK_DIR, LOG_THEME
  Taskfile.yaml              # includes framework libs + your commands
  cmds/
    greet/
      Taskfile.yaml          # task definition (desc, routing, help)
      greet.{sh,py,go,rs,…}  # your command logic, any language

In standard mode, your CLI is a wrapper script on PATH that provides Cobra-style mycli cmd subcmd --flag UX. In task mode, it's a shell alias that invokes task directly. The framework's router handles global flags, logging setup, and dispatching to your command scripts. Commands are Taskfile includes -- each command lives in cmds/<name>/ with its own Taskfile and script.

Modes

clift CLIs support two argument-format styles, chosen at setup time:

  • Standard mode -- mycli cmd subcmd --flag value (Cobra-like, recommended)
  • Task mode -- mycli cmd:subcmd -- --flag value (raw go-task semantics)

Standard mode gives you space-separated subcommands, --flag parsing, did-you-mean errors, and shell completions with flag support. See docs/modes.md.

Creating Commands

mycli new cmd
# Prompts for: command name, short description
# Generates: cmds/<name>/Taskfile.yaml + <name>.sh

The generated script comes pre-wired with logging and the flag env var contract:

#!/usr/bin/env bash
set -euo pipefail

source "${FRAMEWORK_DIR}/lib/log/log.sh"

# Flag values arrive as CLIFT_FLAG_<NAME> (uppercased, dashes→underscores).
# Positional args: CLIFT_POS_1, CLIFT_POS_2, ... CLIFT_POS_COUNT.
log_info "Hello from mycommand"

Subcommands work via task namespacing. A command deploy can have subcommands deploy:staging and deploy:prod by adding tasks to its Taskfile.

Passthrough mode (skip the parser)

Commands with no vars.FLAGS key in their Taskfile are passthrough commands. The router skips the parser entirely and execs your script with raw positional args ($1, $2, …). No CLIFT_FLAG_* env vars, no --help interception, no did-you-mean.

This is a first-class choice — use it when:

  • The script is written in another language (Go, Python, Rust) with its own argument parsing and you don't want clift's parser to second-guess it.
  • The command wraps an external tool transparently (mycli docker … → forward everything to docker).
  • You want to own flag handling end-to-end for a niche UX.

vars.FLAGS: [] (empty list) is parsed mode — it gets the parser, --help, and globals, just with no per-command flags. Omit the key entirely to go passthrough. See docs/scripts.md for the full contract (persistent flags still bind; pre/post hooks still fire).

Argument Parsing

Declare flags in your command's Taskfile.yaml under vars.FLAGS. The router parses them before your script runs and exports them as env vars:

# cmds/deploy/Taskfile.yaml
vars:
  FLAGS:
    - {name: target, short: t, type: string, default: staging, desc: "Target env"}
    - {name: force, short: f, type: bool, desc: "Skip confirmation"}
# cmds/deploy/deploy.sh
target="${CLIFT_FLAG_TARGET}"       # "staging" (default applied)
if [[ "${CLIFT_FLAG_FORCE:-}" == "true" ]]; then ...
file="${CLIFT_POS_1:?missing file}" # positional args

See docs/flags.md for the full schema, docs/scripts.md for the env var contract, and docs/cache.md for how --no-cache / CLIFT_CACHE=rebuild|bypass control the compiled .clift/ cache.

Configuration

mycli config show              # display all config
mycli config get LOG_THEME     # get a single value
mycli config set LOG_THEME brackets-color
mycli config theme             # interactive theme picker
mycli config edit              # open .env in $EDITOR

Configuration lives in .env at the CLI project root. Key variables:

Variable Description
CLI_NAME Name of your CLI
CLI_VERSION Version string
FRAMEWORK_DIR Path to the clift framework
CLI_DIR Path to your CLI project
LOG_THEME Active logging theme

Global Flags

These flags are handled by the router and available to all commands:

Flag Short Description
--help -h Show help for the current command
--version -V Print CLI version
--verbose -v Enable debug log output
--quiet -q Suppress info/success messages
--no-color Disable colored output

The NO_COLOR environment variable is also respected per no-color.org.

Logging

Seven built-in themes control how log messages are formatted:

Theme Example output
icons → message
icons-color → message (colored)
brackets [INFO] message
brackets-color [INFO] message (colored)
minimal message
minimal-color message (colored)
custom User-defined via LOG_FMT_* vars

Log functions available in your scripts after sourcing log.sh:

log_info "informational"
log_warn "warning"
log_error "something broke"
log_success "done"
log_debug "only shown with --verbose"
log_suggest "hint text (dimmed, suppressed by --quiet)"
die "fatal error" 1
clift_exit 2 "invalid arg"   # log+exit helper; prefer over die+exit in new scripts

Custom format strings

For the custom theme, define format strings in your .env:

LOG_FMT_INFO=":: %s"
LOG_FMT_WARN="!! %s"
LOG_FMT_ERROR="** %s"
LOG_FMT_SUCCESS="++ %s"
LOG_FMT_DEBUG=".. %s"

Custom color schemes

Any theme's colors can be overridden via LOG_CLR_* env vars in .env. Values are ANSI escape sequences.

Variable Controls Default
LOG_CLR_INFO Info messages Blue (\033[0;34m)
LOG_CLR_WARN Warnings Yellow (\033[0;33m)
LOG_CLR_ERROR Errors Red (\033[0;31m)
LOG_CLR_SUCCESS Success messages Green (\033[0;32m)
LOG_CLR_DEBUG Debug messages Cyan (\033[0;36m)
LOG_CLR_DIM Suggestions Dim (\033[2m)

Example: Dracula color scheme

# .env — Dracula palette over the default icons-color theme
LOG_CLR_INFO=\033[38;2;139;233;253m
LOG_CLR_WARN=\033[38;2;255;184;108m
LOG_CLR_ERROR=\033[38;2;255;85;85m
LOG_CLR_SUCCESS=\033[38;2;80;250;123m
LOG_CLR_DEBUG=\033[38;2;189;147;249m
LOG_CLR_DIM=\033[38;2;98;114;164m

Example: Catppuccin Mocha

LOG_CLR_INFO=\033[38;2;137;180;250m
LOG_CLR_WARN=\033[38;2;249;226;175m
LOG_CLR_ERROR=\033[38;2;243;139;168m
LOG_CLR_SUCCESS=\033[38;2;166;227;161m
LOG_CLR_DEBUG=\033[38;2;203;166;247m
LOG_CLR_DIM=\033[38;2;108;112;134m

Shell Completions

Bash and zsh completion is wired automatically at setup:cli time and again when the framework is updated. The generated script is cache-derived, so subcommand and flag candidates stay in sync with your Taskfiles without a rebuild step.

# Bash (add to ~/.bashrc — setup:cli does this for you)
eval "$(mycli completion bash)"

# Zsh (add to ~/.zshrc — setup:cli does this for you)
eval "$(mycli completion zsh)"

mycli --help always prints a one-line install hint so new users can't miss it.

For flag values that are only known at runtime (regions, kube contexts, branches), drop a clift_complete_<task>_<flag> function into $CLI_DIR/.clift/overrides/completion.sh (or per-command under cmds/<cmd>/overrides/completion.sh) and clift wires it automatically — no schema change.

See docs/cli/completion.md for the setup-time installer, static-candidate rules, and dynamic flag-value completers.

Team Setup

When sharing a CLI with your team, include the .clift.yaml file in your CLI's directory. It documents what dependencies your CLI needs:

name: my-team-cli
version: 1.0.0
description: "Internal deployment tools"

dependencies:
  required:
    - jq
    - kubectl
  optional:
    - gum
    - fzf

Teammates can read this file to know what to install before using the CLI.

Updating

mycli update    # pulls latest framework from git

Versioning

clift CLIs can be versioned and distributed via cfgd. Versioning is opt-in — enable it during setup or add it later.

Enable During Setup

clift init ~/my-cli --cfgd
# or, equivalently:
CFGD_VERSIONING=true clift init ~/my-cli

This installs cfgd (if missing), configures the CLI as a cfgd module, and adds the version:* commands.

Add to an Existing CLI

task --taskfile ~/.clift/Taskfile.yaml setup:versioning -- ~/my-cli

Or from within the CLI itself (if version namespace is manually included):

mycli version setup

Version Commands

Once versioning is enabled, these commands are available:

Command Description
version Show current version and cfgd status
version setup Set up cfgd versioning (installs cfgd if needed)
version upgrade Upgrade to the latest version via cfgd
version update Alias for version upgrade
version set <ver> Pin to a specific version (e.g., v1.2.3)

Standalone vs Config Repo Mode

By default, version setup treats the CLI as a standalone module in its own git repo. To add it to an existing cfgd config repo instead, set these environment variables:

Variable Description
CFGD_CONFIG_DIR Path to your cfgd config repo (e.g., ~/dotfiles)
CFGD_PROFILES Comma-separated profiles to add the module to (e.g., dev,work)
CFGD_CONFIG_DIR=~/dotfiles CFGD_PROFILES=dev mycli version setup

Publishing a Version

After versioning is set up, tag releases using cfgd's convention:

git tag "mycli/v1.0.0"
git push origin --tags

Consumers upgrade with mycli version upgrade or pin with mycli version set v1.0.0.

cfgd Integration

cfgd is a declarative machine configuration tool. When available, clift uses it as a backend for dependency management, updates, and drift detection. cfgd is never required. Everything works without it.

Framework Module

To manage the clift framework with cfgd, copy cfgd/clift/module.yaml into your cfgd config's modules/ directory:

cp ~/.clift/cfgd/clift/module.yaml ~/dotfiles/modules/clift/module.yaml

This module declares go-task, jq, yq, and gum as packages and clones the framework repo. Add it to your profile:

# profiles/work.yaml
spec:
  modules:
    - clift

Then cfgd apply installs everything.

CLI Modules

When you bootstrap a CLI with CFGD_VERSIONING=true clift init ..., a module.yaml is generated alongside it. This module depends on clift, declares your CLI's dependencies from .clift.yaml, and configures the shell alias. Copy it to your cfgd config to distribute the CLI to other machines or teammates. Without CFGD_VERSIONING=true, no module.yaml is created.

Update Modes

When cfgd manages your framework installation, mycli update detects this and directs you to use cfgd instead. How updates work depends on how you pin the module:

Pin style module.yaml source How to update What you get
Tag ...git@v0.2.0 cfgd module upgrade clift --ref v0.3.0 Explicit version bumps
Latest (any) cfgd module upgrade clift Bumps lockfile to repo HEAD

Both styles lock to a specific commit SHA in modules.lock. Between upgrades, the version never changes — even if new commits are pushed upstream.

Daemon Behavior

If the cfgd daemon is enabled, it periodically verifies that managed installations match their lockfile pin:

  • Dependency healing — if jq or gum gets uninstalled, the daemon reinstalls them on the next reconcile cycle
  • File protection — if framework files are accidentally modified, the daemon restores them to the pinned state
  • No surprise updates — the daemon enforces the current pin, it does not pull new versions. Updates are always explicit via cfgd module upgrade

What happens on drift depends on your driftPolicy (Auto, NotifyOnly, or Prompt) — see cfgd docs.

Without cfgd

If cfgd is not installed, nothing changes:

  • deps.sh checks for jq and yq (required) and gum (optional)
  • mycli update uses git pull as before
  • .clift.yaml documents dependencies for humans to install manually

Example

See examples/bm/ — a small bookmark manager (5 commands, ~575 LOC bash) that demonstrates the framework's primary surface end-to-end. It's deliberately small enough to read in one sitting.

$ bm add https://taskfile.dev --name task --tag rust --tag build
✓ task

$ bm list --tag rust --format json
[
  {
    "name": "task",
    "url": "https://taskfile.dev",
    "description": "",
    "tags": ["rust", "build"],
    "added_at": "2026-05-06T13:21:53Z"
  }
]

$ bm --profile work add https://example.com --name w
✓ w
$ bm --profile home list                # isolated; empty
NAME  URL  TAGS  AGE

Features exercised:

  • 5 commands (add, list, open, rm, tag) with subcommand alias (lslist)
  • String, bool, int, list, choices, pattern flags
  • Mutex group on tag --add vs --remove
  • Persistent --profile flag (per-profile store isolation)
  • Dynamic completion (bm open <TAB> enumerates the active store)

For a full production-grade reference example exercising the entire framework surface — multi-language native helpers (Go, Rust, Python), pluggable integrations (calendar providers, notification channels), profile-aware state, NDJSON contracts — see tj-smith47/jarvis.

Error UX

clift provides production-grade error messages out of the box — no code required:

$ bm lstt
error: unknown command 'lstt'
  did you mean 'list'?
  run 'bm --help' for commands

$ bm list --format bogus
error: flag '--format' requires one of: table, json, yaml, got 'bogus'

$ bm tag mybook --add foo --remove bar
error: flags '--add', '--remove' in group 'mutation' are mutually exclusive

The full reference example tj-smith47/jarvis demonstrates more elaborate scenarios — pattern-validated flags, required-together groups, deprecated-flag warnings:

$ jarvis remind 'standup' --in 5
error: flag '--in' requires value matching pattern '^[0-9]+[smhd]$', got '5'

$ jarvis remind 'standup' --in 10m --at 13:00
error: flags '--in', '--at' in group 'when' are mutually exclusive

Development

bats tests/*.bats           # full suite — see CI for current count
scripts/coverage.sh          # line coverage via kcov (79%+)
scripts/benchmark.sh         # overhead benchmark (~50ms)
shellcheck lib/**/*.sh       # lint

Recording demo GIFs

Demo GIFs in the README are recorded with VHS. Tape files live in .vhs/, GIFs are output to .vhs/gifs/.

# Install VHS (requires Go 1.22+)
go install github.com/charmbracelet/vhs@latest

# Record all demos (requires a TTY)
scripts/record-demos.sh

# Or record a single tape
vhs .vhs/hero.tape

After recording, commit the updated GIFs so they render on GitHub.

License

MIT -- see LICENSE.

About

Batteries-included framework for building custom CLIs with go-task

Resources

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages