diff --git a/.gitignore b/.gitignore
index 0cd81b6a2..47851da6e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -87,6 +87,7 @@
!/freecad/
!/iterm2/
!/slay_the_spire_ii/
+!/eth2-quickstart/
!/rms/
!/renderdoc/
!/cloudcompare/
@@ -153,6 +154,8 @@
/iterm2/.*
/slay_the_spire_ii/*
/slay_the_spire_ii/.*
+/eth2-quickstart/*
+/eth2-quickstart/.*
/rms/*
/rms/.*
/renderdoc/*
@@ -204,6 +207,7 @@
!/iterm2/agent-harness/
!/slay_the_spire_ii/agent-harness/
!/slay_the_spire_ii/README.md
+!/eth2-quickstart/agent-harness/
!/rms/agent-harness/
!/renderdoc/agent-harness/
!/cloudcompare/agent-harness/
diff --git a/README.md b/README.md
index 0b483d09c..6b36f3c3e 100644
--- a/README.md
+++ b/README.md
@@ -861,6 +861,13 @@ Each application received complete, production-ready CLI interfaces — not demo
✅ 138 |
+| ⛓️ ETH2 QuickStart |
+DevOps / Ethereum |
+cli-anything-eth2-quickstart |
+eth2-quickstart shell automation + JSON health checks |
+✅ 18 |
+
+
| 🧜 Mermaid Live Editor |
Diagramming |
cli-anything-mermaid |
@@ -978,6 +985,7 @@ kdenlive 155 passed ✅ (111 unit + 44 e2e)
shotcut 154 passed ✅ (110 unit + 44 e2e)
zoom 22 passed ✅ (22 unit + 0 e2e)
drawio 138 passed ✅ (116 unit + 22 e2e)
+eth2-quickstart 18 passed ✅ (18 unit + 3 e2e skipped)
mermaid 10 passed ✅ (5 unit + 5 e2e)
anygen 50 passed ✅ (40 unit + 10 e2e)
notebooklm 21 passed ✅ (21 unit + 0 e2e)
@@ -1053,6 +1061,7 @@ cli-anything/
├── 📞 zoom/agent-harness/ # Zoom CLI (22 tests)
├── 🎵 musescore/agent-harness/ # MuseScore CLI (56 tests)
├── 📐 drawio/agent-harness/ # Draw.io CLI (138 tests)
+├── ⛓️ eth2-quickstart/agent-harness/ # ETH2 QuickStart CLI (18 unit, 3 e2e skipped)
├── 🧜 mermaid/agent-harness/ # Mermaid Live Editor CLI (10 tests)
├── ✨ anygen/agent-harness/ # AnyGen CLI (50 tests)
├── 🖼️ comfyui/agent-harness/ # ComfyUI CLI (70 tests)
diff --git a/eth2-quickstart/agent-harness/ETH2-QUICKSTART.md b/eth2-quickstart/agent-harness/ETH2-QUICKSTART.md
new file mode 100644
index 000000000..c02b3ea4a
--- /dev/null
+++ b/eth2-quickstart/agent-harness/ETH2-QUICKSTART.md
@@ -0,0 +1,112 @@
+# ETH2 QuickStart CLI Harness SOP
+
+## Overview
+
+This harness adds `cli-anything-eth2-quickstart`, a production-oriented CLI wrapper for the
+[`chimera-defi/eth2-quickstart`](https://github.com/chimera-defi/eth2-quickstart) repository.
+It does not reimplement node installation logic. Instead, it maps agent-friendly commands onto
+the repo's canonical shell entrypoints:
+
+- `scripts/eth2qs.sh`
+- `run_1.sh`
+- `run_2.sh`
+- `install/utils/doctor.sh`
+- `install/utils/stats.sh`
+- `install/web/install_nginx.sh`
+- `install/web/install_nginx_ssl.sh`
+- `install/web/install_caddy.sh`
+- `install/web/install_caddy_ssl.sh`
+
+## Architecture
+
+The package follows standard CLI-Anything layout:
+
+- `cli_anything/eth2_quickstart/eth2_quickstart_cli.py`
+ Click entrypoint with REPL and machine-readable `--json` mode.
+- `cli_anything/eth2_quickstart/core/project.py`
+ Repo-root discovery plus safe updates to `config/user_config.env`.
+- `cli_anything/eth2_quickstart/utils/eth2qs_backend.py`
+ Thin subprocess backend for invoking the upstream wrapper/scripts.
+- `cli_anything/eth2_quickstart/core/*.py`
+ Small orchestration helpers for install, RPC, validator guidance, and health.
+
+## Command Mapping
+
+### `setup-node`
+
+Purpose: orchestrate phase execution with explicit client/MEV flags.
+
+- `--phase phase1` -> `./scripts/eth2qs.sh phase1`
+- `--phase phase2` -> `./scripts/eth2qs.sh phase2 --execution=... --consensus=... --mev=...`
+- `--phase auto` without client flags -> `./scripts/eth2qs.sh ensure --apply --confirm`
+- `--phase auto` with client flags -> phase 2 install
+
+The harness also writes matching values into `config/user_config.env` when provided:
+
+- `ETH_NETWORK`
+- `EXEC_CLIENT`
+- `CONS_CLIENT`
+- `MEV_SOLUTION`
+- `ETHGAS_NETWORK`
+
+### `install-clients`
+
+Direct phase 2 wrapper for execution/consensus/MEV installation, including optional ETHGas.
+
+### `start-rpc`
+
+Configures RPC exposure through either Nginx or Caddy by calling the upstream install scripts.
+When `--server-name` is provided, the harness writes `SERVER_NAME` into `config/user_config.env`
+before invoking the selected web installer.
+
+### `configure-validator`
+
+This command intentionally avoids importing validator keys or generating secrets. It updates
+validator-related config values when provided:
+
+- `FEE_RECIPIENT`
+- `GRAFITTI`
+
+Then it returns client-specific import and follow-up commands for the selected consensus client.
+This keeps secret handling under operator control while still giving agents a composable surface.
+
+### `status`
+
+Returns:
+
+- human mode: `stats` output plus current repo root
+- JSON mode: `doctor --json`, `plan --json`, and raw `stats` output
+
+### `health-check`
+
+Returns the canonical `doctor --json` output and a short harness summary.
+
+## Safety
+
+- The harness requires `--confirm` for phase execution and RPC installer mutations.
+- It never generates validator keys.
+- It never deletes secrets or wallet data.
+- It preserves the upstream reboot boundary between Phase 1 and Phase 2.
+- It relies on upstream scripts for system mutations so behavior remains aligned with the source repo.
+
+## Installation Model
+
+The harness is meant to be installed from the CLI-Anything repo:
+
+```bash
+pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=eth2-quickstart/agent-harness
+```
+
+At runtime it operates against an `eth2-quickstart` checkout discovered by:
+
+1. `--repo-root`
+2. `ETH2QS_REPO_ROOT`
+3. current working directory / parents containing `scripts/eth2qs.sh`
+
+## Testing Strategy
+
+- `test_core.py`
+ Uses mocked backend calls and temporary repo fixtures. No real node backend required.
+- `test_full_e2e.py`
+ Auto-skips unless a real `eth2-quickstart` checkout is available via `ETH2QS_E2E_REPO_ROOT`.
+ E2E coverage is read-only or help-level by default so it is safe to run in CI.
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/README.md b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/README.md
new file mode 100644
index 000000000..28feb6b15
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/README.md
@@ -0,0 +1,81 @@
+# cli-anything-eth2-quickstart
+
+CLI harness for `eth2-quickstart` so agents can deploy and operate hardened Ethereum nodes
+through a consistent command surface with JSON output.
+
+## Prerequisites
+
+- Python 3.10+
+- A local checkout of `https://github.com/chimera-defi/eth2-quickstart`
+- Ubuntu host for real installation workflows
+
+## Installation
+
+```bash
+cd eth2-quickstart/agent-harness
+pip install -e .
+cli-anything-eth2-quickstart --help
+```
+
+## Repo Discovery
+
+The harness needs to know which `eth2-quickstart` checkout to operate on. It resolves the repo in
+this order:
+
+1. `--repo-root`
+2. `ETH2QS_REPO_ROOT`
+3. current working directory or one of its parents
+
+```bash
+export ETH2QS_REPO_ROOT=/srv/eth2-quickstart
+cli-anything-eth2-quickstart --json health-check
+```
+
+## Usage
+
+```bash
+# Interactive REPL
+cli-anything-eth2-quickstart
+
+# Inspect machine-readable health
+cli-anything-eth2-quickstart --json health-check
+
+# Install phase 2 clients
+cli-anything-eth2-quickstart --json install-clients \
+ --network mainnet \
+ --execution-client geth \
+ --consensus-client lighthouse \
+ --mev mev-boost \
+ --confirm
+
+# Configure validator metadata without touching secrets
+cli-anything-eth2-quickstart --json configure-validator \
+ --consensus-client lighthouse \
+ --fee-recipient 0x1111111111111111111111111111111111111111 \
+ --graffiti "CLI-Anything"
+
+# Install and start nginx-backed RPC exposure
+cli-anything-eth2-quickstart --json start-rpc \
+ --web-stack nginx \
+ --server-name rpc.example.org \
+ --confirm
+```
+
+## Commands
+
+- `setup-node`
+- `install-clients`
+- `start-rpc`
+- `configure-validator`
+- `status`
+- `health-check`
+
+All commands support `--json`.
+
+## Tests
+
+```bash
+cd eth2-quickstart/agent-harness
+python3 -m pytest cli_anything/eth2_quickstart/tests/test_core.py -v
+python3 -m pytest cli_anything/eth2_quickstart/tests/test_full_e2e.py -v
+```
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/__init__.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/__init__.py
new file mode 100644
index 000000000..f2d223fd5
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/__init__.py
@@ -0,0 +1,5 @@
+"""cli-anything-eth2-quickstart package."""
+
+__all__ = ["__version__"]
+
+__version__ = "1.0.0"
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/__main__.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/__main__.py
new file mode 100644
index 000000000..8ea9c8eb6
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/__main__.py
@@ -0,0 +1,5 @@
+from cli_anything.eth2_quickstart.eth2_quickstart_cli import main
+
+
+if __name__ == "__main__":
+ main()
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/__init__.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/__init__.py
new file mode 100644
index 000000000..e010d0d14
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/__init__.py
@@ -0,0 +1 @@
+"""Core helpers for cli-anything-eth2-quickstart."""
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/commands.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/commands.py
new file mode 100644
index 000000000..d02c0d276
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/commands.py
@@ -0,0 +1,126 @@
+"""Static command metadata and validator guidance."""
+
+from __future__ import annotations
+
+VALID_NETWORKS = {"mainnet", "holesky"}
+VALID_EXECUTION_CLIENTS = {
+ "geth",
+ "besu",
+ "erigon",
+ "nethermind",
+ "nimbus_eth1",
+ "reth",
+ "ethrex",
+}
+VALID_CONSENSUS_CLIENTS = {
+ "prysm",
+ "lighthouse",
+ "lodestar",
+ "teku",
+ "nimbus",
+ "grandine",
+}
+VALID_MEV_OPTIONS = {"mev-boost", "commit-boost", "none"}
+VALID_WEB_STACKS = {"nginx", "caddy"}
+
+
+def validator_plan(
+ consensus_client: str,
+ fee_recipient: str | None = None,
+ graffiti: str | None = None,
+ keys_dir: str | None = None,
+ secrets_dir: str | None = None,
+ wallet_password_file: str | None = None,
+) -> dict:
+ if consensus_client not in VALID_CONSENSUS_CLIENTS:
+ raise ValueError(f"Unsupported consensus client: {consensus_client}")
+
+ key_locations = {
+ "prysm": {
+ "keys": keys_dir or "/path/to/validator-keys",
+ "secrets": secrets_dir or "~/secrets",
+ "config_file": "~/prysm/prysm_validator_conf.yaml",
+ "import_command": (
+ "~/prysm/prysm.sh validator accounts import "
+ f"--keys-dir={keys_dir or '/path/to/validator-keys'}"
+ ),
+ },
+ "lighthouse": {
+ "keys": keys_dir or "~/.lighthouse/mainnet/validators",
+ "secrets": secrets_dir or "~/.lighthouse/mainnet/secrets",
+ "config_file": "/etc/systemd/system/validator.service",
+ "import_command": (
+ "lighthouse account validator import "
+ f"--directory {keys_dir or '/path/to/validator-keys'} "
+ f"--secrets-dir {secrets_dir or '~/.lighthouse/mainnet/secrets'}"
+ ),
+ },
+ "lodestar": {
+ "keys": keys_dir or "~/.local/share/lodestar/validators/keystores",
+ "secrets": secrets_dir or "~/.local/share/lodestar/validators/secrets",
+ "config_file": "~/lodestar/validator.config.json",
+ "import_command": (
+ "lodestar validator import "
+ f"--keystoresDir {keys_dir or '/path/to/validator-keys'} "
+ f"--secretsDir {secrets_dir or '~/.local/share/lodestar/validators/secrets'}"
+ ),
+ },
+ "teku": {
+ "keys": keys_dir or "~/.local/share/teku/validator/keys",
+ "secrets": secrets_dir or "~/.local/share/teku/validator/passwords",
+ "config_file": "~/teku/validator.yaml",
+ "import_command": (
+ "teku validator-client --help # import keys into the Teku validator key directory"
+ ),
+ },
+ "nimbus": {
+ "keys": keys_dir or "~/.local/share/nimbus/validators",
+ "secrets": secrets_dir or "~/.local/share/nimbus/validators/secrets",
+ "config_file": "~/nimbus/validator.toml",
+ "import_command": (
+ "nimbus_validator_client deposits import "
+ f"--data-dir={keys_dir or '~/.local/share/nimbus/validators'}"
+ ),
+ },
+ "grandine": {
+ "keys": keys_dir or "~/.local/share/grandine/validators",
+ "secrets": secrets_dir or "~/.local/share/grandine/validators/secrets",
+ "config_file": "~/grandine/grandine.toml",
+ "import_command": "Grandine validator import follows the upstream client workflow",
+ },
+ }
+
+ selected = key_locations[consensus_client]
+ post_import = [
+ "sudo systemctl restart validator",
+ "sudo systemctl status validator --no-pager",
+ "./scripts/eth2qs.sh doctor --json",
+ ]
+
+ if consensus_client == "prysm" and wallet_password_file:
+ post_import.insert(
+ 0,
+ f"Ensure wallet-password-file points to {wallet_password_file}",
+ )
+
+ updates = {}
+ if fee_recipient:
+ updates["FEE_RECIPIENT"] = fee_recipient
+ if graffiti:
+ # Upstream eth2-quickstart exports key is intentionally spelled "GRAFITTI".
+ updates["GRAFITTI"] = graffiti
+
+ return {
+ "consensus_client": consensus_client,
+ "config_updates": updates,
+ "config_file": selected["config_file"],
+ "keys_path": selected["keys"],
+ "secrets_path": selected["secrets"],
+ "import_command": selected["import_command"],
+ "post_import_commands": post_import,
+ "notes": [
+ "The harness does not import validator keys automatically.",
+ "Keep validator keystores and password files under operator control.",
+ "Restart validator service after importing keys.",
+ ],
+ }
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/install.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/install.py
new file mode 100644
index 000000000..4b265aa27
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/install.py
@@ -0,0 +1,153 @@
+"""Install orchestration helpers."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from cli_anything.eth2_quickstart.core.commands import (
+ VALID_CONSENSUS_CLIENTS,
+ VALID_EXECUTION_CLIENTS,
+ VALID_MEV_OPTIONS,
+ VALID_NETWORKS,
+)
+from cli_anything.eth2_quickstart.core.project import upsert_user_config
+
+if TYPE_CHECKING:
+ from cli_anything.eth2_quickstart.utils.eth2qs_backend import Eth2QuickStartBackend
+
+
+def _validate_choice(name: str, value: str | None, allowed: set[str]) -> None:
+ if value is not None and value not in allowed:
+ raise ValueError(f"Unsupported {name}: {value}")
+
+
+def _config_updates(
+ network: str | None,
+ execution_client: str | None,
+ consensus_client: str | None,
+ mev: str | None,
+) -> dict[str, str]:
+ updates: dict[str, str] = {}
+ if network:
+ updates["ETH_NETWORK"] = network
+ updates["ETHGAS_NETWORK"] = network
+ if execution_client:
+ updates["EXEC_CLIENT"] = execution_client
+ if consensus_client:
+ updates["CONS_CLIENT"] = consensus_client
+ if mev:
+ updates["MEV_SOLUTION"] = mev
+ return updates
+
+
+def install_clients(
+ backend: Eth2QuickStartBackend,
+ *,
+ network: str | None = None,
+ execution_client: str | None = None,
+ consensus_client: str | None = None,
+ mev: str | None = None,
+ ethgas: bool = False,
+ skip_deps: bool = False,
+) -> dict:
+ _validate_choice("network", network, VALID_NETWORKS)
+ _validate_choice("execution client", execution_client, VALID_EXECUTION_CLIENTS)
+ _validate_choice("consensus client", consensus_client, VALID_CONSENSUS_CLIENTS)
+ _validate_choice("mev", mev, VALID_MEV_OPTIONS)
+
+ if ethgas and mev != "commit-boost":
+ raise ValueError("ETHGas requires mev=commit-boost")
+
+ updates = _config_updates(network, execution_client, consensus_client, mev)
+ config_path = None
+ if updates:
+ config_path = upsert_user_config(backend.repo_root, updates)
+
+ args = ["phase2"]
+ if execution_client:
+ args.append(f"--execution={execution_client}")
+ if consensus_client:
+ args.append(f"--consensus={consensus_client}")
+ if mev:
+ args.append(f"--mev={mev}")
+ if ethgas:
+ args.append("--ethgas")
+ if skip_deps:
+ args.append("--skip-deps")
+
+ result = backend.run_wrapper(*args)
+ result["config_path"] = str(config_path) if config_path else None
+ result["requested"] = {
+ "network": network,
+ "execution_client": execution_client,
+ "consensus_client": consensus_client,
+ "mev": mev,
+ "ethgas": ethgas,
+ "skip_deps": skip_deps,
+ }
+ return result
+
+
+def setup_node(
+ backend: Eth2QuickStartBackend,
+ *,
+ phase: str,
+ network: str | None = None,
+ execution_client: str | None = None,
+ consensus_client: str | None = None,
+ mev: str | None = None,
+ ethgas: bool = False,
+ skip_deps: bool = False,
+) -> dict:
+ if phase not in {"auto", "phase1", "phase2"}:
+ raise ValueError(f"Unsupported phase: {phase}")
+
+ if phase == "phase1":
+ result = backend.run_wrapper("phase1")
+ result["requested_phase"] = phase
+ return result
+
+ if phase == "phase2":
+ result = install_clients(
+ backend,
+ network=network,
+ execution_client=execution_client,
+ consensus_client=consensus_client,
+ mev=mev,
+ ethgas=ethgas,
+ skip_deps=skip_deps,
+ )
+ result["requested_phase"] = phase
+ return result
+
+ if any([execution_client, consensus_client, mev, ethgas]):
+ result = install_clients(
+ backend,
+ network=network,
+ execution_client=execution_client,
+ consensus_client=consensus_client,
+ mev=mev,
+ ethgas=ethgas,
+ skip_deps=skip_deps,
+ )
+ result["requested_phase"] = "auto-phase2"
+ return result
+
+ _validate_choice("network", network, VALID_NETWORKS)
+ config_path = None
+ updates = _config_updates(network, None, None, None)
+ if updates:
+ config_path = upsert_user_config(backend.repo_root, updates)
+
+ result = backend.run_wrapper("ensure", "--apply", "--confirm")
+ result["config_path"] = str(config_path) if config_path else None
+ result["requested"] = {
+ "network": network,
+ "execution_client": execution_client,
+ "consensus_client": consensus_client,
+ "mev": mev,
+ "ethgas": ethgas,
+ "skip_deps": skip_deps,
+ }
+ result["requested_phase"] = "auto-ensure"
+ return result
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/project.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/project.py
new file mode 100644
index 000000000..07a0649b3
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/project.py
@@ -0,0 +1,76 @@
+"""Project discovery and config helpers for eth2-quickstart."""
+
+from __future__ import annotations
+
+import os
+import re
+from pathlib import Path
+
+REPO_ENV_VAR = "ETH2QS_REPO_ROOT"
+WRAPPER_RELATIVE_PATH = Path("scripts") / "eth2qs.sh"
+
+
+def find_repo_root(explicit_root: str | None = None, cwd: str | None = None) -> Path:
+ candidates: list[Path] = []
+
+ if explicit_root:
+ candidates.append(Path(explicit_root).expanduser())
+
+ env_root = os.environ.get(REPO_ENV_VAR)
+ if env_root:
+ candidates.append(Path(env_root).expanduser())
+
+ start = Path(cwd).resolve() if cwd else Path.cwd().resolve()
+ candidates.append(start)
+ candidates.extend(start.parents)
+
+ for candidate in candidates:
+ resolved = candidate.resolve()
+ if (resolved / WRAPPER_RELATIVE_PATH).is_file():
+ return resolved
+
+ raise RuntimeError(
+ "Could not locate an eth2-quickstart checkout. "
+ "Use --repo-root or set ETH2QS_REPO_ROOT."
+ )
+
+
+def wrapper_path(repo_root: Path) -> Path:
+ return repo_root / WRAPPER_RELATIVE_PATH
+
+
+def user_config_path(repo_root: Path) -> Path:
+ return repo_root / "config" / "user_config.env"
+
+
+def ensure_user_config(repo_root: Path) -> Path:
+ config_path = user_config_path(repo_root)
+ config_path.parent.mkdir(parents=True, exist_ok=True)
+ if not config_path.exists():
+ config_path.write_text(
+ "# Managed by cli-anything-eth2-quickstart\n",
+ encoding="utf-8",
+ )
+ return config_path
+
+
+def shell_quote(value: str) -> str:
+ return "'" + value.replace("'", "'\"'\"'") + "'"
+
+
+def upsert_user_config(repo_root: Path, updates: dict[str, str]) -> Path:
+ config_path = ensure_user_config(repo_root)
+ existing = config_path.read_text(encoding="utf-8")
+
+ for key, value in updates.items():
+ line = f"export {key}={shell_quote(value)}"
+ pattern = re.compile(rf"^export {re.escape(key)}=.*$", re.MULTILINE)
+ if pattern.search(existing):
+ existing = pattern.sub(line, existing)
+ else:
+ if existing and not existing.endswith("\n"):
+ existing += "\n"
+ existing += line + "\n"
+
+ config_path.write_text(existing, encoding="utf-8")
+ return config_path
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/rpc.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/rpc.py
new file mode 100644
index 000000000..74c5b0770
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/rpc.py
@@ -0,0 +1,40 @@
+"""RPC exposure helpers."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from cli_anything.eth2_quickstart.core.commands import VALID_WEB_STACKS
+from cli_anything.eth2_quickstart.core.project import upsert_user_config
+
+if TYPE_CHECKING:
+ from cli_anything.eth2_quickstart.utils.eth2qs_backend import Eth2QuickStartBackend
+
+
+def start_rpc(
+ backend: Eth2QuickStartBackend,
+ *,
+ web_stack: str,
+ server_name: str | None = None,
+ ssl: bool = False,
+) -> dict:
+ if web_stack not in VALID_WEB_STACKS:
+ raise ValueError(f"Unsupported web stack: {web_stack}")
+
+ config_path = None
+ if server_name:
+ config_path = upsert_user_config(backend.repo_root, {"SERVER_NAME": server_name})
+
+ suffix = "_ssl" if ssl else ""
+ script = f"install/web/install_{web_stack}{suffix}.sh"
+ result = backend.run_script(script)
+ result["config_path"] = str(config_path) if config_path else None
+ result["rpc_url"] = (
+ f"{'https' if ssl else 'http'}://{server_name}/rpc" if server_name else None
+ )
+ result["ws_url"] = (
+ f"{'wss' if ssl else 'ws'}://{server_name}/ws" if server_name else None
+ )
+ result["web_stack"] = web_stack
+ result["ssl"] = ssl
+ return result
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/status.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/status.py
new file mode 100644
index 000000000..b1796cf00
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/status.py
@@ -0,0 +1,45 @@
+"""Status and health helpers."""
+
+from __future__ import annotations
+
+import json
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from cli_anything.eth2_quickstart.utils.eth2qs_backend import Eth2QuickStartBackend
+
+
+def _try_parse_json(text: str):
+ try:
+ return json.loads(text)
+ except json.JSONDecodeError:
+ return None
+
+
+def health_check(backend: Eth2QuickStartBackend) -> dict:
+ result = backend.run_wrapper("doctor", "--json")
+ parsed = _try_parse_json(result.get("stdout", ""))
+ return {
+ "repo_root": str(backend.repo_root),
+ "command_result": result,
+ "doctor": parsed,
+ "ok": result["ok"],
+ }
+
+
+def status(backend: Eth2QuickStartBackend) -> dict:
+ doctor_result = backend.run_wrapper("doctor", "--json")
+ plan_result = backend.run_wrapper("plan", "--json")
+ stats_result = backend.run_wrapper("stats")
+ return {
+ "repo_root": str(backend.repo_root),
+ "doctor": _try_parse_json(doctor_result.get("stdout", "")),
+ "plan": _try_parse_json(plan_result.get("stdout", "")),
+ "stats_raw": stats_result.get("stdout", ""),
+ "commands": {
+ "doctor": doctor_result,
+ "plan": plan_result,
+ "stats": stats_result,
+ },
+ "ok": doctor_result["ok"] and plan_result["ok"] and stats_result["ok"],
+ }
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/validator.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/validator.py
new file mode 100644
index 000000000..eb63a956b
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/core/validator.py
@@ -0,0 +1,41 @@
+"""Validator configuration helpers."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from cli_anything.eth2_quickstart.core.commands import validator_plan
+from cli_anything.eth2_quickstart.core.project import upsert_user_config
+
+if TYPE_CHECKING:
+ from cli_anything.eth2_quickstart.utils.eth2qs_backend import Eth2QuickStartBackend
+
+
+def configure_validator(
+ backend: Eth2QuickStartBackend,
+ *,
+ consensus_client: str,
+ fee_recipient: str | None = None,
+ graffiti: str | None = None,
+ keys_dir: str | None = None,
+ secrets_dir: str | None = None,
+ wallet_password_file: str | None = None,
+) -> dict:
+ plan = validator_plan(
+ consensus_client=consensus_client,
+ fee_recipient=fee_recipient,
+ graffiti=graffiti,
+ keys_dir=keys_dir,
+ secrets_dir=secrets_dir,
+ wallet_password_file=wallet_password_file,
+ )
+ config_path = None
+ if plan["config_updates"]:
+ config_path = upsert_user_config(backend.repo_root, plan["config_updates"])
+
+ return {
+ "repo_root": str(backend.repo_root),
+ "config_path": str(config_path) if config_path else None,
+ "plan": plan,
+ "ok": True,
+ }
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/eth2_quickstart_cli.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/eth2_quickstart_cli.py
new file mode 100644
index 000000000..d297407da
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/eth2_quickstart_cli.py
@@ -0,0 +1,284 @@
+"""cli-anything-eth2-quickstart CLI."""
+
+from __future__ import annotations
+
+import json
+import shlex
+from typing import NoReturn
+
+import click
+
+from cli_anything.eth2_quickstart import __version__
+from cli_anything.eth2_quickstart.core.commands import (
+ VALID_CONSENSUS_CLIENTS,
+ VALID_EXECUTION_CLIENTS,
+ VALID_MEV_OPTIONS,
+ VALID_NETWORKS,
+)
+from cli_anything.eth2_quickstart.core.install import install_clients, setup_node
+from cli_anything.eth2_quickstart.core.rpc import start_rpc
+from cli_anything.eth2_quickstart.core.status import health_check, status
+from cli_anything.eth2_quickstart.core.validator import configure_validator
+from cli_anything.eth2_quickstart.utils.eth2qs_backend import Eth2QuickStartBackend
+
+CONTEXT_SETTINGS = {"help_option_names": ["-h", "--help"]}
+NETWORK_CHOICES = click.Choice(sorted(VALID_NETWORKS))
+EXECUTION_CLIENT_CHOICES = click.Choice(sorted(VALID_EXECUTION_CLIENTS))
+CONSENSUS_CLIENT_CHOICES = click.Choice(sorted(VALID_CONSENSUS_CLIENTS))
+MEV_CHOICES = click.Choice(sorted(VALID_MEV_OPTIONS))
+
+
+def emit(data, as_json: bool) -> None:
+ if as_json:
+ click.echo(json.dumps(data, indent=2, default=str))
+ return
+
+ if isinstance(data, dict):
+ for key, value in data.items():
+ if isinstance(value, (dict, list)):
+ click.echo(f"{key}: {json.dumps(value, default=str)}")
+ else:
+ click.echo(f"{key}: {value}")
+ return
+
+ click.echo(str(data))
+
+
+def backend_from_context(ctx: click.Context) -> Eth2QuickStartBackend:
+ try:
+ return Eth2QuickStartBackend(ctx.obj["repo_root"])
+ except RuntimeError as exc:
+ fail(str(exc), ctx.obj["as_json"])
+
+
+def fail(message: str, as_json: bool) -> NoReturn:
+ if as_json:
+ click.echo(json.dumps({"error": message}))
+ else:
+ click.echo(message)
+ raise click.exceptions.Exit(1)
+
+
+def require_confirm(ctx: click.Context, confirm: bool = False) -> None:
+ if not (ctx.obj.get("confirm", False) or confirm):
+ fail("This command requires --confirm", ctx.obj["as_json"])
+
+
+def handle_backend_result(result: dict, as_json: bool) -> None:
+ if result.get("ok"):
+ emit(result, as_json)
+ return
+
+ payload = {
+ "error": "Command failed",
+ "result": result,
+ }
+ if as_json:
+ click.echo(json.dumps(payload, indent=2))
+ else:
+ click.echo(f"ERROR {result.get('stderr') or result.get('stdout')}")
+ raise click.exceptions.Exit(1)
+
+
+@click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True)
+@click.option("--repo-root", default=None, help="Path to an eth2-quickstart checkout")
+@click.option("--json", "as_json", is_flag=True, default=False, help="Output as JSON")
+@click.option("--confirm", is_flag=True, default=False, help="Confirm mutating operations")
+@click.pass_context
+def cli(ctx: click.Context, repo_root, as_json, confirm):
+ """CLI harness for eth2-quickstart."""
+ ctx.ensure_object(dict)
+ ctx.obj["repo_root"] = repo_root
+ ctx.obj["as_json"] = as_json
+ ctx.obj["confirm"] = confirm
+
+ if ctx.invoked_subcommand is None:
+ ctx.invoke(repl)
+
+
+def main():
+ cli(obj={})
+
+
+@cli.command(hidden=True)
+@click.pass_context
+def repl(ctx: click.Context):
+ """Interactive REPL mode."""
+ from cli_anything.eth2_quickstart.utils.repl_skin import ReplSkin
+
+ skin = ReplSkin("eth2-quickstart", version=__version__)
+ skin.print_banner()
+ pt_session = skin.create_prompt_session()
+
+ while True:
+ try:
+ line = skin.get_input(pt_session, project_name="repo")
+ except (EOFError, KeyboardInterrupt):
+ break
+
+ line = line.strip()
+ if not line:
+ continue
+ if line in {"exit", "quit"}:
+ break
+ if line == "help":
+ skin.help(
+ {
+ "setup-node": "Run phase1, phase2, or ensure-driven orchestration",
+ "install-clients": "Install execution, consensus, and MEV clients",
+ "start-rpc": "Install and start nginx/caddy RPC exposure",
+ "configure-validator": "Update validator metadata and return import guidance",
+ "status": "Show aggregate status",
+ "health-check": "Run doctor --json",
+ }
+ )
+ continue
+
+ try:
+ args = shlex.split(line)
+ cli.main(args=args, obj=dict(ctx.obj), standalone_mode=False)
+ except click.exceptions.Exit:
+ pass
+ except Exception as exc: # pragma: no cover - REPL fallback
+ skin.error(str(exc))
+
+ skin.print_goodbye()
+
+
+@cli.command("setup-node")
+@click.option("--phase", type=click.Choice(["auto", "phase1", "phase2"]), default="auto")
+@click.option("--network", type=NETWORK_CHOICES, default=None)
+@click.option("--execution-client", type=EXECUTION_CLIENT_CHOICES, default=None)
+@click.option("--consensus-client", type=CONSENSUS_CLIENT_CHOICES, default=None)
+@click.option("--mev", type=MEV_CHOICES, default=None)
+@click.option("--ethgas", is_flag=True, default=False)
+@click.option("--skip-deps", is_flag=True, default=False)
+@click.option("--confirm", is_flag=True, default=False, help="Confirm mutating operations")
+@click.pass_context
+def setup_node_cmd(
+ ctx,
+ phase,
+ network,
+ execution_client,
+ consensus_client,
+ mev,
+ ethgas,
+ skip_deps,
+ confirm,
+):
+ """Set up a node using phase1, phase2, or ensure-driven orchestration."""
+ require_confirm(ctx, confirm)
+ backend = backend_from_context(ctx)
+ result = setup_node(
+ backend,
+ phase=phase,
+ network=network,
+ execution_client=execution_client,
+ consensus_client=consensus_client,
+ mev=mev,
+ ethgas=ethgas,
+ skip_deps=skip_deps,
+ )
+ handle_backend_result(result, ctx.obj["as_json"])
+
+
+@cli.command("install-clients")
+@click.option("--network", type=NETWORK_CHOICES, default=None)
+@click.option("--execution-client", required=True, type=EXECUTION_CLIENT_CHOICES)
+@click.option("--consensus-client", required=True, type=CONSENSUS_CLIENT_CHOICES)
+@click.option("--mev", type=MEV_CHOICES, default="mev-boost")
+@click.option("--ethgas", is_flag=True, default=False)
+@click.option("--skip-deps", is_flag=True, default=False)
+@click.option("--confirm", is_flag=True, default=False, help="Confirm mutating operations")
+@click.pass_context
+def install_clients_cmd(
+ ctx,
+ network,
+ execution_client,
+ consensus_client,
+ mev,
+ ethgas,
+ skip_deps,
+ confirm,
+):
+ """Install execution, consensus, and MEV clients via phase2."""
+ require_confirm(ctx, confirm)
+ backend = backend_from_context(ctx)
+ result = install_clients(
+ backend,
+ network=network,
+ execution_client=execution_client,
+ consensus_client=consensus_client,
+ mev=mev,
+ ethgas=ethgas,
+ skip_deps=skip_deps,
+ )
+ handle_backend_result(result, ctx.obj["as_json"])
+
+
+@cli.command("start-rpc")
+@click.option("--web-stack", type=click.Choice(["nginx", "caddy"]), default="nginx")
+@click.option("--server-name", default=None, help="Public hostname for RPC exposure")
+@click.option("--ssl/--no-ssl", default=False)
+@click.option("--confirm", is_flag=True, default=False, help="Confirm mutating operations")
+@click.pass_context
+def start_rpc_cmd(ctx, web_stack, server_name, ssl, confirm):
+ """Install and start RPC exposure via nginx or caddy."""
+ require_confirm(ctx, confirm)
+ backend = backend_from_context(ctx)
+ result = start_rpc(
+ backend,
+ web_stack=web_stack,
+ server_name=server_name,
+ ssl=ssl,
+ )
+ handle_backend_result(result, ctx.obj["as_json"])
+
+
+@cli.command("configure-validator")
+@click.option("--consensus-client", required=True, type=CONSENSUS_CLIENT_CHOICES)
+@click.option("--fee-recipient", default=None)
+@click.option("--graffiti", default=None)
+@click.option("--keys-dir", default=None)
+@click.option("--secrets-dir", default=None)
+@click.option("--wallet-password-file", default=None)
+@click.pass_context
+def configure_validator_cmd(
+ ctx,
+ consensus_client,
+ fee_recipient,
+ graffiti,
+ keys_dir,
+ secrets_dir,
+ wallet_password_file,
+):
+ """Update validator metadata and return client-specific import guidance."""
+ backend = backend_from_context(ctx)
+ result = configure_validator(
+ backend,
+ consensus_client=consensus_client,
+ fee_recipient=fee_recipient,
+ graffiti=graffiti,
+ keys_dir=keys_dir,
+ secrets_dir=secrets_dir,
+ wallet_password_file=wallet_password_file,
+ )
+ emit(result, ctx.obj["as_json"])
+
+
+@cli.command("status")
+@click.pass_context
+def status_cmd(ctx):
+ """Show aggregate node status."""
+ backend = backend_from_context(ctx)
+ result = status(backend)
+ emit(result, ctx.obj["as_json"])
+
+
+@cli.command("health-check")
+@click.pass_context
+def health_check_cmd(ctx):
+ """Run the canonical doctor --json health check."""
+ backend = backend_from_context(ctx)
+ result = health_check(backend)
+ emit(result, ctx.obj["as_json"])
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/skills/SKILL.md b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/skills/SKILL.md
new file mode 100644
index 000000000..f634131bf
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/skills/SKILL.md
@@ -0,0 +1,73 @@
+---
+name: cli-anything-eth2-quickstart
+description: >-
+ Use eth2-quickstart to autonomously deploy a hardened Ethereum node, install
+ execution and consensus clients, configure validator metadata, expose RPC
+ safely, and inspect node health with structured JSON output.
+---
+
+# cli-anything-eth2-quickstart
+
+Agent-native harness for the `chimera-defi/eth2-quickstart` automation repo.
+This CLI wraps the repo's canonical shell scripts instead of replacing them.
+
+## When To Use
+
+Use this skill when the task involves:
+
+- bootstrapping a fresh Ethereum node host
+- installing execution and consensus clients with explicit client diversity
+- enabling MEV-Boost or Commit-Boost workflows
+- exposing RPC through Nginx or Caddy
+- updating validator fee recipient or graffiti settings without handling secrets
+- checking machine-readable health with `--json`
+
+## Core Commands
+
+```bash
+# Canonical machine-readable health
+cli-anything-eth2-quickstart --json health-check
+
+# Phase 2 install with explicit client choices
+cli-anything-eth2-quickstart --json install-clients \
+ --network mainnet \
+ --execution-client geth \
+ --consensus-client lighthouse \
+ --mev mev-boost \
+ --confirm
+
+# Guided node setup
+cli-anything-eth2-quickstart --json setup-node \
+ --phase auto \
+ --execution-client geth \
+ --consensus-client prysm \
+ --mev commit-boost \
+ --confirm
+
+# Validator metadata only; no key import
+cli-anything-eth2-quickstart --json configure-validator \
+ --consensus-client prysm \
+ --fee-recipient 0x1111111111111111111111111111111111111111 \
+ --graffiti "CLI-Anything"
+
+# Install nginx-backed RPC exposure
+cli-anything-eth2-quickstart --json start-rpc \
+ --web-stack nginx \
+ --server-name rpc.example.org \
+ --confirm
+```
+
+## Safety Rules
+
+- Always use `--json` for agent parsing.
+- Require human confirmation before `setup-node`, `install-clients`, or `start-rpc`.
+- Do not generate validator keys.
+- Do not remove secrets or wallet material.
+- Treat `configure-validator` as metadata and operator-guidance only.
+- Respect the reboot boundary between Phase 1 and Phase 2.
+
+## Runtime Expectations
+
+- Operates on a local `eth2-quickstart` checkout.
+- Discovers repo root from `--repo-root`, `ETH2QS_REPO_ROOT`, or current working directory.
+- Writes compatible overrides into `config/user_config.env` when flags map directly to repo settings.
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/TEST.md b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/TEST.md
new file mode 100644
index 000000000..9a21c4aa2
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/TEST.md
@@ -0,0 +1,67 @@
+# cli-anything-eth2-quickstart Test Plan
+
+## Unit
+
+- repo root detection from explicit path and environment
+- config file upsert behavior
+- CLI help and JSON output
+- phase 2 command construction
+- validator guidance generation
+- status and health aggregation with mocked subprocess results
+
+## E2E
+
+- skip automatically when no real `eth2-quickstart` checkout is configured
+- verify wrapper discovery
+- verify read-only commands (`help`, `health-check`) against a real checkout
+
+## Latest Pytest Results
+
+### Unit
+
+```text
+============================= test session starts ==============================
+platform linux -- Python 3.12.3, pytest-7.4.4, pluggy-1.4.0 -- /usr/bin/python3
+cachedir: .pytest_cache
+rootdir: /root/.openclaw/workspace/dev/CLI-Anything/eth2-quickstart/agent-harness
+plugins: anyio-4.12.1
+collecting ... collected 18 items
+
+cli_anything/eth2_quickstart/tests/test_core.py::TestProjectHelpers::test_find_repo_root_from_explicit_path PASSED [ 5%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestProjectHelpers::test_find_repo_root_from_env PASSED [ 11%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestProjectHelpers::test_upsert_user_config PASSED [ 16%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestValidatorPlan::test_prysm_plan PASSED [ 22%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestValidatorPlan::test_invalid_client PASSED [ 27%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_help PASSED [ 33%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_missing_repo_root_returns_clean_json_error PASSED [ 38%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_health_check_json PASSED [ 44%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_install_clients_json PASSED [ 50%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_install_clients_rejects_unknown_execution_client PASSED [ 55%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_setup_node_auto_with_network_only_uses_ensure PASSED [ 61%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_setup_node_auto_with_client_selection_uses_phase2 PASSED [ 66%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_start_rpc_requires_confirm PASSED [ 72%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_configure_validator_json PASSED [ 77%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_configure_validator_rejects_unknown_consensus_client PASSED [ 83%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestCLI::test_status_json PASSED [ 88%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestBackendErrors::test_run_handles_missing_wrapper PASSED [ 94%]
+cli_anything/eth2_quickstart/tests/test_core.py::TestBackendErrors::test_run_handles_permission_error PASSED [100%]
+
+============================== 18 passed in 0.07s ==============================
+```
+
+### E2E
+
+```text
+============================= test session starts ==============================
+platform linux -- Python 3.12.3, pytest-7.4.4, pluggy-1.4.0 -- /usr/bin/python3
+cachedir: .pytest_cache
+rootdir: /root/.openclaw/workspace/dev/CLI-Anything/eth2-quickstart/agent-harness
+plugins: anyio-4.12.1
+collecting ... collected 3 items
+
+cli_anything/eth2_quickstart/tests/test_full_e2e.py::TestRealCheckoutE2E::test_help SKIPPED [ 33%]
+cli_anything/eth2_quickstart/tests/test_full_e2e.py::TestRealCheckoutE2E::test_health_check_json SKIPPED [ 66%]
+cli_anything/eth2_quickstart/tests/test_full_e2e.py::TestRealCheckoutE2E::test_status_json SKIPPED [100%]
+
+============================== 3 skipped in 0.02s ==============================
+```
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/__init__.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/__init__.py
new file mode 100644
index 000000000..e972cad37
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/__init__.py
@@ -0,0 +1 @@
+"""Tests for cli-anything-eth2-quickstart."""
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/test_core.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/test_core.py
new file mode 100644
index 000000000..77c692d13
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/test_core.py
@@ -0,0 +1,347 @@
+"""Unit tests for cli-anything-eth2-quickstart."""
+
+from __future__ import annotations
+
+import json
+import subprocess
+from pathlib import Path
+from unittest.mock import patch
+
+import pytest
+from click.testing import CliRunner
+
+from cli_anything.eth2_quickstart.core import project
+from cli_anything.eth2_quickstart.core.commands import validator_plan
+from cli_anything.eth2_quickstart.eth2_quickstart_cli import cli
+
+
+@pytest.fixture
+def runner():
+ return CliRunner()
+
+
+@pytest.fixture
+def repo_root(tmp_path: Path) -> Path:
+ repo = tmp_path / "eth2-quickstart"
+ (repo / "scripts").mkdir(parents=True)
+ (repo / "scripts" / "eth2qs.sh").write_text("#!/bin/bash\n", encoding="utf-8")
+ (repo / "config").mkdir()
+ return repo
+
+
+class TestProjectHelpers:
+ def test_find_repo_root_from_explicit_path(self, repo_root: Path):
+ resolved = project.find_repo_root(str(repo_root))
+ assert resolved == repo_root
+
+ def test_find_repo_root_from_env(self, repo_root: Path, monkeypatch: pytest.MonkeyPatch):
+ monkeypatch.setenv("ETH2QS_REPO_ROOT", str(repo_root))
+ resolved = project.find_repo_root()
+ assert resolved == repo_root
+
+ def test_upsert_user_config(self, repo_root: Path):
+ config_path = project.upsert_user_config(
+ repo_root,
+ {
+ "ETH_NETWORK": "holesky",
+ "EXEC_CLIENT": "geth",
+ },
+ )
+ content = config_path.read_text(encoding="utf-8")
+ assert "export ETH_NETWORK='holesky'" in content
+ assert "export EXEC_CLIENT='geth'" in content
+
+ project.upsert_user_config(repo_root, {"ETH_NETWORK": "mainnet"})
+ updated = config_path.read_text(encoding="utf-8")
+ assert "export ETH_NETWORK='mainnet'" in updated
+ assert "export ETH_NETWORK='holesky'" not in updated
+
+
+class TestValidatorPlan:
+ def test_prysm_plan(self):
+ plan = validator_plan(
+ consensus_client="prysm",
+ fee_recipient="0xabc",
+ graffiti="hello",
+ wallet_password_file="~/secrets/pass.txt",
+ )
+ assert plan["config_updates"]["FEE_RECIPIENT"] == "0xabc"
+ assert "validator accounts import" in plan["import_command"]
+ assert "wallet-password-file" in plan["post_import_commands"][0]
+
+ def test_invalid_client(self):
+ with pytest.raises(ValueError, match="Unsupported consensus client"):
+ validator_plan(consensus_client="bad-client")
+
+
+class TestCLI:
+ def test_help(self, runner: CliRunner):
+ result = runner.invoke(cli, ["--help"])
+ assert result.exit_code == 0
+ assert "setup-node" in result.output
+ assert "health-check" in result.output
+
+ def test_missing_repo_root_returns_clean_json_error(self, runner: CliRunner):
+ result = runner.invoke(cli, ["--json", "health-check"])
+ assert result.exit_code == 1
+ payload = json.loads(result.output)
+ assert "Could not locate an eth2-quickstart checkout" in payload["error"]
+
+ @patch("cli_anything.eth2_quickstart.eth2_quickstart_cli.Eth2QuickStartBackend")
+ def test_health_check_json(self, backend_cls, runner: CliRunner, repo_root: Path):
+ backend = backend_cls.return_value
+ backend.repo_root = repo_root
+ backend.run_wrapper.return_value = {
+ "command": ["doctor", "--json"],
+ "cwd": str(repo_root),
+ "exit_code": 0,
+ "stdout": json.dumps({"summary": {"status": "pass"}}),
+ "stderr": "",
+ "ok": True,
+ }
+ result = runner.invoke(
+ cli,
+ ["--repo-root", str(repo_root), "--json", "health-check"],
+ )
+ assert result.exit_code == 0
+ payload = json.loads(result.output)
+ assert payload["doctor"]["summary"]["status"] == "pass"
+
+ @patch("cli_anything.eth2_quickstart.eth2_quickstart_cli.Eth2QuickStartBackend")
+ def test_install_clients_json(self, backend_cls, runner: CliRunner, repo_root: Path):
+ backend = backend_cls.return_value
+ backend.repo_root = repo_root
+ backend.run_wrapper.return_value = {
+ "command": ["phase2"],
+ "cwd": str(repo_root),
+ "exit_code": 0,
+ "stdout": "installed",
+ "stderr": "",
+ "ok": True,
+ }
+ result = runner.invoke(
+ cli,
+ [
+ "--repo-root",
+ str(repo_root),
+ "--json",
+ "install-clients",
+ "--network",
+ "mainnet",
+ "--execution-client",
+ "geth",
+ "--consensus-client",
+ "lighthouse",
+ "--mev",
+ "mev-boost",
+ "--confirm",
+ ],
+ )
+ assert result.exit_code == 0
+ payload = json.loads(result.output)
+ assert payload["requested"]["execution_client"] == "geth"
+ assert payload["requested"]["consensus_client"] == "lighthouse"
+
+ def test_install_clients_rejects_unknown_execution_client(self, runner: CliRunner, repo_root: Path):
+ result = runner.invoke(
+ cli,
+ [
+ "--repo-root",
+ str(repo_root),
+ "install-clients",
+ "--execution-client",
+ "bad-client",
+ "--consensus-client",
+ "lighthouse",
+ "--confirm",
+ ],
+ )
+ assert result.exit_code == 2
+ assert "Invalid value for '--execution-client'" in result.output
+
+ @patch("cli_anything.eth2_quickstart.eth2_quickstart_cli.Eth2QuickStartBackend")
+ def test_setup_node_auto_with_network_only_uses_ensure(self, backend_cls, runner: CliRunner, repo_root: Path):
+ backend = backend_cls.return_value
+ backend.repo_root = repo_root
+ backend.run_wrapper.return_value = {
+ "command": ["ensure", "--apply", "--confirm"],
+ "cwd": str(repo_root),
+ "exit_code": 0,
+ "stdout": "ensured",
+ "stderr": "",
+ "ok": True,
+ }
+ result = runner.invoke(
+ cli,
+ [
+ "--repo-root",
+ str(repo_root),
+ "--json",
+ "setup-node",
+ "--network",
+ "holesky",
+ "--confirm",
+ ],
+ )
+ assert result.exit_code == 0
+ backend.run_wrapper.assert_called_once_with("ensure", "--apply", "--confirm")
+ payload = json.loads(result.output)
+ assert payload["requested_phase"] == "auto-ensure"
+ assert payload["requested"]["network"] == "holesky"
+ assert payload["config_path"] is not None
+
+ @patch("cli_anything.eth2_quickstart.eth2_quickstart_cli.Eth2QuickStartBackend")
+ def test_setup_node_auto_with_client_selection_uses_phase2(self, backend_cls, runner: CliRunner, repo_root: Path):
+ backend = backend_cls.return_value
+ backend.repo_root = repo_root
+ backend.run_wrapper.return_value = {
+ "command": ["phase2", "--execution=geth", "--consensus=lighthouse"],
+ "cwd": str(repo_root),
+ "exit_code": 0,
+ "stdout": "installed",
+ "stderr": "",
+ "ok": True,
+ }
+ result = runner.invoke(
+ cli,
+ [
+ "--repo-root",
+ str(repo_root),
+ "--json",
+ "setup-node",
+ "--network",
+ "holesky",
+ "--execution-client",
+ "geth",
+ "--consensus-client",
+ "lighthouse",
+ "--confirm",
+ ],
+ )
+ assert result.exit_code == 0
+ backend.run_wrapper.assert_called_once_with(
+ "phase2",
+ "--execution=geth",
+ "--consensus=lighthouse",
+ )
+ payload = json.loads(result.output)
+ assert payload["requested_phase"] == "auto-phase2"
+ assert payload["requested"]["network"] == "holesky"
+
+ @patch("cli_anything.eth2_quickstart.eth2_quickstart_cli.Eth2QuickStartBackend")
+ def test_start_rpc_requires_confirm(self, backend_cls, runner: CliRunner, repo_root: Path):
+ backend = backend_cls.return_value
+ backend.repo_root = repo_root
+ result = runner.invoke(
+ cli,
+ [
+ "--repo-root",
+ str(repo_root),
+ "start-rpc",
+ "--web-stack",
+ "nginx",
+ ],
+ )
+ assert result.exit_code == 1
+ assert "requires --confirm" in result.output
+
+ @patch("cli_anything.eth2_quickstart.eth2_quickstart_cli.Eth2QuickStartBackend")
+ def test_configure_validator_json(self, backend_cls, runner: CliRunner, repo_root: Path):
+ backend = backend_cls.return_value
+ backend.repo_root = repo_root
+ result = runner.invoke(
+ cli,
+ [
+ "--repo-root",
+ str(repo_root),
+ "--json",
+ "configure-validator",
+ "--consensus-client",
+ "prysm",
+ "--fee-recipient",
+ "0x1111111111111111111111111111111111111111",
+ "--graffiti",
+ "test",
+ ],
+ )
+ assert result.exit_code == 0
+ payload = json.loads(result.output)
+ assert payload["plan"]["consensus_client"] == "prysm"
+ assert payload["plan"]["config_updates"]["GRAFITTI"] == "test"
+
+ def test_configure_validator_rejects_unknown_consensus_client(
+ self, runner: CliRunner, repo_root: Path
+ ):
+ result = runner.invoke(
+ cli,
+ [
+ "--repo-root",
+ str(repo_root),
+ "configure-validator",
+ "--consensus-client",
+ "bad-client",
+ ],
+ )
+ assert result.exit_code == 2
+ assert "Invalid value for '--consensus-client'" in result.output
+
+ @patch("cli_anything.eth2_quickstart.eth2_quickstart_cli.Eth2QuickStartBackend")
+ def test_status_json(self, backend_cls, runner: CliRunner, repo_root: Path):
+ backend = backend_cls.return_value
+ backend.repo_root = repo_root
+ backend.run_wrapper.side_effect = [
+ {
+ "command": ["doctor", "--json"],
+ "cwd": str(repo_root),
+ "exit_code": 0,
+ "stdout": json.dumps({"summary": {"status": "warn"}}),
+ "stderr": "",
+ "ok": True,
+ },
+ {
+ "command": ["plan", "--json"],
+ "cwd": str(repo_root),
+ "exit_code": 0,
+ "stdout": json.dumps({"next_action": "phase2"}),
+ "stderr": "",
+ "ok": True,
+ },
+ {
+ "command": ["stats"],
+ "cwd": str(repo_root),
+ "exit_code": 0,
+ "stdout": "service status",
+ "stderr": "",
+ "ok": True,
+ },
+ ]
+ result = runner.invoke(
+ cli,
+ ["--repo-root", str(repo_root), "--json", "status"],
+ )
+ assert result.exit_code == 0
+ payload = json.loads(result.output)
+ assert payload["doctor"]["summary"]["status"] == "warn"
+ assert payload["plan"]["next_action"] == "phase2"
+
+
+class TestBackendErrors:
+ def test_run_handles_missing_wrapper(self, repo_root: Path):
+ from cli_anything.eth2_quickstart.utils.eth2qs_backend import Eth2QuickStartBackend
+
+ backend = Eth2QuickStartBackend(str(repo_root))
+ result = backend._run(["/definitely/missing/eth2qs.sh"])
+ assert result["ok"] is False
+ assert result["exit_code"] == 127
+ assert "command not found" in result["stderr"]
+
+ @patch("cli_anything.eth2_quickstart.utils.eth2qs_backend.subprocess.run")
+ def test_run_handles_permission_error(self, run_mock, repo_root: Path):
+ from cli_anything.eth2_quickstart.utils.eth2qs_backend import Eth2QuickStartBackend
+
+ run_mock.side_effect = PermissionError("no execute bit")
+ backend = Eth2QuickStartBackend(str(repo_root))
+ result = backend._run(["/tmp/not-executable"])
+ assert result["ok"] is False
+ assert result["exit_code"] == 126
+ assert "permission denied" in result["stderr"]
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/test_full_e2e.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/test_full_e2e.py
new file mode 100644
index 000000000..384bc68b0
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/tests/test_full_e2e.py
@@ -0,0 +1,54 @@
+"""E2E tests for cli-anything-eth2-quickstart."""
+
+from __future__ import annotations
+
+import json
+import os
+from pathlib import Path
+
+import pytest
+from click.testing import CliRunner
+
+from cli_anything.eth2_quickstart.eth2_quickstart_cli import cli
+
+E2E_REPO_ROOT = os.environ.get("ETH2QS_E2E_REPO_ROOT")
+WRAPPER_EXISTS = bool(E2E_REPO_ROOT) and (Path(E2E_REPO_ROOT) / "scripts" / "eth2qs.sh").is_file()
+
+pytestmark = pytest.mark.skipif(
+ not WRAPPER_EXISTS,
+ reason="Set ETH2QS_E2E_REPO_ROOT to a real eth2-quickstart checkout to run E2E tests",
+)
+
+
+@pytest.fixture
+def runner():
+ return CliRunner()
+
+
+class TestRealCheckoutE2E:
+ def test_help(self, runner: CliRunner):
+ result = runner.invoke(
+ cli,
+ ["--repo-root", E2E_REPO_ROOT, "--help"],
+ )
+ assert result.exit_code == 0
+
+ def test_health_check_json(self, runner: CliRunner):
+ result = runner.invoke(
+ cli,
+ ["--repo-root", E2E_REPO_ROOT, "--json", "health-check"],
+ )
+ assert result.exit_code == 0
+ payload = json.loads(result.output)
+ assert "command_result" in payload
+ assert payload["command_result"]["command"][-2:] == ["doctor", "--json"]
+
+ def test_status_json(self, runner: CliRunner):
+ result = runner.invoke(
+ cli,
+ ["--repo-root", E2E_REPO_ROOT, "--json", "status"],
+ )
+ assert result.exit_code == 0
+ payload = json.loads(result.output)
+ assert "plan" in payload
+ assert "stats_raw" in payload
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/__init__.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/__init__.py
new file mode 100644
index 000000000..dce03e3b5
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/__init__.py
@@ -0,0 +1 @@
+"""Utility helpers for cli-anything-eth2-quickstart."""
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/eth2qs_backend.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/eth2qs_backend.py
new file mode 100644
index 000000000..86a2ce240
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/eth2qs_backend.py
@@ -0,0 +1,56 @@
+"""Subprocess backend for eth2-quickstart."""
+
+from __future__ import annotations
+
+import subprocess
+from pathlib import Path
+
+from cli_anything.eth2_quickstart.core.project import find_repo_root, wrapper_path
+
+
+class Eth2QuickStartBackend:
+ def __init__(self, repo_root: str | None = None):
+ self.repo_root = find_repo_root(repo_root)
+
+ def _run(self, command: list[str]) -> dict:
+ try:
+ completed = subprocess.run(
+ command,
+ cwd=str(self.repo_root),
+ text=True,
+ capture_output=True,
+ check=False,
+ )
+ return {
+ "command": command,
+ "cwd": str(self.repo_root),
+ "exit_code": completed.returncode,
+ "stdout": completed.stdout.strip(),
+ "stderr": completed.stderr.strip(),
+ "ok": completed.returncode == 0,
+ }
+ except FileNotFoundError as exc:
+ return {
+ "command": command,
+ "cwd": str(self.repo_root),
+ "exit_code": 127,
+ "stdout": "",
+ "stderr": f"{command[0]}: command not found ({exc})",
+ "ok": False,
+ }
+ except PermissionError as exc:
+ return {
+ "command": command,
+ "cwd": str(self.repo_root),
+ "exit_code": 126,
+ "stdout": "",
+ "stderr": f"{command[0]}: permission denied ({exc})",
+ "ok": False,
+ }
+
+ def run_wrapper(self, *args: str) -> dict:
+ return self._run([str(wrapper_path(self.repo_root)), *args])
+
+ def run_script(self, relative_path: str, *args: str) -> dict:
+ script_path = self.repo_root / Path(relative_path)
+ return self._run([str(script_path), *args])
diff --git a/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/repl_skin.py b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/repl_skin.py
new file mode 100644
index 000000000..47260bebd
--- /dev/null
+++ b/eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/utils/repl_skin.py
@@ -0,0 +1,498 @@
+"""cli-anything REPL Skin — Unified terminal interface for all CLI harnesses.
+
+Copy this file into your CLI package at:
+ cli_anything//utils/repl_skin.py
+
+Usage:
+ from cli_anything..utils.repl_skin import ReplSkin
+
+ skin = ReplSkin("shotcut", version="1.0.0")
+ skin.print_banner()
+ prompt_text = skin.prompt(project_name="my_video.mlt", modified=True)
+ skin.success("Project saved")
+ skin.error("File not found")
+ skin.warning("Unsaved changes")
+ skin.info("Processing 24 clips...")
+ skin.status("Track 1", "3 clips, 00:02:30")
+ skin.table(headers, rows)
+ skin.print_goodbye()
+"""
+
+import os
+import sys
+
+# ── ANSI color codes (no external deps for core styling) ──────────────
+
+_RESET = "\033[0m"
+_BOLD = "\033[1m"
+_DIM = "\033[2m"
+_ITALIC = "\033[3m"
+_UNDERLINE = "\033[4m"
+
+# Brand colors
+_CYAN = "\033[38;5;80m" # cli-anything brand cyan
+_CYAN_BG = "\033[48;5;80m"
+_WHITE = "\033[97m"
+_GRAY = "\033[38;5;245m"
+_DARK_GRAY = "\033[38;5;240m"
+_LIGHT_GRAY = "\033[38;5;250m"
+
+# Software accent colors — each software gets a unique accent
+_ACCENT_COLORS = {
+ "gimp": "\033[38;5;214m", # warm orange
+ "blender": "\033[38;5;208m", # deep orange
+ "inkscape": "\033[38;5;39m", # bright blue
+ "audacity": "\033[38;5;33m", # navy blue
+ "libreoffice": "\033[38;5;40m", # green
+ "obs_studio": "\033[38;5;55m", # purple
+ "kdenlive": "\033[38;5;69m", # slate blue
+ "shotcut": "\033[38;5;35m", # teal green
+}
+_DEFAULT_ACCENT = "\033[38;5;75m" # default sky blue
+
+# Status colors
+_GREEN = "\033[38;5;78m"
+_YELLOW = "\033[38;5;220m"
+_RED = "\033[38;5;196m"
+_BLUE = "\033[38;5;75m"
+_MAGENTA = "\033[38;5;176m"
+
+# ── Brand icon ────────────────────────────────────────────────────────
+
+# The cli-anything icon: a small colored diamond/chevron mark
+_ICON = f"{_CYAN}{_BOLD}◆{_RESET}"
+_ICON_SMALL = f"{_CYAN}▸{_RESET}"
+
+# ── Box drawing characters ────────────────────────────────────────────
+
+_H_LINE = "─"
+_V_LINE = "│"
+_TL = "╭"
+_TR = "╮"
+_BL = "╰"
+_BR = "╯"
+_T_DOWN = "┬"
+_T_UP = "┴"
+_T_RIGHT = "├"
+_T_LEFT = "┤"
+_CROSS = "┼"
+
+
+def _strip_ansi(text: str) -> str:
+ """Remove ANSI escape codes for length calculation."""
+ import re
+ return re.sub(r"\033\[[^m]*m", "", text)
+
+
+def _visible_len(text: str) -> int:
+ """Get visible length of text (excluding ANSI codes)."""
+ return len(_strip_ansi(text))
+
+
+class ReplSkin:
+ """Unified REPL skin for cli-anything CLIs.
+
+ Provides consistent branding, prompts, and message formatting
+ across all CLI harnesses built with the cli-anything methodology.
+ """
+
+ def __init__(self, software: str, version: str = "1.0.0",
+ history_file: str | None = None):
+ """Initialize the REPL skin.
+
+ Args:
+ software: Software name (e.g., "gimp", "shotcut", "blender").
+ version: CLI version string.
+ history_file: Path for persistent command history.
+ Defaults to ~/.cli-anything-/history
+ """
+ self.software = software.lower().replace("-", "_")
+ self.display_name = software.replace("_", " ").title()
+ self.version = version
+ self.accent = _ACCENT_COLORS.get(self.software, _DEFAULT_ACCENT)
+
+ # History file
+ if history_file is None:
+ from pathlib import Path
+ hist_dir = Path.home() / f".cli-anything-{self.software}"
+ hist_dir.mkdir(parents=True, exist_ok=True)
+ self.history_file = str(hist_dir / "history")
+ else:
+ self.history_file = history_file
+
+ # Detect terminal capabilities
+ self._color = self._detect_color_support()
+
+ def _detect_color_support(self) -> bool:
+ """Check if terminal supports color."""
+ if os.environ.get("NO_COLOR"):
+ return False
+ if os.environ.get("CLI_ANYTHING_NO_COLOR"):
+ return False
+ if not hasattr(sys.stdout, "isatty"):
+ return False
+ return sys.stdout.isatty()
+
+ def _c(self, code: str, text: str) -> str:
+ """Apply color code if colors are supported."""
+ if not self._color:
+ return text
+ return f"{code}{text}{_RESET}"
+
+ # ── Banner ────────────────────────────────────────────────────────
+
+ def print_banner(self):
+ """Print the startup banner with branding."""
+ inner = 54
+
+ def _box_line(content: str) -> str:
+ """Wrap content in box drawing, padding to inner width."""
+ pad = inner - _visible_len(content)
+ vl = self._c(_DARK_GRAY, _V_LINE)
+ return f"{vl}{content}{' ' * max(0, pad)}{vl}"
+
+ top = self._c(_DARK_GRAY, f"{_TL}{_H_LINE * inner}{_TR}")
+ bot = self._c(_DARK_GRAY, f"{_BL}{_H_LINE * inner}{_BR}")
+
+ # Title: ◆ cli-anything · Shotcut
+ icon = self._c(_CYAN + _BOLD, "◆")
+ brand = self._c(_CYAN + _BOLD, "cli-anything")
+ dot = self._c(_DARK_GRAY, "·")
+ name = self._c(self.accent + _BOLD, self.display_name)
+ title = f" {icon} {brand} {dot} {name}"
+
+ ver = f" {self._c(_DARK_GRAY, f' v{self.version}')}"
+ tip = f" {self._c(_DARK_GRAY, ' Type help for commands, quit to exit')}"
+ empty = ""
+
+ print(top)
+ print(_box_line(title))
+ print(_box_line(ver))
+ print(_box_line(empty))
+ print(_box_line(tip))
+ print(bot)
+ print()
+
+ # ── Prompt ────────────────────────────────────────────────────────
+
+ def prompt(self, project_name: str = "", modified: bool = False,
+ context: str = "") -> str:
+ """Build a styled prompt string for prompt_toolkit or input().
+
+ Args:
+ project_name: Current project name (empty if none open).
+ modified: Whether the project has unsaved changes.
+ context: Optional extra context to show in prompt.
+
+ Returns:
+ Formatted prompt string.
+ """
+ parts = []
+
+ # Icon
+ if self._color:
+ parts.append(f"{_CYAN}◆{_RESET} ")
+ else:
+ parts.append("> ")
+
+ # Software name
+ parts.append(self._c(self.accent + _BOLD, self.software))
+
+ # Project context
+ if project_name or context:
+ ctx = context or project_name
+ mod = "*" if modified else ""
+ parts.append(f" {self._c(_DARK_GRAY, '[')}")
+ parts.append(self._c(_LIGHT_GRAY, f"{ctx}{mod}"))
+ parts.append(self._c(_DARK_GRAY, ']'))
+
+ parts.append(self._c(_GRAY, " ❯ "))
+
+ return "".join(parts)
+
+ def prompt_tokens(self, project_name: str = "", modified: bool = False,
+ context: str = ""):
+ """Build prompt_toolkit formatted text tokens for the prompt.
+
+ Use with prompt_toolkit's FormattedText for proper ANSI handling.
+
+ Returns:
+ list of (style, text) tuples for prompt_toolkit.
+ """
+ accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
+ tokens = []
+
+ tokens.append(("class:icon", "◆ "))
+ tokens.append(("class:software", self.software))
+
+ if project_name or context:
+ ctx = context or project_name
+ mod = "*" if modified else ""
+ tokens.append(("class:bracket", " ["))
+ tokens.append(("class:context", f"{ctx}{mod}"))
+ tokens.append(("class:bracket", "]"))
+
+ tokens.append(("class:arrow", " ❯ "))
+
+ return tokens
+
+ def get_prompt_style(self):
+ """Get a prompt_toolkit Style object matching the skin.
+
+ Returns:
+ prompt_toolkit.styles.Style
+ """
+ try:
+ from prompt_toolkit.styles import Style
+ except ImportError:
+ return None
+
+ accent_hex = _ANSI_256_TO_HEX.get(self.accent, "#5fafff")
+
+ return Style.from_dict({
+ "icon": "#5fdfdf bold", # cyan brand color
+ "software": f"{accent_hex} bold",
+ "bracket": "#585858",
+ "context": "#bcbcbc",
+ "arrow": "#808080",
+ # Completion menu
+ "completion-menu.completion": "bg:#303030 #bcbcbc",
+ "completion-menu.completion.current": f"bg:{accent_hex} #000000",
+ "completion-menu.meta.completion": "bg:#303030 #808080",
+ "completion-menu.meta.completion.current": f"bg:{accent_hex} #000000",
+ # Auto-suggest
+ "auto-suggest": "#585858",
+ # Bottom toolbar
+ "bottom-toolbar": "bg:#1c1c1c #808080",
+ "bottom-toolbar.text": "#808080",
+ })
+
+ # ── Messages ──────────────────────────────────────────────────────
+
+ def success(self, message: str):
+ """Print a success message with green checkmark."""
+ icon = self._c(_GREEN + _BOLD, "✓")
+ print(f" {icon} {self._c(_GREEN, message)}")
+
+ def error(self, message: str):
+ """Print an error message with red cross."""
+ icon = self._c(_RED + _BOLD, "✗")
+ print(f" {icon} {self._c(_RED, message)}", file=sys.stderr)
+
+ def warning(self, message: str):
+ """Print a warning message with yellow triangle."""
+ icon = self._c(_YELLOW + _BOLD, "⚠")
+ print(f" {icon} {self._c(_YELLOW, message)}")
+
+ def info(self, message: str):
+ """Print an info message with blue dot."""
+ icon = self._c(_BLUE, "●")
+ print(f" {icon} {self._c(_LIGHT_GRAY, message)}")
+
+ def hint(self, message: str):
+ """Print a subtle hint message."""
+ print(f" {self._c(_DARK_GRAY, message)}")
+
+ def section(self, title: str):
+ """Print a section header."""
+ print()
+ print(f" {self._c(self.accent + _BOLD, title)}")
+ print(f" {self._c(_DARK_GRAY, _H_LINE * len(title))}")
+
+ # ── Status display ────────────────────────────────────────────────
+
+ def status(self, label: str, value: str):
+ """Print a key-value status line."""
+ lbl = self._c(_GRAY, f" {label}:")
+ val = self._c(_WHITE, f" {value}")
+ print(f"{lbl}{val}")
+
+ def status_block(self, items: dict[str, str], title: str = ""):
+ """Print a block of status key-value pairs.
+
+ Args:
+ items: Dict of label -> value pairs.
+ title: Optional title for the block.
+ """
+ if title:
+ self.section(title)
+
+ max_key = max(len(k) for k in items) if items else 0
+ for label, value in items.items():
+ lbl = self._c(_GRAY, f" {label:<{max_key}}")
+ val = self._c(_WHITE, f" {value}")
+ print(f"{lbl}{val}")
+
+ def progress(self, current: int, total: int, label: str = ""):
+ """Print a simple progress indicator.
+
+ Args:
+ current: Current step number.
+ total: Total number of steps.
+ label: Optional label for the progress.
+ """
+ pct = int(current / total * 100) if total > 0 else 0
+ bar_width = 20
+ filled = int(bar_width * current / total) if total > 0 else 0
+ bar = "█" * filled + "░" * (bar_width - filled)
+ text = f" {self._c(_CYAN, bar)} {self._c(_GRAY, f'{pct:3d}%')}"
+ if label:
+ text += f" {self._c(_LIGHT_GRAY, label)}"
+ print(text)
+
+ # ── Table display ─────────────────────────────────────────────────
+
+ def table(self, headers: list[str], rows: list[list[str]],
+ max_col_width: int = 40):
+ """Print a formatted table with box-drawing characters.
+
+ Args:
+ headers: Column header strings.
+ rows: List of rows, each a list of cell strings.
+ max_col_width: Maximum column width before truncation.
+ """
+ if not headers:
+ return
+
+ # Calculate column widths
+ col_widths = [min(len(h), max_col_width) for h in headers]
+ for row in rows:
+ for i, cell in enumerate(row):
+ if i < len(col_widths):
+ col_widths[i] = min(
+ max(col_widths[i], len(str(cell))), max_col_width
+ )
+
+ def pad(text: str, width: int) -> str:
+ t = str(text)[:width]
+ return t + " " * (width - len(t))
+
+ # Header
+ header_cells = [
+ self._c(_CYAN + _BOLD, pad(h, col_widths[i]))
+ for i, h in enumerate(headers)
+ ]
+ sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
+ header_line = f" {sep.join(header_cells)}"
+ print(header_line)
+
+ # Separator
+ sep_parts = [self._c(_DARK_GRAY, _H_LINE * w) for w in col_widths]
+ sep_line = self._c(_DARK_GRAY, f" {'───'.join([_H_LINE * w for w in col_widths])}")
+ print(sep_line)
+
+ # Rows
+ for row in rows:
+ cells = []
+ for i, cell in enumerate(row):
+ if i < len(col_widths):
+ cells.append(self._c(_LIGHT_GRAY, pad(str(cell), col_widths[i])))
+ row_sep = self._c(_DARK_GRAY, f" {_V_LINE} ")
+ print(f" {row_sep.join(cells)}")
+
+ # ── Help display ──────────────────────────────────────────────────
+
+ def help(self, commands: dict[str, str]):
+ """Print a formatted help listing.
+
+ Args:
+ commands: Dict of command -> description pairs.
+ """
+ self.section("Commands")
+ max_cmd = max(len(c) for c in commands) if commands else 0
+ for cmd, desc in commands.items():
+ cmd_styled = self._c(self.accent, f" {cmd:<{max_cmd}}")
+ desc_styled = self._c(_GRAY, f" {desc}")
+ print(f"{cmd_styled}{desc_styled}")
+ print()
+
+ # ── Goodbye ───────────────────────────────────────────────────────
+
+ def print_goodbye(self):
+ """Print a styled goodbye message."""
+ print(f"\n {_ICON_SMALL} {self._c(_GRAY, 'Goodbye!')}\n")
+
+ # ── Prompt toolkit session factory ────────────────────────────────
+
+ def create_prompt_session(self):
+ """Create a prompt_toolkit PromptSession with skin styling.
+
+ Returns:
+ A configured PromptSession, or None if prompt_toolkit unavailable.
+ """
+ try:
+ from prompt_toolkit import PromptSession
+ from prompt_toolkit.history import FileHistory
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
+ from prompt_toolkit.formatted_text import FormattedText
+
+ style = self.get_prompt_style()
+
+ session = PromptSession(
+ history=FileHistory(self.history_file),
+ auto_suggest=AutoSuggestFromHistory(),
+ style=style,
+ enable_history_search=True,
+ )
+ return session
+ except ImportError:
+ return None
+
+ def get_input(self, pt_session, project_name: str = "",
+ modified: bool = False, context: str = "") -> str:
+ """Get input from user using prompt_toolkit or fallback.
+
+ Args:
+ pt_session: A prompt_toolkit PromptSession (or None).
+ project_name: Current project name.
+ modified: Whether project has unsaved changes.
+ context: Optional context string.
+
+ Returns:
+ User input string (stripped).
+ """
+ if pt_session is not None:
+ from prompt_toolkit.formatted_text import FormattedText
+ tokens = self.prompt_tokens(project_name, modified, context)
+ return pt_session.prompt(FormattedText(tokens)).strip()
+ else:
+ raw_prompt = self.prompt(project_name, modified, context)
+ return input(raw_prompt).strip()
+
+ # ── Toolbar builder ───────────────────────────────────────────────
+
+ def bottom_toolbar(self, items: dict[str, str]):
+ """Create a bottom toolbar callback for prompt_toolkit.
+
+ Args:
+ items: Dict of label -> value pairs to show in toolbar.
+
+ Returns:
+ A callable that returns FormattedText for the toolbar.
+ """
+ def toolbar():
+ from prompt_toolkit.formatted_text import FormattedText
+ parts = []
+ for i, (k, v) in enumerate(items.items()):
+ if i > 0:
+ parts.append(("class:bottom-toolbar.text", " │ "))
+ parts.append(("class:bottom-toolbar.text", f" {k}: "))
+ parts.append(("class:bottom-toolbar", v))
+ return FormattedText(parts)
+ return toolbar
+
+
+# ── ANSI 256-color to hex mapping (for prompt_toolkit styles) ─────────
+
+_ANSI_256_TO_HEX = {
+ "\033[38;5;33m": "#0087ff", # audacity navy blue
+ "\033[38;5;35m": "#00af5f", # shotcut teal
+ "\033[38;5;39m": "#00afff", # inkscape bright blue
+ "\033[38;5;40m": "#00d700", # libreoffice green
+ "\033[38;5;55m": "#5f00af", # obs purple
+ "\033[38;5;69m": "#5f87ff", # kdenlive slate blue
+ "\033[38;5;75m": "#5fafff", # default sky blue
+ "\033[38;5;80m": "#5fd7d7", # brand cyan
+ "\033[38;5;208m": "#ff8700", # blender deep orange
+ "\033[38;5;214m": "#ffaf00", # gimp warm orange
+}
diff --git a/eth2-quickstart/agent-harness/setup.py b/eth2-quickstart/agent-harness/setup.py
new file mode 100644
index 000000000..168862f39
--- /dev/null
+++ b/eth2-quickstart/agent-harness/setup.py
@@ -0,0 +1,33 @@
+from setuptools import find_namespace_packages, setup
+
+with open("cli_anything/eth2_quickstart/README.md", "r", encoding="utf-8") as fh:
+ long_description = fh.read()
+
+setup(
+ name="cli-anything-eth2-quickstart",
+ version="1.0.0",
+ description="CLI harness for eth2-quickstart - hardened Ethereum node deployment and operations",
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ url="https://github.com/HKUDS/CLI-Anything",
+ packages=find_namespace_packages(include=["cli_anything.*"]),
+ install_requires=[
+ "click>=8.0.0",
+ "prompt-toolkit>=3.0.0",
+ ],
+ extras_require={
+ "dev": [
+ "pytest>=7.0.0",
+ ]
+ },
+ entry_points={
+ "console_scripts": [
+ "cli-anything-eth2-quickstart=cli_anything.eth2_quickstart.eth2_quickstart_cli:main",
+ ]
+ },
+ package_data={
+ "cli_anything.eth2_quickstart": ["skills/*.md"],
+ },
+ include_package_data=True,
+ python_requires=">=3.10",
+)
diff --git a/registry.json b/registry.json
index 13165f9e2..62991b258 100644
--- a/registry.json
+++ b/registry.json
@@ -117,6 +117,20 @@
"contributor": "zhangxilong-43",
"contributor_url": "https://github.com/zhangxilong-43"
},
+ {
+ "name": "eth2-quickstart",
+ "display_name": "ETH2 QuickStart",
+ "version": "1.0.0",
+ "description": "Hardened Ethereum node deployment and operations via the eth2-quickstart automation scripts",
+ "requires": "local eth2-quickstart checkout with scripts/eth2qs.sh; Linux host required for real install workflows",
+ "homepage": "https://github.com/chimera-defi/eth2-quickstart",
+ "install_cmd": "pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=eth2-quickstart/agent-harness",
+ "entry_point": "cli-anything-eth2-quickstart",
+ "skill_md": "eth2-quickstart/agent-harness/cli_anything/eth2_quickstart/skills/SKILL.md",
+ "category": "devops",
+ "contributor": "chimera-defi",
+ "contributor_url": "https://github.com/chimera-defi"
+ },
{
"name": "gimp",
"display_name": "GIMP",