Skip to main content

Custom Commands

Fastman allows you to extend the CLI with your own commands. This is incredibly powerful for automating tasks like cron jobs, maintenance scripts, data imports, or domain-specific operations.

Creating a Command

To create a new command, use the make:command generator:

fastman make:command {Name}

For example, to create a command that cleans up inactive users:

fastman make:command PruneUsers

This will generate a file at app/console/commands/prune_users.py.

Command Structure

A command is a Python class decorated with @register that inherits from Command. It must implement a handle method.

The signature

The signature property defines how your command is called from the CLI. It supports arguments and options.

  • Command Name: The first part of the signature (e.g., email:send).
  • Arguments: Required parameters enclosed in braces (e.g., {user}).
  • Options: Optional parameters prefixed with -- (e.g., {--dry-run}). You can also specify defaults (e.g., {--days=30}).

The handle Method

This is the entry point of your command. Inside handle, you can access:

  • self.argument(index): Get a positional argument.
  • self.option(name, default): Get a named option.
  • self.flag(name): Check if a boolean flag is present.
  • self.context: Access project context (root path, package manager).

Real-World Example: Pruning Inactive Users

Let's build a command that deletes users who haven't logged in for a certain number of days.

File: app/console/commands/prune_users.py

from datetime import datetime, timedelta
from fastman.cli import Command, register, Output
from app.core.database import SessionLocal
from app.models.user import User

@register
class PruneUsersCommand(Command):
"""
Delete users who haven't logged in for X days.

Usage:
fastman users:prune {--days=30} {--dry-run}
"""

# Signature defines the command name and options
signature = "users:prune {--days=30} {--dry-run}"
description = "Delete inactive users"

def handle(self):
# 1. Parse options
days = int(self.option("days", "30"))
dry_run = self.flag("dry-run")

# 2. Calculate cutoff date
cutoff_date = datetime.utcnow() - timedelta(days=days)

Output.info(f"Finding users inactive since {cutoff_date.date()}...")

# 3. Database operations
db = SessionLocal()
try:
# Find users
query = db.query(User).filter(User.last_login < cutoff_date)
users_to_delete = query.all()
count = len(users_to_delete)

if count == 0:
Output.success("No inactive users found.")
return

# Display users to be deleted
rows = [[str(u.id), u.email, str(u.last_login)] for u in users_to_delete]
Output.table(["ID", "Email", "Last Login"], rows, title="Inactive Users")

# 4. Confirm and Execute
if dry_run:
Output.info(f"[DRY RUN] Would delete {count} users.")
return

if Output.confirm(f"Are you sure you want to delete {count} users?"):
# Bulk delete for efficiency
query.delete(synchronize_session=False)
db.commit()
Output.success(f"Successfully deleted {count} users.")
else:
Output.info("Operation cancelled.")

except Exception as e:
Output.error(f"An error occurred: {e}")
db.rollback()
finally:
db.close()

Running Your Command

Once you save the file, Fastman automatically discovers it.

# List commands to see it registered
fastman list

# Run help (if you implemented it, or just run it)
fastman users:prune --days=60 --dry-run

Output Helpers

The Output class provides styled output methods to make your CLI tools look professional.

MethodDescription
Output.info("msg")Prints blue informational text.
Output.success("msg")Prints green success text with a checkmark.
Output.error("msg")Prints red error text with an X.
Output.warn("msg")Prints yellow warning text.
Output.table(headers, rows)Renders a formatted ASCII table.
Output.confirm("msg")Prompts the user for Yes/No confirmation.
Output.banner()Prints the Fastman banner.