manta-node 0.5b0__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.
Files changed (43) hide show
  1. manta_node/__init__.py +5 -0
  2. manta_node/__main__.py +57 -0
  3. manta_node/cli/__init__.py +6 -0
  4. manta_node/cli/commands/__init__.py +21 -0
  5. manta_node/cli/commands/cluster.py +419 -0
  6. manta_node/cli/commands/config.py +490 -0
  7. manta_node/cli/commands/logs.py +168 -0
  8. manta_node/cli/commands/start.py +459 -0
  9. manta_node/cli/commands/status.py +204 -0
  10. manta_node/cli/commands/stop.py +253 -0
  11. manta_node/cli/config_manager.py +139 -0
  12. manta_node/cli/main.py +133 -0
  13. manta_node/cli/version.py +106 -0
  14. manta_node/domain/__init__.py +8 -0
  15. manta_node/domain/task_lifecycle.py +90 -0
  16. manta_node/infrastructure/__init__.py +13 -0
  17. manta_node/infrastructure/config/__init__.py +31 -0
  18. manta_node/infrastructure/config/node_config_manager.py +918 -0
  19. manta_node/infrastructure/container/__init__.py +11 -0
  20. manta_node/infrastructure/container/docker_adapter.py +253 -0
  21. manta_node/infrastructure/container/image_executor.py +222 -0
  22. manta_node/infrastructure/container/manager.py +549 -0
  23. manta_node/infrastructure/filesystem/__init__.py +7 -0
  24. manta_node/infrastructure/filesystem/dataset_manager.py +469 -0
  25. manta_node/infrastructure/grpc/__init__.py +11 -0
  26. manta_node/infrastructure/grpc/client.py +373 -0
  27. manta_node/infrastructure/grpc/local_servicer.py +151 -0
  28. manta_node/infrastructure/grpc/world_servicer.py +284 -0
  29. manta_node/infrastructure/metrics/__init__.py +10 -0
  30. manta_node/infrastructure/metrics/collector.py +94 -0
  31. manta_node/infrastructure/metrics/metrics_collector.py +351 -0
  32. manta_node/infrastructure/mqtt/__init__.py +7 -0
  33. manta_node/infrastructure/mqtt/command_handler.py +450 -0
  34. manta_node/node_orchestrator.py +462 -0
  35. manta_node/task_manager.py +677 -0
  36. manta_node/tasks.py +273 -0
  37. manta_node/utils.py +52 -0
  38. manta_node-0.5b0.dist-info/METADATA +794 -0
  39. manta_node-0.5b0.dist-info/RECORD +43 -0
  40. manta_node-0.5b0.dist-info/WHEEL +5 -0
  41. manta_node-0.5b0.dist-info/entry_points.txt +2 -0
  42. manta_node-0.5b0.dist-info/licenses/LICENSE +683 -0
  43. manta_node-0.5b0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,490 @@
1
+ """Node configuration management command implementation."""
2
+
3
+ import argparse
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+ from typing import List
9
+
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.prompt import Confirm
13
+ from rich.table import Table
14
+
15
+ from ...infrastructure.config import (
16
+ NodeConfigManager,
17
+ NodeConfiguration,
18
+ )
19
+
20
+ console = Console()
21
+
22
+
23
+ def print_error(message: str):
24
+ """Print error message in red."""
25
+ console.print(f"[red]Error: {message}[/red]")
26
+
27
+
28
+ def print_success(message: str):
29
+ """Print success message in green."""
30
+ console.print(f"[green]{message}[/green]")
31
+
32
+
33
+ def print_warning(message: str):
34
+ """Print warning message in yellow."""
35
+ console.print(f"[yellow]{message}[/yellow]")
36
+
37
+
38
+ def init_config_from_file(config_file_path: str) -> int:
39
+ """Create configuration from a YAML or TOML file.
40
+
41
+ Args:
42
+ config_file_path: Path to the configuration file (YAML or TOML)
43
+
44
+ Returns:
45
+ Exit code (0 for success, non-zero for error)
46
+ """
47
+ import yaml
48
+
49
+ try:
50
+ import tomllib
51
+ except ImportError:
52
+ import tomli as tomllib
53
+
54
+ console.print(
55
+ Panel(
56
+ "[bold cyan]Node Configuration Setup[/bold cyan]\n\n"
57
+ "Loading configuration from file...\n"
58
+ "Configuration will be saved in ~/.manta/nodes/",
59
+ title="manta_node Configuration",
60
+ )
61
+ )
62
+
63
+ try:
64
+ # Validate input file exists
65
+ source_path = Path(config_file_path).expanduser().absolute()
66
+ if not source_path.exists():
67
+ print_error(f"Configuration file not found: {source_path}")
68
+ return 1
69
+
70
+ # Extract config name from filename (without extension)
71
+ config_name = source_path.stem
72
+
73
+ # Determine file format from extension
74
+ file_ext = source_path.suffix.lower()
75
+ if file_ext not in [".yaml", ".yml", ".toml"]:
76
+ print_error(f"Unsupported file format: {file_ext}")
77
+ console.print("Supported formats: .yaml, .yml, .toml")
78
+ return 1
79
+
80
+ console.print(f"Loading configuration from: [cyan]{source_path}[/cyan]")
81
+ console.print(f"Configuration name: [cyan]{config_name}[/cyan]")
82
+
83
+ # Load configuration data based on file format
84
+ try:
85
+ with open(source_path, "rb") as f:
86
+ if file_ext in [".yaml", ".yml"]:
87
+ config_data = yaml.safe_load(f)
88
+ else: # .toml
89
+ config_data = tomllib.load(f)
90
+ except yaml.YAMLError as e:
91
+ print_error(f"Invalid YAML format: {e}")
92
+ return 1
93
+ except Exception as e:
94
+ print_error(f"Failed to parse configuration file: {e}")
95
+ return 1
96
+
97
+ if not isinstance(config_data, dict):
98
+ print_error(
99
+ "Configuration file must contain a dictionary/object at the root level"
100
+ )
101
+ return 1
102
+
103
+ # Create the target directory if it doesn't exist
104
+ nodes_dir = Path.home() / ".manta" / "nodes"
105
+ nodes_dir.mkdir(parents=True, exist_ok=True)
106
+ console.print(f"[green]✓[/green] Configuration directory ready: {nodes_dir}")
107
+
108
+ # Check if configuration already exists
109
+ target_path = nodes_dir / f"{config_name}.toml"
110
+ if target_path.exists():
111
+ print_warning(
112
+ f"Configuration '{config_name}' already exists at: {target_path}"
113
+ )
114
+ overwrite = Confirm.ask(
115
+ "Overwrite existing configuration?",
116
+ default=False,
117
+ )
118
+ if not overwrite:
119
+ console.print("[yellow]Configuration creation cancelled[/yellow]")
120
+ return 1
121
+
122
+ # Create NodeConfiguration from the loaded data
123
+ try:
124
+ config = NodeConfiguration.from_dict(config_data)
125
+ except Exception as e:
126
+ print_error(f"Invalid configuration structure: {e}")
127
+ console.print(
128
+ "\n[dim]Please ensure your configuration file contains all required fields.[/dim]"
129
+ )
130
+ console.print("[dim]Required: identity.secured_token[/dim]")
131
+ return 1
132
+
133
+ # Update metadata with the config name if not already set
134
+ if not config.metadata.name or config.metadata.name == "default":
135
+ config.metadata.name = config_name
136
+
137
+ # Update logging filename based on config name
138
+ if config.identity.alias:
139
+ node_name = config.identity.alias
140
+ elif config.metadata.name and config.metadata.name != "default":
141
+ node_name = config.metadata.name
142
+ else:
143
+ node_name = "node"
144
+
145
+ # Clean the name to be filesystem-safe
146
+ safe_name = "".join(c if c.isalnum() or c in "-_" else "_" for c in node_name)
147
+ # Set to centralized logs directory with nodes subfolder
148
+ config.logging.filename = str(
149
+ Path.home() / ".manta" / "logs" / "nodes" / f"{safe_name}.log"
150
+ )
151
+
152
+ # Save configuration as TOML
153
+ config_manager = NodeConfigManager(target_path)
154
+ config_manager._config = config
155
+ config_manager.save()
156
+
157
+ console.print(
158
+ f"\n[green]✓[/green] Configuration '{config_name}' created successfully!"
159
+ )
160
+ console.print(f"[dim]Saved to: {target_path}[/dim]")
161
+
162
+ # Validate configuration
163
+ issues = config_manager.validate()
164
+ if issues:
165
+ print_warning("Configuration has validation issues:")
166
+ for issue in issues:
167
+ console.print(f" - {issue}")
168
+ else:
169
+ console.print("[green]✓[/green] Configuration is valid")
170
+
171
+ console.print("\nYou can now start a node with:")
172
+ console.print(f" [cyan]manta_node start {config_name}[/cyan]")
173
+
174
+ return 0
175
+
176
+ except KeyboardInterrupt:
177
+ console.print("\n[yellow]Configuration creation cancelled[/yellow]")
178
+ return 1
179
+ except Exception as e:
180
+ print_error(f"Failed to create configuration: {e}")
181
+ import traceback
182
+
183
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
184
+ return 1
185
+
186
+
187
+ def list_configs() -> int:
188
+ """List all available configurations."""
189
+ config_manager = NodeConfigManager()
190
+ configs = config_manager.list_available_configs()
191
+
192
+ if not configs:
193
+ console.print("No configurations found.")
194
+ console.print("")
195
+ console.print("Create a configuration with:")
196
+ console.print(" manta_node config init <path/to/config.yaml>")
197
+ return 0
198
+
199
+ table = Table(title="Available Node Configurations")
200
+ table.add_column("Name", style="cyan")
201
+ table.add_column("Path", style="dim")
202
+ table.add_column("Status", style="magenta")
203
+
204
+ for config_name in configs:
205
+ config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
206
+
207
+ # Try to load and validate config
208
+ try:
209
+ temp_manager = NodeConfigManager(config_path)
210
+ issues = temp_manager.validate()
211
+ status = (
212
+ "[green]Valid[/green]"
213
+ if not issues
214
+ else f"[yellow]{len(issues)} issues[/yellow]"
215
+ )
216
+ except Exception:
217
+ status = "[red]Invalid[/red]"
218
+
219
+ table.add_row(config_name, str(config_path), status)
220
+
221
+ console.print(table)
222
+ return 0
223
+
224
+
225
+ def show_config(config_name: str) -> int:
226
+ """Show details of a specific configuration."""
227
+ config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
228
+
229
+ if not config_path.exists():
230
+ print_error(f"Configuration '{config_name}' not found")
231
+ console.print("Available configurations:")
232
+ config_manager = NodeConfigManager()
233
+ for cfg in config_manager.list_available_configs():
234
+ console.print(f" - {cfg}")
235
+ return 1
236
+
237
+ try:
238
+ config_manager = NodeConfigManager(config_path)
239
+ config = config_manager.config
240
+
241
+ console.print(f"\n[bold]Configuration: {config_name}[/bold]")
242
+ console.print(f"Path: [dim]{config_path}[/dim]")
243
+
244
+ # Show identity
245
+ console.print("\n[bold cyan]Identity:[/bold cyan]")
246
+ console.print(f" Alias: {config.identity.alias or 'Auto-generated'}")
247
+ console.print(f" Random ID: {config.identity.random_id}")
248
+ console.print(
249
+ f" Secured Token: {'Set' if config.identity.secured_token else 'Not set'}"
250
+ )
251
+
252
+ # Show network
253
+ console.print("\n[bold cyan]Network:[/bold cyan]")
254
+ console.print(
255
+ f" Manager: {config.network.manager_host}:{config.network.manager_port}"
256
+ )
257
+ # Don't show light service configuration by default (simplify for users)
258
+
259
+ # Show datasets if configured
260
+ if config.datasets.mappings:
261
+ console.print("\n[bold cyan]Datasets:[/bold cyan]")
262
+ for name, path in config.datasets.mappings.items():
263
+ console.print(f" {name}: {path}")
264
+
265
+ # Show metadata
266
+ console.print("\n[bold cyan]Metadata:[/bold cyan]")
267
+ console.print(f" Name: {config.metadata.name}")
268
+ console.print(f" Description: {config.metadata.description}")
269
+ console.print(f" Created: {config.metadata.created_at}")
270
+ console.print(f" Version: {config.metadata.version}")
271
+
272
+ # Validate and show issues
273
+ issues = config_manager.validate()
274
+ if issues:
275
+ console.print("\n[bold red]Validation Issues:[/bold red]")
276
+ for issue in issues:
277
+ console.print(f" - {issue}")
278
+ else:
279
+ console.print("\n[green]✓ Configuration is valid[/green]")
280
+
281
+ return 0
282
+
283
+ except Exception as e:
284
+ print_error(f"Failed to load configuration: {e}")
285
+ return 1
286
+
287
+
288
+ def validate_config(config_name: str) -> int:
289
+ """Validate a configuration."""
290
+ config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
291
+
292
+ if not config_path.exists():
293
+ print_error(f"Configuration '{config_name}' not found")
294
+ return 1
295
+
296
+ try:
297
+ config_manager = NodeConfigManager(config_path)
298
+ issues = config_manager.validate()
299
+
300
+ if not issues:
301
+ print_success(f"Configuration '{config_name}' is valid")
302
+ return 0
303
+ else:
304
+ print_error(
305
+ f"Configuration '{config_name}' has {len(issues)} validation issue(s):"
306
+ )
307
+ for issue in issues:
308
+ console.print(f" - {issue}")
309
+ return 1
310
+
311
+ except Exception as e:
312
+ print_error(f"Failed to validate configuration: {e}")
313
+ return 1
314
+
315
+
316
+ def delete_config(config_name: str) -> int:
317
+ """Delete a configuration file."""
318
+ config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
319
+
320
+ if not config_path.exists():
321
+ print_error(f"Configuration '{config_name}' not found")
322
+ console.print("Available configurations:")
323
+ config_manager = NodeConfigManager()
324
+ for cfg in config_manager.list_available_configs():
325
+ console.print(f" - {cfg}")
326
+ return 1
327
+
328
+ # Confirm deletion
329
+ console.print(f"Configuration to delete: [cyan]{config_name}[/cyan]")
330
+ console.print(f"File: [dim]{config_path}[/dim]")
331
+
332
+ if not Confirm.ask(
333
+ f"Are you sure you want to delete configuration '{config_name}'?", default=False
334
+ ):
335
+ console.print("[yellow]Deletion cancelled[/yellow]")
336
+ return 0
337
+
338
+ try:
339
+ config_path.unlink()
340
+ print_success(f"Configuration '{config_name}' deleted successfully")
341
+ return 0
342
+
343
+ except Exception as e:
344
+ print_error(f"Failed to delete configuration: {e}")
345
+ return 1
346
+
347
+
348
+ def edit_config(config_name: str) -> int:
349
+ """Edit a configuration file in the default text editor."""
350
+ try:
351
+ # Build configuration path
352
+ config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
353
+
354
+ if not config_path.exists():
355
+ print_error(f"Configuration '{config_name}' not found")
356
+ console.print("Available configurations:")
357
+ config_manager = NodeConfigManager()
358
+ for cfg in config_manager.list_available_configs():
359
+ console.print(f" - {cfg}")
360
+ return 1
361
+
362
+ # Determine which editor to use
363
+ editor = os.environ.get("EDITOR") or os.environ.get("VISUAL")
364
+
365
+ if not editor:
366
+ # Try common editors
367
+ for cmd in ["nano", "vim", "vi", "emacs", "code", "notepad"]:
368
+ try:
369
+ subprocess.run(["which", cmd], capture_output=True, check=True)
370
+ editor = cmd
371
+ break
372
+ except (subprocess.CalledProcessError, FileNotFoundError):
373
+ continue
374
+
375
+ if not editor:
376
+ print_error(
377
+ "No text editor found. Please set the EDITOR environment variable."
378
+ )
379
+ console.print(f"Configuration file location: {config_path}")
380
+ return 1
381
+
382
+ # Open the file in the editor
383
+ console.print(f"Opening {config_path} in {editor}...")
384
+
385
+ try:
386
+ result = subprocess.run([editor, str(config_path)])
387
+ if result.returncode == 0:
388
+ print_success(f"Configuration '{config_name}' edited successfully")
389
+
390
+ # Validate the configuration after editing
391
+ config_manager = NodeConfigManager(config_path)
392
+ issues = config_manager.validate()
393
+ if issues:
394
+ print_warning("Configuration has validation issues:")
395
+ for issue in issues:
396
+ console.print(f" - {issue}")
397
+
398
+ return 0
399
+ else:
400
+ print_error(f"Editor exited with error code {result.returncode}")
401
+ return 1
402
+
403
+ except Exception as e:
404
+ print_error(f"Failed to open editor: {e}")
405
+ console.print(f"Configuration file location: {config_path}")
406
+ return 1
407
+
408
+ except Exception as e:
409
+ print_error(f"Failed to edit configuration: {e}")
410
+ return 1
411
+
412
+
413
+ def config_command(args: List[str]) -> int:
414
+ """Handle node config command.
415
+
416
+ Args:
417
+ args: Command line arguments
418
+
419
+ Returns:
420
+ Exit code (0 for success, non-zero for error)
421
+ """
422
+ parser = argparse.ArgumentParser(
423
+ prog="manta_node config", description="Manage node configurations"
424
+ )
425
+ subparsers = parser.add_subparsers(dest="action", help="Configuration actions")
426
+
427
+ # Init subcommand
428
+ init_parser = subparsers.add_parser(
429
+ "init", help="Create a new configuration from a YAML or TOML file"
430
+ )
431
+ init_parser.add_argument(
432
+ "config_file", help="Path to configuration file (.yaml, .yml, or .toml)"
433
+ )
434
+
435
+ # List subcommand
436
+ subparsers.add_parser("list", help="List all configurations")
437
+
438
+ # Show subcommand
439
+ show_parser = subparsers.add_parser("show", help="Show configuration details")
440
+ show_parser.add_argument("config", help="Configuration name to show")
441
+
442
+ # Validate subcommand
443
+ validate_parser = subparsers.add_parser("validate", help="Validate a configuration")
444
+ validate_parser.add_argument("config", help="Configuration name to validate")
445
+
446
+ # Delete subcommand
447
+ delete_parser = subparsers.add_parser("delete", help="Delete a configuration")
448
+ delete_parser.add_argument("config", help="Configuration name to delete")
449
+
450
+ # Edit subcommand
451
+ edit_parser = subparsers.add_parser(
452
+ "edit", help="Edit configuration file in text editor"
453
+ )
454
+ edit_parser.add_argument(
455
+ "config",
456
+ nargs="?",
457
+ default="default",
458
+ help="Configuration name to edit (default: default)",
459
+ )
460
+
461
+ try:
462
+ parsed_args = parser.parse_args(args)
463
+ except SystemExit:
464
+ return 1
465
+
466
+ if not parsed_args.action:
467
+ parser.print_help()
468
+ return 1
469
+
470
+ if parsed_args.action == "init":
471
+ return init_config_from_file(parsed_args.config_file)
472
+ elif parsed_args.action == "list":
473
+ return list_configs()
474
+ elif parsed_args.action == "show":
475
+ return show_config(parsed_args.config)
476
+ elif parsed_args.action == "validate":
477
+ return validate_config(parsed_args.config)
478
+ elif parsed_args.action == "delete":
479
+ return delete_config(parsed_args.config)
480
+ elif parsed_args.action == "edit":
481
+ return edit_config(parsed_args.config)
482
+ else:
483
+ print_error(f"Unknown action: {parsed_args.action}")
484
+ return 1
485
+
486
+
487
+ if __name__ == "__main__":
488
+ import sys
489
+
490
+ sys.exit(config_command(sys.argv[1:]))
@@ -0,0 +1,168 @@
1
+ """Node logs command implementation."""
2
+
3
+ import argparse
4
+ import time
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ from rich.console import Console
9
+
10
+ console = Console()
11
+
12
+
13
+ def print_error(message: str):
14
+ """Print error message in red."""
15
+ console.print(f"[red]Error: {message}[/red]")
16
+
17
+
18
+ def find_log_files():
19
+ """Find available log files."""
20
+ logs_dir = Path.home() / ".manta" / "logs" / "nodes"
21
+ log_files = {}
22
+
23
+ if not logs_dir.exists():
24
+ return log_files
25
+
26
+ for log_file in logs_dir.glob("*.log"):
27
+ instance_id = log_file.stem
28
+ log_files[instance_id] = log_file
29
+
30
+ return log_files
31
+
32
+
33
+ def tail_log_file(log_file: Path, lines: int = 50, follow: bool = False) -> None:
34
+ """Tail a log file, similar to 'tail -f'."""
35
+ try:
36
+ if not log_file.exists():
37
+ print_error(f"Log file not found: {log_file}")
38
+ return
39
+
40
+ # Read last N lines if not following
41
+ if not follow:
42
+ with open(log_file, "r") as f:
43
+ # Read all lines and take the last N
44
+ all_lines = f.readlines()
45
+ last_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
46
+ for line in last_lines:
47
+ console.print(line.rstrip())
48
+ return
49
+
50
+ # Follow mode - continuously read new lines
51
+ console.print(f"Following log file: {log_file}")
52
+ console.print("Press Ctrl+C to stop")
53
+ console.print("-" * 60)
54
+
55
+ with open(log_file, "r") as f:
56
+ # First, show last N lines
57
+ all_lines = f.readlines()
58
+ last_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
59
+ for line in last_lines:
60
+ console.print(line.rstrip())
61
+
62
+ # Then follow new lines
63
+ while True:
64
+ line = f.readline()
65
+ if line:
66
+ console.print(line.rstrip())
67
+ else:
68
+ time.sleep(0.1)
69
+
70
+ except KeyboardInterrupt:
71
+ console.print("\n[yellow]Stopped following log file[/yellow]")
72
+ except Exception as e:
73
+ print_error(f"Error reading log file: {e}")
74
+
75
+
76
+ def logs_command(args: List[str]) -> int:
77
+ """Handle node logs command.
78
+
79
+ Args:
80
+ args: Command line arguments
81
+
82
+ Returns:
83
+ Exit code (0 for success, non-zero for error)
84
+ """
85
+ parser = argparse.ArgumentParser(
86
+ prog="manta_node logs", description="View node logs"
87
+ )
88
+ parser.add_argument(
89
+ "instance", nargs="?", help="Instance ID or name to show logs for"
90
+ )
91
+ parser.add_argument(
92
+ "--follow", "-f", action="store_true", help="Follow log output (like tail -f)"
93
+ )
94
+ parser.add_argument(
95
+ "--lines",
96
+ "-n",
97
+ type=int,
98
+ default=50,
99
+ help="Number of lines to show (default: 50)",
100
+ )
101
+ parser.add_argument("--list", action="store_true", help="List available log files")
102
+
103
+ try:
104
+ parsed_args = parser.parse_args(args)
105
+ except SystemExit:
106
+ return 1
107
+
108
+ log_files = find_log_files()
109
+
110
+ if parsed_args.list:
111
+ if not log_files:
112
+ console.print("No log files found.")
113
+ console.print("")
114
+ console.print("Log files are created when nodes are started.")
115
+ console.print("Start a node with: manta_node start")
116
+ else:
117
+ console.print("Available log files:")
118
+ for instance_id, log_file in log_files.items():
119
+ size_mb = log_file.stat().st_size / (1024 * 1024)
120
+ console.print(f" - {instance_id} ({size_mb:.1f} MB) - {log_file}")
121
+ return 0
122
+
123
+ if not parsed_args.instance:
124
+ if not log_files:
125
+ print_error("No log files found and no instance specified")
126
+ console.print("")
127
+ console.print("Available options:")
128
+ console.print(" - Start a node: manta_node start")
129
+ console.print(" - List log files: manta_node logs --list")
130
+ return 1
131
+
132
+ if len(log_files) == 1:
133
+ # Only one log file, use it
134
+ instance_id, log_file = next(iter(log_files.items()))
135
+ console.print(f"Using log file for instance: {instance_id}")
136
+ else:
137
+ # Multiple log files, ask user to specify
138
+ print_error("Multiple log files found. Please specify an instance:")
139
+ for instance_id in log_files.keys():
140
+ console.print(f" - {instance_id}")
141
+ return 1
142
+ else:
143
+ instance_name = parsed_args.instance
144
+
145
+ # Find matching log file
146
+ log_file = None
147
+ for instance_id, file_path in log_files.items():
148
+ if instance_id == instance_name or instance_id.startswith(instance_name):
149
+ log_file = file_path
150
+ break
151
+
152
+ if not log_file:
153
+ print_error(f"No log file found for instance '{instance_name}'")
154
+ if log_files:
155
+ console.print("Available log files:")
156
+ for instance_id in log_files.keys():
157
+ console.print(f" - {instance_id}")
158
+ return 1
159
+
160
+ # Show logs
161
+ tail_log_file(log_file, parsed_args.lines, parsed_args.follow)
162
+ return 0
163
+
164
+
165
+ if __name__ == "__main__":
166
+ import sys
167
+
168
+ sys.exit(logs_command(sys.argv[1:]))