Design CLI tools with simple installs, command routing, and layered config.
Copy the install command and let the AI configure it · recommended for beginners
Please install the "cli-packaging-patterns" skill from askskill: 1. Download https://raw.githubusercontent.com/microsoft/amplifier-bundle-skills/main/skills/cli-packaging-patterns/SKILL.md 2. Save it as ~/.claude/skills/cli-packaging-patterns/SKILL.md 3. Reload skills and tell me it's ready
I'm building a Python CLI tool and want users to install it with a one-line uv command. Recommend the project structure, packaging approach, entry-point configuration, and publishing setup, and explain how to ensure the command works right after installation.
A CLI packaging plan for uv distribution, including structure, entry points, and release guidance.
Help me design a CLI dispatch pattern that supports multiple subcommands and runs a default action when the user omits the subcommand. Provide command structure recommendations, argument parsing logic, and example pseudocode.
A clear subcommand and default-action design with implementable parsing logic examples.
I need a configuration system for a CLI tool with precedence of CLI flags > config file > hardcoded defaults. Help define merge rules, error handling, and an example configuration flow.
A three-tier config resolution strategy covering precedence, validation, and a typical implementation flow.
Problem: You want a CLI tool that installs cleanly from a git URL with zero manual steps — no cloning, no virtual env, no PATH fiddling.
Approach: Combine pyproject.toml [project.scripts] with hatchling build backend, a __main__.py dual entry point, and argparse with a default-action subcommand design so mytool and mytool serve do the same thing.
Pattern proven in production across multiple Python CLI tools and web services.
This skill assumes a pure Python CLI distributed via uv tool install. If your project does not fit that profile, use a different pattern:
| Project shape | Use instead |
|---|---|
| Multi-language stack (Python + Node + Docker) | one-line-installer-patterns |
| Raw TS/React app with no Python wrapper | one-line-installer-patterns (or publish to npm) |
| Tool that bootstraps system prerequisites | one-line-installer-patterns |
| Containerized multi-service app | Ship docker-compose.yml; see container-orchestration-patterns |
| Single static binary (Go/Rust) | GitHub releases + curl -L .../bin -o ~/.local/bin/tool |
If the project IS a pure Python CLI, the rest of this skill applies.
[project.scripts] — Not setuptoolsUse hatchling as the build backend. The entry point declaration is:
[project.scripts]
my-tool = "my_tool.cli:main"
Why hatchling: simpler than setuptools, no setup.py, no MANIFEST.in. The [tool.hatch.build.targets.wheel] section lets you exclude test files from the published wheel:
[tool.hatch.build.targets.wheel]
packages = ["my_tool"]
exclude = ["my_tool/tests", "my_tool/frontend/tests"]
__main__.py dual entry — python -m always worksInclude a minimal __main__.py so the tool works even when the script entry point isn't on PATH:
# my_tool/__main__.py
"""Allow running as: python -m my_tool"""
from my_tool.cli import main
main()
This matters because uv tool install creates a wrapper script, but during development or in edge cases, python -m my_tool is a reliable fallback. Service management code should use this as a fallback too:
def _resolve_tool_bin() -> str:
which = shutil.which("my-tool")
if which:
return which
return f"{sys.executable} -m my_tool"
tool equals tool serveUse argparse with shared flags on the root parser AND on the serve subcommand, so the bare command runs the server:
def main() -> None:
parser = argparse.ArgumentParser(prog="my-tool", ...)
_add_serve_flags(parser) # flags on root parser
sub = parser.add_subparsers(dest="command")
serve_parser = sub.add_parser("serve", help="Start the server (default)")
_add_serve_flags(serve_parser) # same flags on 'serve' subcommand
The dispatch at the bottom falls through to serve() when no subcommand is given:
else:
serve(host=args.host, port=args.port, ...)
The serve() function resolves every setting with the same pattern — CLI flag wins, then settings file, then hardcoded default:
settings = load_settings()
host = host if host is not None else settings.get("host", "127.0.0.1")
port = port if port is not None else settings.get("port", 8088)
log_level = log_level if log_level is not None else settings.get("log_level", "info")
Using None as the argparse default (not a value like "127.0.0.1") is critical — it distinguishes "user didn't pass a flag" from "user explicitly set it."
uv tool install git+https://... compatibilityNo special config needed — hatchling + [project.scripts] is all uv requires. The install command is:
uv tool install git+https://github.com/yourorg/your-tool
…
Research, plan, and execute large code changes with parallel PR-producing agents.
Analyze images, extract text, and answer visual questions with LLM vision models.
Design robust config and state file handling with safe defaults and crash recovery.
Get skeptical, practical guidance on architecture, legacy refactors, and tooling decisions.
Design auth and TLS patterns for smooth local use and secure remote access.
Design extensible systems with runtime plugin discovery, registries, and validation.
Design and evaluate CLI tools and developer SDK architecture and usability.
Design and evaluate one-line installer scripts with security and fit considerations.
Improve APM CLI command UX, help text, and first-run flows.
Create a composable CLI from docs, specs, SDKs, apps, or scripts.
Turn MCP, OpenAPI, or GraphQL services into CLIs with no codegen.
Run and manage CLI commands in natural language with recursive help parsing.