Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 .
```
Expand Down Expand Up @@ -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` |


---
Expand Down
10 changes: 10 additions & 0 deletions fireblocks_cli/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0

# Author: Shohei KAMON <cameong@stir.network>

from fireblocks_cli.main import app

if __name__ == "__main__":
app()
56 changes: 55 additions & 1 deletion fireblocks_cli/commands/configure.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,68 @@
# Author: Shohei KAMON <cameong@stir.network>

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)
Expand Down
31 changes: 25 additions & 6 deletions fireblocks_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,31 @@
# SPDX-License-Identifier: MPL-2.0

# Author: Shohei KAMON <cameong@stir.network>
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",
},
}
}
7 changes: 5 additions & 2 deletions fireblocks_cli/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]:
Expand All @@ -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}"
Expand Down
5 changes: 5 additions & 0 deletions fireblocks_cli/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0

# Author: Shohei KAMON <cameong@stir.network>
12 changes: 12 additions & 0 deletions fireblocks_cli/utils/toml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0

# Author: Shohei KAMON <cameong@stir.network>

import toml
from pathlib import Path


def save_toml(data: dict, path: Path):
path.write_text(toml.dumps(data), encoding="utf-8")
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"typer[all]>0.15.0",
"toml>0.10.0"

]

[project.scripts]
Expand Down
1 change: 1 addition & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ reuse
pre-commit
build
twine
pytest
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
typer
toml
92 changes: 92 additions & 0 deletions tests/test_configure_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# SPDX-FileCopyrightText: 2025 Ethersecurity Inc.
#
# SPDX-License-Identifier: MPL-2.0

# Author: Shohei KAMON <cameong@stir.network>

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"