From 51209844c975d0b5200ad33bc01da28590b47a29 Mon Sep 17 00:00:00 2001 From: aitbc Date: Tue, 26 May 2026 22:03:58 +0200 Subject: [PATCH] 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. --- cli/aitbc_cli/commands/agent_sdk.py | 88 +++++++++++++++++++ cli/aitbc_cli/commands/edge.py | 64 +++++++++++++- cli/aitbc_cli/commands/exchange.py | 89 ++++++++++++++++++- cli/aitbc_cli/commands/gpu_marketplace.py | 83 ++++++++++++++++++ cli/aitbc_cli/commands/hermes.py | 69 ++++++++++++++- cli/aitbc_cli/commands/marketplace_cmd.py | 100 ++++++++++++++++++++++ cli/aitbc_cli/commands/operations.py | 92 +++++++++++++++++++- cli/aitbc_cli/commands/resource.py | 53 +++++++++++- cli/aitbc_cli/commands/simulate.py | 74 +++++++++++++++- cli/aitbc_cli/commands/system.py | 82 ++++++++++++++++++ cli/aitbc_cli/commands/wallet.py | 81 ++++++++++++++++++ 11 files changed, 865 insertions(+), 10 deletions(-) diff --git a/cli/aitbc_cli/commands/agent_sdk.py b/cli/aitbc_cli/commands/agent_sdk.py index 292dea5c..f972a070 100644 --- a/cli/aitbc_cli/commands/agent_sdk.py +++ b/cli/aitbc_cli/commands/agent_sdk.py @@ -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 diff --git a/cli/aitbc_cli/commands/edge.py b/cli/aitbc_cli/commands/edge.py index a8fe00c3..7eca1d65 100644 --- a/cli/aitbc_cli/commands/edge.py +++ b/cli/aitbc_cli/commands/edge.py @@ -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() diff --git a/cli/aitbc_cli/commands/exchange.py b/cli/aitbc_cli/commands/exchange.py index 617b1e3b..6c8336ed 100755 --- a/cli/aitbc_cli/commands/exchange.py +++ b/cli/aitbc_cli/commands/exchange.py @@ -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() diff --git a/cli/aitbc_cli/commands/gpu_marketplace.py b/cli/aitbc_cli/commands/gpu_marketplace.py index dbc9618e..a071a099 100644 --- a/cli/aitbc_cli/commands/gpu_marketplace.py +++ b/cli/aitbc_cli/commands/gpu_marketplace.py @@ -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}") + diff --git a/cli/aitbc_cli/commands/hermes.py b/cli/aitbc_cli/commands/hermes.py index f016b926..5da76ba2 100644 --- a/cli/aitbc_cli/commands/hermes.py +++ b/cli/aitbc_cli/commands/hermes.py @@ -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}") + diff --git a/cli/aitbc_cli/commands/marketplace_cmd.py b/cli/aitbc_cli/commands/marketplace_cmd.py index 900b350e..9951f29c 100755 --- a/cli/aitbc_cli/commands/marketplace_cmd.py +++ b/cli/aitbc_cli/commands/marketplace_cmd.py @@ -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}") + diff --git a/cli/aitbc_cli/commands/operations.py b/cli/aitbc_cli/commands/operations.py index b602e60d..67d3b4fc 100644 --- a/cli/aitbc_cli/commands/operations.py +++ b/cli/aitbc_cli/commands/operations.py @@ -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}") + + diff --git a/cli/aitbc_cli/commands/resource.py b/cli/aitbc_cli/commands/resource.py index 60b63802..4b09b753 100644 --- a/cli/aitbc_cli/commands/resource.py +++ b/cli/aitbc_cli/commands/resource.py @@ -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}") + diff --git a/cli/aitbc_cli/commands/simulate.py b/cli/aitbc_cli/commands/simulate.py index e5bb2ae1..d1313510 100644 --- a/cli/aitbc_cli/commands/simulate.py +++ b/cli/aitbc_cli/commands/simulate.py @@ -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() diff --git a/cli/aitbc_cli/commands/system.py b/cli/aitbc_cli/commands/system.py index 51bb0e89..220a19ad 100644 --- a/cli/aitbc_cli/commands/system.py +++ b/cli/aitbc_cli/commands/system.py @@ -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() diff --git a/cli/aitbc_cli/commands/wallet.py b/cli/aitbc_cli/commands/wallet.py index ae69d236..8ce07b0c 100644 --- a/cli/aitbc_cli/commands/wallet.py +++ b/cli/aitbc_cli/commands/wallet.py @@ -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}") +