CLI Deep Dives
This document covers architecture and conventions in the CLI (cli/) to help agents work effectively.
Overview
The CLI is a Python package (transformerlab-cli) built with Typer for command parsing and Textual for interactive TUI screens. It communicates with the API server over HTTP — it does not import or depend on the lab-sdk.
The entry point is lab = transformerlab_cli.main:app.
Two UI Paradigms: Typer vs Textual
The CLI has two distinct UI modes. Know when to use each:
Typer Commands (simple, non-interactive)
Use Typer for commands that take input, call the API, and print output. This covers most commands: lab job list, lab task add, lab status, lab login, etc.
Output is formatted with Rich — tables, panels, styled text. Commands must support both --format pretty (default, Rich-formatted) and --format json (machine-readable).
@app.command()
def list(format: str = typer.Option(None)):
"""List all jobs."""
format_type = format or cli_state.output_format
response = api.get("/jobs/list")
if format_type == "json":
print(json.dumps(response.json()))
else:
render_table(response.json(), format_type, columns, title="Jobs")
Textual TUI (interactive, long-running)
Use Textual for interactive screens where the user monitors, navigates, or takes actions in real time. Currently used for the job monitor (lab job monitor) which shows a live-updating job list with log streaming. This command is for human terminal use only and is not suitable for non-interactive automation or AI agents.
The TUI lives in commands/job_monitor/ and is a full Textual App with:
- Custom widgets (JobDetails, JobLogs, modals)
- Textual CSS styling (
styles.tcss) - Background workers via
@workdecorator for auto-refresh - Tokyo Night theme with custom color variables
When to use Textual: Only for screens that need live updates, keyboard navigation, or multi-pane layouts. For everything else, use Typer + Rich.
Project Structure
cli/src/transformerlab_cli/
├── main.py # Typer app, command registration, global callback
├── state.py # CLIState singleton (output_format)
├── commands/ # One module per command group
│ ├── login.py # lab login
│ ├── logout.py # lab logout
│ ├── whoami.py # lab whoami
│ ├── status.py # lab status
│ ├── version.py # lab version
│ ├── config.py # lab config
│ ├── job.py # lab job {list,info,machine-logs,task-logs,request-logs,download,chart,metrics,artifacts,discard,stop,delete,delete-all,monitor,publish ...}
│ ├── task.py # lab task {list,init,add,validate,edit,upload,delete,info,queue,gallery,interactive}
│ ├── interactive.py # implementation for `lab task interactive`
│ ├── provider.py # lab provider {list,add,info,update,delete,check,enable,disable,set-default,clear-default}
│ ├── team.py # lab team {setup, secret} (onboarding wizard + secret subgroup)
│ ├── secret.py # lab team secret {set,list,keys,delete} (registered as a subgroup of `team`)
│ ├── server.py # lab server {install,version,start,stop,restart,update}
│ ├── experiment.py # lab experiment {list,create,delete,set-default,tag,tags}
│ ├── notes.py # lab notes {show,edit,append}
│ ├── dataset.py # lab dataset {list,info,delete,edit,upload,download}
│ ├── model.py # lab model {list,info,create,edit,upload,download,delete}
│ └── job_monitor/ # Textual TUI app (flat layout — widgets and modals sit alongside the app entry point)
│ ├── job_monitor.py # entry point (`run_monitor`)
│ ├── JobMonitorApp.py
│ ├── JobDetails.py
│ ├── JobLogs.py
│ ├── TaskAddModal.py
│ ├── TaskListModal.py
│ ├── ExperimentSelectModal.py
│ ├── GalleryModal.py
│ ├── InteractiveTaskModal.py
│ ├── util.py
│ └── styles.tcss # Textual CSS
├── templates/
│ └── task_init/ # files written by `lab task init`
└── util/
├── api.py # HTTP client wrapper (httpx)
├── auth.py # API key validation, user/team fetching
├── browser_login.py # loopback browser login flow used by `lab login`
├── config.py # ~/.lab/config.json management
├── share.py # Public share link helpers (`job chart --share`, `notes show --share`)
├── shared.py # Constants (BASE_URL, credential paths)
├── ui.py # Rich console, themes, render_table()
├── logo.py # ASCII logo printed by the root help
├── version_check.py # update-available check shown above every command
├── pypi.py # PyPI version helpers
├── telemetry.py # Segment analytics wrapper
├── asset_paths.py # path/glob walking for `dataset/model upload`
├── chunked_upload.py # resumable upload client
└── chunked_download.py # resumable download client
API Communication
The CLI uses httpx (sync mode) via the wrapper in util/api.py:
from transformerlab_cli.util import api
response = api.get("/jobs/list")
response = api.post_json("/tasks/create", json_data={"name": "my-task"})
response = api.delete(f"/jobs/{job_id}")
Available methods: get(), post(), post_json(), post_text(), patch(), delete(), check_server_status().
All requests automatically include:
Authorization: Bearer <api_key>headerX-Team-Idheader from config
Base URL defaults to http://alpha.lab.cloud:8338 and is configurable via lab config set server <url>.
Authentication
The CLI uses API key auth (not cookies):
lab loginprompts for an API key.- The key is validated against
/users/meon the server. - On success, the key is saved to
~/.lab/credentialsand user info to~/.lab/config.json. - All subsequent requests include the key as a Bearer token.
Configuration
Stored in ~/.lab/config.json. Valid keys: server, team_id, team_name, user_email, current_experiment.
Profiles (multiple servers in parallel)
Each lab invocation binds to one profile, bundling a server URL, team, experiment, and
API key. The default profile lives at the legacy root (~/.lab/config.json,
~/.lab/credentials); named profiles live under ~/.lab/profiles/<name>/.
Selection is per-process (no stored "current profile"), so two commands can hit two servers at once:
# Create/authenticate profiles (login is the profile-creating command):
lab --profile prod login --server https://prod.example.com:8338
lab --profile staging login --server https://staging.example.com:8338
# Run against different servers in parallel — fully isolated, safe:
LAB_PROFILE=prod lab job list &
LAB_PROFILE=staging lab job list &
# One-off override without exporting:
lab --profile prod job list
Precedence: --profile <name> (must precede the subcommand, like --format) > LAB_PROFILE
env var > default.
Manage profiles:
lab profile list # all profiles; marks active + which have credentials
lab profile show [name] # config for a profile (defaults to the active one)
lab profile delete <name> # remove a named profile ('default' cannot be deleted)
from transformerlab_cli.util.config import get_config, set_config, check_configs
server = get_config("server")
set_config("team_id", "abc-123", output_format)
check_configs(output_format) # Validates required keys are present
Per-command experiment override
Experiment-scoped commands use current_experiment from config by default, but can be overridden for a single invocation with
--experiment / -e.
Examples:
# Uses current_experiment from ~/.lab/config.json
lab job list
# One-off override (does not change config)
lab job list --experiment exp-b
lab task add ./my-task -e exp-c
lab notes show -e exp-d
lab job monitor -e exp-z
Team setup and secrets
Team-scoped configuration lives under lab team:
lab team setup— an onboarding wizard for a new team. Interactively walks through creating a compute provider (delegating tocreate_provider_interactivelyinprovider.py), optionally setting it as the team default, and adding secrets. It is fully scriptable for non-interactive use via flags:--name,--type,--config,--credentials-file,--secret KEY=VALUE(repeatable),--set-default/--no-set-default, and--check/--no-check(run the provider health check at the end).lab team secret— secret management lives underlab team secretwithset,list,keys, anddelete. (keysshows the platform-recognized secret key names.)
--no-interactive and --format json are global flags on the root callback, so they must come immediately after lab, before the team subgroup.
# Interactive onboarding
lab team setup
# Non-interactive (the global --no-interactive flag comes right after `lab`)
lab --no-interactive team setup --name my-provider --type ssh \
--credentials-file ./creds.json --set-default --secret _HF_TOKEN=hf_xxx --check
# Secrets
lab team secret set _HF_TOKEN hf_xxx
lab team secret list
lab team secret keys
lab team secret delete _HF_TOKEN
# JSON output (the global --format json flag also comes right after `lab`)
lab --format json team secret list
Adding a New Command
- Create
cli/src/transformerlab_cli/commands/mycommand.py:
import typer
from transformerlab_cli import state as cli_state
from transformerlab_cli.util import api
from transformerlab_cli.util.ui import console, render_table
app = typer.Typer()
@app.command()
def list(format: str = typer.Option(None)):
"""List my things."""
format_type = format or cli_state.output_format
response = api.get("/my-things/list")
if response.status_code != 200:
console.print("[error]Error:[/error] Failed to fetch things")
raise typer.Exit(1)
render_table(response.json(), format_type, ["id", "name"], title="Things")
- Register in
main.py:
from .commands.mycommand import app as mycommand_app
app.add_typer(mycommand_app, name="mycommand")
Output Conventions
- Pretty mode (default): Use Rich —
console.print(),render_table(),console.status()spinners. - JSON mode (
--format json): Print raw JSON viaprint(json.dumps(...)). - Errors:
console.print("[error]Error:[/error] message")for pretty;print(json.dumps({"error": "message"}))for JSON. - No results: Call
exit_with_no_results(format_type, message)which exits with code 2. - Loading: Use
with console.status("[bold]Loading...", spinner="dots"):context manager.
Testing
Tests use typer.testing.CliRunner with mocked API responses:
from typer.testing import CliRunner
from unittest.mock import patch
from transformerlab_cli.main import app
runner = CliRunner()
def test_list():
with patch("transformerlab_cli.util.api.get") as mock_get:
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = [{"id": 1, "name": "test"}]
result = runner.invoke(app, ["mycommand", "list"])
assert result.exit_code == 0
Run tests: cd cli && python -m pytest tests/ -v
Key Libraries
| Library | Purpose |
|---|---|
typer |
CLI command framework |
httpx |
HTTP client (sync mode) |
rich |
Terminal formatting (tables, panels, styled text) |
textual |
Interactive TUI apps (job monitor) |
pyyaml |
YAML parsing for task definitions |
Common Mistakes to Avoid
- Do not import from lab-sdk — the CLI talks to the API over HTTP, it doesn't use the SDK directly.
- Do not use asyncio in Typer commands — the CLI uses httpx in sync mode. Only the Textual TUI uses async (via Textual's own event loop).
- Always support both output formats — every command must handle
--format prettyand--format json. - Always run CLI tests after changes —
cd cli && python -m pytest tests/ -v.