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 ComputeConsumer = None
AITBCAgent = 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: def get_agent_config_dir() -> Path:
"""Get the agent configuration directory""" """Get the agent configuration directory"""
@@ -602,6 +608,88 @@ try:
error(f"Error exporting configuration: {str(e)}") error(f"Error exporting configuration: {str(e)}")
raise click.Abort() 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: except ImportError:
# Click not available, commands will be added programmatically # Click not available, commands will be added programmatically
pass pass

View File

@@ -8,9 +8,10 @@ import httpx
from typing import Optional from typing import Optional
from ..utils import output, error, success, info, warning from ..utils import output, error, success, info, warning
from ..config import get_config from ..config import get_config
from aitbc import get_logger, AITBCHTTPClient, NetworkError
# Initialize logger # Initialize logger
logger = None logger = get_logger(__name__)
@click.group() @click.group()
@@ -19,6 +20,67 @@ def edge():
pass 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(): def get_edge_client():
"""Get Edge API HTTP client""" """Get Edge API HTTP client"""
config = get_config() config = get_config()

View File

@@ -878,10 +878,91 @@ def pairs(ctx, exchange: str):
if len(base_currencies[base]) > 10: if len(base_currencies[base]) > 10:
success(f" ... and {len(base_currencies[base]) - 10} more") 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: 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() @exchange.command()

View File

@@ -670,3 +670,86 @@ def providers(ctx):
except Exception as e: except Exception as e:
error(f"Error querying GPU providers: {str(e)}") error(f"Error querying GPU providers: {str(e)}")
raise click.Abort() 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 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() @click.group()
@@ -156,3 +160,66 @@ def stop(agent_id: Optional[str]):
"""Stop Hermes training""" """Stop Hermes training"""
success(f"Stop Hermes training for agent {agent_id}") success(f"Stop Hermes training for agent {agent_id}")
# TODO: Implement actual stop command via coordinator API # 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 ..utils import output, error, success
from ..config import get_config from ..config import get_config
from aitbc import get_logger, AITBCHTTPClient, NetworkError
logger = get_logger(__name__)
@click.group() @click.group()
@click.option("--chain-id", help="Chain ID for multichain operations (e.g., ait-mainnet, ait-devnet)") @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: except Exception as e:
error(f"Error during monitoring: {str(e)}") error(f"Error during monitoring: {str(e)}")
raise click.Abort() 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 import click
from ..utils import error, success from ..utils import error, success, output
from ..utils.wallet import decrypt_private_key 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.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
logger = get_logger(__name__)
DEFAULT_RPC_URL = "http://localhost:8006" DEFAULT_RPC_URL = "http://localhost:8006"
DEFAULT_KEYSTORE_DIR = KEYSTORE_DIR 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: except Exception as e:
error(f"Error sending message: {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 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() @click.group()
@@ -126,3 +130,50 @@ def optimize(target: str, agent_id: Optional[str], mock: bool):
click.echo("Improvement: 12.5%") click.echo("Improvement: 12.5%")
click.echo("Status: Optimized") click.echo("Status: Optimized")
return 0 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 json
import time import time
import random import random
from typing import Dict, Any, List from typing import Dict, Any, List, Optional
import sys import sys
import os import os
@@ -26,6 +26,10 @@ except ImportError:
def get_config(config_file=None, role=None): def get_config(config_file=None, role=None):
return {} return {}
from aitbc import get_logger, AITBCHTTPClient, NetworkError
logger = get_logger(__name__)
@click.group() @click.group()
def simulate(): def simulate():
@@ -338,5 +342,73 @@ def ai_jobs(jobs, models, duration_range):
click.echo(f" {model}: {count} jobs") 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__': if __name__ == '__main__':
simulate() simulate()

View File

@@ -5,6 +5,13 @@ System commands for AITBC CLI
import click import click
import os 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() @click.group()
def system(): def system():
@@ -50,5 +57,80 @@ def check(service):
else: else:
click.echo(f"{svc}: {service_file}") 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__': if __name__ == '__main__':
system() system()

View File

@@ -1561,3 +1561,84 @@ def fund(ctx, address: str, amount: int, chain_id: str):
error(f"HTTP error calling faucet: {e}") error(f"HTTP error calling faucet: {e}")
except Exception as e: except Exception as e:
error(f"Error funding wallet: {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}")