README
ΒΆ
Lota
A configurable task runner for rapid development. Define commands in a YAML file and run them from the terminal.
Features
- β¨ Configurable tasks β Define tasks and command groups in a single
lota.yml. No code required: commands, arguments, variables, dependencies and execution hooks are all declared in YAML. - π§ Flexible arguments β Support positional arguments, named flags (
-f/--flag), boolean flags with negation (--!verbose), wildcard capture (...args) and typed arrays (files:arr[5]). CLI values are validated and exported as environment variables. - π Variable interpolation β Inject variables and arguments into scripts with
$name. Scopes follow app β group β command, so local definitions naturally override global ones. - π Shell-aware execution β Auto-detects the system shell and falls back to
bashif unsure. Override the shell per app, group or command. Supports bash, zsh, fish, sh, dash and others. - ποΈ Dry-run mode β Run with
--dry-runto print the exact scripts and resolved environment without executing them. - π‘οΈ Graceful shutdown β
SIGINT/SIGTERMare propagated to child processes with a graceful period before forced termination. - π Env file imports β Load variables from
.envfiles viaimport:env .envinsidevars. - π YAML config imports β Import nested YAML files as variables with
import:yaml <file>[@section] [prefix]and access flattened keys like$app.database.host. - π Nested groups β Organize commands in hierarchical groups (
lota infra docker up) with arbitrary nesting depth. - π Working directory β Set
dirper group or command. Relative paths resolve against the config file; use$CWDto run from the current directory. - π Tee logging β Mirror command output to log files while still printing to the terminal. Logs are inherited down the group tree and can be made
independent. - π Command dependencies β Declare
dependswith full dot-paths. Independent dependencies run in parallel by default, shared ones run once, and circular dependencies are detected automatically. - π Upward config search β Run commands from any subdirectory: Lota walks up looking for
lota.yml/lota.yaml. A.gitdirectory is a soft boundary β Lota still checks one level above it, so a shared config works for nested repos. - π¨ Colored help β Highlight group and command names with named ANSI colors or hex values (e.g.
#FF5733). - β±οΈ Command timeout β Set a per-run timeout with
--timeout 5m. - π§© Execution hooks β Build pipelines with
before,script,after,fallbackandfinallystages. - π§± Native commands β Embed Lota as a Go library and register Go functions as handlers for
native: truecommands. - π― Shell completion β Built-in completion for bash, zsh and fish.
- π‘ Smart suggestions β Typo a command name? Lota suggests the closest match using Levenshtein distance ("Did you mean: ...?").
- π₯ Config imports β Merge other Lota config files or remote URLs with optional namespacing via
imports. - π₯οΈ PTY support β Pseudo-terminal allocation preserves ANSI colors when tee-logging to files.
- π» Hidden commands β Set
show: falseto keep commands executable but out of help output.
π¦ Installation
Quick Install (Linux/macOS)
curl -fsSL https://raw.githubusercontent.com/quonaro/lota/main/scripts/install.sh | bash
Or with specific version:
curl -fsSL https://raw.githubusercontent.com/quonaro/lota/main/scripts/install.sh | bash -s -- -V v0.1.0
Script verifies SHA256 checksum and installs to ~/.local/bin (or /usr/local/bin with sudo).
Build from Source
Requires Go 1.26+
go install github.com/quonaro/lota@latest
Or manually:
git clone https://github.com/quonaro/lota.git
cd lota && go build -o lota . && sudo mv lota /usr/local/bin/
π Documentation
- Configuration Reference β Complete reference for every YAML field
- CLI Usage β All CLI flags, config resolution, completion, exit codes
- Arguments, Variables & Interpolation β Deep dive into args, vars, and
$nameinterpolation - Execution, Architecture & Examples β Hooks, dependencies, PTY, architecture, example walkthroughs
- Embedding Guide β How to embed Lota in your Go application
- Examples β Complete configuration examples for various use cases
π Quick Start
Initialize a new configuration:
lota --init
This creates a lota.yml in your current directory.
Or create it manually:
build:
desc: Build the application
script: go build -o bin/app .
dev:
desc: Development commands
run:
desc: Run with hot reload
script: air
test:
desc: Run tests
script: go test ./...
Run a command:
lota build
lota dev run
lota dev test
Comparison
| Feature | Lota | Task | Just | npm scripts |
|---|---|---|---|---|
| Declarative YAML | β | β | β | β |
| Type-safe arguments | β | β | β | β |
| Variable interpolation | β | β | β | β |
| Nested groups | β | β | β | β |
Working directory (dir) |
β | β | β | β |
| Command dependencies | β | β | β | β |
| Upward config search | β | β | β | β |
| Env file imports | β | β | β | β |
| Shell auto-detection | β | β | β | β |
| Dry-run mode | β | β | β | β |
| Tee logging | β | β | β | β |
| Execution hooks | β | β | β | β |
| Native Go commands | β | β | β | β |
| Config file imports | β | β | β | β |
Syntax Comparison
Simple build command:
# Lota
build:
script: go build -o app .
# Task (Taskfile.yml)
build:
cmds:
- go build -o app .
# Just
build:
go build -o app .
# npm scripts
"build": "go build -o app ."
With arguments:
# Lota
dev:
args:
- port|p:int=3000
script: npm start -- --port $port
# Task (Taskfile.yml)
build:
vars:
PORT: 3000
cmds:
- npm start -- --port {{.PORT}}
# Just
port := "3000"
dev:
npm start -- --port {{port}}
# npm scripts
"dev": "npm start -- --port ${PORT:-3000}"
With dependencies:
# Lota
test:
depends:
- build
script: go test ./...
# Task (Taskfile.yml)
test:
deps: [build]
cmds:
- go test ./...
# Just
build:
go build -o app .
test: build
go test ./...
# npm scripts
"test": "npm run build && go test ./..."
Examples
See the examples/ directory for complete working configurations:
- embedded.yaml - Full-featured example with native commands, groups, colors, variables, and arguments
- simple-web-project.yaml - Web development workflow
- devops-infrastructure.yaml - Docker and Kubernetes operations
- go-project.yaml - Go build, test, and release workflows
- multi-environment.yaml - Environment-specific configurations
βοΈ Configuration
π Structure
shell: bash # Optional: default shell (auto-detected if omitted)
vars: # global environment variables
- KEY=value
- import:env .env # Import from .env file
- import:yaml config.yaml # Import from YAML file
args: # global argument definitions
- name|short:type=default
imports: # optional: import other config files / URLs
- url: extra.yaml
namespace: ext # optional; wraps imported commands/groups under `ext:`
group-name: # command group
desc: ...
color: cyan # Optional: highlight group name in help
inherit_color: true # Optional: inherit color from parent group
show: false # Optional: hide from help output
shell: sh # Optional: override shell for this group
dir: ./subdir # Optional: working directory for this group
vars: # group-level variables
- KEY=value
args: # group-level arguments
- name|short:type=default
log: # Optional: group-level tee logging
path: group.log
nested-group: # nested group
desc: ...
command-name: # command inside group
desc: ...
color: green # Optional: highlight command name in help
inherit_color: true # Optional: inherit color from parent group
show: false # Optional: hide from help output
shell: sh # Optional: override shell for this command
dir: ./src # Optional: working directory for this command
vars: # command-level variables
- KEY=value
args: # command-level arguments
- name|short:type=default
before: ... # Optional: pre-execution hook
script: ... # Main script
after: ... # Optional: post-success hook
fallback: ... # Optional: error recovery hook
finally: ... # Optional: always-runs cleanup hook
depends: # Optional: commands to run before this one
- other-command
parallel: true # Optional: run dependencies in parallel (default: true)
native: true # Optional: mark as native Go command (embedded mode only)
log: # Optional: command-level tee logging
path: cmd.log
truncate: true
independent: true
command-name: # top-level command
desc: ...
color: red # Optional: highlight command name in help
script: ...
before: ... # Optional: pre-execution hook
after: ... # Optional: post-success hook
fallback: ... # Optional: error recovery hook
finally: ... # Optional: always-runs cleanup hook
log: # Optional: command-level tee logging
path: cmd.log
π Variables (vars)
Variables are exported as environment variables into scripts. Both vars and args share a unified environment pool β CLI args override vars on name collision. They support three scopes with priority: app < group < command.
Reserved system variables cannot be overridden in vars:
PATH, HOME, USER, SHELL, LANG, LC_ALL, TERM, PWD, OLDPWD, HOSTNAME, LOGNAME, MAIL, TMPDIR, DISPLAY, XAUTHORITY, EDITOR, VISUAL, PAGER
Special variables: $CWD is reserved for working directory resolution (see Working Directory).
vars:
- DOCKER=docker compose # app-level
dev:
vars:
- DOCKER=docker # overrides app-level for this group
run:
vars:
- DOCKER=podman # overrides group-level for this command
script: $DOCKER up
π Import from .env files
Load variables from environment files:
vars:
- import:env .env
- import:env config/prod.env
π Import from YAML files
Import nested YAML configurations with automatic flattening to dot-notation:
vars:
- import:yaml config.yaml # Import all with original keys
- import:yaml config.yaml app # Import all with 'app.' prefix
- import:yaml config.yaml@public # Import only 'public' section
- import:yaml secrets.yaml@db cfg # Import 'db' section with 'cfg.' prefix
Syntax: import:yaml <file>[@<section>] [<prefix>]
Note: The old
!import:yamlsyntax is deprecated but still supported. Useimport:yamlinstead.
- file - Path to YAML file
- section (optional) - Import only specific top-level section via
@section - prefix (optional) - Add prefix to all imported keys
Example YAML file:
# config.yaml
public:
app_name: MyApp
version: 1.0.0
database:
host: localhost
port: 5432
private:
api_key: secret123
Resulting variables:
# import:yaml config.yaml@public app
vars:
app.app_name: "MyApp"
app.version: "1.0.0"
app.database.host: "localhost"
app.database.port: "5432"
Access in scripts: $app.app_name, $app.database.host
π― Arguments (args)
Arguments are passed from the CLI and exported as environment variables, accessible via $name in scripts.
Format: name|short:type=default
| Part | Description | Example |
|---|---|---|
name |
Long name | output |
|short |
Short alias (optional) | |o |
:type |
Type (optional) | :str, :int, :bool, :arr |
:type? |
Optional marker (no default) | :str? β arg not required |
=default |
Default value (optional) | =./bin |
A default value (
=default) automatically makes an argument optional. The?marker is useful when you want an argument to be optional without a default value (it will be empty string if not provided).
π Argument Types
Positional β passed by position, no flag needed:
args:
- filename:str
- count:int
script: process "$filename" "$count"
lota cmd file.txt 5
Flag β passed by name using --flag or -f. Any arg with a short alias (|short) or type bool becomes a flag:
args:
- output|o:str=./bin
- verbose|v:bool
script: go build -o "$output"
lota cmd --output ./dist
lota cmd -o ./dist --verbose
Wildcard β captures all remaining positional arguments:
args:
- service:str
- ...cmd
script: docker exec "$service" "$cmd"
lota cmd backend python manage.py shell
# service=backend, cmd="python manage.py shell"
Array β collects multiple consecutive positional values:
args:
- files:arr[5] # collect up to 5 values
script: lint $files
lota cmd a.go b.go c.go
Boolean Flags
Bool args support negation via --!name:
lota cmd --verbose # verbose=true
lota cmd --!verbose # verbose=false
lota cmd --verbose=false # verbose=false
Flag Syntax
Flags can be passed in several forms:
lota cmd --output ./dist # space-separated
lota cmd --output=./dist # equals-separated
lota cmd -o ./dist # short flag
lota cmd -o=./dist # short flag, equals-separated
Use -- to signal end of flags β everything after is treated as positional arguments:
lota cmd -- --flag-like-value
# '--flag-like-value' is passed as a positional arg, not parsed as a flag
Argument Scopes
Like vars, args can be defined at app, group, or command level and are merged with the same priority (command wins):
args:
- env:str=dev # available to all commands
deploy:
args:
- env:str=prod # overrides app-level for this group
run:
script: ./deploy.sh --env="$env"
Deprecation: Using
{{name}}for variable and argument interpolation is deprecated. Use$nameinstead.{{name}}will be removed in a future version.
π₯ Config Imports (imports)
Import other Lota configuration files and merge them into the current config. Supports local files and remote URLs.
imports:
- url: ./extra.yaml # local file (relative to lota.yml)
- url: https://example.com/shared.yaml # remote URL
namespace: shared # wrap imported commands/groups under 'shared'
| Field | Type | Description |
|---|---|---|
url |
string | Path to local file or HTTP/HTTPS URL |
namespace |
string | Optional. Wraps all imported commands and groups under a new top-level group with this name |
Merge rules:
- Without namespace: Imported commands and groups merge directly into the root. Local definitions override imported ones with the same name.
- With namespace: Imported commands and groups are wrapped under a new top-level group named after the namespace.
- Variables: Always merged into root (no namespace wrapping). Local vars override imported vars.
- Nested imports: Imported configs cannot have their own
importsfield (silently ignored).
URL fetching:
- Configs are downloaded with a 100MB size limit
- Cached at
/tmp/lota_cache/<sha256>.ymlfor subsequent runs - TLS verification is skipped (use with trusted sources only)
π Shell Configuration
Important: Lota selects the shell interpreter, but the script itself is shell-specific. Write scripts for the shell you target.
Lota auto-detects the shell binary from the system environment. If detection fails, it falls back to bash.
Override the shell at any level:
shell: zsh # app-level
dev:
shell: bash # group-level override
run:
shell: sh # command-level override
script: echo $0
Supported shells: bash, sh, zsh, dash, ksh, mksh, pdksh, ash, busybox, sash, tcsh, csh, fish
π Working Directory (dir)
Set the working directory for commands and groups. The path is resolved relative to the lota.yml file location.
backend:
dir: ./backend # group-level default
build:
desc: Build backend
script: go build .
test:
desc: Run backend tests
dir: ./backend/tests # command-level override
script: go test ./...
Priority: command > group > config file dir. Use $CWD or $CWD/... to target the current working directory. Useful in monorepos where different commands run in different subprojects.
π Command Dependencies (depends)
Reference other commands that must run before the current one. Dependencies are specified as full dot-separated paths.
build:
desc: Build the application
script: go build -o bin/app .
test:
desc: Run tests
depends:
- build
script: go test ./...
deploy:
desc: Deploy to production
depends:
- build
- test
script: ./deploy.sh
Dependencies execute with their own context (shell, vars, dir, default args). Circular dependencies are detected automatically and produce an error.
Independent dependencies run in parallel by default, with output prefixed by colored task name (like Docker Compose):
\x1b[35m[build]\x1b[0m go build -o bin/app .
\x1b[36m[lint]\x1b[0m golangci-lint run
\x1b[35m[build]\x1b[0m β done
\x1b[36m[lint]\x1b[0m β done
\x1b[33m[deploy]\x1b[0m ./deploy.sh
Each task gets its own color: either from its color field, inherited from a parent group, or a deterministic color derived from the task name.
To force sequential execution, set parallel: false:
ci:
depends:
- lint
- test
parallel: false
script: echo "CI done"
Shared dependencies are executed once (deduplication). If
buildis a dependency of bothtestanddeploy, runninglota deploywill executebuildexactly once.
TODO: A TUI for interactive task monitoring is under consideration for future versions.
β‘ Hooks Tutorial
Lota provides five execution stages per command. You only use what you need β a simple script is enough for most tasks.
before β script β after β finally
β
fallback β finally
| Stage | Purpose | Runs on error? |
|---|---|---|
before |
Preparation (compile, check env) | Skips script, triggers fallback |
script |
Main command | Triggers fallback |
after |
Post-success action (notify, log) | Triggers fallback |
fallback |
Recovery / alternative path (rollback, alert, degrade) | If succeeds, command returns 0 |
finally |
Cleanup (stop containers, remove temp files) | Always runs |
Return code:
0ifbefore+script+aftersucceeded, or iffallbacksucceeded after a failure. Otherwise the first error's exit code.finallyerrors are printed to stderr but do not change the return code.
Example 1: Basic Pipeline
build:
before: echo "Compiling..."
script: go build -o bin/app .
after: echo "Build complete"
Happy path: before β script β after β return 0
If script fails: before β script (exit 1) β return 1. after is skipped.
Example 2: Cleanup with finally
Use finally for operations that must run regardless of success or failure.
test:
before: docker-compose up -d test-db
script: go test ./...
finally: docker-compose down test-db
Any outcome: before β script β finally β return 0 or 1. The database container is always stopped.
Example 3: Error Handling with fallback
Use fallback to react to failures β rollback, send alerts, write crash reports.
deploy:
before: echo "Starting deploy..."
script: ./deploy.sh
after: echo "Deploy successful"
fallback: ./rollback.sh
finally: echo "Deploy finished"
Happy path: before β script β after β finally β return 0 Script fails: before β script (fail) β fallback β finally β return 0 (if fallback succeeds) After fails: before β script β after (fail) β fallback β finally β return 0 (if fallback succeeds)
Example 4: Full Pipeline β Database Migration
db:
migrate:
before: |
echo "Creating backup..."
pg_dump mydb > /tmp/backup.sql
script: |
echo "Running migrations..."
migrate -path ./migrations -database "$DATABASE_URL" up
after: echo "Migration complete"
fallback: |
echo "Migration failed, restoring backup..."
psql mydb < /tmp/backup.sql
finally: rm -f /tmp/backup.sql
| Scenario | Flow |
|---|---|
| Success | before β script β after β finally (backup deleted) |
| Migration fails | before β script (fail) β fallback (restore) β finally (backup deleted) |
| After fails | before β script β after (fail) β fallback (restore) β finally (backup deleted) |
π Tee Logging (log)
Write command output to log files while still printing to the terminal. Logs support additive inheritance: a command writes to its own log file plus all ancestor log files, unless independent: true breaks the chain.
log:
path: logs/all.log # app-level: all commands inherit this
build:
desc: Build the application
script: go build -o bin/app .
log:
path: logs/build.log # writes to both all.log and build.log
truncate: true # overwrite on each run (default: append)
test:
desc: Run tests
script: go test ./...
log:
path: logs/test.log
independent: true # writes ONLY to test.log, skips all.log
| Field | Type | Default | Description |
|---|---|---|---|
path |
string | required | Log file path (relative to lota.yml). Supports variable interpolation ($var). |
truncate |
bool | false |
If true, overwrite the file on each run. If false, append. |
independent |
bool | false |
If true, discard all ancestor logs and write only to this file. Not allowed at app level. |
Inheritance behavior:
independent: false(default): the command writes to its ownpathplus all ancestorpaths.independent: true: the command writes only to its ownpath; ancestor logs are skipped.truncateapplies only to thepathdeclared on the same level.
log:
path: logs/global.log
infra:
desc: Infrastructure
log:
path: logs/infra.log
independent: true # infra commands skip global.log
docker:
desc: Docker ops
log:
path: logs/docker.log # writes to infra.log + docker.log
up:
script: docker-compose up -d
Runtime errors (missing parent dir, permission denied, path is a directory) are printed to stderr as [log error] but do not fail the command.
π Nested Groups
Organize commands in hierarchical groups:
infra:
desc: Infrastructure commands
docker:
desc: Docker operations
up:
script: docker-compose up
down:
script: docker-compose down
k8s:
desc: Kubernetes operations
apply:
script: kubectl apply -f k8s/
lota infra docker up
lota infra k8s apply
π¨ Help Colors
Highlight group and command names in lota help output using named ANSI colors or hex values:
dev:
desc: Development commands
color: cyan
frontend:
desc: Frontend commands
inherit_color: true
start:
desc: Start dev server
inherit_color: true
script: npm run dev
build:
desc: Build frontend
color: yellow
script: npm run build
| Option | Description |
|---|---|
color |
Named ANSI color (black, red, green, yellow, blue, magenta, cyan, white, and hi* variants) or any #RRGGBB hex value (e.g. #FF5733) |
inherit_color |
true to inherit the nearest ancestor color. Defaults to null (no inheritance) |
Color resolution priority: direct color > inherited color > default. inherit_color: true walks up the group chain and uses the first non-empty color found. Hex colors work in true-color capable terminals.
π» Hiding Commands (show)
Set show: false to hide a command or group from the help output. Hidden commands remain executable but don't appear in lota --help or group listings:
dev:
desc: Development commands
run:
desc: Run dev server
script: air
internal-task:
show: false
script: ./scripts/internal.sh
lota dev run # visible in help
lota dev internal-task # hidden from help, but still works
π§± Native Commands (native)
Commands marked with native: true are executed as Go functions instead of shell scripts. This is only available when embedding Lota as a Go library β the CLI has no native handlers registered.
server:
desc: Start HTTP server
native: true
args:
- port:int=8080
- host:str=0.0.0.0
admin:
desc: Admin utilities
users:
reset-password:
desc: Reset user password
native: true
args:
- username:str
- password:str
See the Embedding Guide for how to register native handlers in Go.
π© Global Flags
| Flag | Description |
|---|---|
-h, --help |
Show help |
-V |
Show version only (machine-friendly) |
--version |
Show version with ASCII banner |
-v, --verbose |
Enable verbose output |
--dry-run |
Show commands without executing |
--init |
Create a template lota.yml |
--config |
Specify config file, directory, or URL |
-g |
Use system-wide config (/etc/lota.yml) |
-u |
Use user config (~/.local/share/lota.yml) |
--timeout <duration> |
Set a command timeout (e.g. 30s, 5m) |
--install-completion |
Install shell completion script (auto-detects shell) |
--install-completion zsh|bash|fish |
Install completion for a specific shell |
--completion-script zsh|bash|fish |
Print completion script to stdout |
π Shell Completion
Lota provides built-in shell completion for bash, zsh, and fish.
Auto-install
lota --install-completion
Lota detects your shell from $SHELL and writes the completion script to the standard location.
Install for a specific shell
lota --install-completion bash
lota --install-completion zsh
lota --install-completion fish
Manual install (print to stdout)
Bash:
lota --completion-script bash >> ~/.bashrc
Zsh:
lota --completion-script zsh > ~/.config/zsh/completions/_lota
Fish:
lota --completion-script fish > ~/.config/fish/completions/lota.fish
Troubleshooting
If lota behaves like a completion engine instead of executing commands:
unset COMP_LINE COMP_POINT
Then regenerate and reinstall the completion script for your shell.
π Config Resolution
Upward Config Search
If lota.yml is not found in the current directory, Lota searches upward through parent directories until it finds one or reaches the filesystem root (/). A .git directory is a soft boundary: when encountered, Lota still checks one level above the git root before stopping. This lets a shared lota.yml in a workspace folder serve nested repositories.
This is critical for monorepos and nested projects where you might run commands from subdirectories:
cd backend/src
lota build # finds lota.yml in project root
Config from URL
Load configuration directly from a remote URL:
lota --config https://example.com/lota.yml build
Remote configs are cached at /tmp/lota_cache/<sha256>.yml for subsequent runs.
Config from Directory
Point --config at a directory and Lota will search for lota.yml or lota.yaml inside it:
lota --config ./my-project build
System-wide and User Configs
Use -g for a system-wide config at /etc/lota.yml, or -u for a user-level config at ~/.local/share/lota.yml:
lota -g build # use /etc/lota.yml
lota -u build # use ~/.local/share/lota.yml
Command Help
Pass --help after a command to see its arguments:
lota dev run --help
Smart Suggestions
When a command is not found, Lota suggests the closest matches using Levenshtein distance:
$ lota biuld
command not found: biuld
Did you mean:
- build
Suggestions are also provided for unknown YAML fields and unknown flags during command execution.
ποΈ Dry Run Mode
Preview what would be executed without actually running it:
lota build --dry-run
Dry run prints the resolved scripts with all variables and arguments interpolated, and shows the environment that would be set β without executing anything.
οΏ½οΈ Terminal & PTY
On Unix systems, Lota allocates a pseudo-terminal (PTY) for command execution when output needs to be tee'd to log files. This preserves ANSI colors from child processes that check isatty. If PTY allocation fails, Lota falls back to normal pipes.
SIGINT/SIGTERM are propagated to child processes. Lota sends SIGTERM first, waits a grace period, then sends SIGKILL if the process hasn't exited. The ^C echo on the terminal is disabled during execution for cleaner output.
οΏ½οΏ½βπ» Development
Prerequisites
- Go 1.26+
- Python 3.8+ (for pre-commit)
- cocogitto (cog) - for conventional commits
Setup Git Hooks
Install git hooks for commit validation and code quality:
# Install pre-commit
pip install pre-commit
# Install pre-commit hooks
pre-commit install
# Install commit-msg hook (for conventional commits)
pre-commit install --hook-type commit-msg
This installs:
- pre-commit hooks - runs go fmt, go vet, go test, and golangci-lint
- commit-msg hook - validates conventional commits via cocogitto
Manual Pre-commit
Run pre-commit manually without committing:
# Run all hooks
pre-commit run --all-files
# Run specific hook
pre-commit run go-fmt --all-files
Testing
# Run all tests
go test ./...
# Run with race detector
go test -race ./...
# Run with coverage
go test -cover ./...
Linting
# Run golangci-lint
golangci-lint run
# Run go vet
go vet ./...
# Format code
gofmt -w -s .
ποΈ Architecture
Lota follows a strict layered architecture:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β main.go β
β (signal handling, terminal setup, exit codes) β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββ
β
ββββββββΌβββββββ
β cli/ β
β (orchestrator)β
ββββ¬ββββββββ¬ββββ
β β
ββββββββΌβββ ββββΌβββββββ
β config/ β β engine/ β
β (parse) β β (orchestrate)β
βββββββββββ ββββ¬βββββββ
β
ββββββΌβββββ
β runner/ β
β (execute)β
ββββββββββββ
- config/ β YAML parsing, configuration models, imports, env file loading, URL fetching, validation
- runner/ β Command execution, argument parsing, variable interpolation, shell resolution, PTY allocation, tee logging
- engine/ β High-level orchestration: dependency resolution, parallel execution, native commands, help generation, shell completion
- cli/ β CLI orchestration: flag parsing, config loading, command lookup, output formatting, suggestions
- shared/ β Constants (app name, config file names, version info)
- logger/ β Debug logging with verbose mode support
- internal/ β Internal utilities (Levenshtein distance for suggestions, terminal helpers)
Layering rules:
runnermust not importcliconfigmust not importrunnercliis the top-level orchestrator that bindsconfigandengineenginebridgesconfigandrunner
Key design principles:
- Stateless β no global variables; state (verbose, dry-run) passed through context or arguments
- Context-aware execution with graceful shutdown via
exec.CommandContext - Pure functions for interpolation and parsing (testable without file I/O)
- Clean error handling with wrapped errors (
fmt.Errorf("context: %w", err)) - Cyclic dependency detection for both variable interpolation (max depth 10) and command dependencies