Skip to main content

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

Discovery order

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:

  1. $GOSPELIB_CONFIG — explicit path override via environment variable
  2. .gospelib.toml in the current working directory
  3. .gospelib.toml in the repository root (/workspaces/main)
  4. $XDG_CONFIG_HOME/gospelib/config.toml
  5. ~/.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.

VariableMaps to Config KeyExample Value
GOSPELIB_CLI_DEFAULT_OUTPUT[cli].default_outputjson
GOSPELIB_CLI_COLOR[cli].colorfalse
GOSPELIB_DEV_DEFAULT_SERVICES[dev].default_servicesgateway,content,web
GOSPELIB_DEV_AUTO_INFRA[dev].auto_infratrue
GOSPELIB_TEST_DEFAULT_STACK[test].default_stackpython
GOSPELIB_DOWNLOAD_OUTPUT_DIR[download].output_dirdata
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

Consistency matters

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:

VerbosityStack Trace Display
-qHidden — only Error: <message> shown
defaultHidden — error message + hint shown
-vHidden — error message + hint + detail shown
-vvShown — 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)