diff --git a/README.md b/README.md index c2cf4a9..a57c375 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Please ensure you are using Python 3.11+ before running or contributing to this You can install fireblocks-cli locally as a Python project: ```bash -git clone https://github.com/your-org/fireblocks-cli.git +git clone https://github.com/stirnetwork/fireblocks-cli.git cd fireblocks-cli pip install . ``` @@ -64,15 +64,15 @@ fireblocks-cli --help # fireblocks-cli configure Subcommand List -| Subcommand | Status | Description | Notes | -|-------------------|------------------|----------------------------------------------------------------|-----------------------------------------------------------------------| -| `init` | Not implemented | Initialize the default configuration files | Creates `~/.config/fireblocks-cli/config.toml` and `~/.config/fireblocks-cli/keys/` | -| `gen-keys` | βœ… Implemented | Generate Fireblocks-compatible private key and CSR | Outputs to `.config/fireblocks-cli/keys/{name}.csr`, etc. | -| `list` | Not implemented | List all configured profiles | Displays `[profile]` sections from `config.toml` | -| `edit` | Not implemented | Open the config file in your default `$EDITOR` | Falls back to `vi` or `nano` if `$EDITOR` is not set | -| `validate` | Not implemented | Validate the structure and contents of the config file | Checks for invalid or missing keys and values | -| `add` | Not implemented | Append a new profile to the configuration file | Will add to the bottom of the file without auto-formatting | -| `remove` | Not implemented | Remove a profile from the configuration | Deletes the corresponding section from `config.toml` | +| Subcommand | Implemented | Test | Description | Notes | +|-------------------|-------------|--------------|----------------------------------------------------------------|-----------------------------------------------------------------------| +| `init` | βœ… | βœ… | Initialize the default configuration files | Creates `~/.config/fireblocks-cli/config.toml` and `~/.config/fireblocks-cli/keys/` | +| `gen-keys` | βœ… | n/a | Generate Fireblocks-compatible private key and CSR | Outputs to `.config/fireblocks-cli/keys/{name}.csr`, etc. | +| `list` | n/a | n/a | List all configured profiles | Displays `[profile]` sections from `config.toml` | +| `edit` | n/a | n/a | Open the config file in your default `$EDITOR` | Falls back to `vi` or `nano` if `$EDITOR` is not set | +| `validate` | n/a | n/a | Validate the structure and contents of the config file | Checks for invalid or missing keys and values | +| `add` | n/a | n/a | Append a new profile to the configuration file | Will add to the bottom of the file without auto-formatting | +| `remove` | n/a | n/a | Remove a profile from the configuration | Deletes the corresponding section from `config.toml` | --- diff --git a/fireblocks_cli/__main__.py b/fireblocks_cli/__main__.py new file mode 100644 index 0000000..49ce970 --- /dev/null +++ b/fireblocks_cli/__main__.py @@ -0,0 +1,10 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 + +# Author: Shohei KAMON + +from fireblocks_cli.main import app + +if __name__ == "__main__": + app() diff --git a/fireblocks_cli/commands/configure.py b/fireblocks_cli/commands/configure.py index 0ae4fa6..2153f23 100644 --- a/fireblocks_cli/commands/configure.py +++ b/fireblocks_cli/commands/configure.py @@ -5,14 +5,68 @@ # Author: Shohei KAMON import typer +from pathlib import Path from fireblocks_cli.crypto import generate_key_and_csr +from fireblocks_cli.config import ( + get_config_dir, + get_config_file, + get_api_key_dir, + get_credentials_file, + DEFAULT_CONFIG, +) +from fireblocks_cli.utils.toml import save_toml configure_app = typer.Typer() +@configure_app.command("init") +def init(): + """Initialize configuration files and key directories.""" + typer.secho("πŸ›  Starting Fireblocks CLI initialization...", fg=typer.colors.CYAN) + + # Create the config directory if it doesn't exist + config_dir = get_config_dir() + config_dir.mkdir(parents=True, exist_ok=True) + typer.secho(f"βœ… Config directory ensured: {config_dir}", fg=typer.colors.GREEN) + + # Create config.toml if it does not exist + config_file = get_config_file() + if not config_file.exists(): + config = DEFAULT_CONFIG.copy() + + # If credentials file exists, use its values to populate config + credentials_file = get_credentials_file() + if credentials_file.exists(): + lines = credentials_file.read_text().splitlines() + for line in lines: + if "api_id" in line: + config["default"]["api_id"] = line.split("=")[-1].strip() + elif "api_secret_key" in line: + config["default"]["api_secret_key"] = line.split("=")[-1].strip() + typer.secho( + f"βœ… Loaded credentials from: {credentials_file}", + fg=typer.colors.YELLOW, + ) + + # Save the populated config to file + save_toml(config, config_file) + typer.secho(f"βœ… Created config.toml: {config_file}", fg=typer.colors.GREEN) + else: + typer.secho( + f"⚠ config.toml already exists: {config_file}", fg=typer.colors.YELLOW + ) + + # Ensure ~/.config/fireblocks-cli/keys directory exists + api_key_dir = get_api_key_dir() + api_key_dir.mkdir(parents=True, exist_ok=True) + typer.secho(f"βœ… Keys directory ensured: {api_key_dir}", fg=typer.colors.GREEN) + + typer.secho("πŸŽ‰ Initialization complete!", fg=typer.colors.CYAN) + + @configure_app.command("gen-keys") def gen_keys(): - """η§˜ε―†ι΅γ¨CSRγ‚’ ~/.fireblocks/keys γ«η”Ÿζˆγ—γΎγ™""" + """η§˜ε―†ι΅γ¨CSRγ‚’ api_key_dir γ«η”Ÿζˆγ—γΎγ™""" org = typer.prompt("πŸ” 硄織名をε…₯εŠ›γ—γ¦γγ γ•γ„οΌˆδΎ‹: MyCompanyοΌ‰").strip() if not org: typer.secho("❌ η΅„ηΉ”εγ―εΏ…ι ˆγ§γ™γ€‚ε‡¦η†γ‚’δΈ­ζ­’γ—γΎγ™γ€‚", fg=typer.colors.RED) diff --git a/fireblocks_cli/config.py b/fireblocks_cli/config.py index a149d4d..e6b916d 100644 --- a/fireblocks_cli/config.py +++ b/fireblocks_cli/config.py @@ -3,12 +3,31 @@ # SPDX-License-Identifier: MPL-2.0 # Author: Shohei KAMON +from pathlib import Path -from fireblocks_cli.commands.configure import configure_app -import typer -app = typer.Typer() -app.add_typer(configure_app, name="configure") +def get_config_dir() -> Path: + return Path.home() / ".config" / "fireblocks-cli" -if __name__ == "__main__": - app() + +def get_config_file() -> Path: + return get_config_dir() / "config.toml" + + +def get_api_key_dir() -> Path: + return get_config_dir() / "keys" + + +def get_credentials_file() -> Path: + return get_config_dir() / "credentials" + + +DEFAULT_CONFIG = { + "default": { + "api_id": "get-api_id-from-fireblocks-dashboard", + "api_secret_key": { + "type": "file", + "value": "~/.config/fireblocks-cli/keys/abcd.key", + }, + } +} diff --git a/fireblocks_cli/crypto.py b/fireblocks_cli/crypto.py index 5776012..af83434 100644 --- a/fireblocks_cli/crypto.py +++ b/fireblocks_cli/crypto.py @@ -9,6 +9,9 @@ from pathlib import Path import subprocess import typer +from fireblocks_cli.config import ( + get_api_key_dir, +) def generate_unique_basename(base_dir: Path) -> tuple[str, Path, Path]: @@ -21,8 +24,8 @@ def generate_unique_basename(base_dir: Path) -> tuple[str, Path, Path]: def generate_key_and_csr(org_name: str) -> tuple[Path, Path]: - base_dir = Path.home() / ".fireblocks" / "keys" - base_dir.mkdir(parents=True, exist_ok=True) + api_key_dir = get_api_key_dir() + api_key_dir.mkdir(parents=True, exist_ok=True) basename, key_path, csr_path = generate_unique_basename(base_dir) subj = f"/O={org_name}" diff --git a/fireblocks_cli/utils/__init__.py b/fireblocks_cli/utils/__init__.py new file mode 100644 index 0000000..4d611ca --- /dev/null +++ b/fireblocks_cli/utils/__init__.py @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 + +# Author: Shohei KAMON diff --git a/fireblocks_cli/utils/toml.py b/fireblocks_cli/utils/toml.py new file mode 100644 index 0000000..ca2196e --- /dev/null +++ b/fireblocks_cli/utils/toml.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 + +# Author: Shohei KAMON + +import toml +from pathlib import Path + + +def save_toml(data: dict, path: Path): + path.write_text(toml.dumps(data), encoding="utf-8") diff --git a/pyproject.toml b/pyproject.toml index 91582f5..7879f17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,6 +7,8 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "typer[all]>0.15.0", + "toml>0.10.0" + ] [project.scripts] diff --git a/requirements-dev.txt b/requirements-dev.txt index cb9e066..f054329 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,3 +5,4 @@ reuse pre-commit build twine +pytest diff --git a/requirements.txt b/requirements.txt index ec184f6..325a915 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ typer +toml diff --git a/tests/test_configure_init.py b/tests/test_configure_init.py new file mode 100644 index 0000000..741aa9d --- /dev/null +++ b/tests/test_configure_init.py @@ -0,0 +1,92 @@ +# SPDX-FileCopyrightText: 2025 Ethersecurity Inc. +# +# SPDX-License-Identifier: MPL-2.0 + +# Author: Shohei KAMON + +import os +from typer.testing import CliRunner +from fireblocks_cli.main import app +from pathlib import Path +import toml +import pytest + +runner = CliRunner() + + +@pytest.fixture +def mock_home(tmp_path, monkeypatch): + """ + Redirect the HOME environment to a temporary path to isolate file system side effects. + + Ensures that the following paths are created under a clean test directory: + - ~/.config/fireblocks-cli/config.toml + - ~/.config/fireblocks-cli/keys/ + """ + monkeypatch.setattr(Path, "home", lambda: tmp_path) + return tmp_path + + +def test_init_creates_config_and_keys_dir(mock_home): + """ + Test that `configure init` creates the expected configuration file and keys directory. + + Verifies: + - ~/.config/fireblocks-cli/config.toml is created + - ~/.config/fireblocks-cli/keys directory is created + - config file contains default (empty) API values + """ + config_dir = mock_home / ".config/fireblocks-cli" + config_file = config_dir / "config.toml" + keys_dir = config_dir / "keys" + + result = runner.invoke(app, ["configure", "init"]) + assert result.exit_code == 0 + + assert config_file.exists() + assert keys_dir.exists() + + config_data = toml.load(config_file) + assert isinstance(config_data, dict) + assert "default" in config_data + default_section = config_data["default"] + assert isinstance(default_section, dict) + + # api_id: str + assert isinstance(default_section["api_id"], str) + + # api_secret_key: dict with {"type": str, "value": str} + secret_key = default_section["api_secret_key"] + assert isinstance(secret_key, dict) + assert isinstance(secret_key.get("type"), str) + assert isinstance(secret_key.get("value"), str) + + assert secret_key["type"] in {"file", "text", "vault"} # 必要γͺら + + +def test_init_when_config_already_exists(mock_home): + """ + Test that `configure init` does not fail when config.toml already exists. + + Verifies: + - config.toml is not overwritten + - CLI exits successfully (exit_code == 0) + - The message indicates the config already exists + """ + config_dir = mock_home / ".config/fireblocks-cli" + config_file = config_dir / "config.toml" + config_dir.mkdir(parents=True, exist_ok=True) + + # Pre-create config file + original_config = { + "default": {"api_id": "existing_id", "api_secret_key": "existing_secret"} + } + config_file.write_text(toml.dumps(original_config), encoding="utf-8") + + result = runner.invoke(app, ["configure", "init"]) + assert result.exit_code == 0 + assert "already exists" in result.stdout + + config_data = toml.load(config_file) + assert config_data["default"]["api_id"] == "existing_id" + assert config_data["default"]["api_secret_key"] == "existing_secret"