| name | cli-command-style |
| description | Design and review the project's CLI commands so they follow the modular router-plus-libexec pattern with consistent help, validation, and exit codes. Use when adding a new subcommand to `pi-usb-backup`, modifying an existing one, or reviewing command-line UX. |
| argument-hint | Add or review a pi-usb-backup subcommand |
| user-invocable | true |
| disable-model-invocation | false |
CLI Command Style
Every command exposed by this project (currently pi-usb-backup and its subcommands) follows the same shape, so users always get predictable help text, predictable error behavior, and predictable exit codes. This skill captures that shape so new subcommands stay coherent with the existing ones.
When to Use
- Adding a new subcommand to
pi-usb-backup.
- Modifying the CLI surface of an existing subcommand (flags, positional actions, help text, exit codes).
- Reviewing PRs that change command-line behavior.
- Designing a new top-level command in this project (the router-plus-libexec layout below applies as-is).
File Layout
Repo paths mirror deploy paths. The only divergence is the .sh extension on the router, which is stripped at install time so the command can be invoked as a bare name.
| Repo path | Deployed path | Notes |
|---|
usr/local/sbin/<name>.sh | /usr/local/sbin/<name> | Router / entry point. Installer strips .sh at deploy. |
usr/local/libexec/<name>/<subcmd>.sh | /usr/local/libexec/<name>/<subcmd>.sh | One file per subcommand. Same name in repo and on disk. |
Where <name> is the program name (currently pi-usb-backup).
Router Contract
The router lives at usr/local/sbin/<name>.sh and is intentionally minimal.
- Shebang
#!/bin/bash, set -euo pipefail.
- Constants only:
PROG (the program name) and LIBEXEC_DIR (the deployed libexec path). No subcommand-specific knowledge.
print_help <stream> writes the top-level help text to file descriptor $stream (1 for stdout, 2 for stderr).
dispatch "$@" is the only dispatcher:
- No args →
print_help 1, exit 0.
- First arg in
help, -h, --help → print_help 1, exit 0.
- Unknown command (file
${LIBEXEC_DIR}/<cmd>.sh does not exist or is not executable) → write "<prog>: unknown command: <cmd>" then print_help 2, exit 1.
- Known command →
exec "${LIBEXEC_DIR}/<cmd>.sh" "$@" so the helper inherits stdio and replaces the process; the helper's exit code becomes the router's exit code without an extra wait.
- The router does not parse subcommand args, does not validate flags, and does not call
require_root. All of that lives in the subcommand helpers.
- The top-level help lists subcommands explicitly (it is not auto-discovered from
libexec). Adding a new subcommand requires adding exactly one line to the help's Commands: block in the router. This is the only router edit needed when adding a subcommand.
Canonical implementation: usr/local/sbin/pi-usb-backup.sh.
Subcommand Contract
Each subcommand lives in its own file at usr/local/libexec/<name>/<subcmd>.sh.
- Shebang
#!/bin/bash, set -euo pipefail.
- Self-contained: each helper defines its own
log/die/require_root/have_cmd utilities. Shared helpers are not sourced from a common library today — duplication is tolerated until it actually hurts (see Decision Rules).
print_<subcmd>_help <stream> writes the subcommand help to fd $stream.
parse_and_dispatch "$@" is the single parse-and-act function:
- No args →
print_<subcmd>_help 1, exit 0.
- Iterate
"$@". Recognise only:
- Long flags in the form
--flag=value (with =).
- Positional sub-actions (short, lower-case nouns like
status, info).
- Track a single
action="" variable. Each recognised token sets it. Setting action a second time is an error.
- Any of these conditions →
print_<subcmd>_help 2, exit 1:
- Unknown token (including the space form
--flag value).
- Missing action after iterating all args.
- Duplicate action.
--flag=value where the value is not in the allowed set.
- After parsing succeeds: call
require_root (only if the action mutates state), run any tool-presence checks (have_cmd ip, etc.), then dispatch via a small case on $action to the implementation function.
- Long-flag style is strictly
--flag=value. The space form (--flag value) is explicitly rejected. Document this in the subcommand help line and in the command's section of docs/manual-installation.md if relevant.
- Positional sub-actions occupy the same
action slot as --flag=value. They cannot be combined.
Canonical implementation: usr/local/libexec/pi-usb-backup/wifi.sh (--mode=ap, --mode=client, status, duplicate-action rejection, space-form rejection).
Help Format
Top-level (router)
<prog> — <one-line description>
Usage:
<prog> <command> [options]
<prog> help
Commands:
<subcmd> <one-line description>
help Show this help message
Run '<prog> <command>' with no further arguments to see per-command help.
Examples:
sudo <prog> <subcmd> --flag=value
Subcommand
<prog> <subcmd> — <one-line description>
Usage:
<prog> <subcmd> --flag=value <description>
<prog> <subcmd> status <description>
This command must be run as root (use sudo).
The closing This command must be run as root (use sudo). line is included only if the subcommand actually requires root.
Stream and exit code rules
- Help requested explicitly (no args,
help, -h, --help) → written to stdout, exit code 0.
- Help printed because of invalid input → written to stderr, exit code 1.
- The "unknown command" line printed by the router goes to stderr and precedes the help block.
Canonical Examples
Read both files before adding a new subcommand and follow them as templates.
Decision Rules
- If a new operation logically belongs to an existing subcommand, add it as a new positional or
--flag=value of that subcommand. Do not create a new top-level subcommand for closely-related actions.
- If validation rules drift between subcommands (different error formats, mixed stream behavior), reconcile at PR review. Subcommands should feel like the same program.
- If shared helper code grows non-trivial (more than two helpers duplicating a non-trivial function), factor it into
usr/local/libexec/<name>/_lib.sh and source it from each subcommand. Do not introduce the shared library preemptively — let duplication justify it.
- If
--flag=value form is reasonable but inconvenient (e.g. interactive input, multi-word values), prefer a positional subcommand over relaxing the flag syntax. Keep the rule.
- Help output is part of the contract. Treat help-text changes the same as flag changes — review them carefully.
Completion Checklist
When adding a new subcommand: