Skip to content

quonaro/Lota

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

116 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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 in YAML, no code needed
  • πŸ”§ Flexible arguments - Positional, flags, wildcards, arrays with type validation
  • πŸ”„ Variable interpolation - Environment variables with hierarchical scoping
  • 🐚 Shell-aware execution - Auto-detects shell binary, overridable at any level
  • πŸ‘οΈ Dry-run mode - Preview commands before execution
  • πŸ›‘οΈ Graceful shutdown - Proper process management on interrupt signals
  • πŸ“„ Env file imports - Load variables from .env files
  • πŸ“Š YAML config imports - Import nested YAML configs with dot-notation access
  • πŸ“‚ Nested groups - Organize commands in hierarchical groups
  • πŸ“ Working directory - Set dir per command or group (relative to lota.yml)
  • οΏ½ Tee logging - Write stdout/stderr to files while still printing to terminal
  • οΏ½ Command dependencies - depends for automatic prerequisite execution with cycle detection
  • πŸ” Upward config search - Find lota.yml in parent directories up to the git root

πŸ“¦ 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/

πŸš€ 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 βœ… ❌ ❌ ❌

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

Simple Web Project

shell: bash

vars:
  - import:env .env
  - NODE_ENV=development

args:
  - port|p:int=3000

dev:
  desc: Development commands
  install:
    desc: Install dependencies
    script: npm install
  start:
    desc: Start development server
    args:
      - hot|h:bool
    script: |
      if [ "$hot" = "true" ]; then
        npm run dev
      else
        npm start
      fi

build:
  desc: Build for production
  before: npm run clean
  script: npm run build
  after: echo "Build completed successfully"

test:
  desc: Run tests
  args:
    - coverage|c:bool
  script: |
    if [ "$coverage" = "true" ]; then
      npm test -- --coverage
    else
      npm test
    fi

DevOps / Infrastructure

shell: bash

vars:
  - DOCKER_COMPOSE=docker-compose
  - KUBECTL=kubectl

infra:
  desc: Infrastructure management
  docker:
    desc: Docker operations
    up:
      desc: Start all services
      script: $DOCKER_COMPOSE up -d
    down:
      desc: Stop all services
      script: $DOCKER_COMPOSE down
    logs:
      desc: View logs
      args:
        - service:str
        - ...tail
      script: $DOCKER_COMPOSE logs -f "$service" "$tail"
  k8s:
    desc: Kubernetes operations
    namespace:
      desc: Namespace operations
      create:
        desc: Create namespace
        args:
          - name:str
        script: $KUBECTL create namespace "$name"
      delete:
        desc: Delete namespace
        args:
          - name:str
        script: $KUBECTL delete namespace "$name"
    deploy:
      desc: Deploy application
      args:
        - env|e:str=dev
        - dry|d:bool
      script: |
        if [ "$dry" = "true" ]; then
          $KUBECTL apply --dry-run=client -f "k8s/$env/"
        else
          $KUBECTL apply -f "k8s/$env/"
        fi

Go Project

shell: bash

vars:
  - GOOS=linux
  - GOARCH=amd64
  - BINARY_NAME=app

args:
  - target|t:str=linux/amd64

build:
  desc: Build the application
  args:
    - output|o:str=./bin
    - race|r:bool
  script: |
    if [ "$race" = "true" ]; then
      go build -race -o "$output/$BINARY_NAME" .
    else
      go build -o "$output/$BINARY_NAME" .
    fi

test:
  desc: Run tests
  args:
    - verbose|v:bool
    - cover|c:bool
  script: |
    FLAGS=""
    if [ "$verbose" = "true" ]; then
      FLAGS="$FLAGS -v"
    fi
    if [ "$cover" = "true" ]; then
      FLAGS="$FLAGS -cover"
    fi
    go test $FLAGS ./...

release:
  desc: Build release binaries
  before: echo "Building release for $target"
  script: |
    IFS=/ read -r GOOS GOARCH <<< "$target"
    go build -o ./dist/${BINARY_NAME}-${GOOS}-${GOARCH} .
  after: ls -lh ./dist/

Multi-Environment Project

shell: bash

vars:
  - import:env .env.local
  - import:env .env.shared
  - import:yaml config/secrets.yaml@public app # Import public config section

args:
  - environment|env:str=dev

db:
  desc: Database operations
  migrate:
    desc: Run database migrations
    script: |
      case "$environment" in
        dev)   npm run db:migrate:dev ;;
        staging) npm run db:migrate:staging ;;
        prod)  npm run db:migrate:prod ;;
        *)     echo "Unknown environment" ;;
      esac
  seed:
    desc: Seed database with test data
    before: echo "Seeding $environment database..."
    script: npm run "db:seed:$environment"
    after: echo "Database seeded successfully"

deploy:
  desc: Deployment operations
  staging:
    desc: Deploy to staging
    script: |
      npm run build:staging
      npm run deploy:staging
  production:
    desc: Deploy to production
    args:
      - confirm|c:bool
    script: |
      if [ "$confirm" != "true" ]; then
        echo "Use --confirm to deploy to production"
        exit 1
      fi
      npm run build:prod
      npm run deploy:prod

βš™οΈ Configuration

πŸ“‹ Structure

shell: bash # Optional: default shell (auto-detected if omitted)

vars: # global environment variables
  - KEY=value
  - import:env .env # Import from .env file

args: # global argument definitions
  - name:type=default

group-name: # command group
  desc: ...
  color: cyan # Optional: highlight group name in help
  inherit_color: true # Optional: inherit color from parent group
  shell: sh # Optional: override shell for this group
  vars: # group-level variables
    - KEY=value
  args: # group-level arguments
    - name:type=default
  log: # Optional: group-level tee logging
    path: group.log
  command-name:
    desc: ...
    color: green # Optional: highlight command name in help
    script: ...

command-name: # top-level command
  desc: ...
  color: red # Optional: highlight command name in help
  script: ...
  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.

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:yaml syntax is deprecated but still supported. Use import:yaml instead.

  • 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
=default Default value (optional) =./bin

πŸ“ 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

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 $name instead. {{name}} will be removed in a future version.

Reserved Variables: System environment variable names (PATH, HOME, USER, SHELL, etc.) are reserved and cannot be overridden in vars.

🐚 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 > cwd. 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 build is a dependency of both test and deploy, running lota deploy will execute build exactly 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: 0 if before+script+after succeeded, or if fallback succeeded after a failure. Otherwise the first error's exit code. finally errors 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 1 After fails: before β†’ script β†’ after (fail) β†’ fallback β†’ finally β†’ return 1

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 own path plus all ancestor paths.
  • independent: true: the command writes only to its own path; ancestor logs are skipped.
  • truncate applies only to the path declared 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.

🚩 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 or directory
--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.

οΏ½οΏ½ 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 git root (.git directory) or the filesystem root (/).

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

Pass --help after a command to see its arguments:

lota dev run --help

πŸ‘οΈ Dry Run Mode

Preview what would be executed without actually running it:

lota build --dry-run

πŸ‘¨β€πŸ’» 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 git hooks
./scripts/install-hooks.sh

This installs:

  • commit-msg hook - validates conventional commits via cocogitto
  • post-commit hook - automatic version tagging
  • pre-commit hooks - runs go fmt, go vet, go test, and golangci-lint

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:

  • config/ - YAML parsing and configuration models
  • runner/ - Command execution, argument parsing, interpolation
  • cli/ - CLI orchestration (only orchestrates, doesn't import runner)
  • shared/ - Constants and shared utilities

Key design principles:

  • Stateless - no global variables
  • Context-aware execution with graceful shutdown
  • Pure functions for interpolation and parsing (testable)
  • Clean error handling with wrapped errors

πŸ“œ License

Apache License 2.0

About

πŸš€ Lota β€” configurable task runner for Go. Define commands in YAML, run from terminal. With variables, arguments, and hooks.

Topics

Resources

License

Stars

Watchers

Forks

Contributors