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
"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.
# 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?
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
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
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.
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:
Compare to: - Rails: 50+ directories, complex structure - Django: App-per-feature, nested structure - CGI: No structure (chaos) - dbbasic: 5 directories, flat and simple
# 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.
# Same thing, with CLI helper
dbbasic new blog myblog
cd myblog
python app.py
The CLI creates the standard structure automatically.
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.
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 listLists 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 runRuns 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 testRuns tests.
Usage:
dbbasic test
# Same as: pytest
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
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.
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:tail
# Tail application logs
dbbasic logs:search "ERROR"
# Search logs for pattern
dbbasic logs:compress
# Compress old log files
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: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)
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.
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
dbbasic scaffolddbbasic 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.
#!/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.
Lines of code: ~10,000+
Features: Generators, console, server, migrations, etc.
Philosophy: Framework does everything
Lines of code: ~5,000+
Features: Extensible, discoverable
Philosophy: Apps extend the CLI
Lines of code: 0
Features: None
Philosophy: Just write scripts
Lines of code: ~100
Features: Templates, extensible
Philosophy: Thin wrapper around Unix tools
Rails: CLI does everything (generate, run, migrate, console, etc.)
dbbasic: CLI does one thing - helps you start projects and discovers module commands.
Rails: Generates code you must edit carefully
dbbasic: Generates code you're expected to modify freely
Rails: Binary formats, complex migrations
dbbasic: TSV files, readable by any tool
Rails: Must use rails command
dbbasic: CLI is optional, can use git/cp/sed directly
# 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
# Convenience wrapper
dbbasic new blog myblog
cd myblog
python app.py
Same result, CLI just automates the Unix 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
# 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
pip install dbbasic-cli
That's it. No configuration needed.
# 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.
# 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
dbbasic-cli follows the Unix philosophy:
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.
dbbasic plugin install stripe-paymentsdbbasic deploy productionNext Steps: Implement basic CLI, test with blog template, ship.
Remember: CLI is for convenience. The framework works without it.