← Back to Modules

dbbasic-cli Specification

Version: 1.0 Status: Specification Author: DBBasic Project Date: October 2025

Links: - PyPI: https://pypi.org/project/dbbasic-cli/ - GitHub: https://github.com/askrobots/dbbasic-cli-new - Specification: http://dbbasic.com/cli-spec


Philosophy

"Tools, not frameworks. Scripts, not magic."

The Unix way is small composable tools. CGI was successful because it was just scripts in a directory. No build step, no generators, no magic.

Design Principles

  1. Unix Tools First: Use git, cp, sed - not custom generators
  2. Scripts Over Frameworks: CLI is thin wrapper around standard tools
  3. Optional: Can use dbbasic without CLI
  4. Extensible: Modules can add commands (like Django's manage.py)
  5. No Magic: Everything is visible, debuggable

The CGI Way (1993-2005)

How CGI Apps Were "Generated"

# No generator needed
mkdir -p /var/www/cgi-bin/myblog
cd /var/www/cgi-bin/myblog

# Create script
cat > index.cgi << 'EOF'
#!/bin/bash
echo "Content-Type: text/html"
echo ""
echo "<h1>My Blog</h1>"
EOF

chmod +x index.cgi

# Done. That's it.

What made this work: - Just files in a directory - No build step - No framework installation - Just execute the script

The "generator" was: - mkdir (make directory) - cat or vi (create file) - chmod +x (make executable)

This is the ideal. Can we get close?


The Rails Way (2005+)

Rails Generators

rails new blog
# Generates 50+ files, tons of directories

rails generate model Post title:string body:text
# Generates:
# - app/models/post.rb
# - db/migrate/20231009_create_posts.rb
# - test/models/post_test.rb
# - test/fixtures/posts.yml

rails generate controller Posts index show
# Generates:
# - app/controllers/posts_controller.rb
# - app/views/posts/index.html.erb
# - app/views/posts/show.html.erb
# - test/controllers/posts_controller_test.rb

What Rails got right: - Consistent structure - Generators reduce boilerplate - Extensible (gems can add generators)

What Rails got wrong: - Too much generated code - Magic conventions - Hard to understand what was generated - Can't build app without generators


The Django Way (2008+)

Django manage.py

django-admin startproject myblog
# Generates project structure

python manage.py startapp posts
# Generates app structure

# But the genius is extensibility:
python manage.py migrate          # Built-in
python manage.py runserver        # Built-in
python manage.py custom_command   # Your app adds this

How Django does extensibility:

# myapp/management/commands/custom_command.py
from django.core.management.base import BaseCommand

class Command(BaseCommand):
    def handle(self, *args, **options):
        self.stdout.write("Running custom command")

Now python manage.py custom_command works!

What Django got right: - manage.py is discoverable (lists all commands) - Apps can add commands (extensible) - Standard interface - No magic - just Python modules

What Django got wrong: - Still generates too much boilerplate - Requires Django structure


The Unix Way for DBBasic

Core Insight

CGI taught us: You don't need generators if the framework is simple enough.

Rails taught us: Generators reduce boilerplate.

Django taught us: CLI should be extensible.

Synthesis: Make framework so simple you rarely need generators, but provide thin CLI for convenience.


Standard Directory Structure

Every dbbasic app follows this structure:

myapp/
├── app.py              # Main application file
├── data/               # TSV data files
│   ├── posts.tsv
│   └── users.tsv
├── templates/          # HTML templates
│   ├── layout.html
│   ├── home.html
│   └── posts/
│       ├── list.html
│       ├── show.html
│       └── edit.html
├── static/             # CSS, JS, images
│   ├── css/
│   ├── js/
│   └── img/
├── tests/              # Test files
│   └── test_app.py
├── requirements.txt    # Python dependencies
└── README.md           # Documentation

Why this matters:

  1. Consistency - All dbbasic apps look the same
  2. Familiarity - Know where everything is
  3. Tooling - CLI can work with any app
  4. Onboarding - New developers understand structure instantly

Compare to: - Rails: 50+ directories, complex structure - Django: App-per-feature, nested structure - CGI: No structure (chaos) - dbbasic: 5 directories, flat and simple


The dbbasic-cli Design

Level 1: No CLI Needed (Pure Unix)

# Start a new blog app - pure Unix way
mkdir myblog
cd myblog

# Create standard structure
mkdir -p data templates static/{css,js,img} tests

# Create app
cat > app.py << 'EOF'
from dbbasic_web import app, route, render
from dbbasic_tsv import get

@route('/')
def home():
    posts = get('data/posts.tsv')
    return render('home.html', posts=posts)

if __name__ == '__main__':
    app.run()
EOF

# Create data
echo -e "id\ttitle\tbody" > data/posts.tsv

# Create template
cat > templates/home.html << 'EOF'
<h1>My Blog</h1>
{% for post in posts %}
  <h2>{{ post.title }}</h2>
{% endfor %}
EOF

# Create requirements
cat > requirements.txt << 'EOF'
dbbasic-web
dbbasic-tsv
dbbasic-sessions
EOF

# Run it
python app.py

This works! No CLI tool needed.

Level 2: Optional CLI (Convenience)

# Same thing, with CLI helper
dbbasic new blog myblog
cd myblog
python app.py

The CLI creates the standard structure automatically.

Level 3: Working with Existing Apps

Once an app exists, the CLI can work with it:

cd myblog

# Add new resource
dbbasic generate posts
# Creates: templates/posts/, data/posts.tsv, CRUD routes

# Run tests
dbbasic test

# Start development server
dbbasic run

# Deploy
dbbasic deploy production

The CLI recognizes the standard structure and works with it.


CLI Commands

Core Commands (Thin Wrappers)

dbbasic new <type> <name>

Creates new app from template.

Usage:

dbbasic new blog myblog
dbbasic new api myapi
dbbasic new shop myshop

What it does:

# Equivalent to:
git clone https://github.com/askrobots/dbbasic-examples
cp -r dbbasic-examples/blog myblog
cd myblog
rm -rf .git
sed -i 's/blog/myblog/g' app.py

Just Unix commands!

dbbasic list

Lists available templates.

Usage:

dbbasic list

# Output:
Available templates:
  blog        - WordPress-like blog (~200 lines)
  microblog   - Twitter-like social app (~300 lines)
  api         - REST API (~150 lines)
  shop        - E-commerce (~400 lines)
  intranet    - Basecamp-like project manager (~500 lines)

Use: dbbasic new <type> <name>

dbbasic run

Runs the app (optional, just python app.py works too).

Usage:

dbbasic run
# Same as: python app.py

dbbasic run --port 8000
# Same as: python app.py --port 8000

dbbasic test

Runs tests.

Usage:

dbbasic test
# Same as: pytest

Extension System (Django-style)

How Modules Add Commands

Modules can provide CLI commands by including a cli.py:

# dbbasic_queue/cli.py
def worker_command(args):
    """Run the queue worker"""
    from .worker import process_jobs
    print("Starting queue worker...")
    while True:
        process_jobs()

def stats_command(args):
    """Show queue statistics"""
    from .queue import get_stats
    stats = get_stats()
    print(f"Pending: {stats['pending']}")
    print(f"Failed: {stats['failed']}")

# Register commands
COMMANDS = {
    'queue:worker': worker_command,
    'queue:stats': stats_command,
}

Now these work:

dbbasic queue:worker
# Runs the queue worker

dbbasic queue:stats
# Shows queue stats

Discovery

The CLI auto-discovers commands:

# dbbasic_cli/__init__.py
import importlib
import pkgutil

def discover_commands():
    """Find all CLI commands from installed dbbasic modules"""
    commands = {}

    for finder, name, ispkg in pkgutil.iter_modules():
        if name.startswith('dbbasic_'):
            try:
                module = importlib.import_module(f'{name}.cli')
                if hasattr(module, 'COMMANDS'):
                    commands.update(module.COMMANDS)
            except ImportError:
                pass  # Module doesn't have CLI commands

    return commands

No registration needed! Just install the module, CLI finds it.


Example Module Commands

dbbasic-queue

dbbasic queue:worker
# Start queue worker

dbbasic queue:stats
# Show queue statistics

dbbasic queue:failed
# List failed jobs

dbbasic queue:retry <job_id>
# Retry a failed job

dbbasic-logs

dbbasic logs:tail
# Tail application logs

dbbasic logs:search "ERROR"
# Search logs for pattern

dbbasic logs:compress
# Compress old log files

dbbasic-accounts

dbbasic accounts:create user@example.com
# Create new user account

dbbasic accounts:reset-password user@example.com
# Send password reset email

dbbasic accounts:list
# List all user accounts

dbbasic-tsv

dbbasic tsv:export data/users.tsv users.json
# Export TSV to JSON

dbbasic tsv:import users.json data/users.tsv
# Import JSON to TSV

dbbasic tsv:optimize data/users.tsv
# Optimize TSV file (rebuild indexes)

Generators (Minimal)

The Generator Question

Rails approach: Generate everything - rails g model Post - rails g controller Posts - rails g scaffold Post

CGI approach: No generators, just create files

dbbasic approach: Make framework so simple that most generators aren't needed.

When Generators Make Sense

Don't generate: - Routes (just add function) - Controllers (just add function) - Views (just create HTML file)

Do generate: - Database migrations (if using SQL) - Complex boilerplate (CRUD operations) - Testing scaffolds

Minimal Generator: dbbasic scaffold

dbbasic scaffold posts
# Generates:
# - CRUD functions in app.py (append to existing file)
# - templates/posts/ directory with list/show/edit/new.html
# - data/posts.tsv with example columns

Generated code (app.py additions):

# Posts CRUD
@route('/posts')
def posts_list():
    posts = get('data/posts.tsv')
    return render('posts/list.html', posts=posts)

@route('/posts/<id>')
def posts_show(id):
    post = get('data/posts.tsv', id=id)
    return render('posts/show.html', post=post)

@route('/posts/new', methods=['GET', 'POST'])
def posts_new():
    if request.method == 'POST':
        insert('data/posts.tsv', request.form)
        return redirect('/posts')
    return render('posts/new.html')

@route('/posts/<id>/edit', methods=['GET', 'POST'])
def posts_edit(id):
    post = get('data/posts.tsv', id=id)
    if request.method == 'POST':
        update('data/posts.tsv', id, request.form)
        return redirect(f'/posts/{id}')
    return render('posts/edit.html', post=post)

@route('/posts/<id>/delete', methods=['POST'])
def posts_delete(id):
    delete('data/posts.tsv', id)
    return redirect('/posts')

Simple, readable, modifiable.


Implementation

Core CLI (~100 lines)

#!/usr/bin/env python3
"""
dbbasic CLI - Unix-style tool wrapper
"""
import sys
import os
import shutil
import subprocess
from pathlib import Path

# Template locations
TEMPLATES_REPO = "https://github.com/askrobots/dbbasic-examples"
TEMPLATES_DIR = Path.home() / ".dbbasic" / "templates"

def ensure_templates():
    """Clone templates repo if not present"""
    if not TEMPLATES_DIR.exists():
        print("Downloading templates...")
        subprocess.run([
            'git', 'clone', TEMPLATES_REPO, str(TEMPLATES_DIR)
        ])

def cmd_new(args):
    """Create new app from template"""
    if len(args) < 2:
        print("Usage: dbbasic new <type> <name>")
        return

    template_type = args[0]
    app_name = args[1]

    ensure_templates()

    template_path = TEMPLATES_DIR / template_type
    if not template_path.exists():
        print(f"Template '{template_type}' not found")
        print("Run: dbbasic list")
        return

    # Copy template
    print(f"Creating {app_name} from {template_type} template...")
    shutil.copytree(template_path, app_name)

    # Remove .git
    git_dir = Path(app_name) / ".git"
    if git_dir.exists():
        shutil.rmtree(git_dir)

    print(f"\nCreated {app_name}!")
    print(f"  cd {app_name}")
    print(f"  pip install -r requirements.txt")
    print(f"  python app.py")

def cmd_list(args):
    """List available templates"""
    ensure_templates()

    print("Available templates:\n")
    for template in TEMPLATES_DIR.iterdir():
        if template.is_dir() and not template.name.startswith('.'):
            readme = template / "README.md"
            description = "No description"
            if readme.exists():
                # Extract first line from README
                description = readme.read_text().split('\n')[0].strip('# ')

            print(f"  {template.name:15} - {description}")

    print("\nUsage: dbbasic new <type> <name>")

def cmd_run(args):
    """Run the app"""
    if Path('app.py').exists():
        subprocess.run(['python', 'app.py'] + args)
    else:
        print("No app.py found in current directory")

def discover_commands():
    """Discover commands from installed modules"""
    import importlib
    import pkgutil

    commands = {}
    for finder, name, ispkg in pkgutil.iter_modules():
        if name.startswith('dbbasic_'):
            try:
                module = importlib.import_module(f'{name}.cli')
                if hasattr(module, 'COMMANDS'):
                    commands.update(module.COMMANDS)
            except (ImportError, AttributeError):
                pass

    return commands

def main():
    if len(sys.argv) < 2:
        print("Usage: dbbasic <command> [args]")
        print("\nCore commands:")
        print("  new <type> <name>  - Create new app from template")
        print("  list               - List available templates")
        print("  run                - Run the app")
        print("\nModule commands:")

        commands = discover_commands()
        for name in sorted(commands.keys()):
            print(f"  {name}")

        return

    command = sys.argv[1]
    args = sys.argv[2:]

    # Core commands
    if command == 'new':
        cmd_new(args)
    elif command == 'list':
        cmd_list(args)
    elif command == 'run':
        cmd_run(args)
    else:
        # Try module commands
        commands = discover_commands()
        if command in commands:
            commands[command](args)
        else:
            print(f"Unknown command: {command}")
            print("Run 'dbbasic' to see available commands")

if __name__ == '__main__':
    main()

That's it. ~100 lines.


Comparison to Other CLIs

Rails CLI (rails command)

Lines of code: ~10,000+
Features: Generators, console, server, migrations, etc.
Philosophy: Framework does everything

Django CLI (manage.py)

Lines of code: ~5,000+
Features: Extensible, discoverable
Philosophy: Apps extend the CLI

CGI (no CLI)

Lines of code: 0
Features: None
Philosophy: Just write scripts

dbbasic CLI

Lines of code: ~100
Features: Templates, extensible
Philosophy: Thin wrapper around Unix tools

The Unix Philosophy Applied

Principle 1: Do One Thing Well

Rails: CLI does everything (generate, run, migrate, console, etc.)

dbbasic: CLI does one thing - helps you start projects and discovers module commands.

Principle 2: Expect the Output to Be Input

Rails: Generates code you must edit carefully

dbbasic: Generates code you're expected to modify freely

Principle 3: Use Text Streams

Rails: Binary formats, complex migrations

dbbasic: TSV files, readable by any tool

Principle 4: Tools, Not Frameworks

Rails: Must use rails command

dbbasic: CLI is optional, can use git/cp/sed directly


Examples

Starting a Blog (No CLI)

# Pure Unix way
git clone https://github.com/askrobots/dbbasic-examples
cp -r dbbasic-examples/blog myblog
cd myblog
rm -rf .git
python app.py

Starting a Blog (With CLI)

# Convenience wrapper
dbbasic new blog myblog
cd myblog
python app.py

Same result, CLI just automates the Unix commands.

Using Module Commands

# Queue worker (provided by dbbasic-queue)
dbbasic queue:worker

# View logs (provided by dbbasic-logs)
dbbasic logs:tail

# Create user (provided by dbbasic-accounts)
dbbasic accounts:create admin@example.com

Adding Custom Commands

# myapp/cli.py
def backup_command(args):
    """Backup all data"""
    import shutil
    shutil.copytree('data/', 'backup/')
    print("Data backed up to backup/")

COMMANDS = {
    'backup': backup_command,
}

Now works:

dbbasic backup

Installation

pip install dbbasic-cli

That's it. No configuration needed.


Configuration

Optional: ~/.dbbasicrc

# Default template source
DBBASIC_TEMPLATES=https://github.com/askrobots/dbbasic-examples

# Default port for 'dbbasic run'
DBBASIC_PORT=3000

But this is optional. Works fine without config.


Testing

Test the CLI

# Create test app
dbbasic new blog test-blog
cd test-blog

# Should have:
# - app.py
# - templates/
# - data/
# - requirements.txt

# Should run:
python app.py

# Should have working blog
curl http://localhost:3000

Summary

dbbasic-cli follows the Unix philosophy:

  1. Optional - Can use dbbasic without it
  2. Simple - ~100 lines of code
  3. Thin wrapper - Just automates Unix commands
  4. Extensible - Modules can add commands
  5. No magic - Everything is visible

It's closer to CGI than Rails: - CGI: No CLI, just scripts - dbbasic: Optional CLI, mostly just scripts - Rails: Must use CLI, lots of magic

But takes the good parts from Django: - Extensible (modules add commands) - Discoverable (lists all commands) - Standard interface

Result: Simple tool that helps without getting in the way.


Future Considerations

What We Might Add Later

  1. Plugin system - dbbasic plugin install stripe-payments
  2. Deployment helpers - dbbasic deploy production
  3. Database migrations - If we add SQL support

What We Won't Add

  1. Complex generators - Keep it simple
  2. Magic conventions - Be explicit
  3. Framework lock-in - Stay portable

References


Next Steps: Implement basic CLI, test with blog template, ship.

Remember: CLI is for convenience. The framework works without it.