cartha-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cartha_cli/__init__.py +34 -0
- cartha_cli/bt.py +206 -0
- cartha_cli/commands/__init__.py +25 -0
- cartha_cli/commands/common.py +76 -0
- cartha_cli/commands/config.py +294 -0
- cartha_cli/commands/health.py +463 -0
- cartha_cli/commands/help.py +49 -0
- cartha_cli/commands/miner_password.py +283 -0
- cartha_cli/commands/miner_status.py +524 -0
- cartha_cli/commands/pair_status.py +484 -0
- cartha_cli/commands/pools.py +121 -0
- cartha_cli/commands/prove_lock.py +1260 -0
- cartha_cli/commands/register.py +274 -0
- cartha_cli/commands/shared_options.py +235 -0
- cartha_cli/commands/version.py +15 -0
- cartha_cli/config.py +75 -0
- cartha_cli/display.py +62 -0
- cartha_cli/eth712.py +7 -0
- cartha_cli/main.py +237 -0
- cartha_cli/pair.py +201 -0
- cartha_cli/utils.py +274 -0
- cartha_cli/verifier.py +342 -0
- cartha_cli/wallet.py +59 -0
- cartha_cli-1.0.0.dist-info/METADATA +180 -0
- cartha_cli-1.0.0.dist-info/RECORD +28 -0
- cartha_cli-1.0.0.dist-info/WHEEL +4 -0
- cartha_cli-1.0.0.dist-info/entry_points.txt +2 -0
- cartha_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
cartha_cli/__init__.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Cartha CLI package."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def main() -> None: # pragma: no cover
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
argv = sys.argv[1:]
|
|
8
|
+
|
|
9
|
+
if not argv:
|
|
10
|
+
sys.argv = [sys.argv[0]]
|
|
11
|
+
from .commands.help import print_root_help
|
|
12
|
+
|
|
13
|
+
print_root_help()
|
|
14
|
+
raise SystemExit(0)
|
|
15
|
+
|
|
16
|
+
if argv[0] in {"-h", "--help"}:
|
|
17
|
+
original = sys.argv[:]
|
|
18
|
+
sys.argv = [sys.argv[0]]
|
|
19
|
+
from .commands.help import print_root_help
|
|
20
|
+
|
|
21
|
+
print_root_help()
|
|
22
|
+
sys.argv = original
|
|
23
|
+
raise SystemExit(0)
|
|
24
|
+
|
|
25
|
+
original = sys.argv[:]
|
|
26
|
+
sanitized = [original[0]] + [arg for arg in argv if arg not in {"-h", "--help"}]
|
|
27
|
+
sys.argv = sanitized
|
|
28
|
+
from .main import app
|
|
29
|
+
|
|
30
|
+
sys.argv = original
|
|
31
|
+
app()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
__all__ = ["main"]
|
cartha_cli/bt.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Bittensor convenience wrappers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import bittensor as bt
|
|
10
|
+
except ImportError: # pragma: no cover - surfaced at call time
|
|
11
|
+
bt = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_subtensor(network: str) -> bt.Subtensor:
|
|
15
|
+
if bt is None: # pragma: no cover - safeguarded for tests
|
|
16
|
+
raise RuntimeError("bittensor is not installed")
|
|
17
|
+
return bt.subtensor(network=network)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_wallet(name: str, hotkey: str) -> bt.wallet:
|
|
21
|
+
if bt is None: # pragma: no cover
|
|
22
|
+
raise RuntimeError("bittensor is not installed")
|
|
23
|
+
return bt.wallet(name=name, hotkey=hotkey)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class RegistrationResult:
|
|
28
|
+
status: str
|
|
29
|
+
success: bool
|
|
30
|
+
uid: int | None
|
|
31
|
+
hotkey: str
|
|
32
|
+
extrinsic: str | None = None # Extrinsic hash (e.g., "5759123-5")
|
|
33
|
+
balance_before: float | None = None # Balance before registration
|
|
34
|
+
balance_after: float | None = None # Balance after registration
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def register_hotkey(
|
|
38
|
+
*,
|
|
39
|
+
network: str,
|
|
40
|
+
wallet_name: str,
|
|
41
|
+
hotkey_name: str,
|
|
42
|
+
netuid: int,
|
|
43
|
+
burned: bool = True,
|
|
44
|
+
cuda: bool = False,
|
|
45
|
+
wait_for_finalization: bool = True,
|
|
46
|
+
wait_for_inclusion: bool = False,
|
|
47
|
+
dev_id: int | list[int] | None = 0,
|
|
48
|
+
tpb: int = 256,
|
|
49
|
+
num_processes: int | None = None,
|
|
50
|
+
) -> RegistrationResult:
|
|
51
|
+
"""Register a hotkey on the target subnet and return the resulting UID."""
|
|
52
|
+
|
|
53
|
+
subtensor = get_subtensor(network)
|
|
54
|
+
wallet = get_wallet(wallet_name, hotkey_name)
|
|
55
|
+
hotkey_ss58 = wallet.hotkey.ss58_address
|
|
56
|
+
|
|
57
|
+
if subtensor.is_hotkey_registered(hotkey_ss58, netuid=netuid):
|
|
58
|
+
neuron = subtensor.get_neuron_for_pubkey_and_subnet(hotkey_ss58, netuid)
|
|
59
|
+
uid = None if getattr(neuron, "is_null", False) else getattr(neuron, "uid", None)
|
|
60
|
+
return RegistrationResult(status="already", success=True, uid=uid, hotkey=hotkey_ss58)
|
|
61
|
+
|
|
62
|
+
# Get balance before registration
|
|
63
|
+
balance_before = None
|
|
64
|
+
balance_after = None
|
|
65
|
+
extrinsic = None
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
balance_obj = subtensor.get_balance(wallet.coldkeypub.ss58_address)
|
|
69
|
+
# Convert Balance object to float using .tao property
|
|
70
|
+
balance_before = balance_obj.tao if hasattr(balance_obj, "tao") else float(balance_obj)
|
|
71
|
+
except Exception:
|
|
72
|
+
pass # Balance may not be available, continue anyway
|
|
73
|
+
|
|
74
|
+
if burned:
|
|
75
|
+
# burned_register returns (success, block_info) or just success
|
|
76
|
+
registration_result = subtensor.burned_register(
|
|
77
|
+
wallet=wallet,
|
|
78
|
+
netuid=netuid,
|
|
79
|
+
wait_for_finalization=wait_for_finalization,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Handle both return types: bool or (bool, message)
|
|
83
|
+
if isinstance(registration_result, tuple):
|
|
84
|
+
ok, message = registration_result
|
|
85
|
+
if isinstance(message, str) and message:
|
|
86
|
+
extrinsic = message
|
|
87
|
+
else:
|
|
88
|
+
ok = registration_result
|
|
89
|
+
|
|
90
|
+
status = "burned"
|
|
91
|
+
else:
|
|
92
|
+
ok = subtensor.register(
|
|
93
|
+
wallet=wallet,
|
|
94
|
+
netuid=netuid,
|
|
95
|
+
wait_for_finalization=wait_for_finalization,
|
|
96
|
+
wait_for_inclusion=wait_for_inclusion,
|
|
97
|
+
cuda=cuda,
|
|
98
|
+
dev_id=dev_id,
|
|
99
|
+
tpb=tpb,
|
|
100
|
+
num_processes=num_processes,
|
|
101
|
+
log_verbose=False,
|
|
102
|
+
)
|
|
103
|
+
status = "pow"
|
|
104
|
+
if isinstance(ok, tuple) and len(ok) == 2:
|
|
105
|
+
ok, message = ok
|
|
106
|
+
if isinstance(message, str):
|
|
107
|
+
extrinsic = message
|
|
108
|
+
|
|
109
|
+
if not ok:
|
|
110
|
+
return RegistrationResult(
|
|
111
|
+
status=status,
|
|
112
|
+
success=False,
|
|
113
|
+
uid=None,
|
|
114
|
+
hotkey=hotkey_ss58,
|
|
115
|
+
balance_before=balance_before,
|
|
116
|
+
balance_after=balance_after,
|
|
117
|
+
extrinsic=extrinsic,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Get balance after registration
|
|
121
|
+
try:
|
|
122
|
+
balance_obj = subtensor.get_balance(wallet.coldkeypub.ss58_address)
|
|
123
|
+
# Convert Balance object to float using .tao property
|
|
124
|
+
balance_after = balance_obj.tao if hasattr(balance_obj, "tao") else float(balance_obj)
|
|
125
|
+
except Exception:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
neuron = subtensor.get_neuron_for_pubkey_and_subnet(hotkey_ss58, netuid)
|
|
129
|
+
uid = None if getattr(neuron, "is_null", False) else getattr(neuron, "uid", None)
|
|
130
|
+
|
|
131
|
+
return RegistrationResult(
|
|
132
|
+
status=status,
|
|
133
|
+
success=True,
|
|
134
|
+
uid=uid,
|
|
135
|
+
hotkey=hotkey_ss58,
|
|
136
|
+
balance_before=balance_before,
|
|
137
|
+
balance_after=balance_after,
|
|
138
|
+
extrinsic=extrinsic,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_burn_cost(network: str, netuid: int) -> float | None:
|
|
143
|
+
"""Get the burn cost (registration cost) for a subnet.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
network: Bittensor network name (e.g., "test", "finney")
|
|
147
|
+
netuid: Subnet netuid
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Burn cost in TAO as a float, or None if unavailable
|
|
151
|
+
"""
|
|
152
|
+
# Try async SubtensorInterface method (most reliable)
|
|
153
|
+
try:
|
|
154
|
+
from bittensor_cli.src.bittensor.balances import Balance # type: ignore[import-not-found]
|
|
155
|
+
from bittensor_cli.src.bittensor.subtensor_interface import ( # type: ignore[import-not-found]
|
|
156
|
+
SubtensorInterface,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
async def _fetch_burn_cost() -> float:
|
|
160
|
+
async with SubtensorInterface(network=network) as subtensor_async:
|
|
161
|
+
block_hash = await subtensor_async.substrate.get_chain_head()
|
|
162
|
+
burn_raw = await subtensor_async.get_hyperparameter(
|
|
163
|
+
param_name="Burn",
|
|
164
|
+
netuid=netuid,
|
|
165
|
+
block_hash=block_hash,
|
|
166
|
+
)
|
|
167
|
+
register_cost = (
|
|
168
|
+
Balance.from_rao(int(burn_raw)) if burn_raw is not None else Balance(0)
|
|
169
|
+
)
|
|
170
|
+
tao_value = (
|
|
171
|
+
register_cost.tao if hasattr(register_cost, "tao") else float(register_cost)
|
|
172
|
+
)
|
|
173
|
+
return float(tao_value)
|
|
174
|
+
|
|
175
|
+
return asyncio.run(_fetch_burn_cost())
|
|
176
|
+
except ImportError:
|
|
177
|
+
# bittensor-cli not available, try fallback synchronous method
|
|
178
|
+
try:
|
|
179
|
+
subtensor = get_subtensor(network)
|
|
180
|
+
# Use get_hyperparameter("Burn", netuid) - this works synchronously
|
|
181
|
+
if hasattr(subtensor, "get_hyperparameter"):
|
|
182
|
+
burn_raw = subtensor.get_hyperparameter("Burn", netuid=netuid)
|
|
183
|
+
if burn_raw is not None:
|
|
184
|
+
# Convert from rao to TAO
|
|
185
|
+
from bittensor import Balance
|
|
186
|
+
|
|
187
|
+
balance_obj = Balance.from_rao(int(burn_raw))
|
|
188
|
+
return balance_obj.tao if hasattr(balance_obj, "tao") else float(balance_obj)
|
|
189
|
+
except Exception:
|
|
190
|
+
pass
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
# Log other exceptions but don't show warnings in normal operation
|
|
193
|
+
import warnings
|
|
194
|
+
|
|
195
|
+
warnings.warn(f"Failed to fetch burn cost: {exc}", UserWarning, stacklevel=2)
|
|
196
|
+
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
__all__ = [
|
|
201
|
+
"get_subtensor",
|
|
202
|
+
"get_wallet",
|
|
203
|
+
"register_hotkey",
|
|
204
|
+
"get_burn_cost",
|
|
205
|
+
"RegistrationResult",
|
|
206
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""CLI command modules."""
|
|
2
|
+
|
|
3
|
+
from . import (
|
|
4
|
+
config,
|
|
5
|
+
health,
|
|
6
|
+
miner_password,
|
|
7
|
+
miner_status,
|
|
8
|
+
pair_status,
|
|
9
|
+
pools,
|
|
10
|
+
prove_lock,
|
|
11
|
+
register,
|
|
12
|
+
version,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"config",
|
|
17
|
+
"health",
|
|
18
|
+
"miner_password",
|
|
19
|
+
"miner_status",
|
|
20
|
+
"pair_status",
|
|
21
|
+
"pools",
|
|
22
|
+
"prove_lock",
|
|
23
|
+
"register",
|
|
24
|
+
"version",
|
|
25
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Common utilities and helpers for CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import NoReturn
|
|
6
|
+
|
|
7
|
+
import bittensor as bt
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from ..config import settings
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
_TRACE_ENABLED = False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def set_trace_enabled(enabled: bool) -> None:
|
|
19
|
+
"""Set whether trace mode is enabled."""
|
|
20
|
+
global _TRACE_ENABLED
|
|
21
|
+
_TRACE_ENABLED = enabled
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def trace_enabled() -> bool:
|
|
25
|
+
"""Check if trace mode is enabled."""
|
|
26
|
+
return _TRACE_ENABLED
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def exit_with_error(message: str, code: int = 1) -> NoReturn:
|
|
30
|
+
"""Exit with an error message."""
|
|
31
|
+
console.print(f"[bold red]{message}[/]")
|
|
32
|
+
raise typer.Exit(code=code)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def handle_wallet_exception(
|
|
36
|
+
*,
|
|
37
|
+
wallet_name: str | None,
|
|
38
|
+
wallet_hotkey: str | None,
|
|
39
|
+
exc: Exception,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Handle wallet-related exceptions."""
|
|
42
|
+
detail = str(exc).strip()
|
|
43
|
+
name = wallet_name or "<unknown>"
|
|
44
|
+
hotkey = wallet_hotkey or "<unknown>"
|
|
45
|
+
message = (
|
|
46
|
+
f"Unable to open coldkey '{name}' hotkey '{hotkey}'. "
|
|
47
|
+
"Ensure the wallet exists, hotkey files are present, and the key is unlocked."
|
|
48
|
+
)
|
|
49
|
+
if detail:
|
|
50
|
+
message += f" ({detail})"
|
|
51
|
+
exit_with_error(message)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def handle_unexpected_exception(context: str, exc: Exception) -> None:
|
|
55
|
+
"""Handle unexpected exceptions."""
|
|
56
|
+
if trace_enabled():
|
|
57
|
+
raise
|
|
58
|
+
detail = str(exc).strip()
|
|
59
|
+
message = context
|
|
60
|
+
if detail:
|
|
61
|
+
message += f" ({detail})"
|
|
62
|
+
exit_with_error(message)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def log_endpoint_banner() -> None:
|
|
66
|
+
"""Log the endpoint banner based on verifier URL."""
|
|
67
|
+
verifier_url = settings.verifier_url.lower()
|
|
68
|
+
if verifier_url.startswith("http://127.0.0.1"):
|
|
69
|
+
console.print("[bold cyan]Using local verifier endpoint[/]")
|
|
70
|
+
elif "pr-" in verifier_url:
|
|
71
|
+
console.print("[bold cyan]Using Cartha DEV network verifier[/]")
|
|
72
|
+
elif "cartha-verifier-826542474079.us-central1.run.app" in verifier_url:
|
|
73
|
+
console.print("[bold cyan]Using Cartha Testnet Verifier[/]")
|
|
74
|
+
else:
|
|
75
|
+
console.print("[bold cyan]Using Cartha network verifier[/]")
|
|
76
|
+
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Configuration command - view and set environment variables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from ..config import Settings
|
|
13
|
+
from .common import console
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Environment variable documentation
|
|
17
|
+
ENV_VAR_DOCS: dict[str, dict[str, Any]] = {
|
|
18
|
+
"CARTHA_VERIFIER_URL": {
|
|
19
|
+
"description": "URL of the Cartha verifier service",
|
|
20
|
+
"default": "https://cartha-verifier-826542474079.us-central1.run.app",
|
|
21
|
+
"required": False,
|
|
22
|
+
},
|
|
23
|
+
"CARTHA_NETWORK": {
|
|
24
|
+
"description": "Bittensor network name (e.g., 'finney', 'test')",
|
|
25
|
+
"default": "finney",
|
|
26
|
+
"required": False,
|
|
27
|
+
},
|
|
28
|
+
"CARTHA_NETUID": {
|
|
29
|
+
"description": "Subnet UID (netuid) for the Cartha subnet",
|
|
30
|
+
"default": "35",
|
|
31
|
+
"required": False,
|
|
32
|
+
},
|
|
33
|
+
"CARTHA_EVM_PK": {
|
|
34
|
+
"description": "EVM private key for signing transactions (sensitive)",
|
|
35
|
+
"default": None,
|
|
36
|
+
"required": False,
|
|
37
|
+
"sensitive": True,
|
|
38
|
+
},
|
|
39
|
+
"CARTHA_RETRY_MAX_ATTEMPTS": {
|
|
40
|
+
"description": "Maximum number of retry attempts for failed requests",
|
|
41
|
+
"default": "3",
|
|
42
|
+
"required": False,
|
|
43
|
+
},
|
|
44
|
+
"CARTHA_RETRY_BACKOFF_FACTOR": {
|
|
45
|
+
"description": "Backoff factor for exponential retry delays",
|
|
46
|
+
"default": "1.5",
|
|
47
|
+
"required": False,
|
|
48
|
+
},
|
|
49
|
+
"CARTHA_LOCK_UI_URL": {
|
|
50
|
+
"description": "URL of the Cartha Lock UI frontend",
|
|
51
|
+
"default": "https://cartha.finance",
|
|
52
|
+
"required": False,
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_env_file_path() -> Path:
|
|
58
|
+
"""Get the path to the .env file in the current directory."""
|
|
59
|
+
return Path.cwd() / ".env"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _read_env_file() -> dict[str, str]:
|
|
63
|
+
"""Read existing .env file and return as dict."""
|
|
64
|
+
env_file = _get_env_file_path()
|
|
65
|
+
env_vars: dict[str, str] = {}
|
|
66
|
+
|
|
67
|
+
if env_file.exists():
|
|
68
|
+
try:
|
|
69
|
+
with open(env_file, "r", encoding="utf-8") as f:
|
|
70
|
+
for line in f:
|
|
71
|
+
line = line.strip()
|
|
72
|
+
# Skip empty lines and comments
|
|
73
|
+
if not line or line.startswith("#"):
|
|
74
|
+
continue
|
|
75
|
+
# Parse KEY=VALUE format
|
|
76
|
+
if "=" in line:
|
|
77
|
+
key, value = line.split("=", 1)
|
|
78
|
+
env_vars[key.strip()] = value.strip().strip('"').strip("'")
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
return env_vars
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _write_env_file(env_vars: dict[str, str], remove_vars: set[str] | None = None) -> None:
|
|
86
|
+
"""Write environment variables to .env file.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
env_vars: Dictionary of variables to set/update
|
|
90
|
+
remove_vars: Optional set of variable names to remove from file
|
|
91
|
+
"""
|
|
92
|
+
env_file = _get_env_file_path()
|
|
93
|
+
remove_vars = remove_vars or set()
|
|
94
|
+
|
|
95
|
+
# Read existing file to preserve comments and other vars
|
|
96
|
+
existing_lines: list[str] = []
|
|
97
|
+
existing_vars: set[str] = set()
|
|
98
|
+
|
|
99
|
+
if env_file.exists():
|
|
100
|
+
with open(env_file, "r", encoding="utf-8") as f:
|
|
101
|
+
for line in f:
|
|
102
|
+
stripped = line.strip()
|
|
103
|
+
if stripped and not stripped.startswith("#") and "=" in stripped:
|
|
104
|
+
key = stripped.split("=", 1)[0].strip()
|
|
105
|
+
existing_vars.add(key)
|
|
106
|
+
# Skip lines for variables we want to remove
|
|
107
|
+
if key in remove_vars:
|
|
108
|
+
continue
|
|
109
|
+
existing_lines.append(line.rstrip())
|
|
110
|
+
|
|
111
|
+
# Update or add new vars
|
|
112
|
+
for key, value in env_vars.items():
|
|
113
|
+
if key in existing_vars and key not in remove_vars:
|
|
114
|
+
# Update existing line
|
|
115
|
+
for i, line in enumerate(existing_lines):
|
|
116
|
+
if line.strip() and not line.strip().startswith("#") and line.startswith(f"{key}="):
|
|
117
|
+
existing_lines[i] = f"{key}={value}"
|
|
118
|
+
break
|
|
119
|
+
elif key not in remove_vars:
|
|
120
|
+
# Add new line
|
|
121
|
+
existing_lines.append(f"{key}={value}")
|
|
122
|
+
|
|
123
|
+
# Write back to file
|
|
124
|
+
with open(env_file, "w", encoding="utf-8") as f:
|
|
125
|
+
for line in existing_lines:
|
|
126
|
+
f.write(line + "\n")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def config_list() -> None:
|
|
130
|
+
"""List all available environment variables and their current values."""
|
|
131
|
+
console.print("\n[bold cyan]━━━ Configuration ━━━[/]")
|
|
132
|
+
console.print()
|
|
133
|
+
|
|
134
|
+
# Get default values
|
|
135
|
+
default_settings = Settings()
|
|
136
|
+
|
|
137
|
+
# Get current values from environment
|
|
138
|
+
current_values: dict[str, str | None] = {}
|
|
139
|
+
for env_var in ENV_VAR_DOCS.keys():
|
|
140
|
+
current_values[env_var] = os.getenv(env_var)
|
|
141
|
+
|
|
142
|
+
# Create table
|
|
143
|
+
table = Table(show_header=True, header_style="bold cyan")
|
|
144
|
+
table.add_column("Variable", style="cyan", no_wrap=True)
|
|
145
|
+
table.add_column("Description", style="dim")
|
|
146
|
+
table.add_column("Current Value", style="green")
|
|
147
|
+
table.add_column("Default", style="dim")
|
|
148
|
+
table.add_column("Source", justify="center")
|
|
149
|
+
|
|
150
|
+
for env_var, doc in ENV_VAR_DOCS.items():
|
|
151
|
+
current_val = current_values.get(env_var)
|
|
152
|
+
default_val = doc.get("default")
|
|
153
|
+
is_sensitive = doc.get("sensitive", False)
|
|
154
|
+
|
|
155
|
+
# Format current value
|
|
156
|
+
if current_val is not None:
|
|
157
|
+
if is_sensitive:
|
|
158
|
+
display_current = "[REDACTED]"
|
|
159
|
+
else:
|
|
160
|
+
display_current = current_val
|
|
161
|
+
source = "[green]●[/] env"
|
|
162
|
+
else:
|
|
163
|
+
display_current = default_val or "-"
|
|
164
|
+
source = "[dim]○[/] default"
|
|
165
|
+
|
|
166
|
+
# Format default value
|
|
167
|
+
display_default = default_val or "-"
|
|
168
|
+
|
|
169
|
+
table.add_row(
|
|
170
|
+
env_var,
|
|
171
|
+
doc["description"],
|
|
172
|
+
display_current,
|
|
173
|
+
display_default,
|
|
174
|
+
source,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
console.print(table)
|
|
178
|
+
console.print()
|
|
179
|
+
console.print("[dim]● = Set via environment variable[/]")
|
|
180
|
+
console.print("[dim]○ = Using default value[/]")
|
|
181
|
+
console.print()
|
|
182
|
+
console.print(f"[dim]Environment file: {_get_env_file_path()}[/]")
|
|
183
|
+
console.print("[dim]Use 'cartha config set <VAR> <VALUE>' to set a value[/]")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def config_set(
|
|
187
|
+
variable: str = typer.Argument(..., help="Environment variable name (e.g., CARTHA_VERIFIER_URL)"),
|
|
188
|
+
value: str = typer.Argument(..., help="Value to set"),
|
|
189
|
+
env_file: bool = typer.Option(
|
|
190
|
+
True,
|
|
191
|
+
"--env-file/--no-env-file",
|
|
192
|
+
help="Write to .env file (default: True). If False, only sets in current session.",
|
|
193
|
+
),
|
|
194
|
+
) -> None:
|
|
195
|
+
"""Set an environment variable value.
|
|
196
|
+
|
|
197
|
+
By default, writes to .env file in the current directory. Use --no-env-file
|
|
198
|
+
to only set for the current session (doesn't persist).
|
|
199
|
+
"""
|
|
200
|
+
# Validate variable name
|
|
201
|
+
if variable not in ENV_VAR_DOCS:
|
|
202
|
+
console.print(f"[bold red]✗ Unknown variable: {variable}[/]")
|
|
203
|
+
console.print(f"\nAvailable variables:")
|
|
204
|
+
for var_name in ENV_VAR_DOCS.keys():
|
|
205
|
+
console.print(f" • {var_name}")
|
|
206
|
+
raise typer.Exit(code=1)
|
|
207
|
+
|
|
208
|
+
doc = ENV_VAR_DOCS[variable]
|
|
209
|
+
is_sensitive = doc.get("sensitive", False)
|
|
210
|
+
|
|
211
|
+
if env_file:
|
|
212
|
+
# Read existing .env file
|
|
213
|
+
env_vars = _read_env_file()
|
|
214
|
+
|
|
215
|
+
# Update the variable
|
|
216
|
+
env_vars[variable] = value
|
|
217
|
+
|
|
218
|
+
# Write back
|
|
219
|
+
try:
|
|
220
|
+
_write_env_file(env_vars)
|
|
221
|
+
display_value = "[REDACTED]" if is_sensitive else value
|
|
222
|
+
console.print(f"[bold green]✓ Set {variable}={display_value}[/]")
|
|
223
|
+
console.print(f"[dim]Written to: {_get_env_file_path()}[/]")
|
|
224
|
+
console.print("[yellow]Note: Restart your terminal or run 'source .env' to apply changes[/]")
|
|
225
|
+
except Exception as exc:
|
|
226
|
+
console.print(f"[bold red]✗ Failed to write to .env file: {exc}[/]")
|
|
227
|
+
raise typer.Exit(code=1)
|
|
228
|
+
else:
|
|
229
|
+
# Only set for current session
|
|
230
|
+
os.environ[variable] = value
|
|
231
|
+
display_value = "[REDACTED]" if is_sensitive else value
|
|
232
|
+
console.print(f"[bold green]✓ Set {variable}={display_value}[/] (current session only)")
|
|
233
|
+
console.print("[yellow]Note: This will not persist after the terminal session ends[/]")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def config_get(
|
|
237
|
+
variable: str = typer.Argument(..., help="Environment variable name (e.g., CARTHA_VERIFIER_URL)"),
|
|
238
|
+
) -> None:
|
|
239
|
+
"""Get the current value of an environment variable."""
|
|
240
|
+
if variable not in ENV_VAR_DOCS:
|
|
241
|
+
console.print(f"[bold red]✗ Unknown variable: {variable}[/]")
|
|
242
|
+
console.print(f"\nAvailable variables:")
|
|
243
|
+
for var_name in ENV_VAR_DOCS.keys():
|
|
244
|
+
console.print(f" • {var_name}")
|
|
245
|
+
raise typer.Exit(code=1)
|
|
246
|
+
|
|
247
|
+
doc = ENV_VAR_DOCS[variable]
|
|
248
|
+
is_sensitive = doc.get("sensitive", False)
|
|
249
|
+
|
|
250
|
+
# Get current value
|
|
251
|
+
current_val = os.getenv(variable)
|
|
252
|
+
default_val = doc.get("default")
|
|
253
|
+
|
|
254
|
+
console.print(f"\n[bold cyan]{variable}[/]")
|
|
255
|
+
console.print(f"Description: {doc['description']}")
|
|
256
|
+
console.print(f"Default: {default_val or 'None'}")
|
|
257
|
+
|
|
258
|
+
if current_val is not None:
|
|
259
|
+
display_value = "[REDACTED]" if is_sensitive else current_val
|
|
260
|
+
console.print(f"Current Value: [green]{display_value}[/] (from environment)")
|
|
261
|
+
else:
|
|
262
|
+
console.print(f"Current Value: [dim]{default_val or 'None'}[/] (using default)")
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def config_unset(
|
|
266
|
+
variable: str = typer.Argument(..., help="Environment variable name to unset"),
|
|
267
|
+
env_file: bool = typer.Option(
|
|
268
|
+
True,
|
|
269
|
+
"--env-file/--no-env-file",
|
|
270
|
+
help="Remove from .env file (default: True). If False, only unsets in current session.",
|
|
271
|
+
),
|
|
272
|
+
) -> None:
|
|
273
|
+
"""Unset an environment variable (remove from .env file or current session)."""
|
|
274
|
+
if variable not in ENV_VAR_DOCS:
|
|
275
|
+
console.print(f"[bold red]✗ Unknown variable: {variable}[/]")
|
|
276
|
+
raise typer.Exit(code=1)
|
|
277
|
+
|
|
278
|
+
if env_file:
|
|
279
|
+
# Read existing .env file to check if it exists
|
|
280
|
+
env_vars = _read_env_file()
|
|
281
|
+
|
|
282
|
+
if variable in env_vars:
|
|
283
|
+
# Remove from file
|
|
284
|
+
_write_env_file({}, remove_vars={variable})
|
|
285
|
+
console.print(f"[bold green]✓ Removed {variable} from .env file[/]")
|
|
286
|
+
else:
|
|
287
|
+
console.print(f"[yellow]⚠ {variable} not found in .env file[/]")
|
|
288
|
+
else:
|
|
289
|
+
# Remove from current session
|
|
290
|
+
if variable in os.environ:
|
|
291
|
+
del os.environ[variable]
|
|
292
|
+
console.print(f"[bold green]✓ Removed {variable} from current session[/]")
|
|
293
|
+
else:
|
|
294
|
+
console.print(f"[yellow]⚠ {variable} not set in current session[/]")
|