qodalis-cli-server-plugin-aws 1.0.0b13__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.
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from typing import Any
6
+
7
+ from qodalis_cli_server_abstractions import (
8
+ get_positional_text,
9
+ CliCommandParameterDescriptor,
10
+ CliCommandProcessor,
11
+ CliProcessCommand,
12
+ ICliCommandParameterDescriptor,
13
+ ICliCommandProcessor,
14
+ )
15
+
16
+ from ..services.aws_credential_manager import AwsCredentialManager
17
+ from ..utils.output_helpers import (
18
+ build_response,
19
+ build_error_response,
20
+ format_as_table,
21
+ apply_output_format,
22
+ CliServerTextOutput,
23
+ )
24
+
25
+
26
+ class _EcsClustersProcessor(CliCommandProcessor):
27
+ """Lists all ECS clusters with status and task counts."""
28
+
29
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
30
+ super().__init__()
31
+ self._credential_manager = credential_manager
32
+
33
+ @property
34
+ def command(self) -> str:
35
+ return "clusters"
36
+
37
+ @property
38
+ def description(self) -> str:
39
+ return "List ECS clusters"
40
+
41
+ @property
42
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
43
+ return [
44
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
45
+ CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
46
+ ]
47
+
48
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
49
+ return ""
50
+
51
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
52
+ """Fetches cluster ARNs and describes each cluster's status."""
53
+ region = command.args.get("region")
54
+ client = self._credential_manager.get_client("ecs", region=str(region) if region else None)
55
+
56
+ try:
57
+ list_response = client.list_clusters()
58
+ cluster_arns = list_response.get("clusterArns", [])
59
+
60
+ if not cluster_arns:
61
+ return build_response([CliServerTextOutput(value="No ECS clusters found.", style="warning")])
62
+
63
+ describe_response = client.describe_clusters(clusters=cluster_arns)
64
+ clusters = describe_response.get("clusters", [])
65
+
66
+ rows = [
67
+ [
68
+ c.get("clusterName", "(unknown)"),
69
+ c.get("status", "(unknown)"),
70
+ str(c.get("runningTasksCount", 0)),
71
+ str(c.get("pendingTasksCount", 0)),
72
+ ]
73
+ for c in clusters
74
+ ]
75
+
76
+ table_output = format_as_table(["Cluster Name", "Status", "Running Tasks", "Pending Tasks"], rows)
77
+ return build_response([apply_output_format(command, table_output, clusters)])
78
+ except Exception as exc:
79
+ return build_error_response(f"Failed to list ECS clusters: {exc}")
80
+
81
+
82
+ class _EcsServicesProcessor(CliCommandProcessor):
83
+ """Lists ECS services in a specified cluster."""
84
+
85
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
86
+ super().__init__()
87
+ self._credential_manager = credential_manager
88
+
89
+ @property
90
+ def command(self) -> str:
91
+ return "services"
92
+
93
+ @property
94
+ def description(self) -> str:
95
+ return "List ECS services in a cluster"
96
+
97
+ @property
98
+ def value_required(self) -> bool | None:
99
+ return True
100
+
101
+ @property
102
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
103
+ return [
104
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
105
+ CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
106
+ ]
107
+
108
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
109
+ return ""
110
+
111
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
112
+ """Fetches and describes all services in the specified ECS cluster."""
113
+ cluster = get_positional_text(command)
114
+ if not cluster:
115
+ return build_error_response("Cluster name or ARN is required. Usage: ecs services <cluster>")
116
+
117
+ region = command.args.get("region")
118
+ client = self._credential_manager.get_client("ecs", region=str(region) if region else None)
119
+
120
+ try:
121
+ list_response = client.list_services(cluster=cluster)
122
+ service_arns = list_response.get("serviceArns", [])
123
+
124
+ if not service_arns:
125
+ return build_response([CliServerTextOutput(value=f'No ECS services found in cluster "{cluster}".', style="warning")])
126
+
127
+ describe_response = client.describe_services(cluster=cluster, services=service_arns)
128
+ services = describe_response.get("services", [])
129
+
130
+ rows = [
131
+ [
132
+ s.get("serviceName", "(unknown)"),
133
+ s.get("status", "(unknown)"),
134
+ str(s.get("desiredCount", 0)),
135
+ str(s.get("runningCount", 0)),
136
+ str(s.get("pendingCount", 0)),
137
+ ]
138
+ for s in services
139
+ ]
140
+
141
+ table_output = format_as_table(["Service Name", "Status", "Desired", "Running", "Pending"], rows)
142
+ return build_response([apply_output_format(command, table_output, services)])
143
+ except Exception as exc:
144
+ return build_error_response(f"Failed to list ECS services: {exc}")
145
+
146
+
147
+ class _EcsTasksProcessor(CliCommandProcessor):
148
+ """Lists ECS tasks in a specified cluster."""
149
+
150
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
151
+ super().__init__()
152
+ self._credential_manager = credential_manager
153
+
154
+ @property
155
+ def command(self) -> str:
156
+ return "tasks"
157
+
158
+ @property
159
+ def description(self) -> str:
160
+ return "List ECS tasks in a cluster"
161
+
162
+ @property
163
+ def value_required(self) -> bool | None:
164
+ return True
165
+
166
+ @property
167
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
168
+ return [
169
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
170
+ CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
171
+ ]
172
+
173
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
174
+ return ""
175
+
176
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
177
+ """Fetches and describes all tasks running in the specified ECS cluster."""
178
+ cluster = get_positional_text(command)
179
+ if not cluster:
180
+ return build_error_response("Cluster name or ARN is required. Usage: ecs tasks <cluster>")
181
+
182
+ region = command.args.get("region")
183
+ client = self._credential_manager.get_client("ecs", region=str(region) if region else None)
184
+
185
+ try:
186
+ list_response = client.list_tasks(cluster=cluster)
187
+ task_arns = list_response.get("taskArns", [])
188
+
189
+ if not task_arns:
190
+ return build_response([CliServerTextOutput(value=f'No ECS tasks found in cluster "{cluster}".', style="warning")])
191
+
192
+ describe_response = client.describe_tasks(cluster=cluster, tasks=task_arns)
193
+ tasks = describe_response.get("tasks", [])
194
+
195
+ rows = []
196
+ for task in tasks:
197
+ task_arn = task.get("taskArn", "")
198
+ task_id = task_arn.split("/")[-1] if task_arn else "(unknown)"
199
+ task_def_arn = task.get("taskDefinitionArn", "")
200
+ task_def = task_def_arn.split("/")[-1] if task_def_arn else "(unknown)"
201
+ started_at = task.get("startedAt")
202
+ started_str = started_at.strftime("%Y-%m-%d %H:%M:%S") if started_at else "(not started)"
203
+ rows.append([task_id, task.get("lastStatus", "(unknown)"), task_def, started_str])
204
+
205
+ table_output = format_as_table(["Task ID", "Status", "Definition", "Started"], rows)
206
+ return build_response([apply_output_format(command, table_output, tasks)])
207
+ except Exception as exc:
208
+ return build_error_response(f"Failed to list ECS tasks: {exc}")
209
+
210
+
211
+ class AwsEcsProcessor(CliCommandProcessor):
212
+ """Parent processor for ECS sub-commands (clusters, services, tasks)."""
213
+
214
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
215
+ super().__init__()
216
+ self._sub_processors: list[ICliCommandProcessor] = [
217
+ _EcsClustersProcessor(credential_manager),
218
+ _EcsServicesProcessor(credential_manager),
219
+ _EcsTasksProcessor(credential_manager),
220
+ ]
221
+
222
+ @property
223
+ def command(self) -> str:
224
+ return "ecs"
225
+
226
+ @property
227
+ def description(self) -> str:
228
+ return "AWS ECS operations -- clusters, services, tasks"
229
+
230
+ @property
231
+ def processors(self) -> list[ICliCommandProcessor]:
232
+ """Returns sub-processors for ECS operations."""
233
+ return self._sub_processors
234
+
235
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
236
+ return ""
@@ -0,0 +1,208 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ from typing import Any
6
+
7
+ from qodalis_cli_server_abstractions import (
8
+ CliCommandParameterDescriptor,
9
+ CliCommandProcessor,
10
+ CliProcessCommand,
11
+ ICliCommandParameterDescriptor,
12
+ ICliCommandProcessor,
13
+ )
14
+
15
+ from ..services.aws_credential_manager import AwsCredentialManager
16
+ from ..utils.output_helpers import (
17
+ build_response,
18
+ build_error_response,
19
+ format_as_table,
20
+ apply_output_format,
21
+ CliServerTextOutput,
22
+ )
23
+
24
+
25
+ class _IamUsersProcessor(CliCommandProcessor):
26
+ """Lists all IAM users."""
27
+
28
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
29
+ super().__init__()
30
+ self._credential_manager = credential_manager
31
+
32
+ @property
33
+ def command(self) -> str:
34
+ return "users"
35
+
36
+ @property
37
+ def description(self) -> str:
38
+ return "List IAM users"
39
+
40
+ @property
41
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
42
+ return [
43
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
44
+ CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
45
+ ]
46
+
47
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
48
+ return ""
49
+
50
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
51
+ """Fetches and returns all IAM users as a table."""
52
+ region = command.args.get("region")
53
+ client = self._credential_manager.get_client("iam", region=str(region) if region else None)
54
+
55
+ try:
56
+ response = client.list_users()
57
+ users = response.get("Users", [])
58
+
59
+ if not users:
60
+ return build_response([CliServerTextOutput(value="No IAM users found.", style="warning")])
61
+
62
+ rows = [
63
+ [
64
+ user.get("UserName", "(unknown)"),
65
+ user.get("UserId", "(unknown)"),
66
+ user.get("Arn", "(unknown)"),
67
+ user["CreateDate"].strftime("%Y-%m-%d") if user.get("CreateDate") else "(unknown)",
68
+ ]
69
+ for user in users
70
+ ]
71
+
72
+ table_output = format_as_table(["UserName", "UserId", "Arn", "CreateDate"], rows)
73
+ return build_response([apply_output_format(command, table_output, users)])
74
+ except Exception as exc:
75
+ return build_error_response(f"Failed to list IAM users: {exc}")
76
+
77
+
78
+ class _IamRolesProcessor(CliCommandProcessor):
79
+ """Lists all IAM roles."""
80
+
81
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
82
+ super().__init__()
83
+ self._credential_manager = credential_manager
84
+
85
+ @property
86
+ def command(self) -> str:
87
+ return "roles"
88
+
89
+ @property
90
+ def description(self) -> str:
91
+ return "List IAM roles"
92
+
93
+ @property
94
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
95
+ return [
96
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
97
+ CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
98
+ ]
99
+
100
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
101
+ return ""
102
+
103
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
104
+ """Fetches and returns all IAM roles as a table."""
105
+ region = command.args.get("region")
106
+ client = self._credential_manager.get_client("iam", region=str(region) if region else None)
107
+
108
+ try:
109
+ response = client.list_roles()
110
+ roles = response.get("Roles", [])
111
+
112
+ if not roles:
113
+ return build_response([CliServerTextOutput(value="No IAM roles found.", style="warning")])
114
+
115
+ rows = [
116
+ [
117
+ role.get("RoleName", "(unknown)"),
118
+ role.get("RoleId", "(unknown)"),
119
+ role.get("Arn", "(unknown)"),
120
+ role["CreateDate"].strftime("%Y-%m-%d") if role.get("CreateDate") else "(unknown)",
121
+ ]
122
+ for role in roles
123
+ ]
124
+
125
+ table_output = format_as_table(["RoleName", "RoleId", "Arn", "CreateDate"], rows)
126
+ return build_response([apply_output_format(command, table_output, roles)])
127
+ except Exception as exc:
128
+ return build_error_response(f"Failed to list IAM roles: {exc}")
129
+
130
+
131
+ class _IamPoliciesProcessor(CliCommandProcessor):
132
+ """Lists customer-managed IAM policies."""
133
+
134
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
135
+ super().__init__()
136
+ self._credential_manager = credential_manager
137
+
138
+ @property
139
+ def command(self) -> str:
140
+ return "policies"
141
+
142
+ @property
143
+ def description(self) -> str:
144
+ return "List IAM policies (local/customer-managed)"
145
+
146
+ @property
147
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
148
+ return [
149
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
150
+ CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
151
+ ]
152
+
153
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
154
+ return ""
155
+
156
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
157
+ """Fetches and returns customer-managed IAM policies as a table."""
158
+ region = command.args.get("region")
159
+ client = self._credential_manager.get_client("iam", region=str(region) if region else None)
160
+
161
+ try:
162
+ response = client.list_policies(Scope="Local")
163
+ policies = response.get("Policies", [])
164
+
165
+ if not policies:
166
+ return build_response([CliServerTextOutput(value="No IAM policies found.", style="warning")])
167
+
168
+ rows = [
169
+ [
170
+ policy.get("PolicyName", "(unknown)"),
171
+ policy.get("Arn", "(unknown)"),
172
+ str(policy.get("AttachmentCount", 0)),
173
+ ]
174
+ for policy in policies
175
+ ]
176
+
177
+ table_output = format_as_table(["PolicyName", "Arn", "AttachmentCount"], rows)
178
+ return build_response([apply_output_format(command, table_output, policies)])
179
+ except Exception as exc:
180
+ return build_error_response(f"Failed to list IAM policies: {exc}")
181
+
182
+
183
+ class AwsIamProcessor(CliCommandProcessor):
184
+ """Parent processor for IAM sub-commands (users, roles, policies)."""
185
+
186
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
187
+ super().__init__()
188
+ self._sub_processors: list[ICliCommandProcessor] = [
189
+ _IamUsersProcessor(credential_manager),
190
+ _IamRolesProcessor(credential_manager),
191
+ _IamPoliciesProcessor(credential_manager),
192
+ ]
193
+
194
+ @property
195
+ def command(self) -> str:
196
+ return "iam"
197
+
198
+ @property
199
+ def description(self) -> str:
200
+ return "AWS IAM operations -- users, roles, policies"
201
+
202
+ @property
203
+ def processors(self) -> list[ICliCommandProcessor]:
204
+ """Returns sub-processors for IAM operations."""
205
+ return self._sub_processors
206
+
207
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
208
+ return ""
@@ -0,0 +1,247 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ import json
6
+ from typing import Any
7
+
8
+ from qodalis_cli_server_abstractions import (
9
+ get_positional_text,
10
+ CliCommandParameterDescriptor,
11
+ CliCommandProcessor,
12
+ CliProcessCommand,
13
+ ICliCommandParameterDescriptor,
14
+ ICliCommandProcessor,
15
+ )
16
+
17
+ from ..services.aws_credential_manager import AwsCredentialManager
18
+ from ..utils.output_helpers import (
19
+ build_response,
20
+ build_error_response,
21
+ format_as_table,
22
+ format_as_json,
23
+ apply_output_format,
24
+ CliServerTextOutput,
25
+ )
26
+
27
+
28
+ class _LambdaListProcessor(CliCommandProcessor):
29
+ """Lists all Lambda functions with runtime and memory details."""
30
+
31
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
32
+ super().__init__()
33
+ self._credential_manager = credential_manager
34
+
35
+ @property
36
+ def command(self) -> str:
37
+ return "list"
38
+
39
+ @property
40
+ def description(self) -> str:
41
+ return "List Lambda functions"
42
+
43
+ @property
44
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
45
+ return [
46
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
47
+ CliCommandParameterDescriptor(name="output", description="Output format (table|json|text)", required=False, type="string", aliases=["-o"], default_value="table"),
48
+ ]
49
+
50
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
51
+ return ""
52
+
53
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
54
+ """Fetches and returns all Lambda functions as a table."""
55
+ region = command.args.get("region")
56
+ client = self._credential_manager.get_client("lambda", region=str(region) if region else None)
57
+
58
+ try:
59
+ response = client.list_functions()
60
+ functions = response.get("Functions", [])
61
+
62
+ if not functions:
63
+ return build_response([CliServerTextOutput(value="No Lambda functions found.", style="warning")])
64
+
65
+ rows = [
66
+ [
67
+ fn.get("FunctionName", "(unknown)"),
68
+ fn.get("Runtime", "(unknown)"),
69
+ str(fn.get("MemorySize", "(unknown)")),
70
+ fn.get("LastModified", "(unknown)"),
71
+ ]
72
+ for fn in functions
73
+ ]
74
+
75
+ table_output = format_as_table(["Name", "Runtime", "Memory (MB)", "Last Modified"], rows)
76
+ return build_response([apply_output_format(command, table_output, functions)])
77
+ except Exception as exc:
78
+ return build_error_response(f"Failed to list Lambda functions: {exc}")
79
+
80
+
81
+ class _LambdaInvokeProcessor(CliCommandProcessor):
82
+ """Invokes a Lambda function and returns the result."""
83
+
84
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
85
+ super().__init__()
86
+ self._credential_manager = credential_manager
87
+
88
+ @property
89
+ def command(self) -> str:
90
+ return "invoke"
91
+
92
+ @property
93
+ def description(self) -> str:
94
+ return "Invoke a Lambda function"
95
+
96
+ @property
97
+ def value_required(self) -> bool | None:
98
+ return True
99
+
100
+ @property
101
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
102
+ return [
103
+ CliCommandParameterDescriptor(name="payload", description="JSON payload to send to the function", required=False, type="string", aliases=["-p"]),
104
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
105
+ ]
106
+
107
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
108
+ return ""
109
+
110
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
111
+ """Invokes the specified Lambda function with an optional JSON payload."""
112
+ function_name = get_positional_text(command)
113
+ if not function_name:
114
+ return build_error_response("Function name is required. Usage: lambda invoke <function-name>")
115
+
116
+ region = command.args.get("region")
117
+ client = self._credential_manager.get_client("lambda", region=str(region) if region else None)
118
+
119
+ payload_str = command.args.get("payload")
120
+
121
+ try:
122
+ params: dict[str, Any] = {"FunctionName": function_name}
123
+ if payload_str:
124
+ params["Payload"] = str(payload_str).encode("utf-8")
125
+
126
+ response = client.invoke(**params)
127
+
128
+ if response.get("FunctionError"):
129
+ error_body = response["Payload"].read().decode("utf-8") if response.get("Payload") else "(no details)"
130
+ return build_error_response(f"Function error ({response['FunctionError']}): {error_body}")
131
+
132
+ result_body = response["Payload"].read().decode("utf-8") if response.get("Payload") else "null"
133
+
134
+ try:
135
+ parsed = json.loads(result_body)
136
+ except (json.JSONDecodeError, ValueError):
137
+ parsed = result_body
138
+
139
+ return build_response([
140
+ format_as_json({
141
+ "StatusCode": response.get("StatusCode"),
142
+ "ExecutedVersion": response.get("ExecutedVersion", "(unknown)"),
143
+ "Result": parsed,
144
+ }),
145
+ ])
146
+ except Exception as exc:
147
+ return build_error_response(f"Failed to invoke function: {exc}")
148
+
149
+
150
+ class _LambdaLogsProcessor(CliCommandProcessor):
151
+ """Fetches recent CloudWatch logs for a Lambda function."""
152
+
153
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
154
+ super().__init__()
155
+ self._credential_manager = credential_manager
156
+
157
+ @property
158
+ def command(self) -> str:
159
+ return "logs"
160
+
161
+ @property
162
+ def description(self) -> str:
163
+ return "View recent logs for a Lambda function"
164
+
165
+ @property
166
+ def value_required(self) -> bool | None:
167
+ return True
168
+
169
+ @property
170
+ def parameters(self) -> list[ICliCommandParameterDescriptor]:
171
+ return [
172
+ CliCommandParameterDescriptor(name="limit", description="Maximum number of log events (default: 50)", required=False, type="number", aliases=["-l"], default_value="50"),
173
+ CliCommandParameterDescriptor(name="region", description="AWS region override", required=False, type="string", aliases=["-r"]),
174
+ ]
175
+
176
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
177
+ return ""
178
+
179
+ async def handle_structured_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> Any:
180
+ """Queries the ``/aws/lambda/<name>`` log group for recent events."""
181
+ function_name = get_positional_text(command)
182
+ if not function_name:
183
+ return build_error_response("Function name is required. Usage: lambda logs <function-name>")
184
+
185
+ try:
186
+ limit = int(command.args.get("limit", 50))
187
+ except (ValueError, TypeError):
188
+ return build_error_response("--limit must be a positive number.")
189
+
190
+ if limit <= 0:
191
+ return build_error_response("--limit must be a positive number.")
192
+
193
+ region = command.args.get("region")
194
+ client = self._credential_manager.get_client("logs", region=str(region) if region else None)
195
+
196
+ log_group_name = f"/aws/lambda/{function_name}"
197
+
198
+ try:
199
+ response = client.filter_log_events(logGroupName=log_group_name, limit=limit)
200
+ events = response.get("events", [])
201
+
202
+ if not events:
203
+ return build_response([CliServerTextOutput(value=f"No log events found for {log_group_name}.", style="warning")])
204
+
205
+ from datetime import datetime, timezone
206
+
207
+ lines = []
208
+ for event in events:
209
+ ts = event.get("timestamp")
210
+ if ts:
211
+ timestamp = datetime.fromtimestamp(ts / 1000, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
212
+ else:
213
+ timestamp = "(unknown)"
214
+ message = (event.get("message") or "").rstrip()
215
+ lines.append(f"{timestamp} {message}")
216
+
217
+ return build_response([CliServerTextOutput(value="\n".join(lines))])
218
+ except Exception as exc:
219
+ return build_error_response(f"Failed to fetch logs: {exc}")
220
+
221
+
222
+ class AwsLambdaProcessor(CliCommandProcessor):
223
+ """Parent processor for Lambda sub-commands (list, invoke, logs)."""
224
+
225
+ def __init__(self, credential_manager: AwsCredentialManager) -> None:
226
+ super().__init__()
227
+ self._sub_processors: list[ICliCommandProcessor] = [
228
+ _LambdaListProcessor(credential_manager),
229
+ _LambdaInvokeProcessor(credential_manager),
230
+ _LambdaLogsProcessor(credential_manager),
231
+ ]
232
+
233
+ @property
234
+ def command(self) -> str:
235
+ return "lambda"
236
+
237
+ @property
238
+ def description(self) -> str:
239
+ return "AWS Lambda operations -- list, invoke, and view logs"
240
+
241
+ @property
242
+ def processors(self) -> list[ICliCommandProcessor]:
243
+ """Returns sub-processors for Lambda operations."""
244
+ return self._sub_processors
245
+
246
+ async def handle_async(self, command: CliProcessCommand, cancellation_event: asyncio.Event | None = None) -> str:
247
+ return ""