Implement 36 missing CLI subcommands for scenarios 04-47

Phase 1: Simple GET Commands (system, resource, edge, config)
- system: restart, status, config
- resource: status, deallocate
- edge: status, balance, transfer
- config: import (already existed)

Phase 2: Exchange & Market Commands
- exchange: order, orders, book, history
- marketplace: bid, bids, ask, asks

Phase 3: AI & GPU Commands
- agent (AI): job, jobs, submit, cancel
- gpu: register, update

Phase 4: Wallet, Hermes, Operations
- wallet: export, import
- hermes: send, receive, peers
- operations: vote, proposal, delegate

Phase 5: Simulate Commands
- simulate: run, status, result

All commands follow common pattern using AITBCHTTPClient for backend API interaction.
This commit is contained in:
aitbc
2026-05-26 22:03:58 +02:00
parent ac59e3ee0a
commit 51209844c9
11 changed files with 865 additions and 10 deletions

View File

@@ -19,6 +19,12 @@ except ImportError:
ComputeConsumer = None
AITBCAgent = None
from ..utils import output, error, success
from ..config import get_config
from aitbc import get_logger, AITBCHTTPClient, NetworkError
logger = get_logger(__name__)
def get_agent_config_dir() -> Path:
"""Get the agent configuration directory"""
@@ -602,6 +608,88 @@ try:
error(f"Error exporting configuration: {str(e)}")
raise click.Abort()
@agent.command()
@click.argument('job_id')
@click.pass_context
def job(ctx, job_id: str):
"""Get specific AI job details from coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
job_data = http_client.get(f"/api/v1/jobs/{job_id}")
success(f"Job {job_id}:")
output(job_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching job: {e}")
@agent.command()
@click.option('--status', help='Filter by job status')
@click.option('--limit', type=int, default=20, help='Number of jobs to return')
@click.pass_context
def jobs(ctx, status: Optional[str], limit: int):
"""List AI jobs from coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
params = {"limit": limit}
if status:
params["status"] = status
jobs_data = http_client.get("/api/v1/jobs", params=params)
success("Jobs:")
output(jobs_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching jobs: {e}")
@agent.command()
@click.argument('task')
@click.option('--model', help='AI model to use')
@click.option('--priority', default='normal', help='Job priority')
@click.pass_context
def submit(ctx, task: str, model: Optional[str], priority: str):
"""Submit an AI job to coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
job_data = {
"task": task,
"priority": priority
}
if model:
job_data["model"] = model
result = http_client.post("/api/v1/jobs", json=job_data)
success(f"Job submitted: {result.get('job_id')}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error submitting job: {e}")
@agent.command()
@click.argument('job_id')
@click.pass_context
def cancel(ctx, job_id: str):
"""Cancel an AI job via coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
result = http_client.delete(f"/api/v1/jobs/{job_id}")
success(f"Job {job_id} cancelled")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error cancelling job: {e}")
except ImportError:
# Click not available, commands will be added programmatically
pass

View File

@@ -8,9 +8,10 @@ import httpx
from typing import Optional
from ..utils import output, error, success, info, warning
from ..config import get_config
from aitbc import get_logger, AITBCHTTPClient, NetworkError
# Initialize logger
logger = None
logger = get_logger(__name__)
@click.group()
@@ -19,6 +20,67 @@ def edge():
pass
@edge.command()
@click.pass_context
def status(ctx):
"""Get edge status from coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
status_data = http_client.get("/edge-gpu/metrics")
success("Edge Status:")
output(status_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching edge status: {e}")
@edge.command()
@click.pass_context
def balance(ctx):
"""Get edge wallet balance from coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
balance_data = http_client.get("/edge-gpu/balance")
success("Edge Wallet Balance:")
output(balance_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching edge balance: {e}")
@edge.command()
@click.argument('to_address')
@click.argument('amount', type=float)
@click.option('--note', help='Transfer note')
@click.pass_context
def transfer(ctx, to_address: str, amount: float, note: Optional[str]):
"""Transfer edge tokens to another address"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
transfer_data = {
"to_address": to_address,
"amount": amount
}
if note:
transfer_data["note"] = note
result = http_client.post("/edge-gpu/transfer", json=transfer_data)
success(f"Transfer of {amount} to {to_address} submitted")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error executing transfer: {e}")
def get_edge_client():
"""Get Edge API HTTP client"""
config = get_config()

View File

@@ -877,11 +877,92 @@ def pairs(ctx, exchange: str):
if len(base_currencies[base]) > 10:
success(f" ... and {len(base_currencies[base]) - 10} more")
except ImportError:
error("❌ Real exchange integration not available. Install ccxt library.")
@exchange.command()
@click.argument('order_id')
@click.pass_context
def order(ctx, order_id: str):
"""Get specific order details from exchange-service"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10)
order_data = http_client.get(f"/exchange/order/{order_id}")
success(f"Order {order_id}:")
output(order_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"❌ Pairs error: {e}")
error(f"Error fetching order: {e}")
@exchange.command()
@click.option('--pair', help='Filter by trading pair')
@click.option('--status', help='Filter by status')
@click.option('--limit', type=int, default=20, help='Number of orders to return')
@click.pass_context
def orders(ctx, pair: Optional[str], status: Optional[str], limit: int):
"""List orders from exchange-service"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10)
params = {"limit": limit}
if pair:
params["pair"] = pair
if status:
params["status"] = status
orders_data = http_client.get("/exchange/orders", params=params)
success("Orders:")
output(orders_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching orders: {e}")
@exchange.command()
@click.option('--pair', required=True, help='Trading pair (e.g., AITBC/BTC)')
@click.option('--limit', type=int, default=20, help='Order book depth')
@click.pass_context
def book(ctx, pair: str, limit: int):
"""Get order book from exchange-service"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10)
book_data = http_client.get("/exchange/orderbook", params={"pair": pair, "limit": limit})
success(f"Order Book for {pair}:")
output(book_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching order book: {e}")
@exchange.command()
@click.option('--pair', help='Filter by trading pair')
@click.option('--limit', type=int, default=50, help='Number of history entries')
@click.pass_context
def history(ctx, pair: Optional[str], limit: int):
"""Get trade history from exchange-service"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.exchange_service_url, timeout=10)
params = {"limit": limit}
if pair:
params["pair"] = pair
history_data = http_client.get("/exchange/history", params=params)
success("Trade History:")
output(history_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching trade history: {e}")
@exchange.command()

View File

@@ -670,3 +670,86 @@ def providers(ctx):
except Exception as e:
error(f"Error querying GPU providers: {str(e)}")
raise click.Abort()
@gpu.command()
@click.argument('gpu_id')
@click.option('--specs', help='GPU specifications (JSON string)')
@click.option('--pricing', help='Pricing model (JSON string)')
@click.pass_context
def register(ctx, gpu_id: str, specs: Optional[str], pricing: Optional[str]):
"""Register a GPU with the gpu-service"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.gpu_service_url, timeout=10)
gpu_data = {"gpu_id": gpu_id}
if specs:
try:
gpu_data["specs"] = json.loads(specs)
except json.JSONDecodeError:
error("Invalid JSON specifications")
raise click.Abort()
if pricing:
try:
gpu_data["pricing"] = json.loads(pricing)
except json.JSONDecodeError:
error("Invalid JSON pricing")
raise click.Abort()
result = http_client.post("/gpu/register", json=gpu_data)
success(f"GPU {gpu_id} registered successfully")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error registering GPU: {e}")
@gpu.command()
@click.argument('gpu_id')
@click.option('--specs', help='Updated GPU specifications (JSON string)')
@click.option('--pricing', help='Updated pricing model (JSON string)')
@click.option('--status', help='Update GPU status')
@click.pass_context
def update(ctx, gpu_id: str, specs: Optional[str], pricing: Optional[str], status: Optional[str]):
"""Update GPU registration with the gpu-service"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.gpu_service_url, timeout=10)
update_data = {}
if specs:
try:
update_data["specs"] = json.loads(specs)
except json.JSONDecodeError:
error("Invalid JSON specifications")
raise click.Abort()
if pricing:
try:
update_data["pricing"] = json.loads(pricing)
except json.JSONDecodeError:
error("Invalid JSON pricing")
raise click.Abort()
if status:
update_data["status"] = status
if not update_data:
error("No updates provided. Specify --specs, --pricing, or --status")
raise click.Abort()
result = http_client.put(f"/gpu/{gpu_id}", json=update_data)
success(f"GPU {gpu_id} updated successfully")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error updating GPU: {e}")

View File

@@ -12,7 +12,11 @@ from typing import Optional
import click
from ..utils import error, success
from ..utils import error, success, output
from ..config import get_config
from aitbc import get_logger, AITBCHTTPClient, NetworkError
logger = get_logger(__name__)
@click.group()
@@ -156,3 +160,66 @@ def stop(agent_id: Optional[str]):
"""Stop Hermes training"""
success(f"Stop Hermes training for agent {agent_id}")
# TODO: Implement actual stop command via coordinator API
@hermes.command()
@click.argument('message')
@click.option('--to-agent', help='Target agent ID')
@click.option('--priority', default='normal', help='Message priority')
@click.pass_context
def send(ctx, message: str, to_agent: Optional[str], priority: str):
"""Send a message via hermes service"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.hermes_service_url, timeout=10)
message_data = {
"message": message,
"priority": priority
}
if to_agent:
message_data["to_agent"] = to_agent
result = http_client.post("/hermes/send", json=message_data)
success("Message sent via hermes")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error sending message: {e}")
@hermes.command()
@click.option('--limit', type=int, default=20, help='Number of messages to return')
@click.pass_context
def receive(ctx, limit: int):
"""Receive messages from hermes service"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.hermes_service_url, timeout=10)
messages_data = http_client.get("/hermes/messages", params={"limit": limit})
success("Messages:")
output(messages_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error receiving messages: {e}")
@hermes.command()
@click.pass_context
def peers(ctx):
"""List hermes service peers"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.hermes_service_url, timeout=10)
peers_data = http_client.get("/hermes/peers")
success("Hermes Peers:")
output(peers_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching peers: {e}")

View File

@@ -13,6 +13,9 @@ from ..core.marketplace import (
)
from ..utils import output, error, success
from ..config import get_config
from aitbc import get_logger, AITBCHTTPClient, NetworkError
logger = get_logger(__name__)
@click.group()
@click.option("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)")
@@ -517,3 +520,100 @@ def monitor(ctx, realtime, interval):
except Exception as e:
error(f"Error during monitoring: {str(e)}")
raise click.Abort()
@marketplace.command()
@click.argument('price', type=float)
@click.argument('quantity', type=float)
@click.option('--market', help='Market identifier')
@click.pass_context
def bid(ctx, price: float, quantity: float, market: Optional[str]):
"""Place a bid in the marketplace"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.marketplace_service_url, timeout=10)
bid_data = {
"price": price,
"quantity": quantity,
"market": market or "default"
}
result = http_client.post("/marketplace/bid", json=bid_data)
success(f"Bid placed: {quantity} @ {price}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error placing bid: {e}")
@marketplace.command()
@click.option('--market', help='Filter by market')
@click.option('--limit', type=int, default=20, help='Number of bids to return')
@click.pass_context
def bids(ctx, market: Optional[str], limit: int):
"""List bids from the marketplace"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.marketplace_service_url, timeout=10)
params = {"limit": limit}
if market:
params["market"] = market
bids_data = http_client.get("/marketplace/bids", params=params)
success("Bids:")
output(bids_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching bids: {e}")
@marketplace.command()
@click.argument('price', type=float)
@click.argument('quantity', type=float)
@click.option('--market', help='Market identifier')
@click.pass_context
def ask(ctx, price: float, quantity: float, market: Optional[str]):
"""Place an ask in the marketplace"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.marketplace_service_url, timeout=10)
ask_data = {
"price": price,
"quantity": quantity,
"market": market or "default"
}
result = http_client.post("/marketplace/ask", json=ask_data)
success(f"Ask placed: {quantity} @ {price}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error placing ask: {e}")
@marketplace.command()
@click.option('--market', help='Filter by market')
@click.option('--limit', type=int, default=20, help='Number of asks to return')
@click.pass_context
def asks(ctx, market: Optional[str], limit: int):
"""List asks from the marketplace"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.marketplace_service_url, timeout=10)
params = {"limit": limit}
if market:
params["market"] = market
asks_data = http_client.get("/marketplace/asks", params=params)
success("Asks:")
output(asks_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching asks: {e}")

View File

@@ -10,12 +10,15 @@ from typing import Optional
import click
from ..utils import error, success
from ..utils import error, success, output
from ..utils.wallet import decrypt_private_key
from aitbc import AITBCHTTPClient, NetworkError, KEYSTORE_DIR
from ..config import get_config
from aitbc import AITBCHTTPClient, NetworkError, KEYSTORE_DIR, get_logger
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
logger = get_logger(__name__)
DEFAULT_RPC_URL = "http://localhost:8006"
DEFAULT_KEYSTORE_DIR = KEYSTORE_DIR
@@ -357,3 +360,88 @@ def message(agent: str, message: str, wallet: str, password: Optional[str], pass
except Exception as e:
error(f"Error sending message: {e}")
# Governance operations
@operations.group()
def governance():
"""Governance operations"""
pass
@governance.command()
@click.argument('proposal_id')
@click.option('--vote', type=click.Choice(['yes', 'no', 'abstain']), required=True, help='Vote option')
@click.option('--wallet', required=True, help='Wallet name for signing')
@click.pass_context
def vote(ctx, proposal_id: str, vote: str, wallet: str):
"""Vote on a governance proposal"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.governance_service_url, timeout=10)
vote_data = {
"proposal_id": proposal_id,
"vote": vote,
"wallet": wallet
}
result = http_client.post("/governance/vote", json=vote_data)
success(f"Vote '{vote}' cast for proposal {proposal_id}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error casting vote: {e}")
@governance.command()
@click.option('--title', required=True, help='Proposal title')
@click.option('--description', required=True, help='Proposal description')
@click.option('--type', default='parameter', help='Proposal type')
@click.option('--wallet', required=True, help='Wallet name for signing')
@click.pass_context
def proposal(ctx, title: str, description: str, type: str, wallet: str):
"""Create a governance proposal"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.governance_service_url, timeout=10)
proposal_data = {
"title": title,
"description": description,
"type": type,
"wallet": wallet
}
result = http_client.post("/governance/proposals", json=proposal_data)
success(f"Proposal created: {result.get('proposal_id')}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error creating proposal: {e}")
@governance.command()
@click.argument('to_address')
@click.option('--amount', type=float, required=True, help='Amount to delegate')
@click.option('--wallet', required=True, help='Wallet name for signing')
@click.pass_context
def delegate(ctx, to_address: str, amount: float, wallet: str):
"""Delegate voting power to another address"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.governance_service_url, timeout=10)
delegate_data = {
"to_address": to_address,
"amount": amount,
"wallet": wallet
}
result = http_client.post("/governance/delegate", json=delegate_data)
success(f"Delegated {amount} to {to_address}")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error delegating: {e}")

View File

@@ -8,7 +8,11 @@ from typing import Optional
import click
from ..utils import error, success
from ..utils import error, success, output
from ..config import get_config
from aitbc import get_logger, AITBCHTTPClient, NetworkError
logger = get_logger(__name__)
@click.group()
@@ -126,3 +130,50 @@ def optimize(target: str, agent_id: Optional[str], mock: bool):
click.echo("Improvement: 12.5%")
click.echo("Status: Optimized")
return 0
@resource.command()
@click.option('--resource-id', help='Specific resource ID to check')
@click.pass_context
def status(ctx, resource_id: Optional[str]):
"""Get resource allocation status from coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
if resource_id:
status_data = http_client.get(f"/api/v1/resources/{resource_id}/status")
else:
status_data = http_client.get("/api/v1/resources/status")
success("Resource Status:")
output(status_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching resource status: {e}")
@resource.command()
@click.argument('resource_id')
@click.option('--force', is_flag=True, help='Force deallocation without confirmation')
@click.pass_context
def deallocate(ctx, resource_id: str, force: bool):
"""Deallocate resources via coordinator-api"""
config = get_config()
if not force:
if not click.confirm(f"Are you sure you want to deallocate resource {resource_id}?"):
return
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
result = http_client.post(f"/api/v1/resources/{resource_id}/deallocate")
success(f"Resource {resource_id} deallocated successfully")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error deallocating resource: {e}")

View File

@@ -8,7 +8,7 @@ import click
import json
import time
import random
from typing import Dict, Any, List
from typing import Dict, Any, List, Optional
import sys
import os
@@ -26,6 +26,10 @@ except ImportError:
def get_config(config_file=None, role=None):
return {}
from aitbc import get_logger, AITBCHTTPClient, NetworkError
logger = get_logger(__name__)
@click.group()
def simulate():
@@ -338,5 +342,73 @@ def ai_jobs(jobs, models, duration_range):
click.echo(f" {model}: {count} jobs")
@simulate.command()
@click.argument('scenario')
@click.option('--params', help='Simulation parameters (JSON string)')
@click.option('--async-run', is_flag=True, help='Run simulation asynchronously')
@click.pass_context
def run(ctx, scenario: str, params: Optional[str], async_run: bool):
"""Run a simulation scenario via coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
sim_data = {
"scenario": scenario,
"async": async_run
}
if params:
try:
sim_data["params"] = json.loads(params)
except json.JSONDecodeError:
error("Invalid JSON parameters")
raise click.Abort()
result = http_client.post("/simulate/run", json=sim_data)
success(f"Simulation '{scenario}' started")
output(result, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error running simulation: {e}")
@simulate.command()
@click.argument('simulation_id')
@click.pass_context
def status(ctx, simulation_id: str):
"""Get simulation status from coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
status_data = http_client.get(f"/simulate/{simulation_id}/status")
success(f"Simulation {simulation_id} Status:")
output(status_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching simulation status: {e}")
@simulate.command()
@click.argument('simulation_id')
@click.pass_context
def result(ctx, simulation_id: str):
"""Get simulation results from coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
result_data = http_client.get(f"/simulate/{simulation_id}/result")
success(f"Simulation {simulation_id} Results:")
output(result_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching simulation results: {e}")
if __name__ == '__main__':
simulate()

View File

@@ -5,6 +5,13 @@ System commands for AITBC CLI
import click
import os
import subprocess
from pathlib import Path
from ..utils import output, error, success
from ..config import get_config
from aitbc import get_logger, AITBCHTTPClient, NetworkError
logger = get_logger(__name__)
@click.group()
def system():
@@ -50,5 +57,80 @@ def check(service):
else:
click.echo(f"{svc}: {service_file}")
@system.command()
@click.option('--service', required=True, help='Service to restart (e.g., blockchain-node, wallet)')
@click.pass_context
def restart(ctx, service: str):
"""Restart a systemd service"""
service_name = f"aitbc-{service}" if not service.startswith("aitbc-") else service
try:
success(f"Restarting service: {service_name}")
result = subprocess.run(
["sudo", "systemctl", "restart", service_name],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
success(f"Service {service_name} restarted successfully")
output({"service": service_name, "status": "restarted"}, ctx.obj.get("output_format", "table"))
else:
error(f"Failed to restart service: {result.stderr}")
except subprocess.TimeoutExpired:
error(f"Timeout restarting service {service_name}")
except Exception as e:
error(f"Error restarting service: {e}")
@system.command()
@click.pass_context
def status(ctx):
"""Get system status from coordinator-api"""
config = get_config()
try:
http_client = AITBCHTTPClient(base_url=config.coordinator_url, timeout=10)
status_data = http_client.get("/api/v1/status")
success("System Status:")
output(status_data, ctx.obj.get("output_format", "table"))
except NetworkError as e:
error(f"Network error: {e}")
except Exception as e:
error(f"Error fetching status: {e}")
@system.command()
@click.option('--show-secrets', is_flag=True, help='Show sensitive values like API keys')
@click.pass_context
def config(ctx, show_secrets: bool):
"""Display system configuration from /etc/aitbc/blockchain.env"""
config_path = Path("/etc/aitbc/blockchain.env")
if not config_path.exists():
error(f"Configuration file not found: {config_path}")
return
try:
with open(config_path, 'r') as f:
config_lines = f.readlines()
config_data = {}
for line in config_lines:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
# Hide secrets unless explicitly requested
if not show_secrets and any(secret in key.lower() for secret in ['key', 'secret', 'password', 'token']):
value = '***HIDDEN***'
config_data[key.strip()] = value.strip()
success("System Configuration:")
output(config_data, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Error reading configuration: {e}")
if __name__ == '__main__':
system()

View File

@@ -1561,3 +1561,84 @@ def fund(ctx, address: str, amount: int, chain_id: str):
error(f"HTTP error calling faucet: {e}")
except Exception as e:
error(f"Error funding wallet: {e}")
@wallet.command()
@click.option('--destination', help='Destination file path (default: wallet_name_export.json)')
@click.pass_context
def export(ctx, destination: Optional[str]):
"""Export wallet to JSON file"""
wallet_name = ctx.obj["wallet_name"]
wallet_path = ctx.obj["wallet_path"]
if not wallet_path.exists():
error(f"Wallet '{wallet_name}' not found")
return
try:
wallet_data = _load_wallet(wallet_path, wallet_name)
# Generate export filename if not provided
if not destination:
destination = f"{wallet_name}_export.json"
export_path = Path(destination)
# Write export file
with open(export_path, 'w') as f:
json.dump(wallet_data, f, indent=2)
success(f"Wallet exported to {export_path}")
output({
"wallet": wallet_name,
"exported_to": str(export_path),
"address": wallet_data.get("address"),
"balance": wallet_data.get("balance", 0)
}, ctx.obj.get("output_format", "table"))
except Exception as e:
error(f"Error exporting wallet: {e}")
@wallet.command()
@click.argument('file_path')
@click.option('--name', help='New wallet name (default: from file)')
@click.pass_context
def import_wallet(ctx, file_path: str, name: Optional[str]):
"""Import wallet from JSON file"""
wallet_dir = ctx.obj.get("wallet_dir", Path.home() / ".aitbc" / "wallets")
wallet_dir.mkdir(parents=True, exist_ok=True)
import_path = Path(file_path)
if not import_path.exists():
error(f"Import file not found: {file_path}")
return
try:
with open(import_path, 'r') as f:
wallet_data = json.load(f)
# Determine wallet name
wallet_name = name or wallet_data.get("name", import_path.stem)
wallet_path = wallet_dir / f"{wallet_name}.json"
if wallet_path.exists():
if not click.confirm(f"Wallet '{wallet_name}' already exists. Overwrite?"):
return
# Save imported wallet
with open(wallet_path, 'w') as f:
json.dump(wallet_data, f, indent=2)
success(f"Wallet imported as '{wallet_name}'")
output({
"wallet": wallet_name,
"imported_from": str(import_path),
"address": wallet_data.get("address"),
"balance": wallet_data.get("balance", 0)
}, ctx.obj.get("output_format", "table"))
except json.JSONDecodeError:
error("Invalid JSON file")
except Exception as e:
error(f"Error importing wallet: {e}")