mando-cli

YAML-driven workflow orchestrator for Alpiq BESS local development. Replaces the 1,430-line Python monolith (optimization-universe-iac/local/run.py) with a generic, data-driven Rust CLI where all workflow logic lives in YAML recipe files.

Status: Active (updated 2026-03-15)

All 65 tests pass across 2 test suites. 10 step types implemented across 11 handler source files. 35 recipe YAML files covering full BESS local dev workflow. Recipes moved to poc/recipes/ (project root). Non-interactive execution via -e KEY=VALUE flag. Compound when conditions (&&, ||). Repo swap feature for development isolation. No git commits yet.

Overview

mando run <recipe>                          # Execute a recipe (e.g., mando run menu)
mando run <recipe> --replay                 # Re-run with saved user choices
mando run <recipe> -e KEY=VALUE -e K2=V2    # Non-interactive: preset variables
mando list                                  # List all available recipes
mando swap <repo> --branch <branch>         # Swap a repo directory via symlink
mando swap --restore                        # Restore all swapped repos
mando swap --list                           # Show active swaps

Key Principle: The engine is completely generic — it knows nothing about BESS, Docker, or Mando. All domain logic is encoded in YAML recipes. Recipes can be updated without rebuilding the binary.

Architecture

Design Pattern: Interpreter

YAML file -> parse -> Recipe { steps: Vec<Step> } -> Runner::execute(steps, ctx)
                                                      |-- dispatches to step handlers

Core Components:

  • Context — HashMap-based variable store, {{var}} interpolation, env export to child processes
  • Runner — Sequential step executor with circular include detection and when condition evaluation
  • Step enum — 10 distinct step types (including swap)
  • State — Persistence layer (.mando-state.json) for replaying user choices and swap registry

Project Structure

poc/
├── recipes/                  # 35 YAML workflow files (moved from mando-cli/)
│   ├── menu.yaml
│   ├── mando/
│   ├── infra/
│   ├── setup/
│   ├── debug/
│   ├── manage/
│   └── wiremock/
└── mando-cli/
    ├── Cargo.toml
    ├── .mando-state.json         # Persisted user choices + swap registry
    ├── .mando-worktrees/         # Managed worktrees + original dirs (gitignored)
    ├── src/
    │   ├── main.rs               # CLI entry (clap), discovery, Ctrl+C, swap subcommand
    │   ├── recipe.rs             # Recipe/Step types, YAML deser (incl. SwapStep)
    │   ├── runner.rs             # Step execution engine (state threading for swap)
    │   ├── context.rs            # Variable store + interpolation
    │   ├── discovery.rs          # Recipe file discovery, path resolution
    │   ├── state.rs              # State persistence/loading, SwapEntry model
    │   ├── swap.rs               # Core swap logic (dirty check, backup, symlink, restore)
    │   └── steps/
    │       ├── mod.rs            # Step dispatch + shared step traits
    │       ├── run.rs            # Shell command execution (sh -c)
    │       ├── check.rs          # Dependency verification (abort on fail)
    │       ├── capture.rs        # Command stdout -> variable
    │       ├── prompt.rs         # Interactive text/confirm (dialoguer)
    │       ├── choice.rs         # Menu selection -> sub-recipe branching
    │       ├── env.rs            # Load .env/.toml, set inline vars
    │       ├── compose.rs        # Docker compose wrapper
    │       ├── http.rs           # HTTP requests (ureq)
    │       ├── include.rs        # Sub-recipe execution (shared context)
    │       └── swap.rs           # Swap recipe step handler
    ├── tests/integration.rs      # Integration tests
    └── docs/superpowers/
        ├── specs/2026-03-13-mando-cli-design.md   # Full design spec
        └── plans/2026-03-13-mando-cli.md          # Implementation plan

Recipe Location Change (2026-03-15)

Recipes moved from mando-cli/recipes/ to poc/recipes/ so they live at the project root alongside the compose files they reference. All compose file references in recipes now use optimization-universe-iac/local/ prefix. Recipe discovery walks up parent directories, so running from mando-cli/ still finds them.

Step Types

StepPurposeKey Fields
runExecute shell commandcommand, name, cwd, ignore_errors, when
checkVerify dependency (abort on fail)command, name
captureCommand stdout variablecommand, name, cwd, ignore_errors
promptUser input (confirm/text)name, message, type, default
choiceMenu selection sub-recipename, message, options[] (label/value/recipe)
envLoad variablesfile (.env/.toml), vars{} (inline), optional
composeDocker compose wrapperfiles[], profiles[], command, services[], cwd
includeExecute sub-reciperecipe (shared context)
httpHTTP requesturl, method, body, headers{}, capture
swapRepo directory swap via symlinkrepo, path, branch, from, restore_all, when

All steps support when for conditional execution.

Repo Swap (mando swap)

Swap POC repo directories with alternative directories, existing git worktrees, or ad-hoc created worktrees. Enables testing local changes in one repo against the rest of the POC stack without affecting the main checkout.

Motivation

  • Development isolation — test changes in one repo against the rest of the stack without touching the main checkout
  • Branch switching — run the POC stack with a specific repo on a different branch while everything else stays on its current branch

Mechanism

Symlink swap: the original directory is renamed to .mando-worktrees/<repo>.original, and a symlink is created at the original location pointing to the target. All existing tools (Docker Compose, build scripts, etc.) see the same paths transparently.

CLI Usage

# Swap with an external directory
mando swap <repo> --path /some/other/directory
 
# Swap with an existing worktree
mando swap <repo> --worktree /existing/worktree/path
 
# Swap with ad-hoc worktree from existing branch
mando swap <repo> --branch feature/new-algo
 
# Swap with ad-hoc worktree, new branch from base
mando swap <repo> --branch feature/new-algo --from develop
 
# Restore
mando swap --restore              # restore all swapped repos
mando swap --restore <repo>       # restore one specific repo
 
# Status
mando swap --list                 # show active swaps

Recipe Step

The swap step type allows automating repo swaps within recipe workflows:

# Swap to a branch worktree (conditionally)
- swap:
    repo: bess-optimization
    branch: feature/new-algo
    from: develop
    when: "{{isolate}} == true"
 
# Swap to an external directory
- swap:
    repo: mando
    path: /Users/andras/other-mando-checkout
 
# Restore all swaps at the end of a workflow
- swap:
    restore_all: true

Dirty-Repo Auto-Backup

When a repo has uncommitted changes before swapping:

  1. Creates branch mando-backup/<repo>-<timestamp>
  2. Commits all changes with message mando: auto-backup before swap
  3. Returns to original branch
  4. On restore, cherry-picks changes back and preserves the backup branch if there is a conflict

Backup branches persist

The mando-backup/* branches are not automatically deleted. If the cherry-pick on restore succeeds, you can safely delete them. If it conflicts, the backup branch is your safety net.

State

  • Swap registry stored in .mando-state.json alongside recipe state
  • Each active swap tracked as a SwapEntry with repo name, original path, target path, and swap type
  • Managed worktrees stored in .mando-worktrees/ (gitignored)
  • mando swap --list reads from the registry to show active swaps

Testing

6 integration tests covering:

  • Directory swap + restore roundtrip
  • Dirty repo auto-backup
  • Double-swap prevention (error if already swapped)
  • Nonexistent repo handling
  • Restore-all behavior
  • Branch worktree swap

Recipe Example

name: Start Mando
steps:
  - check:
      name: Docker available
      command: docker --version
 
  - env:
      file: .env
      optional: true
 
  - capture:
      name: mando_branch
      command: git -C ../../mando branch --show-current
 
  - prompt:
      name: confirm_branch
      message: "Mando is on branch '{{mando_branch}}'. Continue?"
      type: confirm
 
  - choice:
      name: mando_source
      message: "How to get the mando image?"
      options:
        - label: Build locally
          recipe: mando/build-local.yaml
        - label: Pull from registry
          recipe: mando/pull-registry.yaml
 
  - compose:
      files: [compose.yaml]
      profiles: [app]
      command: up -d

Recipe Ecosystem (35 files)

Entry Point

  • menu.yaml — Main hub with 5 choices: infra only, mando+mocked, mando+algos, service management, WireMock manager

mando/ (6 recipes) — Mando Image & Startup

RecipePurpose
start.yamlMain orchestration (choose local build or registry, OTel toggle)
start-with-algos.yamlFull stack with algo services (OTel toggle)
build-local.yamlBuild from source
build-local-with-algos.yamlBuild with algo overlay
pull-registry.yamlPull from GitLab registry
pull-registry-with-algos.yamlPull with algos
select-tag.yamlRegistry tag selection

OTel Toggle in Start Recipes

Both start.yaml and start-with-algos.yaml include an OTel toggle choice (enable_otel). When enabled, the compose.otel.yaml overlay is applied as a post-build step that layers Jaeger on top. See Jaeger Tracing for details.

# Non-interactive with OTel enabled
cargo run -- run mando/start-with-algos -e enable_otel=true -e mando_source=local

infra/ (2 recipes) — Infrastructure

RecipePurpose
start.yamlStart postgres, flyway, samba, wiremock
teardown.yamlFull cleanup (down -v)

setup/ (5 recipes) — Onboarding

RecipePurpose
check-deps.yamlVerify Docker, git, glab
clone-repos.yamlClone mando & algo repos
configure-env.yamlCreate .env credentials
registry-auth.yamlDocker registry login
show-branches.yamlDisplay current git branches

debug/ (5 recipes) — Memory Profiling

See Memray Memory Debugging for full details.

RecipePurpose
menu.yamlDebug menu entry point
memray-install.yamlInstall memray in containers
memray-container.yamlProfile running containers (attach/live/flamegraph)
memray-local.yamlProfile local pymando/algos (flamegraph/live/stats/leaks)
memray-view.yamlBrowse and open saved profiles

manage/ (6 recipes) — Service Control

status.yaml, stop.yaml, restart.yaml, rebuild.yaml, logs.yaml + menu.yaml

wiremock/ (10 recipes) — WireMock Management

list-stubs.yaml, add-stub.yaml, delete-stub.yaml, stub-details.yaml, request-log.yaml, unmatched.yaml, reset.yaml + menu/utils (10 recipes total)

Key Features

Variable Interpolation

command: "git -C {{repo_path}} branch --show-current"
# Undefined variables -> immediate error at step execution
# Multi-var: "{{a}} and {{b}}"

Conditional Execution (when)

- run:
    command: docker push {{registry_url}}/mando
    when: "{{registry_url}}"              # truthy: non-empty, not "false"
 
- include:
    recipe: mando/registry.yaml
    when: "{{mando_source}} == registry"  # equality check
 
- run:
    command: echo "skipped"
    when: "{{mode}} != production"        # inequality
 
- run:
    command: echo "both set"
    when: "{{a}} && {{b}}"               # compound AND (both must be truthy)
 
- run:
    command: echo "either set"
    when: "{{a}} || {{b}}"               # compound OR (at least one truthy)
 
- run:
    command: echo "match"
    when: "{{x}} == foo && {{y}} != bar"  # compound with equality/inequality

Compound Conditions Fix (2026-03-15)

Prior to this fix, && and || in when conditions silently failed (always evaluated to false). Now properly splits on &&/|| and evaluates each sub-expression independently.

TOML Flattening

[database]
host = "localhost"
 
[database.replica]
host = "replica.local"

DATABASE_HOST=localhost, DATABASE_REPLICA_HOST=replica.local

Non-Interactive Execution (-e)

The -e KEY=VALUE flag presets context variables before recipe execution, enabling fully non-interactive runs. Choice and Prompt steps check if their target variable already exists in context and skip user interaction if so.

# Preset a choice variable to skip the interactive menu
mando run mando/start -e mando_source=local
 
# Multiple presets
mando run debug/memray-container -e debug_service=forecast -e memray_mode=flamegraph
 
# Full stack with algos + OTel tracing, non-interactive
cargo run -- run mando/start-with-algos -e enable_otel=true -e mando_source=local
 
# Combine with --replay for fully automated re-runs
mando run menu -e mode=2 --replay

For Agents

The -e flag makes recipes scriptable from CI or parent orchestrators. Any choice or prompt step whose name matches a preset key will use the preset value without prompting.

State Persistence

  • File: .mando-state.json (sibling of recipes/)
  • Stores all prompt/choice answers per recipe, plus the swap registry
  • --replay re-uses saved values; falls back to interactive if missing
  • Updated after successful recipe completion

Environment Forwarding

All context variables are exported as OS env vars to child processes. Critical for docker compose which reads ${VAR} from process environment.

Recipe Discovery

  1. Look for recipes/ in current directory
  2. Walk up parent directories (like .git discovery)
  3. All *.yaml/*.yml under recipes/ are loadable
  4. Recipe paths always relative to recipes/ root (not current file)

Dependencies

CrateVersionPurpose
clap4CLI argument parsing (derive)
serde + serde_yaml1 / 0.9YAML deserialization
serde_json1State file (JSON)
toml0.8TOML config parsing
dialoguer0.11Interactive prompts (Confirm, Input, Select)
dotenvy0.15.env file loading
ctrlc3Signal handling
chrono0.4Timestamps (RFC3339)
anyhow1Error handling
ureq3HTTP requests (blocking)
tempfile3Test fixtures (dev-only)

Edition: Rust 2024

Test Coverage

65 tests total across 2 test suites — all passing.

  • Integration tests: smoke test, list, conditional when, include context sharing, swap (6 tests: dir swap+restore, dirty backup, double-swap prevention, nonexistent repo, restore-all, branch worktree)
  • Unit tests per module: run, check, capture, prompt, choice, env, compose, http, swap
  • Recipe parsing tests: minimal recipe, all step types, YAML errors
  • Context tests: interpolation, when evaluation, env export
  • Discovery tests: walk-up, resolve, normalize, list
  • State tests: save/load roundtrip, file location, swap registry

Design Decisions

  1. Generic engine — no domain knowledge baked in; YAML encodes all BESS/Docker logic
  2. Interpreter pattern — sequential step walking, no parallel execution
  3. Shared context — single HashMap flows through all steps and sub-recipes
  4. Composable recipes — choice/include enable recursive composition
  5. Replay — deterministic re-runs for CI or repeated use
  6. Symlink-based swap — transparent to all downstream tools; no path rewriting needed

Non-Goals

  • Parallel step execution
  • Remote recipe fetching
  • GUI / web interface
  • Replacing docker compose (wraps it)

Session History

2026-03-15 — Repo Swap Feature

  • mando swap subcommand — swap POC repo directories via symlink for development isolation
  • Swap modes — external directory, existing worktree, ad-hoc worktree from branch (with optional --from base)
  • Dirty-repo auto-backup — uncommitted changes auto-committed to mando-backup/<repo>-<timestamp> branch before swap; cherry-picked back on restore
  • swap recipe step type — automate swaps within YAML workflows with repo, path, branch, from, restore_all fields
  • Swap registry — active swaps tracked in .mando-state.json; managed worktrees in .mando-worktrees/
  • 6 new integration tests — directory swap+restore, dirty backup, double-swap prevention, nonexistent repo, restore-all, branch worktree
  • Files added: src/swap.rs (core logic), src/steps/swap.rs (recipe step handler)
  • Files modified: src/main.rs, src/recipe.rs, src/runner.rs, src/state.rs, src/context.rs, src/steps/check.rs, src/steps/choice.rs, src/steps/compose.rs, src/steps/prompt.rs, src/steps/run.rs, src/steps/capture.rs

2026-03-15 — Enhancements & Bug Fixes

  • Fixed corrupted interpolate functionpub fn int7 was corrupted/truncated; restored full pub fn interpolate signature in context.rs
  • Moved recipes to project rootmando-cli/recipes/ to poc/recipes/; all compose file references updated to optimization-universe-iac/local/ prefix
  • Added -e KEY=VALUE flag — non-interactive recipe execution via preset context variables
  • Fixed compound when conditions&& and || operators were silently failing (always false); now properly evaluated
  • Choice/Prompt preset support — steps skip interactive prompts when variable already in context
  • Code simplifications:
    • Step::when() method — common accessor instead of match-on-every-variant
    • ctx.resolve_cwd() — centralized working directory resolution
    • Replay helpers — cleaner state save/load
    • Recipe description display in CLI output

2026-03-13 to 2026-03-14 — Initial Build

  • Session: 66fa8cfe (4.2M, 21 subagents)
  • Design spec implementation plan full implementation with tests
  • Replaces optimization-universe-iac/local/run.py