omnimancer-cli 0.1.0__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.
- omnimancer/__init__.py +42 -0
- omnimancer/__main__.py +9 -0
- omnimancer/cli/__init__.py +8 -0
- omnimancer/cli/agent_manager.py +826 -0
- omnimancer/cli/agent_wizard.py +944 -0
- omnimancer/cli/approval_formatter.py +734 -0
- omnimancer/cli/approval_integration.py +734 -0
- omnimancer/cli/approval_prompt.py +1286 -0
- omnimancer/cli/batch_approval_display.py +617 -0
- omnimancer/cli/batch_approval_filters.py +729 -0
- omnimancer/cli/batch_operation_monitor.py +775 -0
- omnimancer/cli/commands/performance_cmd.py +624 -0
- omnimancer/cli/commands.py +757 -0
- omnimancer/cli/completion_handler.py +281 -0
- omnimancer/cli/config_setup_wizard.py +783 -0
- omnimancer/cli/dynamic_commands.py +371 -0
- omnimancer/cli/handlers/__init__.py +18 -0
- omnimancer/cli/handlers/agent_handler.py +865 -0
- omnimancer/cli/handlers/agent_persona_handler.py +1048 -0
- omnimancer/cli/handlers/custom_agent_handler.py +352 -0
- omnimancer/cli/handlers/permissions_handler.py +1609 -0
- omnimancer/cli/interface.py +6695 -0
- omnimancer/cli/performance_dashboard.py +612 -0
- omnimancer/core/__init__.py +34 -0
- omnimancer/core/agent/__init__.py +256 -0
- omnimancer/core/agent/agent_config.py +715 -0
- omnimancer/core/agent/agent_switcher.py +668 -0
- omnimancer/core/agent/approval_context.py +460 -0
- omnimancer/core/agent/approval_dialog.py +902 -0
- omnimancer/core/agent/approval_interface.py +628 -0
- omnimancer/core/agent/approval_manager.py +760 -0
- omnimancer/core/agent/cli_integration.py +470 -0
- omnimancer/core/agent/config.py +845 -0
- omnimancer/core/agent/custom_agent_integration.py +654 -0
- omnimancer/core/agent/diff_renderer.py +706 -0
- omnimancer/core/agent/file_content_display.py +503 -0
- omnimancer/core/agent/file_modification_workflow.py +658 -0
- omnimancer/core/agent/file_system_manager.py +1790 -0
- omnimancer/core/agent/input_handler.py +614 -0
- omnimancer/core/agent/metrics_collector.py +771 -0
- omnimancer/core/agent/optimization_engine.py +729 -0
- omnimancer/core/agent/performance_monitor.py +717 -0
- omnimancer/core/agent/persona.py +870 -0
- omnimancer/core/agent/persona_validator.py +672 -0
- omnimancer/core/agent/program_executor.py +748 -0
- omnimancer/core/agent/proposed_changes_integration.py +663 -0
- omnimancer/core/agent/read_before_write_errors.py +492 -0
- omnimancer/core/agent/read_before_write_ui.py +647 -0
- omnimancer/core/agent/rich_renderer.py +763 -0
- omnimancer/core/agent/status_config.py +238 -0
- omnimancer/core/agent/status_core.py +446 -0
- omnimancer/core/agent/status_integrated_engine.py +540 -0
- omnimancer/core/agent/status_manager.py +878 -0
- omnimancer/core/agent/status_ui_components.py +535 -0
- omnimancer/core/agent/template_applicator.py +548 -0
- omnimancer/core/agent/terminal_status_listener.py +394 -0
- omnimancer/core/agent/token_tracker.py +679 -0
- omnimancer/core/agent/types.py +60 -0
- omnimancer/core/agent/web_client.py +745 -0
- omnimancer/core/agent/workflow_orchestrator.py +826 -0
- omnimancer/core/agent_engine.py +1625 -0
- omnimancer/core/agent_mode_manager.py +706 -0
- omnimancer/core/agent_progress_ui.py +545 -0
- omnimancer/core/chat_manager.py +141 -0
- omnimancer/core/config_manager.py +2199 -0
- omnimancer/core/config_migration.py +772 -0
- omnimancer/core/config_provider.py +1974 -0
- omnimancer/core/config_security_validator.py +393 -0
- omnimancer/core/config_validator.py +768 -0
- omnimancer/core/conversation_manager.py +352 -0
- omnimancer/core/engine.py +787 -0
- omnimancer/core/enhanced_input.py +523 -0
- omnimancer/core/env_loader.py +83 -0
- omnimancer/core/fallback_manager.py +568 -0
- omnimancer/core/health_monitor.py +462 -0
- omnimancer/core/history_manager.py +462 -0
- omnimancer/core/mcp_integration_layer.py +878 -0
- omnimancer/core/models.py +1889 -0
- omnimancer/core/provider_capabilities.py +403 -0
- omnimancer/core/provider_initializer.py +450 -0
- omnimancer/core/provider_registry.py +432 -0
- omnimancer/core/security/__init__.py +16 -0
- omnimancer/core/security/approval_workflow.py +406 -0
- omnimancer/core/security/audit_logger.py +511 -0
- omnimancer/core/security/permission_controller.py +511 -0
- omnimancer/core/security/sandbox_manager.py +379 -0
- omnimancer/core/security/security_manager.py +530 -0
- omnimancer/core/setup_wizard.py +345 -0
- omnimancer/core/setup_wizard_core.py +449 -0
- omnimancer/core/setup_wizard_guide_generator.py +376 -0
- omnimancer/core/setup_wizard_provider_setup.py +454 -0
- omnimancer/core/setup_wizard_ui.py +317 -0
- omnimancer/core/setup_wizard_validation.py +371 -0
- omnimancer/core/signal_handler.py +174 -0
- omnimancer/mcp/__init__.py +11 -0
- omnimancer/mcp/client.py +725 -0
- omnimancer/mcp/manager.py +649 -0
- omnimancer/providers/__init__.py +24 -0
- omnimancer/providers/azure.py +608 -0
- omnimancer/providers/base.py +262 -0
- omnimancer/providers/bedrock.py +711 -0
- omnimancer/providers/claude.py +426 -0
- omnimancer/providers/claude_code.py +406 -0
- omnimancer/providers/cohere.py +412 -0
- omnimancer/providers/factory.py +988 -0
- omnimancer/providers/gemini.py +708 -0
- omnimancer/providers/mistral.py +554 -0
- omnimancer/providers/ollama.py +510 -0
- omnimancer/providers/openai.py +362 -0
- omnimancer/providers/openrouter.py +709 -0
- omnimancer/providers/perplexity.py +476 -0
- omnimancer/providers/vertex.py +617 -0
- omnimancer/providers/xai.py +484 -0
- omnimancer/ui/__init__.py +6 -0
- omnimancer/ui/cancellation_handler.py +235 -0
- omnimancer/ui/progress_indicator.py +318 -0
- omnimancer/utils/__init__.py +22 -0
- omnimancer/utils/error_handler.py +589 -0
- omnimancer/utils/errors.py +667 -0
- omnimancer/utils/retry.py +238 -0
- omnimancer_cli-0.1.0.dist-info/METADATA +413 -0
- omnimancer_cli-0.1.0.dist-info/RECORD +126 -0
- omnimancer_cli-0.1.0.dist-info/WHEEL +5 -0
- omnimancer_cli-0.1.0.dist-info/entry_points.txt +4 -0
- omnimancer_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- omnimancer_cli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
"""
|
|
2
|
+
User Interface for Approval System.
|
|
3
|
+
|
|
4
|
+
This module provides interactive user interfaces for handling approval requests,
|
|
5
|
+
including CLI prompts, formatted displays, and batch approval workflows.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from ..security.approval_workflow import ApprovalRequest, RiskLevel
|
|
12
|
+
from .approval_manager import (
|
|
13
|
+
BatchApprovalRequest,
|
|
14
|
+
ChangePreview,
|
|
15
|
+
EnhancedApprovalManager,
|
|
16
|
+
)
|
|
17
|
+
from .types import Operation, OperationType
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ApprovalChoice(Enum):
|
|
21
|
+
"""User approval choices."""
|
|
22
|
+
|
|
23
|
+
APPROVE = "approve"
|
|
24
|
+
DENY = "deny"
|
|
25
|
+
SKIP = "skip"
|
|
26
|
+
APPROVE_ALL = "approve_all"
|
|
27
|
+
DENY_ALL = "deny_all"
|
|
28
|
+
VIEW_DETAILS = "details"
|
|
29
|
+
QUIT = "quit"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ApprovalInterface:
|
|
33
|
+
"""
|
|
34
|
+
Interactive user interface for handling approval requests.
|
|
35
|
+
|
|
36
|
+
Provides CLI-based approval interface with formatted previews,
|
|
37
|
+
diff visualization, and batch approval capabilities.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, approval_manager: EnhancedApprovalManager):
|
|
41
|
+
"""
|
|
42
|
+
Initialize approval interface.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
approval_manager: Enhanced approval manager instance
|
|
46
|
+
"""
|
|
47
|
+
self.approval_manager = approval_manager
|
|
48
|
+
self.approval_manager.set_approval_callback(self.handle_single_approval)
|
|
49
|
+
self.approval_manager.set_batch_approval_callback(self.handle_batch_approval)
|
|
50
|
+
|
|
51
|
+
# Interface configuration
|
|
52
|
+
self.show_colors = True
|
|
53
|
+
self.auto_show_diff = True
|
|
54
|
+
self.max_diff_lines = 50
|
|
55
|
+
self.page_size = 10
|
|
56
|
+
|
|
57
|
+
async def handle_single_approval(self, approval_data: Dict[str, Any]) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Handle approval request for a single operation.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
approval_data: Dictionary containing operation, preview, and approval request
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if approved, False if denied
|
|
66
|
+
"""
|
|
67
|
+
operation = approval_data["operation"]
|
|
68
|
+
preview = approval_data["preview"]
|
|
69
|
+
approval_request = approval_data["approval_request"]
|
|
70
|
+
risk_level = approval_data["risk_level"]
|
|
71
|
+
|
|
72
|
+
# Display operation summary
|
|
73
|
+
self._print_header(
|
|
74
|
+
f"Approval Request - {operation.type.value.replace('_', ' ').title()}"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Show operation details
|
|
78
|
+
self._print_operation_info(operation, risk_level)
|
|
79
|
+
|
|
80
|
+
# Show preview
|
|
81
|
+
self._print_preview(preview)
|
|
82
|
+
|
|
83
|
+
# Get user decision
|
|
84
|
+
while True:
|
|
85
|
+
choice = await self._get_user_choice(
|
|
86
|
+
[
|
|
87
|
+
ApprovalChoice.APPROVE,
|
|
88
|
+
ApprovalChoice.DENY,
|
|
89
|
+
ApprovalChoice.VIEW_DETAILS,
|
|
90
|
+
ApprovalChoice.QUIT,
|
|
91
|
+
]
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if choice == ApprovalChoice.APPROVE:
|
|
95
|
+
self._print_success("✓ Operation approved")
|
|
96
|
+
return True
|
|
97
|
+
elif choice == ApprovalChoice.DENY:
|
|
98
|
+
self._print_warning("✗ Operation denied")
|
|
99
|
+
return False
|
|
100
|
+
elif choice == ApprovalChoice.VIEW_DETAILS:
|
|
101
|
+
await self._show_detailed_view(operation, preview, approval_request)
|
|
102
|
+
elif choice == ApprovalChoice.QUIT:
|
|
103
|
+
self._print_info("Approval cancelled by user")
|
|
104
|
+
return False
|
|
105
|
+
|
|
106
|
+
async def handle_batch_approval(
|
|
107
|
+
self, batch_request: BatchApprovalRequest
|
|
108
|
+
) -> Dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
Handle approval request for a batch of operations.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
batch_request: Batch approval request
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Dictionary with approval decisions
|
|
117
|
+
"""
|
|
118
|
+
self._print_header(
|
|
119
|
+
f"Batch Approval Request - {len(batch_request.operations)} Operations"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Show batch summary
|
|
123
|
+
self._print_batch_summary(batch_request)
|
|
124
|
+
|
|
125
|
+
# Handle different batch approval modes
|
|
126
|
+
approval_mode = await self._get_batch_approval_mode()
|
|
127
|
+
|
|
128
|
+
if approval_mode == "all_at_once":
|
|
129
|
+
return await self._handle_bulk_approval(batch_request)
|
|
130
|
+
elif approval_mode == "individual":
|
|
131
|
+
return await self._handle_individual_approval(batch_request)
|
|
132
|
+
elif approval_mode == "filtered":
|
|
133
|
+
return await self._handle_filtered_approval(batch_request)
|
|
134
|
+
else:
|
|
135
|
+
return {
|
|
136
|
+
"deny_all": True,
|
|
137
|
+
"reason": "User cancelled batch approval",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async def _handle_bulk_approval(
|
|
141
|
+
self, batch_request: BatchApprovalRequest
|
|
142
|
+
) -> Dict[str, Any]:
|
|
143
|
+
"""Handle bulk approval/denial of all operations."""
|
|
144
|
+
self._print_info("Choose action for all operations:")
|
|
145
|
+
|
|
146
|
+
choice = await self._get_user_choice(
|
|
147
|
+
[
|
|
148
|
+
ApprovalChoice.APPROVE_ALL,
|
|
149
|
+
ApprovalChoice.DENY_ALL,
|
|
150
|
+
ApprovalChoice.QUIT,
|
|
151
|
+
]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if choice == ApprovalChoice.APPROVE_ALL:
|
|
155
|
+
self._print_success(
|
|
156
|
+
f"✓ All {len(batch_request.operations)} operations approved"
|
|
157
|
+
)
|
|
158
|
+
return {"approve_all": True}
|
|
159
|
+
elif choice == ApprovalChoice.DENY_ALL:
|
|
160
|
+
reason = input("Reason for denial (optional): ").strip()
|
|
161
|
+
self._print_warning(
|
|
162
|
+
f"✗ All {len(batch_request.operations)} operations denied"
|
|
163
|
+
)
|
|
164
|
+
return {
|
|
165
|
+
"deny_all": True,
|
|
166
|
+
"reason": reason or "User denied all operations",
|
|
167
|
+
}
|
|
168
|
+
else:
|
|
169
|
+
return {"deny_all": True, "reason": "User cancelled"}
|
|
170
|
+
|
|
171
|
+
async def _handle_individual_approval(
|
|
172
|
+
self, batch_request: BatchApprovalRequest
|
|
173
|
+
) -> Dict[str, Any]:
|
|
174
|
+
"""Handle individual approval of each operation in batch."""
|
|
175
|
+
approved_indices = []
|
|
176
|
+
|
|
177
|
+
for i, (operation, preview) in enumerate(
|
|
178
|
+
zip(batch_request.operations, batch_request.previews)
|
|
179
|
+
):
|
|
180
|
+
self._print_separator()
|
|
181
|
+
self._print_info(f"Operation {i + 1} of {len(batch_request.operations)}")
|
|
182
|
+
|
|
183
|
+
# Show operation details
|
|
184
|
+
self._print_operation_info(operation)
|
|
185
|
+
self._print_preview(preview, compact=True)
|
|
186
|
+
|
|
187
|
+
# Get decision for this operation
|
|
188
|
+
while True:
|
|
189
|
+
choice = await self._get_user_choice(
|
|
190
|
+
[
|
|
191
|
+
ApprovalChoice.APPROVE,
|
|
192
|
+
ApprovalChoice.DENY,
|
|
193
|
+
ApprovalChoice.SKIP,
|
|
194
|
+
ApprovalChoice.VIEW_DETAILS,
|
|
195
|
+
ApprovalChoice.APPROVE_ALL,
|
|
196
|
+
ApprovalChoice.DENY_ALL,
|
|
197
|
+
ApprovalChoice.QUIT,
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
if choice == ApprovalChoice.APPROVE:
|
|
202
|
+
approved_indices.append(i)
|
|
203
|
+
self._print_success("✓ Approved")
|
|
204
|
+
break
|
|
205
|
+
elif choice == ApprovalChoice.DENY:
|
|
206
|
+
self._print_warning("✗ Denied")
|
|
207
|
+
break
|
|
208
|
+
elif choice == ApprovalChoice.SKIP:
|
|
209
|
+
self._print_info("→ Skipped")
|
|
210
|
+
break
|
|
211
|
+
elif choice == ApprovalChoice.VIEW_DETAILS:
|
|
212
|
+
await self._show_detailed_view(operation, preview)
|
|
213
|
+
elif choice == ApprovalChoice.APPROVE_ALL:
|
|
214
|
+
# Approve remaining operations
|
|
215
|
+
approved_indices.extend(range(i, len(batch_request.operations)))
|
|
216
|
+
self._print_success(f"✓ Approved all remaining operations")
|
|
217
|
+
return {"approved_indices": approved_indices}
|
|
218
|
+
elif choice == ApprovalChoice.DENY_ALL:
|
|
219
|
+
self._print_warning("✗ Denied all remaining operations")
|
|
220
|
+
return {"approved_indices": approved_indices}
|
|
221
|
+
elif choice == ApprovalChoice.QUIT:
|
|
222
|
+
return {"approved_indices": approved_indices}
|
|
223
|
+
|
|
224
|
+
return {"approved_indices": approved_indices}
|
|
225
|
+
|
|
226
|
+
async def _handle_filtered_approval(
|
|
227
|
+
self, batch_request: BatchApprovalRequest
|
|
228
|
+
) -> Dict[str, Any]:
|
|
229
|
+
"""Handle filtered approval based on operation types or risk levels."""
|
|
230
|
+
self._print_info("Filter options:")
|
|
231
|
+
print("1. Approve all low-risk operations")
|
|
232
|
+
print("2. Approve all read-only operations")
|
|
233
|
+
print("3. Approve by operation type")
|
|
234
|
+
print("4. Custom selection")
|
|
235
|
+
print("5. Cancel")
|
|
236
|
+
|
|
237
|
+
choice = input("Choose filter option (1-5): ").strip()
|
|
238
|
+
approved_indices = []
|
|
239
|
+
|
|
240
|
+
if choice == "1":
|
|
241
|
+
# Approve low-risk operations
|
|
242
|
+
for i, preview in enumerate(batch_request.previews):
|
|
243
|
+
if preview.risk_assessment and "low" in preview.risk_assessment.lower():
|
|
244
|
+
approved_indices.append(i)
|
|
245
|
+
self._print_success(
|
|
246
|
+
f"✓ Approved {len(approved_indices)} low-risk operations"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
elif choice == "2":
|
|
250
|
+
# Approve read-only operations
|
|
251
|
+
for i, operation in enumerate(batch_request.operations):
|
|
252
|
+
if operation.type in [OperationType.FILE_READ]:
|
|
253
|
+
approved_indices.append(i)
|
|
254
|
+
self._print_success(
|
|
255
|
+
f"✓ Approved {len(approved_indices)} read-only operations"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
elif choice == "3":
|
|
259
|
+
# Approve by operation type
|
|
260
|
+
operation_types = list(set(op.type for op in batch_request.operations))
|
|
261
|
+
print("Available operation types:")
|
|
262
|
+
for idx, op_type in enumerate(operation_types, 1):
|
|
263
|
+
print(f"{idx}. {op_type.value}")
|
|
264
|
+
|
|
265
|
+
type_choice = input(
|
|
266
|
+
"Select operation types to approve (comma-separated numbers): "
|
|
267
|
+
).strip()
|
|
268
|
+
try:
|
|
269
|
+
selected_indices = [int(x.strip()) - 1 for x in type_choice.split(",")]
|
|
270
|
+
selected_types = [
|
|
271
|
+
operation_types[i]
|
|
272
|
+
for i in selected_indices
|
|
273
|
+
if 0 <= i < len(operation_types)
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
for i, operation in enumerate(batch_request.operations):
|
|
277
|
+
if operation.type in selected_types:
|
|
278
|
+
approved_indices.append(i)
|
|
279
|
+
|
|
280
|
+
self._print_success(
|
|
281
|
+
f"✓ Approved {len(approved_indices)} operations of selected types"
|
|
282
|
+
)
|
|
283
|
+
except (ValueError, IndexError):
|
|
284
|
+
self._print_error("Invalid selection")
|
|
285
|
+
return {"deny_all": True, "reason": "Invalid filter selection"}
|
|
286
|
+
|
|
287
|
+
elif choice == "4":
|
|
288
|
+
# Custom selection
|
|
289
|
+
print("Enter operation indices to approve (comma-separated, 1-based):")
|
|
290
|
+
indices_input = input(
|
|
291
|
+
f"Operations 1-{len(batch_request.operations)}: "
|
|
292
|
+
).strip()
|
|
293
|
+
try:
|
|
294
|
+
approved_indices = [
|
|
295
|
+
int(x.strip()) - 1 for x in indices_input.split(",")
|
|
296
|
+
]
|
|
297
|
+
approved_indices = [
|
|
298
|
+
i
|
|
299
|
+
for i in approved_indices
|
|
300
|
+
if 0 <= i < len(batch_request.operations)
|
|
301
|
+
]
|
|
302
|
+
self._print_success(
|
|
303
|
+
f"✓ Approved {len(approved_indices)} selected operations"
|
|
304
|
+
)
|
|
305
|
+
except ValueError:
|
|
306
|
+
self._print_error("Invalid indices")
|
|
307
|
+
return {"deny_all": True, "reason": "Invalid custom selection"}
|
|
308
|
+
|
|
309
|
+
else:
|
|
310
|
+
return {"deny_all": True, "reason": "User cancelled"}
|
|
311
|
+
|
|
312
|
+
return {"approved_indices": approved_indices}
|
|
313
|
+
|
|
314
|
+
async def _get_batch_approval_mode(self) -> str:
|
|
315
|
+
"""Get user preference for batch approval mode."""
|
|
316
|
+
self._print_info("Batch approval modes:")
|
|
317
|
+
print("1. All at once (approve/deny all)")
|
|
318
|
+
print("2. Individual review")
|
|
319
|
+
print("3. Filtered approval")
|
|
320
|
+
print("0. Cancel")
|
|
321
|
+
|
|
322
|
+
while True:
|
|
323
|
+
choice = input("Select mode (0-3): ").strip()
|
|
324
|
+
if choice == "1":
|
|
325
|
+
return "all_at_once"
|
|
326
|
+
elif choice == "2":
|
|
327
|
+
return "individual"
|
|
328
|
+
elif choice == "3":
|
|
329
|
+
return "filtered"
|
|
330
|
+
elif choice == "0":
|
|
331
|
+
return "cancel"
|
|
332
|
+
else:
|
|
333
|
+
self._print_error("Invalid choice. Please select 0-3.")
|
|
334
|
+
|
|
335
|
+
async def _get_user_choice(
|
|
336
|
+
self, available_choices: List[ApprovalChoice]
|
|
337
|
+
) -> ApprovalChoice:
|
|
338
|
+
"""Get user choice from available options."""
|
|
339
|
+
choice_map = {
|
|
340
|
+
"y": ApprovalChoice.APPROVE,
|
|
341
|
+
"yes": ApprovalChoice.APPROVE,
|
|
342
|
+
"a": ApprovalChoice.APPROVE,
|
|
343
|
+
"approve": ApprovalChoice.APPROVE,
|
|
344
|
+
"n": ApprovalChoice.DENY,
|
|
345
|
+
"no": ApprovalChoice.DENY,
|
|
346
|
+
"d": ApprovalChoice.DENY,
|
|
347
|
+
"deny": ApprovalChoice.DENY,
|
|
348
|
+
"s": ApprovalChoice.SKIP,
|
|
349
|
+
"skip": ApprovalChoice.SKIP,
|
|
350
|
+
"aa": ApprovalChoice.APPROVE_ALL,
|
|
351
|
+
"all": ApprovalChoice.APPROVE_ALL,
|
|
352
|
+
"approve_all": ApprovalChoice.APPROVE_ALL,
|
|
353
|
+
"dd": ApprovalChoice.DENY_ALL,
|
|
354
|
+
"deny_all": ApprovalChoice.DENY_ALL,
|
|
355
|
+
"v": ApprovalChoice.VIEW_DETAILS,
|
|
356
|
+
"details": ApprovalChoice.VIEW_DETAILS,
|
|
357
|
+
"view": ApprovalChoice.VIEW_DETAILS,
|
|
358
|
+
"q": ApprovalChoice.QUIT,
|
|
359
|
+
"quit": ApprovalChoice.QUIT,
|
|
360
|
+
"exit": ApprovalChoice.QUIT,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# Build prompt with available choices
|
|
364
|
+
prompt_parts = []
|
|
365
|
+
for choice in available_choices:
|
|
366
|
+
if choice == ApprovalChoice.APPROVE:
|
|
367
|
+
prompt_parts.append("[Y]es/[A]pprove")
|
|
368
|
+
elif choice == ApprovalChoice.DENY:
|
|
369
|
+
prompt_parts.append("[N]o/[D]eny")
|
|
370
|
+
elif choice == ApprovalChoice.SKIP:
|
|
371
|
+
prompt_parts.append("[S]kip")
|
|
372
|
+
elif choice == ApprovalChoice.APPROVE_ALL:
|
|
373
|
+
prompt_parts.append("Approve [A]ll")
|
|
374
|
+
elif choice == ApprovalChoice.DENY_ALL:
|
|
375
|
+
prompt_parts.append("[D]eny All")
|
|
376
|
+
elif choice == ApprovalChoice.VIEW_DETAILS:
|
|
377
|
+
prompt_parts.append("[V]iew Details")
|
|
378
|
+
elif choice == ApprovalChoice.QUIT:
|
|
379
|
+
prompt_parts.append("[Q]uit")
|
|
380
|
+
|
|
381
|
+
prompt = f"Choose: {'/'.join(prompt_parts)}: "
|
|
382
|
+
|
|
383
|
+
while True:
|
|
384
|
+
user_input = input(prompt).strip().lower()
|
|
385
|
+
|
|
386
|
+
if user_input in choice_map:
|
|
387
|
+
choice = choice_map[user_input]
|
|
388
|
+
if choice in available_choices:
|
|
389
|
+
return choice
|
|
390
|
+
else:
|
|
391
|
+
self._print_error(
|
|
392
|
+
f"'{user_input}' is not available in this context"
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
self._print_error(f"Invalid choice: '{user_input}'")
|
|
396
|
+
|
|
397
|
+
async def _show_detailed_view(
|
|
398
|
+
self,
|
|
399
|
+
operation: Operation,
|
|
400
|
+
preview: ChangePreview,
|
|
401
|
+
approval_request: Optional[ApprovalRequest] = None,
|
|
402
|
+
):
|
|
403
|
+
"""Show detailed view of operation and preview."""
|
|
404
|
+
self._print_header("Detailed View")
|
|
405
|
+
|
|
406
|
+
# Operation details
|
|
407
|
+
print("OPERATION DETAILS:")
|
|
408
|
+
print(f" Type: {operation.type.value}")
|
|
409
|
+
print(f" Description: {operation.description}")
|
|
410
|
+
print(f" Requires Approval: {operation.requires_approval}")
|
|
411
|
+
print(f" Reversible: {operation.reversible}")
|
|
412
|
+
|
|
413
|
+
if operation.data:
|
|
414
|
+
print(" Data:")
|
|
415
|
+
for key, value in operation.data.items():
|
|
416
|
+
print(f" {key}: {value}")
|
|
417
|
+
|
|
418
|
+
print()
|
|
419
|
+
|
|
420
|
+
# Preview details
|
|
421
|
+
print("PREVIEW DETAILS:")
|
|
422
|
+
print(f" Change Type: {preview.change_type.value}")
|
|
423
|
+
print(f" Risk Assessment: {preview.risk_assessment}")
|
|
424
|
+
print(f" Reversible: {preview.reversible}")
|
|
425
|
+
|
|
426
|
+
if preview.metadata:
|
|
427
|
+
print(" Metadata:")
|
|
428
|
+
for key, value in preview.metadata.items():
|
|
429
|
+
print(f" {key}: {value}")
|
|
430
|
+
|
|
431
|
+
print()
|
|
432
|
+
|
|
433
|
+
# Show diff if available
|
|
434
|
+
if preview.diff and self.auto_show_diff:
|
|
435
|
+
print("CHANGES:")
|
|
436
|
+
diff_lines = preview.diff.split("\n")
|
|
437
|
+
if len(diff_lines) > self.max_diff_lines:
|
|
438
|
+
print(f"Showing first {self.max_diff_lines} lines of diff:")
|
|
439
|
+
for line in diff_lines[: self.max_diff_lines]:
|
|
440
|
+
self._print_diff_line(line)
|
|
441
|
+
print(f"... ({len(diff_lines) - self.max_diff_lines} more lines)")
|
|
442
|
+
else:
|
|
443
|
+
for line in diff_lines:
|
|
444
|
+
self._print_diff_line(line)
|
|
445
|
+
|
|
446
|
+
# Approval request details
|
|
447
|
+
if approval_request:
|
|
448
|
+
print()
|
|
449
|
+
print("APPROVAL REQUEST:")
|
|
450
|
+
print(f" Request ID: {approval_request.id}")
|
|
451
|
+
print(f" Risk Level: {approval_request.risk_level.value}")
|
|
452
|
+
print(f" Requested At: {approval_request.requested_at}")
|
|
453
|
+
if approval_request.expires_at:
|
|
454
|
+
remaining = approval_request.time_remaining()
|
|
455
|
+
print(f" Time Remaining: {remaining}")
|
|
456
|
+
|
|
457
|
+
input("\\nPress Enter to continue...")
|
|
458
|
+
|
|
459
|
+
def _print_operation_info(
|
|
460
|
+
self, operation: Operation, risk_level: Optional[RiskLevel] = None
|
|
461
|
+
):
|
|
462
|
+
"""Print formatted operation information."""
|
|
463
|
+
print(f"Operation: {operation.type.value.replace('_', ' ').title()}")
|
|
464
|
+
print(f"Description: {operation.description}")
|
|
465
|
+
|
|
466
|
+
if risk_level:
|
|
467
|
+
risk_color = self._get_risk_color(risk_level)
|
|
468
|
+
print(f"Risk Level: {self._colorize(risk_level.value.upper(), risk_color)}")
|
|
469
|
+
|
|
470
|
+
print(f"Reversible: {'Yes' if operation.reversible else 'No'}")
|
|
471
|
+
print()
|
|
472
|
+
|
|
473
|
+
def _print_preview(self, preview: ChangePreview, compact: bool = False):
|
|
474
|
+
"""Print formatted preview information."""
|
|
475
|
+
if compact:
|
|
476
|
+
print(f"Preview: {preview.description}")
|
|
477
|
+
if preview.risk_assessment:
|
|
478
|
+
print(f"Risk: {preview.risk_assessment}")
|
|
479
|
+
else:
|
|
480
|
+
print("PREVIEW:")
|
|
481
|
+
print(f" {preview.description}")
|
|
482
|
+
if preview.risk_assessment:
|
|
483
|
+
print(f" Risk: {preview.risk_assessment}")
|
|
484
|
+
|
|
485
|
+
if preview.diff and len(preview.diff.split("\n")) <= 10:
|
|
486
|
+
print(" Changes:")
|
|
487
|
+
for line in preview.diff.split("\n")[:5]: # Show first 5 lines
|
|
488
|
+
print(f" {line}")
|
|
489
|
+
if len(preview.diff.split("\n")) > 5:
|
|
490
|
+
print(" ... (use 'details' to see full diff)")
|
|
491
|
+
print()
|
|
492
|
+
|
|
493
|
+
def _print_batch_summary(self, batch_request: BatchApprovalRequest):
|
|
494
|
+
"""Print summary of batch request."""
|
|
495
|
+
print(f"Total Operations: {len(batch_request.operations)}")
|
|
496
|
+
print(f"Created: {batch_request.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
497
|
+
|
|
498
|
+
if batch_request.expires_at:
|
|
499
|
+
print(f"Expires: {batch_request.expires_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
500
|
+
|
|
501
|
+
# Operation type breakdown
|
|
502
|
+
type_counts = {}
|
|
503
|
+
for operation in batch_request.operations:
|
|
504
|
+
type_counts[operation.type] = type_counts.get(operation.type, 0) + 1
|
|
505
|
+
|
|
506
|
+
print("\\nOperation Types:")
|
|
507
|
+
for op_type, count in type_counts.items():
|
|
508
|
+
print(f" {op_type.value}: {count}")
|
|
509
|
+
|
|
510
|
+
# Risk level breakdown
|
|
511
|
+
risk_counts = {}
|
|
512
|
+
for preview in batch_request.previews:
|
|
513
|
+
if preview.risk_assessment:
|
|
514
|
+
# Extract risk level from assessment
|
|
515
|
+
if "low" in preview.risk_assessment.lower():
|
|
516
|
+
risk = "low"
|
|
517
|
+
elif "medium" in preview.risk_assessment.lower():
|
|
518
|
+
risk = "medium"
|
|
519
|
+
elif "high" in preview.risk_assessment.lower():
|
|
520
|
+
risk = "high"
|
|
521
|
+
elif "critical" in preview.risk_assessment.lower():
|
|
522
|
+
risk = "critical"
|
|
523
|
+
else:
|
|
524
|
+
risk = "unknown"
|
|
525
|
+
risk_counts[risk] = risk_counts.get(risk, 0) + 1
|
|
526
|
+
|
|
527
|
+
if risk_counts:
|
|
528
|
+
print("\\nRisk Distribution:")
|
|
529
|
+
for risk, count in risk_counts.items():
|
|
530
|
+
color = self._get_risk_color_by_name(risk)
|
|
531
|
+
print(f" {self._colorize(risk.title(), color)}: {count}")
|
|
532
|
+
|
|
533
|
+
print()
|
|
534
|
+
|
|
535
|
+
def _print_diff_line(self, line: str):
|
|
536
|
+
"""Print a diff line with appropriate coloring."""
|
|
537
|
+
if not self.show_colors:
|
|
538
|
+
print(line)
|
|
539
|
+
return
|
|
540
|
+
|
|
541
|
+
if line.startswith("+"):
|
|
542
|
+
print(self._colorize(line, "green"))
|
|
543
|
+
elif line.startswith("-"):
|
|
544
|
+
print(self._colorize(line, "red"))
|
|
545
|
+
elif line.startswith("@@"):
|
|
546
|
+
print(self._colorize(line, "cyan"))
|
|
547
|
+
else:
|
|
548
|
+
print(line)
|
|
549
|
+
|
|
550
|
+
def _get_risk_color(self, risk_level: RiskLevel) -> str:
|
|
551
|
+
"""Get color for risk level."""
|
|
552
|
+
colors = {
|
|
553
|
+
RiskLevel.LOW: "green",
|
|
554
|
+
RiskLevel.MEDIUM: "yellow",
|
|
555
|
+
RiskLevel.HIGH: "red",
|
|
556
|
+
RiskLevel.CRITICAL: "magenta",
|
|
557
|
+
}
|
|
558
|
+
return colors.get(risk_level, "white")
|
|
559
|
+
|
|
560
|
+
def _get_risk_color_by_name(self, risk_name: str) -> str:
|
|
561
|
+
"""Get color for risk level by name."""
|
|
562
|
+
colors = {
|
|
563
|
+
"low": "green",
|
|
564
|
+
"medium": "yellow",
|
|
565
|
+
"high": "red",
|
|
566
|
+
"critical": "magenta",
|
|
567
|
+
}
|
|
568
|
+
return colors.get(risk_name.lower(), "white")
|
|
569
|
+
|
|
570
|
+
def _colorize(self, text: str, color: str) -> str:
|
|
571
|
+
"""Add color to text if colors are enabled."""
|
|
572
|
+
if not self.show_colors:
|
|
573
|
+
return text
|
|
574
|
+
|
|
575
|
+
colors = {
|
|
576
|
+
"red": "\033[91m",
|
|
577
|
+
"green": "\033[92m",
|
|
578
|
+
"yellow": "\033[93m",
|
|
579
|
+
"blue": "\033[94m",
|
|
580
|
+
"magenta": "\033[95m",
|
|
581
|
+
"cyan": "\033[96m",
|
|
582
|
+
"white": "\033[97m",
|
|
583
|
+
"reset": "\033[0m",
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
color_code = colors.get(color.lower(), colors["white"])
|
|
587
|
+
reset_code = colors["reset"]
|
|
588
|
+
return f"{color_code}{text}{reset_code}"
|
|
589
|
+
|
|
590
|
+
def _print_header(self, title: str):
|
|
591
|
+
"""Print formatted header."""
|
|
592
|
+
print()
|
|
593
|
+
print("=" * 60)
|
|
594
|
+
print(f" {title}")
|
|
595
|
+
print("=" * 60)
|
|
596
|
+
print()
|
|
597
|
+
|
|
598
|
+
def _print_separator(self):
|
|
599
|
+
"""Print separator line."""
|
|
600
|
+
print("-" * 40)
|
|
601
|
+
|
|
602
|
+
def _print_success(self, message: str):
|
|
603
|
+
"""Print success message."""
|
|
604
|
+
print(self._colorize(message, "green"))
|
|
605
|
+
|
|
606
|
+
def _print_warning(self, message: str):
|
|
607
|
+
"""Print warning message."""
|
|
608
|
+
print(self._colorize(message, "yellow"))
|
|
609
|
+
|
|
610
|
+
def _print_error(self, message: str):
|
|
611
|
+
"""Print error message."""
|
|
612
|
+
print(self._colorize(message, "red"))
|
|
613
|
+
|
|
614
|
+
def _print_info(self, message: str):
|
|
615
|
+
"""Print info message."""
|
|
616
|
+
print(self._colorize(message, "cyan"))
|
|
617
|
+
|
|
618
|
+
def set_colors_enabled(self, enabled: bool):
|
|
619
|
+
"""Enable or disable colored output."""
|
|
620
|
+
self.show_colors = enabled
|
|
621
|
+
|
|
622
|
+
def set_auto_show_diff(self, enabled: bool):
|
|
623
|
+
"""Enable or disable automatic diff display."""
|
|
624
|
+
self.auto_show_diff = enabled
|
|
625
|
+
|
|
626
|
+
def set_max_diff_lines(self, max_lines: int):
|
|
627
|
+
"""Set maximum number of diff lines to display."""
|
|
628
|
+
self.max_diff_lines = max_lines
|