diff options
| author | Danilo M. <danix@danix.xyz> | 2026-06-30 15:09:34 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-06-30 15:09:34 +0200 |
| commit | 9c15e172eb5b50796eb050cc5704471bce09e024 (patch) | |
| tree | 01433901ddd2bb8db3f2498a225c49faae26d295 | |
| parent | 93dbbe18e934d87ebf6ae6c614bb26f0e9e5afa5 (diff) | |
| download | firefly-cli-0.2.1.tar.gz firefly-cli-0.2.1.zip | |
help, completion: descriptive help text and bash completionv0.2.1
Add group/leaf descriptions to argparse help and richer command help
strings. Add generated bash completion (completions/firefly.bash) plus
its generator (scripts/gen_completion.py), wired into the command
checklist in CLAUDE.md and documented in the README. Bump to 0.2.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| -rw-r--r-- | CLAUDE.md | 2 | ||||
| -rw-r--r-- | README.md | 20 | ||||
| -rw-r--r-- | completions/firefly.bash | 64 | ||||
| -rw-r--r-- | firefly_cli/__init__.py | 2 | ||||
| -rw-r--r-- | firefly_cli/cli.py | 15 | ||||
| -rw-r--r-- | firefly_cli/commands/account.py | 8 | ||||
| -rw-r--r-- | firefly_cli/commands/auth.py | 4 | ||||
| -rw-r--r-- | firefly_cli/commands/category.py | 2 | ||||
| -rw-r--r-- | firefly_cli/commands/tag.py | 2 | ||||
| -rw-r--r-- | firefly_cli/commands/transaction.py | 8 | ||||
| -rw-r--r-- | pyproject.toml | 2 | ||||
| -rw-r--r-- | scripts/gen_completion.py | 115 |
12 files changed, 228 insertions, 16 deletions
@@ -25,6 +25,8 @@ The Firefly III source is cloned at `../GITHUB/firefly-iii/` for reference only 3. If it is a new module, add it to the import line in `commands/__init__.py`. 4. Write a unit test under `tests/unit/` (mock `ctx.client` / `ctx.resolver`). 5. Update `SKILL.md` (the agent-operating guide) with the new command. +6. Regenerate bash completion (it is generated, not hand-edited): + `python scripts/gen_completion.py > completions/firefly.bash`. No other files change. ## Conventions @@ -12,6 +12,26 @@ pip install -e . Requires Python 3.11 or newer. No third-party runtime dependencies. +### Bash completion + +A completion script lives at `completions/firefly.bash`. Enable it by sourcing +it from your shell profile, or install it system-wide: + +```bash +# per-user: add to ~/.bashrc +source /path/to/firefly-cli/completions/firefly.bash + +# or system-wide +sudo cp completions/firefly.bash /usr/share/bash-completion/completions/firefly +``` + +It is generated from the command registry, never hand-edited. Regenerate after +adding or changing commands: + +```bash +python scripts/gen_completion.py > completions/firefly.bash +``` + ## Configuration Provide your Firefly III URL and a personal access token in either way: diff --git a/completions/firefly.bash b/completions/firefly.bash new file mode 100644 index 0000000..c118f52 --- /dev/null +++ b/completions/firefly.bash @@ -0,0 +1,64 @@ +# bash completion for firefly (firefly-cli) +# Install: source this file, or drop it in /etc/bash_completion.d/ or +# /usr/share/bash-completion/completions/firefly +# +# Generated by scripts/gen_completion.py -- do not edit by hand. +# Regenerate when commands change (see CLAUDE.md): +# python scripts/gen_completion.py > completions/firefly.bash + +_firefly() { + local cur prev words cword + _init_completion 2>/dev/null || { + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + words=("${COMP_WORDS[@]}") + cword=$COMP_CWORD + } + + local global_opts="--human --url --token -h --help" + local groups="auth account category tag tx" + + # Find the group and leaf among the words (skip the program name at 0). + local group="" leaf="" i + for ((i=1; i < cword; i++)); do + local w="${words[i]}" + case "$w" in + -*) ;; # an option, skip + --url|--token) ((i++));; # option that takes a value + *) + if [[ -z $group ]]; then group="$w" + elif [[ -z $leaf ]]; then leaf="$w" + fi + ;; + esac + done + + # Leaf-specific options. + local leaf_opts="" + case "$group $leaf" in + "auth set") leaf_opts="--token --url";; + "account create") leaf_opts="--currency --opening-balance --type";; + "account list") leaf_opts="--type";; + "tx add") leaf_opts="--category --date --desc --from --tags --to --type";; + "tx list") leaf_opts="--account --limit --since --until";; + esac + + # Leaves per group. + local leaves="" + case "$group" in + auth) leaves="set test";; + account) leaves="balance create get list";; + category) leaves="list";; + tag) leaves="list";; + tx) leaves="add get list search";; + esac + + if [[ -z $group ]]; then + COMPREPLY=($(compgen -W "$groups $global_opts" -- "$cur")) + elif [[ -z $leaf ]]; then + COMPREPLY=($(compgen -W "$leaves" -- "$cur")) + else + COMPREPLY=($(compgen -W "$leaf_opts --help" -- "$cur")) + fi +} +complete -F _firefly firefly diff --git a/firefly_cli/__init__.py b/firefly_cli/__init__.py index f50a029..e025931 100644 --- a/firefly_cli/__init__.py +++ b/firefly_cli/__init__.py @@ -2,4 +2,4 @@ # Copyright (C) 2026 Danilo M. <danix@danix.xyz> # Licensed under the GNU General Public License v2.0 only. -__version__ = "0.1.0" +__version__ = "0.2.1" diff --git a/firefly_cli/cli.py b/firefly_cli/cli.py index d547455..ad54504 100644 --- a/firefly_cli/cli.py +++ b/firefly_cli/cli.py @@ -11,6 +11,16 @@ import firefly_cli.commands # noqa: F401 triggers registration # Commands that must work without a configured client. _NO_CLIENT = {"auth set"} +# Short blurb per command group, shown in `firefly --help` and as the +# description in `firefly <group> --help`. +_GROUP_HELP = { + "auth": "configure and verify the Firefly III connection (url + token)", + "account": "list, create, and inspect accounts (asset, expense, revenue)", + "category": "list categories (categories auto-create when used on a tx)", + "tag": "list tags (tags auto-create when attached to a tx)", + "tx": "record, list, and search transactions", +} + def _build_parser(): parser = argparse.ArgumentParser(prog="firefly", description="CLI for Firefly III") @@ -25,9 +35,10 @@ def _build_parser(): for cmd in registry.all_commands(): group, _, leaf = cmd.name.partition(" ") if group not in groups: - gp = sub.add_parser(group) + blurb = _GROUP_HELP.get(group) + gp = sub.add_parser(group, help=blurb, description=blurb) groups[group] = gp.add_subparsers(dest="_leaf", required=True) - lp = groups[group].add_parser(leaf, help=cmd.help) + lp = groups[group].add_parser(leaf, help=cmd.help, description=cmd.help) if cmd.args: cmd.args(lp) lp.set_defaults(_handler=cmd.handler, _cmdname=cmd.name) diff --git a/firefly_cli/commands/account.py b/firefly_cli/commands/account.py index 0e86b2b..9dbfab6 100644 --- a/firefly_cli/commands/account.py +++ b/firefly_cli/commands/account.py @@ -9,7 +9,7 @@ _CREATE_TYPES = ("asset", "expense", "revenue") def _list_args(p): p.add_argument("--type", help="filter: asset, expense, revenue, liability, ...") -@registry.command("account list", help="list accounts", args=_list_args) +@registry.command("account list", help="list accounts; optionally filter by --type", args=_list_args) def cmd_list(args, ctx): params = {"type": args.type} if getattr(args, "type", None) else None resp = ctx.client.request("GET", "/api/v1/accounts", params=params) @@ -24,7 +24,7 @@ def _create_args(p): help="initial balance (asset accounts); dated today") p.add_argument("--currency", default=None, help="currency code, e.g. EUR") -@registry.command("account create", help="create an account", args=_create_args) +@registry.command("account create", help="create an asset, expense, or revenue account", args=_create_args) def cmd_create(args, ctx): if args.type not in _CREATE_TYPES: raise FireflyError( @@ -46,13 +46,13 @@ def cmd_create(args, ctx): def _name_arg(p): p.add_argument("account", help="account name or id") -@registry.command("account get", help="show one account", args=_name_arg) +@registry.command("account get", help="show full details for one account (name or id)", args=_name_arg) def cmd_get(args, ctx): acc = ctx.resolver.account(args.account) output.emit(acc, human=ctx.human) return 0 -@registry.command("account balance", help="show account balance", args=_name_arg) +@registry.command("account balance", help="show current balance for one account (name or id)", args=_name_arg) def cmd_balance(args, ctx): acc = ctx.resolver.account(args.account) output.emit({"id": acc["id"], "name": acc.get("name"), diff --git a/firefly_cli/commands/auth.py b/firefly_cli/commands/auth.py index d2319e9..99e79c9 100644 --- a/firefly_cli/commands/auth.py +++ b/firefly_cli/commands/auth.py @@ -6,7 +6,7 @@ def _set_args(p): p.add_argument("--url", help="Firefly III base URL") p.add_argument("--token", help="Personal Access Token") -@registry.command("auth set", help="write url+token to config", args=_set_args) +@registry.command("auth set", help="save url + token to the config file (no API call)", args=_set_args) def cmd_set(args, ctx): url = args.url or input("Firefly III URL: ").strip() token = args.token or getpass.getpass("Personal Access Token: ").strip() @@ -14,7 +14,7 @@ def cmd_set(args, ctx): output.emit({"written": str(path)}, human=ctx.human) return 0 -@registry.command("auth test", help="verify connectivity and token") +@registry.command("auth test", help="check the configured url + token reach Firefly") def cmd_test(args, ctx): resp = ctx.client.request("GET", "/api/v1/about") output.emit(resp.get("data", resp), human=ctx.human) diff --git a/firefly_cli/commands/category.py b/firefly_cli/commands/category.py index a7ddbe8..d4bad93 100644 --- a/firefly_cli/commands/category.py +++ b/firefly_cli/commands/category.py @@ -1,7 +1,7 @@ # Copyright (C) 2026 Danilo M. <danix@danix.xyz> GPL-2.0-only from firefly_cli import registry, output -@registry.command("category list", help="list categories") +@registry.command("category list", help="list all categories known to Firefly") def cmd_list(args, ctx): resp = ctx.client.request("GET", "/api/v1/categories") output.emit(output.unwrap(resp), human=ctx.human) diff --git a/firefly_cli/commands/tag.py b/firefly_cli/commands/tag.py index 668c4ae..beed56c 100644 --- a/firefly_cli/commands/tag.py +++ b/firefly_cli/commands/tag.py @@ -1,7 +1,7 @@ # Copyright (C) 2026 Danilo M. <danix@danix.xyz> GPL-2.0-only from firefly_cli import registry, output -@registry.command("tag list", help="list tags") +@registry.command("tag list", help="list all tags known to Firefly") def cmd_list(args, ctx): resp = ctx.client.request("GET", "/api/v1/tags") output.emit(output.unwrap(resp), human=ctx.human) diff --git a/firefly_cli/commands/transaction.py b/firefly_cli/commands/transaction.py index be8d281..67d8b71 100644 --- a/firefly_cli/commands/transaction.py +++ b/firefly_cli/commands/transaction.py @@ -30,7 +30,7 @@ def _add_args(p): p.add_argument("--type", default=None, help="withdrawal|deposit|transfer (overrides inference)") -@registry.command("tx add", help="record a transaction", args=_add_args) +@registry.command("tx add", help="record a transaction; source/destination resolve to accounts, category/tags auto-create", args=_add_args) def cmd_add(args, ctx): src = ctx.resolver.account(args.source) dst = ctx.resolver.account(args.dest) @@ -60,7 +60,7 @@ def _list_args(p): p.add_argument("--account", default=None, help="filter by account name") p.add_argument("--limit", type=int, default=20) -@registry.command("tx list", help="list transactions", args=_list_args) +@registry.command("tx list", help="list recent transactions (newest first)", args=_list_args) def cmd_list(args, ctx): if args.account: acc = ctx.resolver.account(args.account) @@ -79,7 +79,7 @@ def cmd_list(args, ctx): def _id_arg(p): p.add_argument("id") -@registry.command("tx get", help="show one transaction", args=_id_arg) +@registry.command("tx get", help="show full details for one transaction by id", args=_id_arg) def cmd_get(args, ctx): resp = ctx.client.request("GET", f"/api/v1/transactions/{args.id}") output.emit(output.unwrap(resp), human=ctx.human) @@ -88,7 +88,7 @@ def cmd_get(args, ctx): def _query_arg(p): p.add_argument("query") -@registry.command("tx search", help="search transactions", args=_query_arg) +@registry.command("tx search", help="search transactions by Firefly query string", args=_query_arg) def cmd_search(args, ctx): resp = ctx.client.request("GET", "/api/v1/search/transactions", params={"query": args.query}) diff --git a/pyproject.toml b/pyproject.toml index 99cf7d7..fa4a82f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "firefly-iii-agent" -version = "0.2.0" +version = "0.2.1" description = "CLI tool for agent interaction with Firefly III" readme = "README.md" requires-python = ">=3.11" diff --git a/scripts/gen_completion.py b/scripts/gen_completion.py new file mode 100644 index 0000000..a97f26b --- /dev/null +++ b/scripts/gen_completion.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Danilo M. <danix@danix.xyz> GPL-2.0-only +"""Generate completions/firefly.bash from the live command registry. + + python scripts/gen_completion.py > completions/firefly.bash + +No drift: groups, leaves, and per-leaf flags are read straight off the +argparse subparsers the registry builds. +""" +import argparse +from collections import defaultdict + +from firefly_cli import registry +import firefly_cli.commands # noqa: F401 triggers registration + +GROUP_ORDER = ["auth", "account", "category", "tag", "tx"] + + +def collect(): + groups = defaultdict(dict) # group -> {leaf: [--flag, ...]} + for c in registry.all_commands(): + g, _, leaf = c.name.partition(" ") + p = argparse.ArgumentParser() + if c.args: + c.args(p) + opts = [] + for a in p._actions: + opts += [s for s in a.option_strings + if s.startswith("--") and s != "--help"] + groups[g][leaf] = sorted(opts) + return groups + + +def render(groups): + order = [g for g in GROUP_ORDER if g in groups] + order += [g for g in sorted(groups) if g not in GROUP_ORDER] + + leaf_cases, group_cases = [], [] + for g in order: + leaves = " ".join(sorted(groups[g])) + group_cases.append(f' {g}) leaves="{leaves}";;') + for leaf in sorted(groups[g]): + flags = " ".join(groups[g][leaf]) + if flags: + leaf_cases.append( + f' "{g} {leaf}")'.ljust(28) + f'leaf_opts="{flags}";;') + + return TEMPLATE.format( + groups=" ".join(order), + leaf_cases="\n".join(leaf_cases), + group_cases="\n".join(group_cases), + ) + + +TEMPLATE = '''# bash completion for firefly (firefly-cli) +# Install: source this file, or drop it in /etc/bash_completion.d/ or +# /usr/share/bash-completion/completions/firefly +# +# Generated by scripts/gen_completion.py -- do not edit by hand. +# Regenerate when commands change (see CLAUDE.md): +# python scripts/gen_completion.py > completions/firefly.bash + +_firefly() {{ + local cur prev words cword + _init_completion 2>/dev/null || {{ + cur="${{COMP_WORDS[COMP_CWORD]}}" + prev="${{COMP_WORDS[COMP_CWORD-1]}}" + words=("${{COMP_WORDS[@]}}") + cword=$COMP_CWORD + }} + + local global_opts="--human --url --token -h --help" + local groups="{groups}" + + # Find the group and leaf among the words (skip the program name at 0). + local group="" leaf="" i + for ((i=1; i < cword; i++)); do + local w="${{words[i]}}" + case "$w" in + -*) ;; # an option, skip + --url|--token) ((i++));; # option that takes a value + *) + if [[ -z $group ]]; then group="$w" + elif [[ -z $leaf ]]; then leaf="$w" + fi + ;; + esac + done + + # Leaf-specific options. + local leaf_opts="" + case "$group $leaf" in +{leaf_cases} + esac + + # Leaves per group. + local leaves="" + case "$group" in +{group_cases} + esac + + if [[ -z $group ]]; then + COMPREPLY=($(compgen -W "$groups $global_opts" -- "$cur")) + elif [[ -z $leaf ]]; then + COMPREPLY=($(compgen -W "$leaves" -- "$cur")) + else + COMPREPLY=($(compgen -W "$leaf_opts --help" -- "$cur")) + fi +}} +complete -F _firefly firefly +''' + + +if __name__ == "__main__": + print(render(collect()), end="") |
