One CLI. Every project. Chameleon adapts.
Chameleon is a terminal UI command runner that reads a per-project chameleon.yaml and dynamically builds an interactive menu of your commands. Whether a project uses npm, Python, Bash, Make, or Docker — you only need to remember one interface.
┌─────────────────────────────────────┐
│ 🦎 My Project │
│ │
│ ▸ ingest — Run ingestion pipeline │
│ build — Build the project │
│ deploy — Deploy to production │
│ test — Run test suite │
│ │
│ ↑/↓ navigate • enter select • q quit│
└─────────────────────────────────────┘
Developers jump between projects constantly — and in the AI era, that's never been more true. We're all building prototypes faster than ever, spinning up new tools, experimenting with new stacks. The days of living in one or two languages are gone. With AI-assisted development you might have a Go service, a Python data pipeline, a Node.js frontend, and a Rust CLI all in active rotation. Each project has its own tooling, its own scripts, its own way of doing things. One uses npm run build, another uses make build, another uses python scripts/build.py. The cognitive overhead of remembering project-specific commands across all of these adds up fast.
Chameleon provides a consistent abstraction layer. You define a chameleon.yaml in each project that maps familiar command names to project-specific scripts. The TUI dynamically renders whatever commands are available, so you never need to remember which tool a particular project uses.
- Go — single binary, no runtime dependencies
- Bubble Tea — terminal UI framework
- Lip Gloss — TUI styling
- YAML — configuration format (LLM-friendly for AI-assisted editing)
- Go 1.21+
git clone https://github.com/dan-westall/cli-chameleon.git
cd chameleon
make installThis installs the chameleon binary to your $GOPATH/bin.
make build
cp bin/chameleon /usr/local/bin/go install github.com/dan-westall/cli-chameleon@latestNavigate to any project directory and run:
chameleonIf no chameleon.yaml exists in the current directory, Chameleon creates a commented template:
$ cd my-new-project
$ chameleon
Created chameleon.yaml — edit it to configure your commands.Once configured, running chameleon launches the interactive TUI menu showing all available commands for that project.
chameleon --versionCreate a chameleon.yaml in your project root:
name: "My Project"
commands:
- name: ingest
description: "Run the ingestion pipeline"
run: "python scripts/ingest.py"
stream: true
- name: build
description: "Build the project"
run:
- "npm install"
- "npm run build"
- name: deploy
description: "Deploy services in parallel"
run:
- "docker push app"
- "docker push worker"
parallel: true
- name: test
description: "Run test suite"
run: "go test ./..."| Field | Type | Default | Description |
|---|---|---|---|
name |
string | required | Command name displayed in the menu |
description |
string | "" |
Short description shown alongside the name |
run |
string or string[] | required | Shell command(s) to execute |
stream |
bool | false |
Show live output in a split-panel view |
parallel |
bool | false |
Run array commands concurrently instead of sequentially |
Single command — run is a string:
run: "npm run build"Sequential — run is an array (default behaviour):
run:
- "npm install"
- "npm run build"Commands run in order. Execution stops on the first failure.
Parallel — run is an array with parallel: true:
run:
- "docker push app"
- "docker push worker"
parallel: trueAll commands run concurrently. Fails if any command fails.
Non-stream commands (default) execute in the background. On completion, a success or failure toast flashes briefly and the menu returns.
Stream commands (stream: true) open a 70/30 split-panel view:
┌──────────────────────────────────────────────┬──────────────────┐
│ Processing batch 1/10... │ ingest │
│ Processing batch 2/10... │ │
│ Processing batch 3/10... │ ● Running │
│ Processing batch 4/10... │ │
│ │ │
├──────────────────────────────────────────────┴──────────────────┤
│ esc back • q quit │
└─────────────────────────────────────────────────────────────────┘
- 70% left panel — live-streamed stdout/stderr from the running process
- 30% right panel — command name and status (reserved for future use)
| Key | Action |
|---|---|
↑ / k |
Move cursor up |
↓ / j |
Move cursor down |
Enter |
Execute selected command |
Esc |
Return to menu (from stream view) |
q |
Quit |
chameleon/
├── main.go # Entry point
├── Makefile # Build targets
├── go.mod
└── internal/
├── config/
│ ├── config.go # YAML schema and parser
│ └── template.go # Template generation
├── executor/
│ └── executor.go # Command execution (sequential/parallel/stream)
└── tui/
├── model.go # Root Bubble Tea model
├── menu.go # Main menu view
├── toast.go # Success/fail toast
└── stream.go # 70/30 split stream view
make build # Build binary to bin/chameleon
make install # Install to $GOPATH/bin
make test # Run all tests
make clean # Remove build artefactsThe power of Chameleon is that ingest means the same thing to you regardless of which project you're in — but the underlying implementation is completely different.
name: "rag-server"
commands:
- name: ingest
description: "Run document ingestion pipeline"
run: "python scripts/ingest.py --source ./documents"
stream: true
- name: serve
description: "Start the RAG API server"
run: "python -m uvicorn app.main:app --reload"
stream: true
- name: embed
description: "Regenerate embeddings"
run: "python scripts/embed.py"
stream: true
- name: setup
description: "Install dependencies"
run:
- "python -m venv .venv"
- ".venv/bin/pip install -r requirements.txt"name: "data-warehouse"
commands:
- name: ingest
description: "Run Hop ingestion workflow"
run: "hop-run --file workflows/ingest.hwf --runconfig local"
stream: true
- name: validate
description: "Run data quality checks"
run: "hop-run --file workflows/validate.hwf --runconfig local"
stream: true
- name: build
description: "Build and export pipelines"
run: "hop-conf --export-metadata"name: "content-ingestion-cli"
commands:
- name: ingest
description: "Run JS ingestion process"
run: "node src/ingest.js --config config.json"
stream: true
- name: transform
description: "Transform and normalise data"
run: "node src/transform.js"
stream: true
- name: build
description: "Build the CLI"
run:
- "npm ci"
- "npm run build"
- name: test
description: "Run test suite"
run: "npm test"In all three projects, you type chameleon, select ingest, and the right thing happens.
Contributions are welcome. Please open an issue to discuss changes before submitting a pull request.
MIT