hanzo 0.2.3__py3-none-any.whl → 0.2.5__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.

@@ -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]")
@@ -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]")