Configuration & Error Handling
How the gl CLI resolves configuration and presents errors to the user.
Configuration Hierarchy
Configuration values are resolved using a strict priority order — higher sources override lower ones:
CLI flags > Environment variables > Config file (.gospelib.toml) > Defaults
This means a --output json flag always wins over GOSPELIB_CLI_DEFAULT_OUTPUT=human, which always wins over the value in your config file.
Config File Discovery
The CLI searches for .gospelib.toml using a "first found wins" strategy. In most cases the repo-root file is used automatically.
The discovery order is:
$GOSPELIB_CONFIG— explicit path override via environment variable.gospelib.tomlin the current working directory.gospelib.tomlin the repository root (/workspaces/main)$XDG_CONFIG_HOME/gospelib/config.toml~/.config/gospelib/config.toml
Config File Schema
[cli]
default_output = "human" # Default --output format
color = true # Enable colors (overridden by NO_COLOR)
log_file = "~/.local/state/gospelib/gl.log"
[dev]
default_services = ["gateway", "content", "web"]
auto_infra = true # Start infra automatically with dev start
[test]
default_stack = "all" # all, js, python, go
parallel = 3
[download]
output_dir = "data"
cache_dir = ".cache/corpus-downloader"
Environment Variable Naming
All environment variables follow the pattern GOSPELIB_<GROUP>_<KEY> — uppercase, underscore-separated.
| Variable | Maps to Config Key | Example Value |
|---|---|---|
GOSPELIB_CLI_DEFAULT_OUTPUT | [cli].default_output | json |
GOSPELIB_CLI_COLOR | [cli].color | false |
GOSPELIB_DEV_DEFAULT_SERVICES | [dev].default_services | gateway,content,web |
GOSPELIB_DEV_AUTO_INFRA | [dev].auto_infra | true |
GOSPELIB_TEST_DEFAULT_STACK | [test].default_stack | python |
GOSPELIB_DOWNLOAD_OUTPUT_DIR | [download].output_dir | data |
GOSPELIB_DOWNLOAD_CACHE_DIR | [download].cache_dir | /tmp/cache |
Viewing Effective Configuration
Run gl config show to see the resolved configuration with source annotations:
╭─────────────── Effective Configuration ───────────────╮
│ │
│ cli.default_output = "human" (default) │
│ cli.color = true (default) │
│ dev.default_services = ["gateway", "content", "web"] │
│ (.gospelib.toml) │
│ dev.auto_infra = true (env: GOSPELIB_…) │
│ test.default_stack = "python" (env: GOSPELIB_…) │
│ │
╰───────────────────────────────────────────────────────╯
Each value shows where it came from — (default), (.gospelib.toml), or (env: GOSPELIB_…) — so you can diagnose unexpected behavior quickly.
Error Handling
Every error the CLI produces must follow the standard format below. Inconsistent error messages make debugging harder for everyone.
Error Message Format
All errors follow a three-part structure — brief message, optional detail, and actionable hint:
✗ Error: <brief message>
<detailed explanation, if available>
Hint: <actionable suggestion>
Implementation uses Rich for styled stderr output:
console = Console(stderr=True)
console.print(f"[bold red]✗ Error:[/] {brief_message}\n")
if detail:
console.print(f" {detail}\n")
if hint:
console.print(f" [dim]Hint:[/] {hint}")
Warning Format
Warnings use a distinct prefix and do not affect the exit code:
⚠ Warning: <message>
Did-You-Mean Suggestions
When a user provides an unknown command or flag, the CLI performs fuzzy matching using difflib.get_close_matches():
✗ Error: No such command "satrt".
Did you mean one of these?
• start
• status
Run gl dev --help for available commands.
Stack Trace Behavior
The amount of error detail scales with verbosity:
| Verbosity | Stack Trace Display |
|---|---|
-q | Hidden — only Error: <message> shown |
| default | Hidden — error message + hint shown |
-v | Hidden — error message + hint + detail shown |
-vv | Shown — full Python traceback to stderr |
Error Codes Registry
All known error conditions have a machine-readable code for --output json mode:
{
"error": {
"code": "DOCKER_NOT_RUNNING",
"message": "Docker daemon is not running.",
"hint": "Start Docker Desktop or run 'sudo systemctl start docker'.",
"exit_code": 4
}
}
Error codes use UPPER_SNAKE_CASE, consistent with the project-wide API error conventions.
Graceful Interruption (Ctrl+C)
The CLI catches KeyboardInterrupt at the top level, prints a clean message, and exits with code 130. If a subprocess is running, SIGINT is forwarded before exiting.
try:
cli()
except KeyboardInterrupt:
console = Console(stderr=True)
console.print("\n[dim]Interrupted.[/]")
sys.exit(130)