hanzo 0.2.3__py3-none-any.whl → 0.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hanzo might be problematic. Click here for more details.
- hanzo/__init__.py +4 -93
- hanzo/__main__.py +6 -0
- hanzo/cli.py +221 -8
- hanzo/commands/__init__.py +3 -0
- hanzo/commands/agent.py +112 -0
- hanzo/commands/auth.py +324 -0
- hanzo/commands/chat.py +183 -0
- hanzo/commands/cluster.py +428 -0
- hanzo/commands/config.py +240 -0
- hanzo/commands/mcp.py +235 -0
- hanzo/commands/miner.py +323 -0
- hanzo/commands/network.py +333 -0
- hanzo/commands/repl.py +186 -0
- hanzo/commands/tools.py +335 -0
- hanzo/interactive/__init__.py +3 -0
- hanzo/interactive/dashboard.py +125 -0
- hanzo/interactive/repl.py +184 -0
- hanzo/router/__init__.py +13 -7
- hanzo/utils/__init__.py +3 -0
- hanzo/utils/config.py +170 -0
- hanzo/utils/net_check.py +107 -0
- hanzo/utils/output.py +103 -0
- {hanzo-0.2.3.dist-info → hanzo-0.2.6.dist-info}/METADATA +6 -3
- hanzo-0.2.6.dist-info/RECORD +28 -0
- hanzo-0.2.3.dist-info/RECORD +0 -9
- {hanzo-0.2.3.dist-info → hanzo-0.2.6.dist-info}/WHEEL +0 -0
- {hanzo-0.2.3.dist-info → hanzo-0.2.6.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Cluster management commands."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
import httpx
|
|
8
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from ..utils.output import console, handle_errors
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group(name="cluster")
|
|
15
|
+
def cluster_group():
|
|
16
|
+
"""Manage local AI cluster."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@cluster_group.command()
|
|
21
|
+
@click.option("--name", "-n", default="hanzo-local", help="Cluster name")
|
|
22
|
+
@click.option("--port", "-p", default=8000, type=int, help="API port")
|
|
23
|
+
@click.option("--models", "-m", multiple=True, help="Models to load")
|
|
24
|
+
@click.option("--device", type=click.Choice(["cpu", "gpu", "auto"]), default="auto", help="Device to use")
|
|
25
|
+
@click.pass_context
|
|
26
|
+
async def start(ctx, name: str, port: int, models: tuple, device: str):
|
|
27
|
+
"""Start local AI cluster."""
|
|
28
|
+
await start_cluster(ctx, name, port, list(models) if models else None, device)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def start_cluster(ctx, name: str, port: int, models: Optional[List[str]] = None, device: str = "auto"):
|
|
32
|
+
"""Start a local cluster via hanzo-cluster."""
|
|
33
|
+
try:
|
|
34
|
+
from hanzo_cluster import HanzoCluster
|
|
35
|
+
except ImportError:
|
|
36
|
+
console.print("[red]Error:[/red] hanzo-cluster not installed")
|
|
37
|
+
console.print("Install with: pip install hanzo[cluster]")
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
cluster = HanzoCluster(name=name, port=port, device=device)
|
|
41
|
+
|
|
42
|
+
with Progress(
|
|
43
|
+
SpinnerColumn(),
|
|
44
|
+
TextColumn("[progress.description]{task.description}"),
|
|
45
|
+
console=console,
|
|
46
|
+
) as progress:
|
|
47
|
+
task = progress.add_task("Starting cluster...", total=None)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
await cluster.start(models=models)
|
|
51
|
+
progress.update(task, completed=True)
|
|
52
|
+
except Exception as e:
|
|
53
|
+
progress.stop()
|
|
54
|
+
console.print(f"[red]Failed to start cluster: {e}[/red]")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
console.print(f"[green]✓[/green] Cluster started at http://localhost:{port}")
|
|
58
|
+
console.print("Press Ctrl+C to stop\n")
|
|
59
|
+
|
|
60
|
+
# Show cluster info
|
|
61
|
+
info = await cluster.info()
|
|
62
|
+
console.print("[cyan]Cluster Information:[/cyan]")
|
|
63
|
+
console.print(f" Name: {info.get('name', name)}")
|
|
64
|
+
console.print(f" Port: {info.get('port', port)}")
|
|
65
|
+
console.print(f" Device: {info.get('device', device)}")
|
|
66
|
+
console.print(f" Nodes: {info.get('nodes', 1)}")
|
|
67
|
+
if models := info.get('models', models):
|
|
68
|
+
console.print(f" Models: {', '.join(models)}")
|
|
69
|
+
|
|
70
|
+
console.print("\n[dim]Logs:[/dim]")
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
# Stream logs
|
|
74
|
+
async for log in cluster.stream_logs():
|
|
75
|
+
console.print(log, end="")
|
|
76
|
+
except KeyboardInterrupt:
|
|
77
|
+
console.print("\n[yellow]Stopping cluster...[/yellow]")
|
|
78
|
+
await cluster.stop()
|
|
79
|
+
console.print("[green]✓[/green] Cluster stopped")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@cluster_group.command()
|
|
83
|
+
@click.option("--name", "-n", default="hanzo-local", help="Cluster name")
|
|
84
|
+
@click.pass_context
|
|
85
|
+
async def stop(ctx, name: str):
|
|
86
|
+
"""Stop local AI cluster."""
|
|
87
|
+
try:
|
|
88
|
+
from hanzo_cluster import HanzoCluster
|
|
89
|
+
except ImportError:
|
|
90
|
+
console.print("[red]Error:[/red] hanzo-cluster not installed")
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
cluster = HanzoCluster(name=name)
|
|
94
|
+
|
|
95
|
+
console.print("[yellow]Stopping cluster...[/yellow]")
|
|
96
|
+
try:
|
|
97
|
+
await cluster.stop()
|
|
98
|
+
console.print("[green]✓[/green] Cluster stopped")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
console.print(f"[red]Failed to stop cluster: {e}[/red]")
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@cluster_group.command()
|
|
104
|
+
@click.option("--name", "-n", default="hanzo-local", help="Cluster name")
|
|
105
|
+
@click.pass_context
|
|
106
|
+
async def status(ctx, name: str):
|
|
107
|
+
"""Show cluster status."""
|
|
108
|
+
try:
|
|
109
|
+
from hanzo_cluster import HanzoCluster
|
|
110
|
+
except ImportError:
|
|
111
|
+
console.print("[red]Error:[/red] hanzo-cluster not installed")
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
cluster = HanzoCluster(name=name)
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
status = await cluster.status()
|
|
118
|
+
|
|
119
|
+
if status.get('running'):
|
|
120
|
+
console.print("[green]✓[/green] Cluster is running")
|
|
121
|
+
|
|
122
|
+
# Show cluster info
|
|
123
|
+
console.print("\n[cyan]Cluster Information:[/cyan]")
|
|
124
|
+
console.print(f" Name: {status.get('name', name)}")
|
|
125
|
+
console.print(f" Nodes: {status.get('nodes', 0)}")
|
|
126
|
+
console.print(f" Status: {status.get('state', 'unknown')}")
|
|
127
|
+
|
|
128
|
+
# Show models
|
|
129
|
+
if models := status.get('models', []):
|
|
130
|
+
console.print("\n[cyan]Available Models:[/cyan]")
|
|
131
|
+
for model in models:
|
|
132
|
+
console.print(f" • {model}")
|
|
133
|
+
|
|
134
|
+
# Show nodes
|
|
135
|
+
if nodes := status.get('node_details', []):
|
|
136
|
+
console.print("\n[cyan]Nodes:[/cyan]")
|
|
137
|
+
for node in nodes:
|
|
138
|
+
console.print(f" • {node.get('name', 'unknown')} ({node.get('state', 'unknown')})")
|
|
139
|
+
if device := node.get('device'):
|
|
140
|
+
console.print(f" Device: {device}")
|
|
141
|
+
else:
|
|
142
|
+
console.print("[yellow]![/yellow] Cluster is not running")
|
|
143
|
+
console.print("Start with: hanzo cluster start")
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
console.print(f"[red]Error checking status: {e}[/red]")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@cluster_group.command()
|
|
150
|
+
@click.option("--name", "-n", default="hanzo-local", help="Cluster name")
|
|
151
|
+
@click.pass_context
|
|
152
|
+
async def models(ctx, name: str):
|
|
153
|
+
"""List available models."""
|
|
154
|
+
try:
|
|
155
|
+
from hanzo_cluster import HanzoCluster
|
|
156
|
+
except ImportError:
|
|
157
|
+
console.print("[red]Error:[/red] hanzo-cluster not installed")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
cluster = HanzoCluster(name=name)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
models = await cluster.list_models()
|
|
164
|
+
|
|
165
|
+
if models:
|
|
166
|
+
table = Table(title="Available Models")
|
|
167
|
+
table.add_column("Model ID", style="cyan")
|
|
168
|
+
table.add_column("Type", style="green")
|
|
169
|
+
table.add_column("Status", style="yellow")
|
|
170
|
+
table.add_column("Node", style="blue")
|
|
171
|
+
|
|
172
|
+
for model in models:
|
|
173
|
+
table.add_row(
|
|
174
|
+
model.get("id", "unknown"),
|
|
175
|
+
model.get("type", "model"),
|
|
176
|
+
model.get("status", "unknown"),
|
|
177
|
+
model.get("node", "local")
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
console.print(table)
|
|
181
|
+
else:
|
|
182
|
+
console.print("[yellow]No models loaded[/yellow]")
|
|
183
|
+
console.print("Load models with: hanzo cluster load <model>")
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@cluster_group.command()
|
|
190
|
+
@click.argument("model")
|
|
191
|
+
@click.option("--name", "-n", default="hanzo-local", help="Cluster name")
|
|
192
|
+
@click.option("--node", help="Target node (default: auto-select)")
|
|
193
|
+
@click.pass_context
|
|
194
|
+
async def load(ctx, model: str, name: str, node: str = None):
|
|
195
|
+
"""Load a model into the cluster."""
|
|
196
|
+
try:
|
|
197
|
+
from hanzo_cluster import HanzoCluster
|
|
198
|
+
except ImportError:
|
|
199
|
+
console.print("[red]Error:[/red] hanzo-cluster not installed")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
cluster = HanzoCluster(name=name)
|
|
203
|
+
|
|
204
|
+
with console.status(f"Loading model '{model}'..."):
|
|
205
|
+
try:
|
|
206
|
+
result = await cluster.load_model(model, node=node)
|
|
207
|
+
console.print(f"[green]✓[/green] Loaded model: {model}")
|
|
208
|
+
if node_name := result.get('node'):
|
|
209
|
+
console.print(f" Node: {node_name}")
|
|
210
|
+
except Exception as e:
|
|
211
|
+
console.print(f"[red]Failed to load model: {e}[/red]")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@cluster_group.command()
|
|
215
|
+
@click.argument("model")
|
|
216
|
+
@click.option("--name", "-n", default="hanzo-local", help="Cluster name")
|
|
217
|
+
@click.pass_context
|
|
218
|
+
async def unload(ctx, model: str, name: str):
|
|
219
|
+
"""Unload a model from the cluster."""
|
|
220
|
+
try:
|
|
221
|
+
from hanzo_cluster import HanzoCluster
|
|
222
|
+
except ImportError:
|
|
223
|
+
console.print("[red]Error:[/red] hanzo-cluster not installed")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
cluster = HanzoCluster(name=name)
|
|
227
|
+
|
|
228
|
+
if click.confirm(f"Unload model '{model}'?"):
|
|
229
|
+
with console.status(f"Unloading model '{model}'..."):
|
|
230
|
+
try:
|
|
231
|
+
await cluster.unload_model(model)
|
|
232
|
+
console.print(f"[green]✓[/green] Unloaded model: {model}")
|
|
233
|
+
except Exception as e:
|
|
234
|
+
console.print(f"[red]Failed to unload model: {e}[/red]")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@cluster_group.group(name="node")
|
|
238
|
+
def node_group():
|
|
239
|
+
"""Manage cluster nodes."""
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@node_group.command(name="start")
|
|
244
|
+
@click.option("--name", "-n", default="node-1", help="Node name")
|
|
245
|
+
@click.option("--cluster", "-c", default="hanzo-local", help="Cluster to join")
|
|
246
|
+
@click.option("--device", type=click.Choice(["cpu", "gpu", "auto"]), default="auto", help="Device to use")
|
|
247
|
+
@click.option("--port", "-p", type=int, help="Node port (auto-assigned if not specified)")
|
|
248
|
+
@click.option("--blockchain", is_flag=True, help="Enable blockchain features")
|
|
249
|
+
@click.option("--network", is_flag=True, help="Enable network discovery")
|
|
250
|
+
@click.pass_context
|
|
251
|
+
async def node_start(ctx, name: str, cluster: str, device: str, port: int, blockchain: bool, network: bool):
|
|
252
|
+
"""Start this machine as a node in the cluster."""
|
|
253
|
+
try:
|
|
254
|
+
from hanzo_cluster import HanzoNode
|
|
255
|
+
if blockchain or network:
|
|
256
|
+
from hanzo_network import HanzoNetwork
|
|
257
|
+
except ImportError:
|
|
258
|
+
console.print("[red]Error:[/red] Required packages not installed")
|
|
259
|
+
console.print("Install with: pip install hanzo[cluster,network]")
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
node = HanzoNode(name=name, device=device, port=port)
|
|
263
|
+
|
|
264
|
+
with Progress(
|
|
265
|
+
SpinnerColumn(),
|
|
266
|
+
TextColumn("[progress.description]{task.description}"),
|
|
267
|
+
console=console,
|
|
268
|
+
) as progress:
|
|
269
|
+
task = progress.add_task(f"Starting node '{name}'...", total=None)
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
# Start the node
|
|
273
|
+
await node.start(cluster=cluster)
|
|
274
|
+
|
|
275
|
+
# Enable blockchain/network features if requested
|
|
276
|
+
if blockchain or network:
|
|
277
|
+
network_mgr = HanzoNetwork(node=node)
|
|
278
|
+
if blockchain:
|
|
279
|
+
await network_mgr.enable_blockchain()
|
|
280
|
+
if network:
|
|
281
|
+
await network_mgr.enable_discovery()
|
|
282
|
+
|
|
283
|
+
progress.update(task, completed=True)
|
|
284
|
+
except Exception as e:
|
|
285
|
+
progress.stop()
|
|
286
|
+
console.print(f"[red]Failed to start node: {e}[/red]")
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
console.print(f"[green]✓[/green] Node '{name}' started")
|
|
290
|
+
console.print(f" Cluster: {cluster}")
|
|
291
|
+
console.print(f" Device: {device}")
|
|
292
|
+
if port:
|
|
293
|
+
console.print(f" Port: {port}")
|
|
294
|
+
if blockchain:
|
|
295
|
+
console.print(" [cyan]Blockchain enabled[/cyan]")
|
|
296
|
+
if network:
|
|
297
|
+
console.print(" [cyan]Network discovery enabled[/cyan]")
|
|
298
|
+
|
|
299
|
+
console.print("\nPress Ctrl+C to stop\n")
|
|
300
|
+
console.print("[dim]Logs:[/dim]")
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
# Stream logs
|
|
304
|
+
async for log in node.stream_logs():
|
|
305
|
+
console.print(log, end="")
|
|
306
|
+
except KeyboardInterrupt:
|
|
307
|
+
console.print("\n[yellow]Stopping node...[/yellow]")
|
|
308
|
+
await node.stop()
|
|
309
|
+
console.print("[green]✓[/green] Node stopped")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@node_group.command(name="stop")
|
|
313
|
+
@click.option("--name", "-n", help="Node name")
|
|
314
|
+
@click.option("--all", is_flag=True, help="Stop all nodes")
|
|
315
|
+
@click.pass_context
|
|
316
|
+
async def node_stop(ctx, name: str, all: bool):
|
|
317
|
+
"""Stop a node."""
|
|
318
|
+
try:
|
|
319
|
+
from hanzo_cluster import HanzoNode
|
|
320
|
+
except ImportError:
|
|
321
|
+
console.print("[red]Error:[/red] hanzo-cluster not installed")
|
|
322
|
+
return
|
|
323
|
+
|
|
324
|
+
if all:
|
|
325
|
+
if click.confirm("Stop all nodes?"):
|
|
326
|
+
console.print("[yellow]Stopping all nodes...[/yellow]")
|
|
327
|
+
try:
|
|
328
|
+
await HanzoNode.stop_all()
|
|
329
|
+
console.print("[green]✓[/green] All nodes stopped")
|
|
330
|
+
except Exception as e:
|
|
331
|
+
console.print(f"[red]Failed to stop nodes: {e}[/red]")
|
|
332
|
+
elif name:
|
|
333
|
+
node = HanzoNode(name=name)
|
|
334
|
+
console.print(f"[yellow]Stopping node '{name}'...[/yellow]")
|
|
335
|
+
try:
|
|
336
|
+
await node.stop()
|
|
337
|
+
console.print(f"[green]✓[/green] Node stopped")
|
|
338
|
+
except Exception as e:
|
|
339
|
+
console.print(f"[red]Failed to stop node: {e}[/red]")
|
|
340
|
+
else:
|
|
341
|
+
console.print("[red]Error:[/red] Specify --name or --all")
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
@node_group.command(name="list")
|
|
345
|
+
@click.option("--cluster", "-c", help="Filter by cluster")
|
|
346
|
+
@click.pass_context
|
|
347
|
+
async def node_list(ctx, cluster: str):
|
|
348
|
+
"""List all nodes."""
|
|
349
|
+
try:
|
|
350
|
+
from hanzo_cluster import HanzoNode
|
|
351
|
+
except ImportError:
|
|
352
|
+
console.print("[red]Error:[/red] hanzo-cluster not installed")
|
|
353
|
+
return
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
nodes = await HanzoNode.list_nodes(cluster=cluster)
|
|
357
|
+
|
|
358
|
+
if nodes:
|
|
359
|
+
table = Table(title="Cluster Nodes")
|
|
360
|
+
table.add_column("Name", style="cyan")
|
|
361
|
+
table.add_column("Cluster", style="green")
|
|
362
|
+
table.add_column("Device", style="yellow")
|
|
363
|
+
table.add_column("Status", style="blue")
|
|
364
|
+
table.add_column("Models", style="magenta")
|
|
365
|
+
|
|
366
|
+
for node in nodes:
|
|
367
|
+
table.add_row(
|
|
368
|
+
node.get("name", "unknown"),
|
|
369
|
+
node.get("cluster", "unknown"),
|
|
370
|
+
node.get("device", "unknown"),
|
|
371
|
+
node.get("status", "unknown"),
|
|
372
|
+
str(len(node.get("models", [])))
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
console.print(table)
|
|
376
|
+
else:
|
|
377
|
+
console.print("[yellow]No nodes found[/yellow]")
|
|
378
|
+
console.print("Start a node with: hanzo cluster node start")
|
|
379
|
+
|
|
380
|
+
except Exception as e:
|
|
381
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
@node_group.command(name="info")
|
|
385
|
+
@click.argument("name")
|
|
386
|
+
@click.pass_context
|
|
387
|
+
async def node_info(ctx, name: str):
|
|
388
|
+
"""Show detailed node information."""
|
|
389
|
+
try:
|
|
390
|
+
from hanzo_cluster import HanzoNode
|
|
391
|
+
except ImportError:
|
|
392
|
+
console.print("[red]Error:[/red] hanzo-cluster not installed")
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
node = HanzoNode(name=name)
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
info = await node.info()
|
|
399
|
+
|
|
400
|
+
console.print(f"[cyan]Node: {name}[/cyan]")
|
|
401
|
+
console.print(f" Cluster: {info.get('cluster', 'unknown')}")
|
|
402
|
+
console.print(f" Status: {info.get('status', 'unknown')}")
|
|
403
|
+
console.print(f" Device: {info.get('device', 'unknown')}")
|
|
404
|
+
|
|
405
|
+
if uptime := info.get('uptime'):
|
|
406
|
+
console.print(f" Uptime: {uptime}")
|
|
407
|
+
|
|
408
|
+
if resources := info.get('resources'):
|
|
409
|
+
console.print("\n[cyan]Resources:[/cyan]")
|
|
410
|
+
console.print(f" CPU: {resources.get('cpu_percent', 'N/A')}%")
|
|
411
|
+
console.print(f" Memory: {resources.get('memory_used', 'N/A')} / {resources.get('memory_total', 'N/A')}")
|
|
412
|
+
if gpu := resources.get('gpu'):
|
|
413
|
+
console.print(f" GPU: {gpu.get('name', 'N/A')} ({gpu.get('memory_used', 'N/A')} / {gpu.get('memory_total', 'N/A')})")
|
|
414
|
+
|
|
415
|
+
if models := info.get('models'):
|
|
416
|
+
console.print("\n[cyan]Loaded Models:[/cyan]")
|
|
417
|
+
for model in models:
|
|
418
|
+
console.print(f" • {model}")
|
|
419
|
+
|
|
420
|
+
if network := info.get('network'):
|
|
421
|
+
console.print("\n[cyan]Network:[/cyan]")
|
|
422
|
+
console.print(f" Blockchain: {'enabled' if network.get('blockchain') else 'disabled'}")
|
|
423
|
+
console.print(f" Discovery: {'enabled' if network.get('discovery') else 'disabled'}")
|
|
424
|
+
if peers := network.get('peers'):
|
|
425
|
+
console.print(f" Peers: {len(peers)}")
|
|
426
|
+
|
|
427
|
+
except Exception as e:
|
|
428
|
+
console.print(f"[red]Error: {e}[/red]")
|
hanzo/commands/config.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Configuration management commands."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import yaml
|
|
9
|
+
from rich.syntax import Syntax
|
|
10
|
+
|
|
11
|
+
from ..utils.output import console, handle_errors
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group(name="config")
|
|
15
|
+
def config_group():
|
|
16
|
+
"""Manage Hanzo configuration."""
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@config_group.command()
|
|
21
|
+
@click.option("--global", "is_global", is_flag=True, help="Show global config")
|
|
22
|
+
@click.option("--local", "is_local", is_flag=True, help="Show local config")
|
|
23
|
+
@click.option("--system", is_flag=True, help="Show system config")
|
|
24
|
+
@click.pass_context
|
|
25
|
+
def show(ctx, is_global: bool, is_local: bool, system: bool):
|
|
26
|
+
"""Show configuration."""
|
|
27
|
+
from ..utils.config import load_config, get_config_paths
|
|
28
|
+
|
|
29
|
+
# Determine which configs to show
|
|
30
|
+
show_all = not (is_global or is_local or system)
|
|
31
|
+
|
|
32
|
+
configs = {}
|
|
33
|
+
paths = get_config_paths()
|
|
34
|
+
|
|
35
|
+
if show_all or system:
|
|
36
|
+
if paths["system"].exists():
|
|
37
|
+
configs["System"] = (paths["system"], load_config(paths["system"]))
|
|
38
|
+
|
|
39
|
+
if show_all or is_global:
|
|
40
|
+
if paths["global"].exists():
|
|
41
|
+
configs["Global"] = (paths["global"], load_config(paths["global"]))
|
|
42
|
+
|
|
43
|
+
if show_all or is_local:
|
|
44
|
+
if paths["local"].exists():
|
|
45
|
+
configs["Local"] = (paths["local"], load_config(paths["local"]))
|
|
46
|
+
|
|
47
|
+
# Merge and show
|
|
48
|
+
if configs:
|
|
49
|
+
for name, (path, config) in configs.items():
|
|
50
|
+
console.print(f"[cyan]{name} Config:[/cyan] {path}")
|
|
51
|
+
|
|
52
|
+
# Pretty print config
|
|
53
|
+
if config:
|
|
54
|
+
syntax = Syntax(
|
|
55
|
+
yaml.dump(config, default_flow_style=False),
|
|
56
|
+
"yaml",
|
|
57
|
+
theme="monokai",
|
|
58
|
+
line_numbers=False
|
|
59
|
+
)
|
|
60
|
+
console.print(syntax)
|
|
61
|
+
else:
|
|
62
|
+
console.print("[dim]Empty[/dim]")
|
|
63
|
+
console.print()
|
|
64
|
+
else:
|
|
65
|
+
console.print("[yellow]No configuration found[/yellow]")
|
|
66
|
+
console.print("Create one with: hanzo config set <key> <value>")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@config_group.command()
|
|
70
|
+
@click.argument("key")
|
|
71
|
+
@click.argument("value")
|
|
72
|
+
@click.option("--global", "is_global", is_flag=True, help="Set in global config")
|
|
73
|
+
@click.option("--local", "is_local", is_flag=True, help="Set in local config")
|
|
74
|
+
@click.pass_context
|
|
75
|
+
def set(ctx, key: str, value: str, is_global: bool, is_local: bool):
|
|
76
|
+
"""Set configuration value."""
|
|
77
|
+
from ..utils.config import load_config, save_config, get_config_paths
|
|
78
|
+
|
|
79
|
+
# Determine target config
|
|
80
|
+
paths = get_config_paths()
|
|
81
|
+
|
|
82
|
+
if is_local:
|
|
83
|
+
config_path = paths["local"]
|
|
84
|
+
config_name = "local"
|
|
85
|
+
else: # Default to global
|
|
86
|
+
config_path = paths["global"]
|
|
87
|
+
config_name = "global"
|
|
88
|
+
|
|
89
|
+
# Load existing config
|
|
90
|
+
config = load_config(config_path) if config_path.exists() else {}
|
|
91
|
+
|
|
92
|
+
# Parse value
|
|
93
|
+
try:
|
|
94
|
+
# Try to parse as JSON first
|
|
95
|
+
parsed_value = json.loads(value)
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
# Check for boolean strings
|
|
98
|
+
if value.lower() == "true":
|
|
99
|
+
parsed_value = True
|
|
100
|
+
elif value.lower() == "false":
|
|
101
|
+
parsed_value = False
|
|
102
|
+
else:
|
|
103
|
+
# Keep as string
|
|
104
|
+
parsed_value = value
|
|
105
|
+
|
|
106
|
+
# Set value (support nested keys with dot notation)
|
|
107
|
+
keys = key.split(".")
|
|
108
|
+
current = config
|
|
109
|
+
|
|
110
|
+
for k in keys[:-1]:
|
|
111
|
+
if k not in current:
|
|
112
|
+
current[k] = {}
|
|
113
|
+
current = current[k]
|
|
114
|
+
|
|
115
|
+
current[keys[-1]] = parsed_value
|
|
116
|
+
|
|
117
|
+
# Save config
|
|
118
|
+
save_config(config_path, config)
|
|
119
|
+
|
|
120
|
+
console.print(f"[green]✓[/green] Set {key} = {parsed_value} in {config_name} config")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@config_group.command()
|
|
124
|
+
@click.argument("key")
|
|
125
|
+
@click.option("--global", "is_global", is_flag=True, help="Get from global config")
|
|
126
|
+
@click.option("--local", "is_local", is_flag=True, help="Get from local config")
|
|
127
|
+
@click.pass_context
|
|
128
|
+
def get(ctx, key: str, is_global: bool, is_local: bool):
|
|
129
|
+
"""Get configuration value."""
|
|
130
|
+
from ..utils.config import get_config_value
|
|
131
|
+
|
|
132
|
+
scope = "local" if is_local else ("global" if is_global else None)
|
|
133
|
+
value = get_config_value(key, scope=scope)
|
|
134
|
+
|
|
135
|
+
if value is not None:
|
|
136
|
+
if isinstance(value, (dict, list)):
|
|
137
|
+
console.print_json(data=value)
|
|
138
|
+
else:
|
|
139
|
+
console.print(value)
|
|
140
|
+
else:
|
|
141
|
+
console.print(f"[yellow]Key not found: {key}[/yellow]")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@config_group.command()
|
|
145
|
+
@click.argument("key")
|
|
146
|
+
@click.option("--global", "is_global", is_flag=True, help="Unset from global config")
|
|
147
|
+
@click.option("--local", "is_local", is_flag=True, help="Unset from local config")
|
|
148
|
+
@click.pass_context
|
|
149
|
+
def unset(ctx, key: str, is_global: bool, is_local: bool):
|
|
150
|
+
"""Unset configuration value."""
|
|
151
|
+
from ..utils.config import load_config, save_config, get_config_paths
|
|
152
|
+
|
|
153
|
+
# Determine target config
|
|
154
|
+
paths = get_config_paths()
|
|
155
|
+
|
|
156
|
+
if is_local:
|
|
157
|
+
config_path = paths["local"]
|
|
158
|
+
config_name = "local"
|
|
159
|
+
else: # Default to global
|
|
160
|
+
config_path = paths["global"]
|
|
161
|
+
config_name = "global"
|
|
162
|
+
|
|
163
|
+
if not config_path.exists():
|
|
164
|
+
console.print(f"[yellow]No {config_name} config found[/yellow]")
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Load config
|
|
168
|
+
config = load_config(config_path)
|
|
169
|
+
|
|
170
|
+
# Remove value (support nested keys)
|
|
171
|
+
keys = key.split(".")
|
|
172
|
+
current = config
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
for k in keys[:-1]:
|
|
176
|
+
current = current[k]
|
|
177
|
+
|
|
178
|
+
if keys[-1] in current:
|
|
179
|
+
del current[keys[-1]]
|
|
180
|
+
save_config(config_path, config)
|
|
181
|
+
console.print(f"[green]✓[/green] Unset {key} from {config_name} config")
|
|
182
|
+
else:
|
|
183
|
+
console.print(f"[yellow]Key not found: {key}[/yellow]")
|
|
184
|
+
except KeyError:
|
|
185
|
+
console.print(f"[yellow]Key not found: {key}[/yellow]")
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@config_group.command()
|
|
189
|
+
@click.option("--system", is_flag=True, help="Edit system config")
|
|
190
|
+
@click.option("--global", "is_global", is_flag=True, help="Edit global config")
|
|
191
|
+
@click.option("--local", "is_local", is_flag=True, help="Edit local config")
|
|
192
|
+
@click.pass_context
|
|
193
|
+
def edit(ctx, system: bool, is_global: bool, is_local: bool):
|
|
194
|
+
"""Edit configuration file in editor."""
|
|
195
|
+
import os
|
|
196
|
+
import subprocess
|
|
197
|
+
from ..utils.config import get_config_paths
|
|
198
|
+
|
|
199
|
+
# Determine which config to edit
|
|
200
|
+
paths = get_config_paths()
|
|
201
|
+
|
|
202
|
+
if system:
|
|
203
|
+
config_path = paths["system"]
|
|
204
|
+
elif is_local:
|
|
205
|
+
config_path = paths["local"]
|
|
206
|
+
else: # Default to global
|
|
207
|
+
config_path = paths["global"]
|
|
208
|
+
|
|
209
|
+
# Ensure file exists
|
|
210
|
+
if not config_path.exists():
|
|
211
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
212
|
+
config_path.write_text("# Hanzo configuration\n")
|
|
213
|
+
|
|
214
|
+
# Get editor
|
|
215
|
+
editor = os.environ.get("EDITOR", "vi")
|
|
216
|
+
|
|
217
|
+
# Open in editor
|
|
218
|
+
try:
|
|
219
|
+
subprocess.run([editor, str(config_path)], check=True)
|
|
220
|
+
console.print(f"[green]✓[/green] Edited {config_path}")
|
|
221
|
+
except subprocess.CalledProcessError:
|
|
222
|
+
console.print(f"[red]Failed to open editor[/red]")
|
|
223
|
+
except FileNotFoundError:
|
|
224
|
+
console.print(f"[red]Editor not found: {editor}[/red]")
|
|
225
|
+
console.print("Set EDITOR environment variable to specify editor")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@config_group.command()
|
|
229
|
+
@click.pass_context
|
|
230
|
+
def init(ctx):
|
|
231
|
+
"""Initialize configuration."""
|
|
232
|
+
from ..utils.config import init_config
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
paths = init_config()
|
|
236
|
+
console.print("[green]✓[/green] Initialized configuration")
|
|
237
|
+
console.print(f" Global: {paths['global']}")
|
|
238
|
+
console.print(f" Local: {paths.get('local', 'Not in project')}")
|
|
239
|
+
except Exception as e:
|
|
240
|
+
console.print(f"[red]Failed to initialize config: {e}[/red]")
|