diff --git a/CLAUDE.md b/CLAUDE.md
index fd2755d..76e8ee9 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -17,7 +17,7 @@ src/compose_farm/
│ ├── config.py # Config subcommand (init, show, path, validate, edit, symlink)
│ ├── lifecycle.py # up, down, stop, pull, restart, update, apply, compose commands
│ ├── management.py # refresh, check, init-network, traefik-file commands
-│ ├── monitoring.py # logs, ps, stats commands
+│ ├── monitoring.py # logs, ps, stats, list commands
│ ├── ssh.py # SSH key management (setup, status, keygen)
│ └── web.py # Web UI server command
├── compose.py # Compose file parsing (.env, ports, volumes, networks)
@@ -157,6 +157,7 @@ CLI available as `cf` or `compose-farm`.
| `logs` | Show stack logs |
| `ps` | Show status of all stacks |
| `stats` | Show overview (hosts, stacks, pending migrations; `--live` for container counts) |
+| `list` | List stacks and hosts (`--simple` for scripting, `--host` to filter) |
| `refresh` | Update state from reality: discover running stacks, capture image digests |
| `check` | Validate config, traefik labels, mounts, networks; show host compatibility |
| `init-network` | Create Docker network on hosts with consistent subnet/gateway |
diff --git a/README.md b/README.md
index ff536e3..0dd2c79 100644
--- a/README.md
+++ b/README.md
@@ -395,6 +395,7 @@ Multi-host orchestration that Docker Compose can't do:
| `cf traefik-file` | Generate Traefik file-provider config |
| `cf config` | Manage config files (init, show, validate, edit, symlink) |
| `cf ssh` | Manage SSH keys (setup, status, keygen) |
+| `cf list` | List all stacks and their assigned hosts |
### Aliases
@@ -403,10 +404,11 @@ Short aliases for frequently used commands:
| Alias | Command | Alias | Command |
|-------|---------|-------|---------|
| `cf a` | `apply` | `cf s` | `stats` |
-| `cf l` | `logs` | `cf c` | `compose` |
+| `cf l` | `logs` | `cf ls` | `list` |
| `cf r` | `restart` | `cf rf` | `refresh` |
| `cf u` | `update` | `cf ck` | `check` |
| `cf p` | `pull` | `cf tf` | `traefik-file` |
+| `cf c` | `compose` | | |
Each command replaces: look up host → SSH → find compose file → run `ssh host "cd /opt/compose/plex && docker compose up -d"`.
@@ -511,6 +513,7 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
│ service. │
│ ps Show status of stacks. │
│ stats Show overview statistics for hosts and stacks. │
+│ list List all stacks and their assigned hosts. │
╰──────────────────────────────────────────────────────────────────────────────╯
╭─ Server ─────────────────────────────────────────────────────────────────────╮
│ web Start the web UI server. │
@@ -1191,6 +1194,38 @@ Full `--help` output for each command. See the [Usage](#usage) table above for a
+
+See the output of cf list --help
+
+
+
+
+
+
+
+
+
+
+
+```yaml
+
+ Usage: cf list [OPTIONS]
+
+ List all stacks and their assigned hosts.
+
+╭─ Options ────────────────────────────────────────────────────────────────────╮
+│ --host -H TEXT Filter to stacks on this host │
+│ --simple -s Plain output (one stack per line, for scripting) │
+│ --config -c PATH Path to config file │
+│ --help -h Show this message and exit. │
+╰──────────────────────────────────────────────────────────────────────────────╯
+
+```
+
+
+
+
+
**Server**
diff --git a/docs/commands.md b/docs/commands.md
index 43ac1fd..44d59f9 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -23,6 +23,7 @@ Commands are either **Docker Compose wrappers** (`up`, `down`, `stop`, `restart`
| **Monitoring** | `ps` | Show stack status |
| | `logs` | Show stack logs |
| | `stats` | Show overview statistics |
+| | `list` | List stacks and hosts |
| **Configuration** | `check` | Validate config and mounts |
| | `refresh` | Sync state from reality |
| | `init-network` | Create Docker network |
@@ -45,10 +46,11 @@ Short aliases for frequently used commands:
| Alias | Command | Alias | Command |
|-------|---------|-------|---------|
| `cf a` | `apply` | `cf s` | `stats` |
-| `cf l` | `logs` | `cf c` | `compose` |
+| `cf l` | `logs` | `cf ls` | `list` |
| `cf r` | `restart` | `cf rf` | `refresh` |
| `cf u` | `update` | `cf ck` | `check` |
| `cf p` | `pull` | `cf tf` | `traefik-file` |
+| `cf c` | `compose` | | |
---
@@ -466,6 +468,40 @@ cf stats --live
---
+### cf list
+
+List all stacks and their assigned hosts.
+
+```bash
+cf list [OPTIONS]
+```
+
+**Options:**
+
+| Option | Description |
+|--------|-------------|
+| `--host, -H TEXT` | Filter to stacks on this host |
+| `--simple, -s` | Plain output for scripting (one stack per line) |
+| `--config, -c PATH` | Path to config file |
+
+**Examples:**
+
+```bash
+# List all stacks
+cf list
+
+# Filter by host
+cf list --host nas
+
+# Plain output for scripting
+cf list --simple
+
+# Combine: list stack names on a specific host
+cf list --host nuc --simple
+```
+
+---
+
## Configuration Commands
### cf check
diff --git a/src/compose_farm/cli/monitoring.py b/src/compose_farm/cli/monitoring.py
index 0581014..4d930ee 100644
--- a/src/compose_farm/cli/monitoring.py
+++ b/src/compose_farm/cli/monitoring.py
@@ -203,6 +203,43 @@ def stats(
console.print(_build_summary_table(cfg, state, pending))
+@app.command("list", rich_help_panel="Monitoring")
+def list_(
+ host: HostOption = None,
+ simple: Annotated[
+ bool,
+ typer.Option("--simple", "-s", help="Plain output (one stack per line, for scripting)"),
+ ] = False,
+ config: ConfigOption = None,
+) -> None:
+ """List all stacks and their assigned hosts."""
+ cfg = load_config_or_exit(config)
+
+ stacks: list[tuple[str, str | list[str]]] = list(cfg.stacks.items())
+ if host:
+ stacks = [(s, h) for s, h in stacks if str(h) == host or host in str(h).split(",")]
+
+ if simple:
+ for stack, _ in sorted(stacks):
+ console.print(stack)
+ else:
+ # Assign colors to hosts for visual grouping
+ host_colors = ["magenta", "cyan", "green", "yellow", "blue", "red"]
+ unique_hosts = sorted({str(h) for _, h in stacks})
+ host_color_map = {h: host_colors[i % len(host_colors)] for i, h in enumerate(unique_hosts)}
+
+ table = Table(title="Stacks", show_header=True, header_style="bold cyan")
+ table.add_column("Stack")
+ table.add_column("Host")
+
+ for stack, host_val in sorted(stacks):
+ color = host_color_map.get(str(host_val), "white")
+ table.add_row(f"[{color}]{stack}[/]", f"[{color}]{host_val}[/]")
+
+ console.print(table)
+
+
# Aliases (hidden from help)
app.command("l", hidden=True)(logs) # cf l = cf logs
+app.command("ls", hidden=True)(list_) # cf ls = cf list
app.command("s", hidden=True)(stats) # cf s = cf stats