claude-mpm 4.0.22__py3-none-any.whl → 4.0.25__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.
- claude_mpm/BUILD_NUMBER +1 -1
- claude_mpm/VERSION +1 -1
- claude_mpm/agents/BASE_AGENT_TEMPLATE.md +4 -1
- claude_mpm/agents/BASE_PM.md +3 -0
- claude_mpm/agents/templates/code_analyzer.json +3 -3
- claude_mpm/agents/templates/data_engineer.json +2 -2
- claude_mpm/agents/templates/documentation.json +36 -9
- claude_mpm/agents/templates/engineer.json +2 -2
- claude_mpm/agents/templates/ops.json +2 -2
- claude_mpm/agents/templates/qa.json +2 -2
- claude_mpm/agents/templates/refactoring_engineer.json +65 -43
- claude_mpm/agents/templates/security.json +2 -2
- claude_mpm/agents/templates/version_control.json +2 -2
- claude_mpm/agents/templates/web_ui.json +2 -2
- claude_mpm/cli/commands/agents.py +453 -113
- claude_mpm/cli/commands/aggregate.py +107 -15
- claude_mpm/cli/commands/cleanup.py +142 -10
- claude_mpm/cli/commands/config.py +358 -224
- claude_mpm/cli/commands/info.py +184 -75
- claude_mpm/cli/commands/mcp_command_router.py +5 -76
- claude_mpm/cli/commands/mcp_install_commands.py +68 -36
- claude_mpm/cli/commands/mcp_server_commands.py +30 -37
- claude_mpm/cli/commands/memory.py +331 -61
- claude_mpm/cli/commands/monitor.py +101 -7
- claude_mpm/cli/commands/run.py +368 -8
- claude_mpm/cli/commands/tickets.py +206 -24
- claude_mpm/cli/parsers/mcp_parser.py +3 -0
- claude_mpm/cli/shared/__init__.py +40 -0
- claude_mpm/cli/shared/argument_patterns.py +212 -0
- claude_mpm/cli/shared/command_base.py +234 -0
- claude_mpm/cli/shared/error_handling.py +238 -0
- claude_mpm/cli/shared/output_formatters.py +231 -0
- claude_mpm/config/agent_config.py +29 -8
- claude_mpm/core/container.py +6 -4
- claude_mpm/core/framework_loader.py +32 -9
- claude_mpm/core/service_registry.py +4 -2
- claude_mpm/core/shared/__init__.py +17 -0
- claude_mpm/core/shared/config_loader.py +320 -0
- claude_mpm/core/shared/path_resolver.py +277 -0
- claude_mpm/core/shared/singleton_manager.py +208 -0
- claude_mpm/hooks/claude_hooks/memory_integration.py +4 -2
- claude_mpm/hooks/claude_hooks/response_tracking.py +14 -3
- claude_mpm/hooks/memory_integration_hook.py +11 -2
- claude_mpm/services/agents/deployment/agent_deployment.py +43 -23
- claude_mpm/services/agents/deployment/deployment_wrapper.py +71 -0
- claude_mpm/services/agents/deployment/pipeline/pipeline_context.py +1 -0
- claude_mpm/services/agents/deployment/pipeline/steps/agent_processing_step.py +43 -0
- claude_mpm/services/agents/deployment/processors/agent_deployment_context.py +4 -0
- claude_mpm/services/agents/deployment/processors/agent_processor.py +1 -1
- claude_mpm/services/agents/loading/base_agent_manager.py +11 -3
- claude_mpm/services/agents/registry/deployed_agent_discovery.py +14 -5
- claude_mpm/services/event_aggregator.py +4 -2
- claude_mpm/services/mcp_gateway/config/config_loader.py +89 -28
- claude_mpm/services/mcp_gateway/config/configuration.py +29 -0
- claude_mpm/services/mcp_gateway/registry/service_registry.py +22 -5
- claude_mpm/services/memory/builder.py +6 -1
- claude_mpm/services/response_tracker.py +3 -1
- claude_mpm/services/runner_configuration_service.py +15 -6
- claude_mpm/services/shared/__init__.py +20 -0
- claude_mpm/services/shared/async_service_base.py +219 -0
- claude_mpm/services/shared/config_service_base.py +292 -0
- claude_mpm/services/shared/lifecycle_service_base.py +317 -0
- claude_mpm/services/shared/manager_base.py +303 -0
- claude_mpm/services/shared/service_factory.py +308 -0
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/METADATA +19 -13
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/RECORD +70 -54
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/WHEEL +0 -0
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/entry_points.txt +0 -0
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/licenses/LICENSE +0 -0
- {claude_mpm-4.0.22.dist-info → claude_mpm-4.0.25.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base command class to reduce duplication in CLI command implementations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
|
+
|
|
11
|
+
from ...core.config import Config
|
|
12
|
+
from ...core.logger import get_logger
|
|
13
|
+
from ...core.shared.config_loader import ConfigLoader
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class CommandResult:
|
|
18
|
+
"""Standard result structure for CLI commands."""
|
|
19
|
+
|
|
20
|
+
success: bool
|
|
21
|
+
exit_code: int = 0
|
|
22
|
+
message: Optional[str] = None
|
|
23
|
+
data: Optional[Dict[str, Any]] = None
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def success_result(cls, message: str = None, data: Dict[str, Any] = None) -> "CommandResult":
|
|
27
|
+
"""Create a success result."""
|
|
28
|
+
return cls(success=True, exit_code=0, message=message, data=data)
|
|
29
|
+
|
|
30
|
+
@classmethod
|
|
31
|
+
def error_result(cls, message: str, exit_code: int = 1, data: Dict[str, Any] = None) -> "CommandResult":
|
|
32
|
+
"""Create an error result."""
|
|
33
|
+
return cls(success=False, exit_code=exit_code, message=message, data=data)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BaseCommand(ABC):
|
|
37
|
+
"""
|
|
38
|
+
Base class for CLI commands to reduce duplication.
|
|
39
|
+
|
|
40
|
+
Provides common functionality:
|
|
41
|
+
- Logger initialization
|
|
42
|
+
- Configuration loading
|
|
43
|
+
- Working directory handling
|
|
44
|
+
- Standard error handling patterns
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, command_name: str):
|
|
48
|
+
"""
|
|
49
|
+
Initialize base command.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
command_name: Name of the command for logging
|
|
53
|
+
"""
|
|
54
|
+
self.command_name = command_name
|
|
55
|
+
self.logger = get_logger(f"cli.{command_name}")
|
|
56
|
+
self._config: Optional[Config] = None
|
|
57
|
+
self._working_dir: Optional[Path] = None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def config(self) -> Config:
|
|
61
|
+
"""Get configuration instance (lazy loaded)."""
|
|
62
|
+
if self._config is None:
|
|
63
|
+
config_loader = ConfigLoader()
|
|
64
|
+
self._config = config_loader.load_main_config()
|
|
65
|
+
return self._config
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def working_dir(self) -> Path:
|
|
69
|
+
"""Get working directory (respects CLAUDE_MPM_USER_PWD)."""
|
|
70
|
+
if self._working_dir is None:
|
|
71
|
+
# Use CLAUDE_MPM_USER_PWD if available (when called via shell script)
|
|
72
|
+
user_pwd = os.environ.get("CLAUDE_MPM_USER_PWD", os.getcwd())
|
|
73
|
+
self._working_dir = Path(user_pwd)
|
|
74
|
+
return self._working_dir
|
|
75
|
+
|
|
76
|
+
def setup_logging(self, args) -> None:
|
|
77
|
+
"""Setup logging based on command arguments."""
|
|
78
|
+
import logging
|
|
79
|
+
|
|
80
|
+
# Set log level based on arguments
|
|
81
|
+
if hasattr(args, 'debug') and args.debug:
|
|
82
|
+
logging.getLogger().setLevel(logging.DEBUG)
|
|
83
|
+
elif hasattr(args, 'verbose') and args.verbose:
|
|
84
|
+
logging.getLogger().setLevel(logging.INFO)
|
|
85
|
+
elif hasattr(args, 'quiet') and args.quiet:
|
|
86
|
+
logging.getLogger().setLevel(logging.WARNING)
|
|
87
|
+
|
|
88
|
+
def load_config(self, args) -> None:
|
|
89
|
+
"""Load configuration from arguments."""
|
|
90
|
+
config_loader = ConfigLoader()
|
|
91
|
+
if hasattr(args, 'config') and args.config:
|
|
92
|
+
# Use specific config file with ConfigLoader
|
|
93
|
+
from ...core.shared.config_loader import ConfigPattern
|
|
94
|
+
pattern = ConfigPattern(
|
|
95
|
+
filenames=[Path(args.config).name],
|
|
96
|
+
search_paths=[str(Path(args.config).parent)],
|
|
97
|
+
env_prefix="CLAUDE_MPM_"
|
|
98
|
+
)
|
|
99
|
+
self._config = config_loader.load_config(pattern, cache_key=f"cli_{args.config}")
|
|
100
|
+
else:
|
|
101
|
+
self._config = config_loader.load_main_config()
|
|
102
|
+
|
|
103
|
+
def validate_args(self, args) -> Optional[str]:
|
|
104
|
+
"""
|
|
105
|
+
Validate command arguments.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
args: Parsed command arguments
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Error message if validation fails, None if valid
|
|
112
|
+
"""
|
|
113
|
+
# Base validation - subclasses can override
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def execute(self, args) -> CommandResult:
|
|
117
|
+
"""
|
|
118
|
+
Execute the command with standard error handling.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
args: Parsed command arguments
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
CommandResult with execution status
|
|
125
|
+
"""
|
|
126
|
+
try:
|
|
127
|
+
# Setup
|
|
128
|
+
self.setup_logging(args)
|
|
129
|
+
self.load_config(args)
|
|
130
|
+
|
|
131
|
+
# Validate arguments
|
|
132
|
+
validation_error = self.validate_args(args)
|
|
133
|
+
if validation_error:
|
|
134
|
+
return CommandResult.error_result(validation_error)
|
|
135
|
+
|
|
136
|
+
# Execute command-specific logic
|
|
137
|
+
return self.run(args)
|
|
138
|
+
|
|
139
|
+
except KeyboardInterrupt:
|
|
140
|
+
self.logger.info("Command interrupted by user")
|
|
141
|
+
return CommandResult.error_result("Operation cancelled by user", exit_code=130)
|
|
142
|
+
|
|
143
|
+
except Exception as e:
|
|
144
|
+
self.logger.error(f"Command failed: {e}", exc_info=True)
|
|
145
|
+
return CommandResult.error_result(f"Command failed: {e}")
|
|
146
|
+
|
|
147
|
+
@abstractmethod
|
|
148
|
+
def run(self, args) -> CommandResult:
|
|
149
|
+
"""
|
|
150
|
+
Run the command-specific logic.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
args: Parsed command arguments
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
CommandResult with execution status
|
|
157
|
+
"""
|
|
158
|
+
pass
|
|
159
|
+
|
|
160
|
+
def print_result(self, result: CommandResult, args) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Print command result based on output format.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
result: Command result to print
|
|
166
|
+
args: Command arguments (for format options)
|
|
167
|
+
"""
|
|
168
|
+
from .output_formatters import format_output
|
|
169
|
+
|
|
170
|
+
# Determine output format
|
|
171
|
+
output_format = getattr(args, 'format', 'text')
|
|
172
|
+
|
|
173
|
+
# Format and print result
|
|
174
|
+
formatted_output = format_output(result, output_format)
|
|
175
|
+
|
|
176
|
+
if hasattr(args, 'output') and args.output:
|
|
177
|
+
# Write to file
|
|
178
|
+
with open(args.output, 'w') as f:
|
|
179
|
+
f.write(formatted_output)
|
|
180
|
+
self.logger.info(f"Output written to {args.output}")
|
|
181
|
+
else:
|
|
182
|
+
# Print to stdout
|
|
183
|
+
print(formatted_output)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class ServiceCommand(BaseCommand):
|
|
187
|
+
"""Base class for commands that work with services."""
|
|
188
|
+
|
|
189
|
+
def __init__(self, command_name: str, service_class: type):
|
|
190
|
+
"""
|
|
191
|
+
Initialize service command.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
command_name: Name of the command
|
|
195
|
+
service_class: Service class to instantiate
|
|
196
|
+
"""
|
|
197
|
+
super().__init__(command_name)
|
|
198
|
+
self.service_class = service_class
|
|
199
|
+
self._service = None
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def service(self):
|
|
203
|
+
"""Get service instance (lazy loaded)."""
|
|
204
|
+
if self._service is None:
|
|
205
|
+
self._service = self.service_class()
|
|
206
|
+
return self._service
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class AgentCommand(BaseCommand):
|
|
210
|
+
"""Base class for agent-related commands."""
|
|
211
|
+
|
|
212
|
+
def get_agent_dir(self, args) -> Path:
|
|
213
|
+
"""Get agent directory from arguments or default."""
|
|
214
|
+
if hasattr(args, 'agent_dir') and args.agent_dir:
|
|
215
|
+
return args.agent_dir
|
|
216
|
+
|
|
217
|
+
# Default to working directory
|
|
218
|
+
return self.working_dir
|
|
219
|
+
|
|
220
|
+
def get_agent_pattern(self, args) -> Optional[str]:
|
|
221
|
+
"""Get agent name pattern from arguments."""
|
|
222
|
+
return getattr(args, 'agent', None)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
class MemoryCommand(BaseCommand):
|
|
226
|
+
"""Base class for memory-related commands."""
|
|
227
|
+
|
|
228
|
+
def get_memory_dir(self, args) -> Path:
|
|
229
|
+
"""Get memory directory from arguments or default."""
|
|
230
|
+
if hasattr(args, 'memory_dir') and args.memory_dir:
|
|
231
|
+
return args.memory_dir
|
|
232
|
+
|
|
233
|
+
# Default to .claude-mpm/memories in working directory
|
|
234
|
+
return self.working_dir / ".claude-mpm" / "memories"
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Common error handling patterns for CLI commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import traceback
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from ...core.logger import get_logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CLIErrorHandler:
|
|
13
|
+
"""Centralized error handling for CLI commands."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, command_name: str):
|
|
16
|
+
"""
|
|
17
|
+
Initialize error handler.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
command_name: Name of the command for logging context
|
|
21
|
+
"""
|
|
22
|
+
self.command_name = command_name
|
|
23
|
+
self.logger = get_logger(f"cli.{command_name}")
|
|
24
|
+
|
|
25
|
+
def handle_error(self, error: Exception, context: str = None) -> int:
|
|
26
|
+
"""
|
|
27
|
+
Handle an error with appropriate logging and user feedback.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
error: The exception that occurred
|
|
31
|
+
context: Additional context about when the error occurred
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Appropriate exit code
|
|
35
|
+
"""
|
|
36
|
+
# Build error message
|
|
37
|
+
error_msg = str(error)
|
|
38
|
+
if context:
|
|
39
|
+
error_msg = f"{context}: {error_msg}"
|
|
40
|
+
|
|
41
|
+
# Determine error type and appropriate response
|
|
42
|
+
if isinstance(error, KeyboardInterrupt):
|
|
43
|
+
self.logger.info("Operation cancelled by user")
|
|
44
|
+
print("\nOperation cancelled by user.")
|
|
45
|
+
return 130 # Standard exit code for SIGINT
|
|
46
|
+
|
|
47
|
+
elif isinstance(error, FileNotFoundError):
|
|
48
|
+
self.logger.error(f"File not found: {error}")
|
|
49
|
+
print(f"Error: File not found - {error}")
|
|
50
|
+
return 2
|
|
51
|
+
|
|
52
|
+
elif isinstance(error, PermissionError):
|
|
53
|
+
self.logger.error(f"Permission denied: {error}")
|
|
54
|
+
print(f"Error: Permission denied - {error}")
|
|
55
|
+
return 13
|
|
56
|
+
|
|
57
|
+
elif isinstance(error, ValueError):
|
|
58
|
+
self.logger.error(f"Invalid value: {error}")
|
|
59
|
+
print(f"Error: Invalid value - {error}")
|
|
60
|
+
return 22
|
|
61
|
+
|
|
62
|
+
else:
|
|
63
|
+
# Generic error handling
|
|
64
|
+
self.logger.error(f"Command failed: {error}", exc_info=True)
|
|
65
|
+
print(f"Error: {error_msg}")
|
|
66
|
+
|
|
67
|
+
# Show traceback in debug mode
|
|
68
|
+
if self.logger.isEnabledFor(10): # DEBUG level
|
|
69
|
+
traceback.print_exc()
|
|
70
|
+
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
def handle_validation_error(self, message: str) -> int:
|
|
74
|
+
"""
|
|
75
|
+
Handle validation errors.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
message: Validation error message
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Exit code for validation errors
|
|
82
|
+
"""
|
|
83
|
+
self.logger.error(f"Validation error: {message}")
|
|
84
|
+
print(f"Error: {message}")
|
|
85
|
+
return 22 # Invalid argument exit code
|
|
86
|
+
|
|
87
|
+
def handle_config_error(self, error: Exception) -> int:
|
|
88
|
+
"""
|
|
89
|
+
Handle configuration-related errors.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
error: Configuration error
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Exit code for configuration errors
|
|
96
|
+
"""
|
|
97
|
+
self.logger.error(f"Configuration error: {error}")
|
|
98
|
+
print(f"Configuration Error: {error}")
|
|
99
|
+
print("Please check your configuration file and try again.")
|
|
100
|
+
return 78 # Configuration error exit code
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def handle_cli_errors(command_name: str):
|
|
104
|
+
"""
|
|
105
|
+
Decorator to add standard error handling to CLI command functions.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
command_name: Name of the command for error context
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Decorated function with error handling
|
|
112
|
+
"""
|
|
113
|
+
def decorator(func: Callable) -> Callable:
|
|
114
|
+
@wraps(func)
|
|
115
|
+
def wrapper(*args, **kwargs) -> int:
|
|
116
|
+
error_handler = CLIErrorHandler(command_name)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
result = func(*args, **kwargs)
|
|
120
|
+
|
|
121
|
+
# Handle different return types
|
|
122
|
+
if isinstance(result, int):
|
|
123
|
+
return result
|
|
124
|
+
elif hasattr(result, 'exit_code'):
|
|
125
|
+
return result.exit_code
|
|
126
|
+
else:
|
|
127
|
+
return 0 # Success
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
return error_handler.handle_error(e)
|
|
131
|
+
|
|
132
|
+
return wrapper
|
|
133
|
+
return decorator
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def safe_execute(func: Callable, *args, error_handler: CLIErrorHandler = None, **kwargs) -> Any:
|
|
137
|
+
"""
|
|
138
|
+
Safely execute a function with error handling.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
func: Function to execute
|
|
142
|
+
*args: Function arguments
|
|
143
|
+
error_handler: Optional error handler
|
|
144
|
+
**kwargs: Function keyword arguments
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Function result or None if error occurred
|
|
148
|
+
"""
|
|
149
|
+
try:
|
|
150
|
+
return func(*args, **kwargs)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
if error_handler:
|
|
153
|
+
error_handler.handle_error(e)
|
|
154
|
+
else:
|
|
155
|
+
# Fallback error handling
|
|
156
|
+
logger = get_logger("cli.safe_execute")
|
|
157
|
+
logger.error(f"Error executing {func.__name__}: {e}", exc_info=True)
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def validate_file_exists(file_path: str, error_handler: CLIErrorHandler = None) -> bool:
|
|
162
|
+
"""
|
|
163
|
+
Validate that a file exists.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
file_path: Path to validate
|
|
167
|
+
error_handler: Optional error handler
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
True if file exists, False otherwise
|
|
171
|
+
"""
|
|
172
|
+
from pathlib import Path
|
|
173
|
+
|
|
174
|
+
path = Path(file_path)
|
|
175
|
+
if not path.exists():
|
|
176
|
+
message = f"File does not exist: {file_path}"
|
|
177
|
+
if error_handler:
|
|
178
|
+
error_handler.handle_validation_error(message)
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
if not path.is_file():
|
|
182
|
+
message = f"Path is not a file: {file_path}"
|
|
183
|
+
if error_handler:
|
|
184
|
+
error_handler.handle_validation_error(message)
|
|
185
|
+
return False
|
|
186
|
+
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def validate_directory_exists(dir_path: str, error_handler: CLIErrorHandler = None) -> bool:
|
|
191
|
+
"""
|
|
192
|
+
Validate that a directory exists.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
dir_path: Directory path to validate
|
|
196
|
+
error_handler: Optional error handler
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
True if directory exists, False otherwise
|
|
200
|
+
"""
|
|
201
|
+
from pathlib import Path
|
|
202
|
+
|
|
203
|
+
path = Path(dir_path)
|
|
204
|
+
if not path.exists():
|
|
205
|
+
message = f"Directory does not exist: {dir_path}"
|
|
206
|
+
if error_handler:
|
|
207
|
+
error_handler.handle_validation_error(message)
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
if not path.is_dir():
|
|
211
|
+
message = f"Path is not a directory: {dir_path}"
|
|
212
|
+
if error_handler:
|
|
213
|
+
error_handler.handle_validation_error(message)
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def confirm_operation(message: str, force: bool = False) -> bool:
|
|
220
|
+
"""
|
|
221
|
+
Ask user for confirmation unless force flag is set.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
message: Confirmation message
|
|
225
|
+
force: If True, skip confirmation
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
True if operation should proceed
|
|
229
|
+
"""
|
|
230
|
+
if force:
|
|
231
|
+
return True
|
|
232
|
+
|
|
233
|
+
try:
|
|
234
|
+
response = input(f"{message} (y/N): ").strip().lower()
|
|
235
|
+
return response in ('y', 'yes')
|
|
236
|
+
except (EOFError, KeyboardInterrupt):
|
|
237
|
+
print("\nOperation cancelled.")
|
|
238
|
+
return False
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Output formatting utilities for CLI commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Dict, List, Union
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OutputFormatter:
|
|
12
|
+
"""Handles formatting output in different formats."""
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def format_json(data: Any, indent: int = 2) -> str:
|
|
16
|
+
"""Format data as JSON."""
|
|
17
|
+
return json.dumps(data, indent=indent, default=str)
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def format_yaml(data: Any) -> str:
|
|
21
|
+
"""Format data as YAML."""
|
|
22
|
+
return yaml.dump(data, default_flow_style=False, sort_keys=False)
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def format_table(data: Union[List[Dict], Dict], headers: List[str] = None) -> str:
|
|
26
|
+
"""Format data as a simple table."""
|
|
27
|
+
if not data:
|
|
28
|
+
return "No data to display"
|
|
29
|
+
|
|
30
|
+
# Handle single dict
|
|
31
|
+
if isinstance(data, dict):
|
|
32
|
+
data = [data]
|
|
33
|
+
|
|
34
|
+
# Auto-detect headers if not provided
|
|
35
|
+
if headers is None and data:
|
|
36
|
+
headers = list(data[0].keys())
|
|
37
|
+
|
|
38
|
+
if not headers:
|
|
39
|
+
return "No data to display"
|
|
40
|
+
|
|
41
|
+
# Calculate column widths
|
|
42
|
+
col_widths = {}
|
|
43
|
+
for header in headers:
|
|
44
|
+
col_widths[header] = len(header)
|
|
45
|
+
for row in data:
|
|
46
|
+
value = str(row.get(header, ''))
|
|
47
|
+
col_widths[header] = max(col_widths[header], len(value))
|
|
48
|
+
|
|
49
|
+
# Build table
|
|
50
|
+
lines = []
|
|
51
|
+
|
|
52
|
+
# Header row
|
|
53
|
+
header_row = " | ".join(header.ljust(col_widths[header]) for header in headers)
|
|
54
|
+
lines.append(header_row)
|
|
55
|
+
|
|
56
|
+
# Separator row
|
|
57
|
+
separator = " | ".join("-" * col_widths[header] for header in headers)
|
|
58
|
+
lines.append(separator)
|
|
59
|
+
|
|
60
|
+
# Data rows
|
|
61
|
+
for row in data:
|
|
62
|
+
data_row = " | ".join(
|
|
63
|
+
str(row.get(header, '')).ljust(col_widths[header])
|
|
64
|
+
for header in headers
|
|
65
|
+
)
|
|
66
|
+
lines.append(data_row)
|
|
67
|
+
|
|
68
|
+
return "\n".join(lines)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def format_text(data: Any) -> str:
|
|
72
|
+
"""Format data as human-readable text."""
|
|
73
|
+
if isinstance(data, dict):
|
|
74
|
+
lines = []
|
|
75
|
+
for key, value in data.items():
|
|
76
|
+
if isinstance(value, (dict, list)):
|
|
77
|
+
lines.append(f"{key}:")
|
|
78
|
+
# Indent nested content
|
|
79
|
+
nested = OutputFormatter.format_text(value)
|
|
80
|
+
for line in nested.split('\n'):
|
|
81
|
+
if line.strip():
|
|
82
|
+
lines.append(f" {line}")
|
|
83
|
+
else:
|
|
84
|
+
lines.append(f"{key}: {value}")
|
|
85
|
+
return "\n".join(lines)
|
|
86
|
+
|
|
87
|
+
elif isinstance(data, list):
|
|
88
|
+
if not data:
|
|
89
|
+
return "No items"
|
|
90
|
+
|
|
91
|
+
lines = []
|
|
92
|
+
for i, item in enumerate(data):
|
|
93
|
+
if isinstance(item, dict):
|
|
94
|
+
lines.append(f"Item {i + 1}:")
|
|
95
|
+
nested = OutputFormatter.format_text(item)
|
|
96
|
+
for line in nested.split('\n'):
|
|
97
|
+
if line.strip():
|
|
98
|
+
lines.append(f" {line}")
|
|
99
|
+
else:
|
|
100
|
+
lines.append(f"- {item}")
|
|
101
|
+
return "\n".join(lines)
|
|
102
|
+
|
|
103
|
+
else:
|
|
104
|
+
return str(data)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def format_output(data: Any, format_type: str = "text", **kwargs) -> str:
|
|
108
|
+
"""
|
|
109
|
+
Format data according to the specified format.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
data: Data to format
|
|
113
|
+
format_type: Output format ('json', 'yaml', 'table', 'text')
|
|
114
|
+
**kwargs: Additional formatting options
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Formatted string
|
|
118
|
+
"""
|
|
119
|
+
formatter = OutputFormatter()
|
|
120
|
+
|
|
121
|
+
if format_type == "json":
|
|
122
|
+
return formatter.format_json(data, **kwargs)
|
|
123
|
+
elif format_type == "yaml":
|
|
124
|
+
return formatter.format_yaml(data)
|
|
125
|
+
elif format_type == "table":
|
|
126
|
+
return formatter.format_table(data, **kwargs)
|
|
127
|
+
elif format_type == "text":
|
|
128
|
+
return formatter.format_text(data)
|
|
129
|
+
else:
|
|
130
|
+
# Fallback to text format
|
|
131
|
+
return formatter.format_text(data)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def format_success_message(message: str, data: Any = None, format_type: str = "text") -> str:
|
|
135
|
+
"""
|
|
136
|
+
Format a success message with optional data.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
message: Success message
|
|
140
|
+
data: Optional data to include
|
|
141
|
+
format_type: Output format
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Formatted success message
|
|
145
|
+
"""
|
|
146
|
+
if format_type in ("json", "yaml"):
|
|
147
|
+
result = {"success": True, "message": message}
|
|
148
|
+
if data is not None:
|
|
149
|
+
result["data"] = data
|
|
150
|
+
return format_output(result, format_type)
|
|
151
|
+
else:
|
|
152
|
+
# Text format
|
|
153
|
+
lines = [f"✓ {message}"]
|
|
154
|
+
if data is not None:
|
|
155
|
+
lines.append("")
|
|
156
|
+
lines.append(format_output(data, format_type))
|
|
157
|
+
return "\n".join(lines)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def format_error_message(message: str, details: Any = None, format_type: str = "text") -> str:
|
|
161
|
+
"""
|
|
162
|
+
Format an error message with optional details.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
message: Error message
|
|
166
|
+
details: Optional error details
|
|
167
|
+
format_type: Output format
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Formatted error message
|
|
171
|
+
"""
|
|
172
|
+
if format_type in ("json", "yaml"):
|
|
173
|
+
result = {"success": False, "error": message}
|
|
174
|
+
if details is not None:
|
|
175
|
+
result["details"] = details
|
|
176
|
+
return format_output(result, format_type)
|
|
177
|
+
else:
|
|
178
|
+
# Text format
|
|
179
|
+
lines = [f"✗ {message}"]
|
|
180
|
+
if details is not None:
|
|
181
|
+
lines.append("")
|
|
182
|
+
lines.append(format_output(details, format_type))
|
|
183
|
+
return "\n".join(lines)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def format_list_output(items: List[Any],
|
|
187
|
+
title: str = None,
|
|
188
|
+
format_type: str = "text",
|
|
189
|
+
headers: List[str] = None) -> str:
|
|
190
|
+
"""
|
|
191
|
+
Format a list of items for output.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
items: List of items to format
|
|
195
|
+
title: Optional title for the list
|
|
196
|
+
format_type: Output format
|
|
197
|
+
headers: Optional headers for table format
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Formatted list output
|
|
201
|
+
"""
|
|
202
|
+
if not items:
|
|
203
|
+
empty_msg = f"No {title.lower() if title else 'items'} found"
|
|
204
|
+
if format_type in ("json", "yaml"):
|
|
205
|
+
return format_output({"items": [], "message": empty_msg}, format_type)
|
|
206
|
+
else:
|
|
207
|
+
return empty_msg
|
|
208
|
+
|
|
209
|
+
if format_type in ("json", "yaml"):
|
|
210
|
+
result = {"items": items}
|
|
211
|
+
if title:
|
|
212
|
+
result["title"] = title
|
|
213
|
+
return format_output(result, format_type)
|
|
214
|
+
|
|
215
|
+
elif format_type == "table":
|
|
216
|
+
output = ""
|
|
217
|
+
if title:
|
|
218
|
+
output += f"{title}\n{'=' * len(title)}\n\n"
|
|
219
|
+
output += format_output(items, "table", headers=headers)
|
|
220
|
+
return output
|
|
221
|
+
|
|
222
|
+
else:
|
|
223
|
+
# Text format
|
|
224
|
+
lines = []
|
|
225
|
+
if title:
|
|
226
|
+
lines.append(title)
|
|
227
|
+
lines.append("=" * len(title))
|
|
228
|
+
lines.append("")
|
|
229
|
+
|
|
230
|
+
lines.append(format_output(items, "text"))
|
|
231
|
+
return "\n".join(lines)
|