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,459 @@
1
+ """Node start command implementation."""
2
+
3
+ import argparse
4
+ import asyncio
5
+ import json
6
+ import os
7
+ import signal
8
+ import subprocess
9
+ import sys
10
+ import time
11
+ import uuid
12
+ from datetime import datetime
13
+ from pathlib import Path
14
+ from typing import List
15
+
16
+ from rich.console import Console
17
+
18
+ from manta_common.logging_config import configure_logging
19
+
20
+ from ...node_orchestrator import Node
21
+
22
+ from ...infrastructure.config import NodeConfigManager
23
+
24
+ console = Console()
25
+
26
+
27
+ def print_error(message: str):
28
+ """Print error message in red."""
29
+ console.print(f"[red]Error: {message}[/red]")
30
+
31
+
32
+ def print_success(message: str):
33
+ """Print success message in green."""
34
+ console.print(f"[green]{message}[/green]")
35
+
36
+
37
+ def print_warning(message: str):
38
+ """Print warning message in yellow."""
39
+ console.print(f"[yellow]{message}[/yellow]")
40
+
41
+
42
+ def start_node_foreground(
43
+ config_manager: NodeConfigManager,
44
+ instance_id: str,
45
+ instance_alias: str,
46
+ skip_instance_file: bool = False,
47
+ ) -> int:
48
+ """Start a node in foreground mode."""
49
+ console.print(f"[cyan]Starting node '{instance_alias}' in foreground...[/cyan]")
50
+ console.print("[dim]Press Ctrl+C to stop[/dim]")
51
+
52
+ # Create instance file to track running node (unless we're a subprocess of background mode)
53
+ instance_file = None
54
+ if not skip_instance_file:
55
+ instance_dir = Path.home() / ".manta" / "nodes" / "instances"
56
+ instance_dir.mkdir(parents=True, exist_ok=True)
57
+ instance_file = instance_dir / f"{instance_id}.json"
58
+
59
+ console.print(f"Instance ID: {instance_id}")
60
+ console.print(f"Log file: {config_manager.config.logging.filename}")
61
+
62
+ instance_data = {
63
+ "instance_id": instance_id,
64
+ "alias": instance_alias,
65
+ "config_name": config_manager.config.metadata.name,
66
+ "pid": os.getpid(),
67
+ "start_time": datetime.now().isoformat(),
68
+ "status": "running",
69
+ "manager_host": config_manager.config.network.manager_host,
70
+ "manager_port": config_manager.config.network.manager_port,
71
+ "log_file": config_manager.config.logging.filename,
72
+ }
73
+
74
+ with open(instance_file, "w") as f:
75
+ json.dump(instance_data, f, indent=2)
76
+
77
+ loop = None # Initialize loop variable for cleanup
78
+ node = None # Initialize node variable for cleanup
79
+ shutdown_requested = False # Track if shutdown was requested
80
+
81
+ def signal_handler(signum, frame):
82
+ """Handle termination signals (SIGTERM, SIGINT)."""
83
+ nonlocal shutdown_requested, loop
84
+ if not shutdown_requested:
85
+ shutdown_requested = True
86
+ sig_name = signal.Signals(signum).name
87
+ console.print(f"\n[yellow]Received {sig_name}, stopping node...[/yellow]")
88
+ # Schedule shutdown on the event loop (thread-safe)
89
+ if loop and loop.is_running():
90
+ loop.call_soon_threadsafe(loop.stop)
91
+
92
+ # Set up signal handlers for graceful shutdown
93
+ signal.signal(signal.SIGTERM, signal_handler)
94
+ signal.signal(signal.SIGINT, signal_handler)
95
+
96
+ try:
97
+ # Configure logging based on node configuration
98
+ log_config = config_manager.config.logging
99
+
100
+ # Ensure log directory exists
101
+ log_path = Path(log_config.filename)
102
+ log_path.parent.mkdir(parents=True, exist_ok=True)
103
+
104
+ # Create config dictionary based on MANTA_LOGGING_CONFIG template
105
+ config_dict = {
106
+ "version": 1,
107
+ "disable_existing_loggers": False,
108
+ "formatters": {
109
+ "standard": {
110
+ "format": "%(asctime)s - %(levelname)s - %(name)s - %(message)s"
111
+ }
112
+ },
113
+ "filters": {
114
+ "manta_filter": {
115
+ "()": "manta_common.logging_config.filter_manta",
116
+ "level": "INFO",
117
+ },
118
+ "warnings_and_below": {
119
+ "()": "manta_common.logging_config.filter_maker",
120
+ "level": "WARNING",
121
+ },
122
+ },
123
+ "handlers": {
124
+ "file": {
125
+ "class": "logging.FileHandler",
126
+ "formatter": "standard",
127
+ "filename": log_config.filename,
128
+ "filters": ["manta_filter"],
129
+ "mode": "w",
130
+ },
131
+ "rich": {
132
+ "class": "rich.logging.RichHandler",
133
+ "formatter": "standard",
134
+ "markup": True,
135
+ "filters": ["manta_filter"],
136
+ "console": Console(
137
+ _environ={"COLUMNS": "284"},
138
+ ),
139
+ },
140
+ },
141
+ "root": {"level": log_config.level, "handlers": []},
142
+ }
143
+
144
+ # Set up handlers based on configuration
145
+ if log_config.log_to_file:
146
+ config_dict["root"]["handlers"].append("file")
147
+ if log_config.log_to_console:
148
+ config_dict["root"]["handlers"].append("rich")
149
+
150
+ # Call configure_logging with the config dictionary
151
+ configure_logging(config=config_dict, debug=log_config.debug_mode)
152
+
153
+ # Display connection information before starting
154
+ security_mode = (
155
+ "TLS"
156
+ if (
157
+ config_manager.config.security.use_tls
158
+ or config_manager.config.network.manager_port == 443
159
+ )
160
+ else "insecure"
161
+ )
162
+ endpoint = (
163
+ f"{security_mode}://"
164
+ f"{config_manager.config.network.manager_host}:"
165
+ f"{config_manager.config.network.manager_port}"
166
+ )
167
+
168
+ console.print(f"[cyan]Manager endpoint:[/cyan] {endpoint}")
169
+
170
+ # Show JWT token status (without exposing the token)
171
+ token = config_manager.config.identity.secured_token
172
+ if token:
173
+ token_parts = token.count(".") + 1
174
+ token_status = (
175
+ "valid format"
176
+ if token_parts == 3
177
+ else f"invalid format ({token_parts} parts, expected 3)"
178
+ )
179
+ console.print(
180
+ f"[cyan]JWT token:[/cyan] present ({len(token)} chars, {token_status})"
181
+ )
182
+ else:
183
+ console.print("[red]JWT token:[/red] missing")
184
+
185
+ console.print("") # Empty line for readability
186
+
187
+ # Create Node instance with configuration manager
188
+ node = Node(config_manager)
189
+
190
+ # Start node in current thread (blocking)
191
+ loop = asyncio.new_event_loop()
192
+ asyncio.set_event_loop(loop)
193
+ try:
194
+ loop.run_until_complete(node.start())
195
+ except KeyboardInterrupt:
196
+ # Direct Ctrl+C in terminal (not from signal handler)
197
+ shutdown_requested = True
198
+
199
+ # Check if shutdown was requested (either via signal or Ctrl+C)
200
+ if shutdown_requested:
201
+ try:
202
+ if node:
203
+ # Ensure cleanup is called when stopping the node
204
+ loop.run_until_complete(node.stop())
205
+ except Exception as e:
206
+ console.print(f"[red]Error during node cleanup: {e}[/red]")
207
+
208
+ # Clean up instance file
209
+ if instance_file and instance_file.exists():
210
+ instance_file.unlink()
211
+
212
+ console.print("[green]Node stopped gracefully[/green]")
213
+ return 0
214
+
215
+ print_success("Node stopped successfully")
216
+ return 0
217
+
218
+ except Exception as e:
219
+ print_error(f"Node failed with error: {e}")
220
+ # Clean up instance file on error
221
+ if instance_file and instance_file.exists():
222
+ instance_file.unlink()
223
+ return 1
224
+
225
+
226
+ def start_node_background(
227
+ config_manager: NodeConfigManager,
228
+ instance_id: str,
229
+ instance_alias: str,
230
+ log_file: Path,
231
+ ) -> int:
232
+ """Start a node in background mode."""
233
+ console.print(f"[cyan]Starting node '{instance_alias}' in background...[/cyan]")
234
+
235
+ try:
236
+ # Prepare command to run node in background
237
+ # We call the CLI with the start command in foreground mode
238
+ cmd = [
239
+ sys.executable,
240
+ "-m",
241
+ "manta_node",
242
+ "start",
243
+ str(config_manager.config_path.stem), # Use config name without extension
244
+ "--foreground", # This will run in foreground for the subprocess
245
+ "--subprocess", # Tell the subprocess not to create instance file
246
+ ]
247
+
248
+ # If alias was overridden, add it to the command
249
+ if instance_alias != config_manager.config.identity.alias:
250
+ cmd.extend(["--alias", instance_alias])
251
+
252
+ # Ensure log directory exists
253
+ log_file.parent.mkdir(parents=True, exist_ok=True)
254
+
255
+ # Start subprocess
256
+ with open(log_file, "w") as log:
257
+ proc = subprocess.Popen(
258
+ cmd,
259
+ stdout=log,
260
+ stderr=log,
261
+ start_new_session=True, # Detach from parent
262
+ )
263
+
264
+ # Wait a moment and check if process is running
265
+ time.sleep(2)
266
+
267
+ if proc.poll() is None: # Process is still running
268
+ print_success(f"Node '{instance_alias}' started successfully")
269
+ console.print(f"Instance ID: {instance_id}")
270
+ console.print(f"Process ID: {proc.pid}")
271
+ console.print(f"Log file: {log_file}")
272
+
273
+ # Create instance file for tracking (same format as foreground)
274
+ instance_dir = Path.home() / ".manta" / "nodes" / "instances"
275
+ instance_dir.mkdir(parents=True, exist_ok=True)
276
+ instance_file = instance_dir / f"{instance_id}.json"
277
+
278
+ instance_data = {
279
+ "instance_id": instance_id,
280
+ "alias": instance_alias,
281
+ "config_name": config_manager.config.metadata.name,
282
+ "pid": proc.pid,
283
+ "start_time": datetime.now().isoformat(),
284
+ "status": "running",
285
+ "manager_host": config_manager.config.network.manager_host,
286
+ "manager_port": config_manager.config.network.manager_port,
287
+ "log_file": str(log_file),
288
+ }
289
+
290
+ with open(instance_file, "w") as f:
291
+ json.dump(instance_data, f, indent=2)
292
+
293
+ return 0
294
+ else:
295
+ print_error("Node failed to start. Check logs for details.")
296
+ if log_file.exists():
297
+ console.print(f"Log file: {log_file}")
298
+ return 1
299
+
300
+ except Exception as e:
301
+ print_error(f"Failed to start background node: {e}")
302
+ return 1
303
+
304
+
305
+ def start_command(args: List[str]) -> int:
306
+ """Handle node start command.
307
+
308
+ Args:
309
+ args: Command line arguments
310
+
311
+ Returns:
312
+ Exit code (0 for success, non-zero for error)
313
+ """
314
+ parser = argparse.ArgumentParser(
315
+ prog="manta_node start", description="Start a node instance"
316
+ )
317
+ parser.add_argument(
318
+ "config",
319
+ nargs="?",
320
+ default="default",
321
+ help="Configuration name to use (default: default)",
322
+ )
323
+ parser.add_argument("--alias", help="Override node alias")
324
+ parser.add_argument(
325
+ "--background", "-d", action="store_true", help="Run node in background"
326
+ )
327
+ parser.add_argument(
328
+ "--foreground", action="store_true", help="Run node in foreground (default)"
329
+ )
330
+ parser.add_argument(
331
+ "--subprocess",
332
+ action="store_true",
333
+ help=argparse.SUPPRESS, # Hidden flag for internal use
334
+ )
335
+
336
+ try:
337
+ parsed_args = parser.parse_args(args)
338
+ except SystemExit:
339
+ return 1
340
+
341
+ config_name = parsed_args.config
342
+ alias = parsed_args.alias
343
+ background = parsed_args.background and not parsed_args.foreground
344
+
345
+ # Build configuration path
346
+ config_path = Path.home() / ".manta" / "nodes" / f"{config_name}.toml"
347
+
348
+ if not config_path.exists():
349
+ print_error(f"Configuration '{config_name}' not found at {config_path}")
350
+ console.print("Available configurations:")
351
+ config_mgr = NodeConfigManager()
352
+ for cfg in config_mgr.list_available_configs():
353
+ console.print(f" - {cfg}")
354
+ console.print("")
355
+ console.print("Create a new configuration with:")
356
+ console.print(" manta_node config init <path/to/config.yaml>")
357
+ return 1
358
+
359
+ try:
360
+ # Create NodeConfigManager with the config path
361
+ config_manager = NodeConfigManager(config_path)
362
+
363
+ # Override alias if provided
364
+ if alias:
365
+ config_manager.config.identity.alias = alias
366
+ config_manager.save()
367
+ elif (
368
+ config_manager.config.identity.alias is None
369
+ and config_manager.config.metadata.name is not None
370
+ ):
371
+ config_manager.config.identity.alias = config_manager.config.metadata.name
372
+ config_manager.save()
373
+ elif (
374
+ config_manager.config.identity.alias is None
375
+ and config_manager.config.metadata.name is None
376
+ ):
377
+ config_manager.config.identity.alias = config_name
378
+ config_manager.save()
379
+
380
+ # Validate configuration
381
+ issues = config_manager.validate()
382
+ if issues:
383
+ print_error("Configuration validation failed:")
384
+ for issue in issues:
385
+ console.print(f" - {issue}")
386
+ return 1
387
+
388
+ # Generate instance ID
389
+ instance_id = f"{config_name}-{uuid.uuid4().hex[:8]}"
390
+ instance_alias = alias or config_manager.config.identity.alias or config_name
391
+
392
+ # Check for token in configuration
393
+ if (
394
+ not config_manager.config.identity.secured_token
395
+ or config_manager.config.identity.secured_token.startswith("<")
396
+ ):
397
+ print_error("No valid secured token found in configuration")
398
+ console.print("A secured token (JWT) is required for node authentication.")
399
+ console.print("")
400
+ console.print("Solutions:")
401
+ console.print(
402
+ " • Set environment variable: export MANTA_NODE_TOKEN=your_jwt_token"
403
+ )
404
+ console.print(f" • Edit config file: {config_path}")
405
+ console.print(' • Update: secured_token = "your_jwt_token_here"')
406
+ console.print(
407
+ " • Or recreate config: manta_node config init <path/to/config.yaml>"
408
+ )
409
+ return 1
410
+
411
+ # Prepare log file - use configured filename
412
+ # Defensively handle tilde expansion
413
+ log_filename = config_manager.config.logging.filename
414
+
415
+ # Expand tilde if present at the start of the path
416
+ if log_filename.startswith("~"):
417
+ log_path = Path(log_filename).expanduser()
418
+ else:
419
+ log_path = Path(log_filename)
420
+
421
+ # Check if path is now absolute or still relative
422
+ if log_path.is_absolute():
423
+ log_file = log_path
424
+ else:
425
+ # If relative path, place it in the standard log directory
426
+ log_dir = Path.home() / ".manta" / "logs" / "nodes"
427
+ log_dir.mkdir(parents=True, exist_ok=True)
428
+ log_file = log_dir / log_path
429
+
430
+ if background:
431
+ return start_node_background(
432
+ config_manager, instance_id, instance_alias, log_file
433
+ )
434
+ else:
435
+ # Check if we're being called as a subprocess from background mode
436
+ skip_instance_file = getattr(parsed_args, "subprocess", False)
437
+ return start_node_foreground(
438
+ config_manager, instance_id, instance_alias, skip_instance_file
439
+ )
440
+
441
+ except Exception as e:
442
+ print_error(f"Failed to start node: {e}")
443
+ console.print("")
444
+ console.print("Troubleshooting steps:")
445
+ console.print(
446
+ " 1. Check if configuration is valid: manta_node config validate"
447
+ )
448
+ console.print(
449
+ " 2. Verify manager is running: docker ps (look for manta services)"
450
+ )
451
+ console.print(" 3. Check if port is available: netstat -ln | grep :50051")
452
+ console.print(" 4. Validate secured_token is set in config file")
453
+ return 1
454
+
455
+
456
+ if __name__ == "__main__":
457
+ import sys
458
+
459
+ sys.exit(start_command(sys.argv[1:]))
@@ -0,0 +1,204 @@
1
+ """Node status command implementation."""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import List
9
+
10
+ import psutil
11
+ from rich.console import Console
12
+ from rich.table import Table
13
+
14
+ console = Console()
15
+
16
+
17
+ def find_running_instances():
18
+ """Find running node instances by looking for instance JSON files."""
19
+
20
+ instances_dir = Path.home() / ".manta" / "nodes" / "instances"
21
+ instances = []
22
+
23
+ if not instances_dir.exists():
24
+ return instances
25
+
26
+ for instance_file in instances_dir.glob("*.json"):
27
+ try:
28
+ with open(instance_file, "r") as f:
29
+ instance_data = json.load(f)
30
+
31
+ pid = instance_data.get("pid")
32
+ instance_id = instance_data.get("instance_id", instance_file.stem)
33
+
34
+ # Check if process is still running
35
+ try:
36
+ # Send signal 0 to check if process exists
37
+
38
+ os.kill(pid, 0)
39
+
40
+ # Get process info if psutil is available
41
+ try:
42
+ process = psutil.Process(pid)
43
+ cpu_percent = process.cpu_percent()
44
+ memory_mb = process.memory_info().rss // (1024 * 1024)
45
+ create_time = datetime.fromtimestamp(process.create_time())
46
+ status = "running"
47
+ except ImportError:
48
+ cpu_percent = None
49
+ memory_mb = None
50
+ create_time = None
51
+ status = "running"
52
+ except psutil.NoSuchProcess:
53
+ # Process disappeared
54
+ instance_file.unlink(missing_ok=True)
55
+ continue
56
+
57
+ # Add additional info from JSON
58
+ alias = instance_data.get("alias", instance_id)
59
+ config_name = instance_data.get("config_name", "unknown")
60
+ start_time_str = instance_data.get("start_time")
61
+ if start_time_str and not create_time:
62
+ try:
63
+ create_time = datetime.fromisoformat(start_time_str)
64
+ except Exception:
65
+ pass
66
+
67
+ instances.append(
68
+ {
69
+ "instance_id": instance_id,
70
+ "alias": alias,
71
+ "config_name": config_name,
72
+ "pid": pid,
73
+ "status": status,
74
+ "cpu_percent": cpu_percent,
75
+ "memory_mb": memory_mb,
76
+ "start_time": create_time,
77
+ "instance_file": instance_file,
78
+ }
79
+ )
80
+
81
+ except OSError:
82
+ # Process no longer exists, remove stale instance file
83
+ instance_file.unlink(missing_ok=True)
84
+
85
+ except (ValueError, FileNotFoundError):
86
+ # Invalid instance file, remove it
87
+ instance_file.unlink(missing_ok=True)
88
+
89
+ return instances
90
+
91
+
92
+ def show_status_table(instances: List[dict]) -> None:
93
+ """Display instances in a formatted table."""
94
+ if not instances:
95
+ console.print("[yellow]No running node instances found.[/yellow]")
96
+ console.print("")
97
+ console.print("Start a node with:")
98
+ console.print(" manta_node start [config_name]")
99
+ return
100
+
101
+ table = Table(title="Running Node Instances")
102
+ table.add_column("Alias", style="cyan")
103
+ table.add_column("Config", style="magenta")
104
+ table.add_column("PID", style="green")
105
+ table.add_column("Status", style="yellow")
106
+ table.add_column("CPU %", style="blue")
107
+ table.add_column("Memory (MB)", style="blue")
108
+ table.add_column("Started", style="dim")
109
+
110
+ for instance in instances:
111
+ cpu_str = (
112
+ f"{instance['cpu_percent']:.1f}%"
113
+ if instance["cpu_percent"] is not None
114
+ else "N/A"
115
+ )
116
+ memory_str = (
117
+ str(instance["memory_mb"]) if instance["memory_mb"] is not None else "N/A"
118
+ )
119
+ start_str = (
120
+ instance["start_time"].strftime("%Y-%m-%d %H:%M:%S")
121
+ if instance["start_time"]
122
+ else "N/A"
123
+ )
124
+
125
+ table.add_row(
126
+ instance["alias"],
127
+ instance["config_name"],
128
+ str(instance["pid"]),
129
+ instance["status"],
130
+ cpu_str,
131
+ memory_str,
132
+ start_str,
133
+ )
134
+
135
+ console.print(table)
136
+
137
+
138
+ def show_status_plain(instances: List[dict]) -> None:
139
+ """Display instances in plain text format."""
140
+ if not instances:
141
+ console.print("No running node instances found.")
142
+ console.print("")
143
+ console.print("Start a node with:")
144
+ console.print(" manta_node start [config_name]")
145
+ return
146
+
147
+ console.print(f"Found {len(instances)} running instance(s):")
148
+ console.print("")
149
+
150
+ for instance in instances:
151
+ console.print(f"Instance: [cyan]{instance['instance_id']}[/cyan]")
152
+ console.print(f" PID: {instance['pid']}")
153
+ console.print(f" Status: {instance['status']}")
154
+
155
+ if instance["cpu_percent"] is not None:
156
+ console.print(f" CPU: {instance['cpu_percent']:.1f}%")
157
+ if instance["memory_mb"] is not None:
158
+ console.print(f" Memory: {instance['memory_mb']} MB")
159
+ if instance["start_time"]:
160
+ console.print(
161
+ f" Started: {instance['start_time'].strftime('%Y-%m-%d %H:%M:%S')}"
162
+ )
163
+
164
+ console.print("")
165
+
166
+
167
+ def status_command(args: List[str]) -> int:
168
+ """Handle node status command.
169
+
170
+ Args:
171
+ args: Command line arguments
172
+
173
+ Returns:
174
+ Exit code (0 for success, non-zero for error)
175
+ """
176
+ parser = argparse.ArgumentParser(
177
+ prog="manta_node status", description="Show status of running node instances"
178
+ )
179
+ parser.add_argument(
180
+ "--plain", action="store_true", help="Use plain text output instead of table"
181
+ )
182
+ parser.add_argument(
183
+ "--all", action="store_true", help="Show all instances (same as default)"
184
+ )
185
+
186
+ try:
187
+ parsed_args = parser.parse_args(args)
188
+ except SystemExit:
189
+ return 1
190
+
191
+ instances = find_running_instances()
192
+
193
+ if parsed_args.plain:
194
+ show_status_plain(instances)
195
+ else:
196
+ show_status_table(instances)
197
+
198
+ return 0
199
+
200
+
201
+ if __name__ == "__main__":
202
+ import sys
203
+
204
+ sys.exit(status_command(sys.argv[1:]))