@techwavedev/agi-agent-kit 1.1.7 → 1.2.1

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.

Potentially problematic release.


This version of @techwavedev/agi-agent-kit might be problematic. Click here for more details.

Files changed (111) hide show
  1. package/CHANGELOG.md +82 -1
  2. package/README.md +190 -12
  3. package/bin/init.js +30 -2
  4. package/package.json +6 -3
  5. package/templates/base/AGENTS.md +54 -23
  6. package/templates/base/README.md +325 -0
  7. package/templates/base/directives/memory_integration.md +95 -0
  8. package/templates/base/execution/memory_manager.py +309 -0
  9. package/templates/base/execution/session_boot.py +218 -0
  10. package/templates/base/execution/session_init.py +320 -0
  11. package/templates/base/skill-creator/SKILL_skillcreator.md +23 -36
  12. package/templates/base/skill-creator/scripts/init_skill.py +18 -135
  13. package/templates/skills/ec/README.md +31 -0
  14. package/templates/skills/ec/aws/SKILL.md +1020 -0
  15. package/templates/skills/ec/aws/defaults.yaml +13 -0
  16. package/templates/skills/ec/aws/references/common_patterns.md +80 -0
  17. package/templates/skills/ec/aws/references/mcp_servers.md +98 -0
  18. package/templates/skills/ec/aws-terraform/SKILL.md +349 -0
  19. package/templates/skills/ec/aws-terraform/references/best_practices.md +394 -0
  20. package/templates/skills/ec/aws-terraform/references/checkov_reference.md +337 -0
  21. package/templates/skills/ec/aws-terraform/scripts/configure_mcp.py +150 -0
  22. package/templates/skills/ec/confluent-kafka/SKILL.md +655 -0
  23. package/templates/skills/ec/confluent-kafka/references/ansible_playbooks.md +792 -0
  24. package/templates/skills/ec/confluent-kafka/references/ec_deployment.md +579 -0
  25. package/templates/skills/ec/confluent-kafka/references/kraft_migration.md +490 -0
  26. package/templates/skills/ec/confluent-kafka/references/troubleshooting.md +778 -0
  27. package/templates/skills/ec/confluent-kafka/references/upgrade_7x_to_8x.md +488 -0
  28. package/templates/skills/ec/confluent-kafka/scripts/kafka_health_check.py +435 -0
  29. package/templates/skills/ec/confluent-kafka/scripts/upgrade_preflight.py +568 -0
  30. package/templates/skills/ec/confluent-kafka/scripts/validate_config.py +455 -0
  31. package/templates/skills/ec/consul/SKILL.md +427 -0
  32. package/templates/skills/ec/consul/references/acl_setup.md +168 -0
  33. package/templates/skills/ec/consul/references/ha_config.md +196 -0
  34. package/templates/skills/ec/consul/references/troubleshooting.md +267 -0
  35. package/templates/skills/ec/consul/references/upgrades.md +213 -0
  36. package/templates/skills/ec/consul/scripts/consul_health_report.py +530 -0
  37. package/templates/skills/ec/consul/scripts/consul_status.py +264 -0
  38. package/templates/skills/ec/consul/scripts/generate_values.py +170 -0
  39. package/templates/skills/ec/documentation/SKILL.md +351 -0
  40. package/templates/skills/ec/documentation/references/best_practices.md +201 -0
  41. package/templates/skills/ec/documentation/scripts/analyze_code.py +307 -0
  42. package/templates/skills/ec/documentation/scripts/detect_changes.py +460 -0
  43. package/templates/skills/ec/documentation/scripts/generate_changelog.py +312 -0
  44. package/templates/skills/ec/documentation/scripts/sync_docs.py +272 -0
  45. package/templates/skills/ec/documentation/scripts/update_skill_docs.py +366 -0
  46. package/templates/skills/ec/gitlab/SKILL.md +529 -0
  47. package/templates/skills/ec/gitlab/references/agent_installation.md +416 -0
  48. package/templates/skills/ec/gitlab/references/api_reference.md +508 -0
  49. package/templates/skills/ec/gitlab/references/gitops_flux.md +465 -0
  50. package/templates/skills/ec/gitlab/references/troubleshooting.md +518 -0
  51. package/templates/skills/ec/gitlab/scripts/generate_agent_values.py +329 -0
  52. package/templates/skills/ec/gitlab/scripts/gitlab_agent_status.py +414 -0
  53. package/templates/skills/ec/jira/SKILL.md +484 -0
  54. package/templates/skills/ec/jira/references/jql_reference.md +148 -0
  55. package/templates/skills/ec/jira/scripts/add_comment.py +91 -0
  56. package/templates/skills/ec/jira/scripts/bulk_log_work.py +124 -0
  57. package/templates/skills/ec/jira/scripts/create_ticket.py +162 -0
  58. package/templates/skills/ec/jira/scripts/get_ticket.py +191 -0
  59. package/templates/skills/ec/jira/scripts/jira_client.py +383 -0
  60. package/templates/skills/ec/jira/scripts/log_work.py +154 -0
  61. package/templates/skills/ec/jira/scripts/search_tickets.py +104 -0
  62. package/templates/skills/ec/jira/scripts/update_comment.py +67 -0
  63. package/templates/skills/ec/jira/scripts/update_ticket.py +161 -0
  64. package/templates/skills/ec/karpenter/SKILL.md +301 -0
  65. package/templates/skills/ec/karpenter/references/ec2nodeclasses.md +421 -0
  66. package/templates/skills/ec/karpenter/references/migration.md +396 -0
  67. package/templates/skills/ec/karpenter/references/nodepools.md +400 -0
  68. package/templates/skills/ec/karpenter/references/troubleshooting.md +359 -0
  69. package/templates/skills/ec/karpenter/scripts/generate_ec2nodeclass.py +187 -0
  70. package/templates/skills/ec/karpenter/scripts/generate_nodepool.py +245 -0
  71. package/templates/skills/ec/karpenter/scripts/karpenter_status.py +359 -0
  72. package/templates/skills/ec/opensearch/SKILL.md +720 -0
  73. package/templates/skills/ec/opensearch/references/ml_neural_search.md +576 -0
  74. package/templates/skills/ec/opensearch/references/operator.md +532 -0
  75. package/templates/skills/ec/opensearch/references/query_dsl.md +532 -0
  76. package/templates/skills/ec/opensearch/scripts/configure_mcp.py +148 -0
  77. package/templates/skills/ec/victoriametrics/SKILL.md +598 -0
  78. package/templates/skills/ec/victoriametrics/references/kubernetes.md +531 -0
  79. package/templates/skills/ec/victoriametrics/references/prometheus_migration.md +333 -0
  80. package/templates/skills/ec/victoriametrics/references/troubleshooting.md +442 -0
  81. package/templates/skills/knowledge/SKILLS_CATALOG.md +274 -4
  82. package/templates/skills/knowledge/intelligent-routing/SKILL.md +237 -164
  83. package/templates/skills/knowledge/parallel-agents/SKILL.md +345 -73
  84. package/templates/skills/knowledge/plugin-discovery/SKILL.md +582 -0
  85. package/templates/skills/knowledge/plugin-discovery/scripts/platform_setup.py +1083 -0
  86. package/templates/skills/knowledge/design-md/README.md +0 -34
  87. package/templates/skills/knowledge/design-md/SKILL.md +0 -193
  88. package/templates/skills/knowledge/design-md/examples/DESIGN.md +0 -154
  89. package/templates/skills/knowledge/notebooklm-mcp/SKILL.md +0 -71
  90. package/templates/skills/knowledge/notebooklm-mcp/assets/example_asset.txt +0 -24
  91. package/templates/skills/knowledge/notebooklm-mcp/references/api_reference.md +0 -34
  92. package/templates/skills/knowledge/notebooklm-mcp/scripts/example.py +0 -19
  93. package/templates/skills/knowledge/react-components/README.md +0 -36
  94. package/templates/skills/knowledge/react-components/SKILL.md +0 -53
  95. package/templates/skills/knowledge/react-components/examples/gold-standard-card.tsx +0 -80
  96. package/templates/skills/knowledge/react-components/package-lock.json +0 -231
  97. package/templates/skills/knowledge/react-components/package.json +0 -16
  98. package/templates/skills/knowledge/react-components/resources/architecture-checklist.md +0 -15
  99. package/templates/skills/knowledge/react-components/resources/component-template.tsx +0 -37
  100. package/templates/skills/knowledge/react-components/resources/stitch-api-reference.md +0 -14
  101. package/templates/skills/knowledge/react-components/resources/style-guide.json +0 -27
  102. package/templates/skills/knowledge/react-components/scripts/fetch-stitch.sh +0 -30
  103. package/templates/skills/knowledge/react-components/scripts/validate.js +0 -68
  104. package/templates/skills/knowledge/self-update/SKILL.md +0 -60
  105. package/templates/skills/knowledge/self-update/scripts/update_kit.py +0 -103
  106. package/templates/skills/knowledge/stitch-loop/README.md +0 -54
  107. package/templates/skills/knowledge/stitch-loop/SKILL.md +0 -235
  108. package/templates/skills/knowledge/stitch-loop/examples/SITE.md +0 -73
  109. package/templates/skills/knowledge/stitch-loop/examples/next-prompt.md +0 -25
  110. package/templates/skills/knowledge/stitch-loop/resources/baton-schema.md +0 -61
  111. package/templates/skills/knowledge/stitch-loop/resources/site-template.md +0 -104
@@ -0,0 +1,530 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Consul Health Report Generator
4
+
5
+ Generates a PDF health report for Consul clusters running on EKS.
6
+
7
+ Usage:
8
+ python consul_health_report.py --environment nonprod --output ./reports/
9
+ python consul_health_report.py --environment prod --output ./reports/ --format pdf
10
+ python consul_health_report.py --environment nonprod --format markdown
11
+
12
+ Arguments:
13
+ --environment, -e Environment name (nonprod, prod, etc.)
14
+ --output, -o Output directory (default: current directory)
15
+ --format, -f Output format: pdf, markdown, or both (default: both)
16
+ --namespace, -n Consul namespace (default: consul)
17
+
18
+ Exit Codes:
19
+ 0 - Success
20
+ 1 - Invalid arguments
21
+ 2 - Kubectl not available or cluster access failed
22
+ 3 - Report generation failed
23
+ """
24
+
25
+ import argparse
26
+ import json
27
+ import subprocess
28
+ import sys
29
+ import os
30
+ from datetime import datetime
31
+ from pathlib import Path
32
+
33
+ # Environment to EKS cluster mapping
34
+ CLUSTER_MAP = {
35
+ 'nonprod': 'eks-nonprod',
36
+ 'prod': 'eks-prod',
37
+ }
38
+
39
+ def run_kubectl(cmd, namespace='consul'):
40
+ """Execute kubectl command and return output."""
41
+ full_cmd = f"kubectl {cmd}"
42
+ try:
43
+ result = subprocess.run(
44
+ full_cmd, shell=True, capture_output=True, text=True, timeout=30
45
+ )
46
+ return result.stdout.strip(), result.returncode
47
+ except subprocess.TimeoutExpired:
48
+ return "Command timed out", 1
49
+ except Exception as e:
50
+ return str(e), 1
51
+
52
+ def run_consul_cmd(cmd, namespace='consul', token=None):
53
+ """Execute command inside Consul server pod."""
54
+ token_flag = f'-token="{token}"' if token else ''
55
+ full_cmd = f'kubectl exec -n {namespace} consul-server-0 -- consul {cmd} {token_flag}'
56
+ return run_kubectl(full_cmd.replace('kubectl ', ''))
57
+
58
+ def get_bootstrap_token(namespace='consul'):
59
+ """Retrieve Consul bootstrap ACL token from Kubernetes secret."""
60
+ cmd = f"get secret -n {namespace} consul-bootstrap-acl-token -o jsonpath='{{.data.token}}'"
61
+ output, rc = run_kubectl(cmd)
62
+ if rc == 0 and output:
63
+ import base64
64
+ try:
65
+ return base64.b64decode(output).decode('utf-8')
66
+ except:
67
+ return None
68
+ return None
69
+
70
+ def set_cluster_context(environment, region='eu-west-1'):
71
+ """Set kubectl context to the target EKS cluster."""
72
+ cluster_name = CLUSTER_MAP.get(environment, f'eks-{environment}')
73
+ cmd = f"aws eks update-kubeconfig --name {cluster_name} --region {region}"
74
+ result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
75
+ return result.returncode == 0
76
+
77
+ def collect_health_data(namespace='consul'):
78
+ """Collect all health data from Consul cluster."""
79
+ data = {
80
+ 'timestamp': datetime.now().isoformat(),
81
+ 'pods': {},
82
+ 'helm': {},
83
+ 'members': [],
84
+ 'raft_peers': [],
85
+ 'autopilot': {},
86
+ 'services': [],
87
+ 'ca_config': {},
88
+ 'warnings': [],
89
+ }
90
+
91
+ # Get pods
92
+ output, rc = run_kubectl(f'-n {namespace} get pods -o json')
93
+ if rc == 0:
94
+ try:
95
+ pods_json = json.loads(output)
96
+ data['pods'] = pods_json
97
+ except:
98
+ pass
99
+
100
+ # Get helm release info
101
+ output, rc = run_kubectl(f'-n {namespace} get pods -o wide'.replace('kubectl ', ''))
102
+ result = subprocess.run(f'helm list -n {namespace} -o json', shell=True, capture_output=True, text=True)
103
+ if result.returncode == 0:
104
+ try:
105
+ data['helm'] = json.loads(result.stdout)
106
+ except:
107
+ pass
108
+
109
+ # Get Consul members
110
+ output, rc = run_consul_cmd('members')
111
+ if rc == 0:
112
+ lines = output.strip().split('\n')
113
+ for line in lines[1:]: # Skip header
114
+ if line.strip():
115
+ parts = line.split()
116
+ if len(parts) >= 6:
117
+ data['members'].append({
118
+ 'node': parts[0],
119
+ 'address': parts[1],
120
+ 'status': parts[2],
121
+ 'type': parts[3],
122
+ 'build': parts[4],
123
+ 'dc': parts[6] if len(parts) > 6 else '',
124
+ })
125
+
126
+ # Get bootstrap token for ACL-protected commands
127
+ token = get_bootstrap_token(namespace)
128
+
129
+ # Get Raft peers
130
+ if token:
131
+ output, rc = run_consul_cmd('operator raft list-peers', token=token)
132
+ if rc == 0:
133
+ lines = output.strip().split('\n')
134
+ for line in lines[1:]: # Skip header
135
+ if line.strip() and 'Defaulted' not in line:
136
+ parts = line.split()
137
+ if len(parts) >= 6:
138
+ data['raft_peers'].append({
139
+ 'node': parts[0],
140
+ 'id': parts[1],
141
+ 'address': parts[2],
142
+ 'state': parts[3],
143
+ 'voter': parts[4],
144
+ 'commit_index': parts[6] if len(parts) > 6 else '',
145
+ })
146
+
147
+ # Get autopilot state
148
+ output, rc = run_consul_cmd('operator autopilot state', token=token)
149
+ if rc == 0:
150
+ data['autopilot']['raw'] = output
151
+ data['autopilot']['healthy'] = 'Healthy: true' in output
152
+
153
+ # Get CA config
154
+ output, rc = run_consul_cmd('connect ca get-config', token=token)
155
+ if rc == 0:
156
+ try:
157
+ # Find JSON in output
158
+ json_start = output.find('{')
159
+ if json_start >= 0:
160
+ data['ca_config'] = json.loads(output[json_start:])
161
+ except:
162
+ pass
163
+
164
+ # Get registered services
165
+ output, rc = run_consul_cmd('catalog services')
166
+ if rc == 0:
167
+ services = [s.strip() for s in output.split('\n') if s.strip() and 'Defaulted' not in s]
168
+ data['services'] = services
169
+
170
+ # Check for warnings in logs
171
+ result = subprocess.run(
172
+ f'kubectl logs -n {namespace} -l app=consul,component=server --tail=100 2>&1 | grep -iE "(error|warn|fail)" | tail -20',
173
+ shell=True, capture_output=True, text=True
174
+ )
175
+ if result.returncode == 0 and result.stdout:
176
+ for line in result.stdout.strip().split('\n'):
177
+ if 'Check is now critical' in line:
178
+ # Extract service name
179
+ if 'check=service:' in line:
180
+ svc = line.split('check=service:')[1].strip()
181
+ data['warnings'].append({
182
+ 'type': 'critical_check',
183
+ 'service': svc,
184
+ 'message': f'Health check failing for {svc}'
185
+ })
186
+ elif 'deprecated' in line.lower():
187
+ if not any(w['type'] == 'deprecated_token' for w in data['warnings']):
188
+ data['warnings'].append({
189
+ 'type': 'deprecated_token',
190
+ 'message': 'Deprecated token query parameter in use'
191
+ })
192
+
193
+ return data
194
+
195
+ def generate_pdf_report(data, environment, output_path):
196
+ """Generate PDF report from collected data."""
197
+ try:
198
+ from fpdf import FPDF
199
+ from fpdf.enums import XPos, YPos
200
+ except ImportError:
201
+ print("Error: fpdf2 not installed. Install with: pip install fpdf2", file=sys.stderr)
202
+ return False
203
+
204
+ class ConsulHealthReport(FPDF):
205
+ def __init__(self, env):
206
+ super().__init__()
207
+ self.env = env
208
+
209
+ def header(self):
210
+ self.set_font('Helvetica', 'B', 16)
211
+ self.set_text_color(0, 102, 204)
212
+ self.cell(0, 10, 'Consul Health Report', border=0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
213
+ self.set_font('Helvetica', '', 10)
214
+ self.set_text_color(100, 100, 100)
215
+ self.cell(0, 6, f'Environment: {self.env}', border=0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
216
+ self.cell(0, 6, f'Generated: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}', border=0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='C')
217
+ self.ln(5)
218
+ self.set_draw_color(0, 102, 204)
219
+ self.line(10, self.get_y(), 200, self.get_y())
220
+ self.ln(5)
221
+
222
+ def footer(self):
223
+ self.set_y(-15)
224
+ self.set_font('Helvetica', 'I', 8)
225
+ self.set_text_color(128, 128, 128)
226
+ self.cell(0, 10, f'Page {self.page_no()}', align='C')
227
+
228
+ def section_title(self, title):
229
+ self.set_font('Helvetica', 'B', 14)
230
+ self.set_text_color(0, 51, 102)
231
+ self.cell(0, 10, title, new_x=XPos.LMARGIN, new_y=YPos.NEXT)
232
+ self.ln(2)
233
+
234
+ def status_badge(self, status):
235
+ if status.upper() in ['HEALTHY', 'ALIVE', 'RUNNING', 'LEADER', 'OK']:
236
+ self.set_fill_color(40, 167, 69)
237
+ elif status.upper() in ['WARNING', 'WARN', 'FOLLOWER']:
238
+ self.set_fill_color(255, 193, 7)
239
+ else:
240
+ self.set_fill_color(220, 53, 69)
241
+ self.set_text_color(255, 255, 255)
242
+ self.set_font('Helvetica', 'B', 10)
243
+ self.cell(25, 6, status.upper(), fill=True, align='C')
244
+ self.set_text_color(0, 0, 0)
245
+
246
+ def add_table(self, headers, rows, col_widths=None):
247
+ self.set_font('Helvetica', 'B', 9)
248
+ self.set_fill_color(240, 240, 240)
249
+ self.set_text_color(0, 0, 0)
250
+ if col_widths is None:
251
+ col_widths = [190 / len(headers)] * len(headers)
252
+ for i, header in enumerate(headers):
253
+ self.cell(col_widths[i], 7, header, border=1, fill=True, align='C')
254
+ self.ln()
255
+ self.set_font('Helvetica', '', 8)
256
+ for row in rows:
257
+ for i, cell in enumerate(row):
258
+ self.cell(col_widths[i], 6, str(cell), border=1, align='C')
259
+ self.ln()
260
+ self.ln(3)
261
+
262
+ def add_key_value(self, key, value, indent=0):
263
+ self.set_font('Helvetica', 'B', 10)
264
+ self.set_x(10 + indent)
265
+ self.cell(50, 6, f'{key}:', new_x=XPos.RIGHT, new_y=YPos.TOP)
266
+ self.set_font('Helvetica', '', 10)
267
+ self.cell(0, 6, str(value), new_x=XPos.LMARGIN, new_y=YPos.NEXT)
268
+
269
+ pdf = ConsulHealthReport(environment)
270
+ pdf.add_page()
271
+
272
+ # Overall Status
273
+ is_healthy = data.get('autopilot', {}).get('healthy', False) and len(data.get('members', [])) >= 3
274
+ pdf.section_title('Overall Status')
275
+ pdf.status_badge('HEALTHY' if is_healthy else 'UNHEALTHY')
276
+ pdf.ln(10)
277
+
278
+ # Cluster Summary
279
+ pdf.section_title('Cluster Summary')
280
+ pdf.add_key_value('Datacenter', CLUSTER_MAP.get(environment, environment))
281
+ helm_info = data.get('helm', [{}])
282
+ if helm_info and len(helm_info) > 0:
283
+ h = helm_info[0]
284
+ pdf.add_key_value('Chart Version', h.get('chart', 'N/A'))
285
+ pdf.add_key_value('App Version', h.get('app_version', 'N/A'))
286
+ pdf.add_key_value('Revision', h.get('revision', 'N/A'))
287
+ pdf.add_key_value('Autopilot Health', 'Healthy' if data.get('autopilot', {}).get('healthy') else 'Unknown')
288
+ pdf.ln(5)
289
+
290
+ # Raft Consensus
291
+ if data.get('raft_peers'):
292
+ pdf.section_title('Raft Consensus (HA Health)')
293
+ headers = ['Node', 'Address', 'State', 'Voter', 'Commit Index']
294
+ rows = [[p['node'], p['address'], p['state'], p['voter'], p['commit_index']] for p in data['raft_peers']]
295
+ pdf.add_table(headers, rows, [40, 45, 25, 20, 30])
296
+
297
+ # Consul Members
298
+ if data.get('members'):
299
+ pdf.section_title('Consul Members')
300
+ headers = ['Node', 'Address', 'Status', 'Type', 'Build']
301
+ rows = [[m['node'], m['address'], m['status'], m['type'], m['build']] for m in data['members']]
302
+ pdf.add_table(headers, rows, [40, 45, 25, 25, 25])
303
+
304
+ # Services
305
+ if data.get('services'):
306
+ pdf.section_title(f'Registered Services ({len(data["services"])} total)')
307
+ pdf.set_font('Helvetica', '', 9)
308
+ for i, svc in enumerate(data['services']):
309
+ if i % 3 == 0 and i > 0:
310
+ pdf.ln()
311
+ pdf.cell(60, 5, f'- {svc}', new_x=XPos.RIGHT, new_y=YPos.TOP)
312
+ pdf.ln(10)
313
+
314
+ # CA Config
315
+ if data.get('ca_config'):
316
+ pdf.section_title('Connect CA (mTLS)')
317
+ cfg = data['ca_config'].get('Config', {})
318
+ pdf.add_key_value('Provider', data['ca_config'].get('Provider', 'N/A'))
319
+ pdf.add_key_value('Root Cert TTL', cfg.get('RootCertTTL', 'N/A'))
320
+ pdf.add_key_value('Intermediate TTL', cfg.get('IntermediateCertTTL', 'N/A'))
321
+ pdf.add_key_value('Leaf Cert TTL', cfg.get('LeafCertTTL', 'N/A'))
322
+ pdf.ln(5)
323
+
324
+ # Warnings
325
+ if data.get('warnings'):
326
+ pdf.add_page()
327
+ pdf.section_title('Warnings Detected')
328
+ for w in data['warnings']:
329
+ pdf.set_font('Helvetica', 'B', 10)
330
+ pdf.set_fill_color(255, 243, 205)
331
+ pdf.set_text_color(133, 100, 4)
332
+ pdf.multi_cell(190, 6, f"WARNING: {w.get('message', w.get('type', 'Unknown'))}", fill=True)
333
+ pdf.set_text_color(0, 0, 0)
334
+ pdf.set_font('Helvetica', '', 9)
335
+ if w.get('service'):
336
+ pdf.add_key_value('Service', w['service'], indent=5)
337
+ pdf.ln(5)
338
+
339
+ # Save
340
+ pdf.output(output_path)
341
+ return True
342
+
343
+ def generate_markdown_report(data, environment, output_path):
344
+ """Generate Markdown report from collected data."""
345
+ is_healthy = data.get('autopilot', {}).get('healthy', False) and len(data.get('members', [])) >= 3
346
+
347
+ lines = [
348
+ f"# Consul Health Report",
349
+ "",
350
+ f"**Environment:** {environment} ({CLUSTER_MAP.get(environment, environment)})",
351
+ f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
352
+ "",
353
+ "---",
354
+ "",
355
+ "## Overall Status",
356
+ "",
357
+ f"| Status | Result |",
358
+ f"|--------|--------|",
359
+ f"| **Cluster Health** | {'✅ **HEALTHY**' if is_healthy else '❌ **UNHEALTHY**'} |",
360
+ "",
361
+ "---",
362
+ "",
363
+ "## Cluster Summary",
364
+ "",
365
+ "| Metric | Value |",
366
+ "|--------|-------|",
367
+ f"| **Datacenter** | `{CLUSTER_MAP.get(environment, environment)}` |",
368
+ ]
369
+
370
+ helm_info = data.get('helm', [{}])
371
+ if helm_info and len(helm_info) > 0:
372
+ h = helm_info[0]
373
+ lines.append(f"| **Chart Version** | `{h.get('chart', 'N/A')}` |")
374
+ lines.append(f"| **App Version** | `{h.get('app_version', 'N/A')}` |")
375
+ lines.append(f"| **Revision** | {h.get('revision', 'N/A')} |")
376
+
377
+ lines.append(f"| **Autopilot Health** | {'✅ Healthy' if data.get('autopilot', {}).get('healthy') else 'Unknown'} |")
378
+ lines.extend(["", "---", ""])
379
+
380
+ # Raft peers
381
+ if data.get('raft_peers'):
382
+ lines.extend([
383
+ "## Raft Consensus (HA Health)",
384
+ "",
385
+ "| Node | Address | State | Voter | Commit Index |",
386
+ "|------|---------|-------|-------|--------------|",
387
+ ])
388
+ for p in data['raft_peers']:
389
+ state = f"**{p['state']}**" if p['state'].lower() == 'leader' else p['state']
390
+ lines.append(f"| `{p['node']}` | {p['address']} | {state} | {p['voter']} | {p['commit_index']} |")
391
+ lines.extend(["", "---", ""])
392
+
393
+ # Members
394
+ if data.get('members'):
395
+ lines.extend([
396
+ "## Consul Members",
397
+ "",
398
+ "| Node | Address | Status | Type | Build |",
399
+ "|------|---------|--------|------|-------|",
400
+ ])
401
+ for m in data['members']:
402
+ lines.append(f"| `{m['node']}` | {m['address']} | {m['status']} | {m['type']} | {m['build']} |")
403
+ lines.extend(["", "---", ""])
404
+
405
+ # Services
406
+ if data.get('services'):
407
+ lines.extend([
408
+ f"## Registered Services ({len(data['services'])} total)",
409
+ "",
410
+ ])
411
+ for svc in data['services']:
412
+ lines.append(f"- `{svc}`")
413
+ lines.extend(["", "---", ""])
414
+
415
+ # CA Config
416
+ if data.get('ca_config'):
417
+ cfg = data['ca_config'].get('Config', {})
418
+ lines.extend([
419
+ "## Connect CA (mTLS)",
420
+ "",
421
+ "| Setting | Value |",
422
+ "|---------|-------|",
423
+ f"| **Provider** | {data['ca_config'].get('Provider', 'N/A')} |",
424
+ f"| **Root Cert TTL** | {cfg.get('RootCertTTL', 'N/A')} |",
425
+ f"| **Intermediate TTL** | {cfg.get('IntermediateCertTTL', 'N/A')} |",
426
+ f"| **Leaf Cert TTL** | {cfg.get('LeafCertTTL', 'N/A')} |",
427
+ "",
428
+ "---",
429
+ "",
430
+ ])
431
+
432
+ # Warnings
433
+ if data.get('warnings'):
434
+ lines.extend([
435
+ "## Warnings Detected",
436
+ "",
437
+ ])
438
+ for w in data['warnings']:
439
+ lines.append(f"### ⚠️ {w.get('message', w.get('type', 'Unknown'))}")
440
+ lines.append("")
441
+ if w.get('service'):
442
+ lines.append(f"- **Service:** `{w['service']}`")
443
+ lines.append("")
444
+ lines.extend(["---", ""])
445
+
446
+ lines.extend([
447
+ "",
448
+ "*Report generated by Consul health monitoring skill*",
449
+ ])
450
+
451
+ with open(output_path, 'w') as f:
452
+ f.write('\n'.join(lines))
453
+ return True
454
+
455
+
456
+ def main():
457
+ parser = argparse.ArgumentParser(
458
+ description='Generate Consul health report for EKS clusters',
459
+ formatter_class=argparse.RawDescriptionHelpFormatter,
460
+ epilog=__doc__
461
+ )
462
+ parser.add_argument('-e', '--environment', required=True,
463
+ help='Environment name (nonprod, prod)')
464
+ parser.add_argument('-o', '--output', default='.',
465
+ help='Output directory (default: current)')
466
+ parser.add_argument('-f', '--format', choices=['pdf', 'markdown', 'both'], default='both',
467
+ help='Output format (default: both)')
468
+ parser.add_argument('-n', '--namespace', default='consul',
469
+ help='Consul namespace (default: consul)')
470
+ parser.add_argument('-r', '--region', default='eu-west-1',
471
+ help='AWS region (default: eu-west-1)')
472
+
473
+ args = parser.parse_args()
474
+
475
+ # Set cluster context
476
+ print(f"Setting context to {args.environment} cluster...")
477
+ if not set_cluster_context(args.environment, args.region):
478
+ print(f"Error: Failed to set cluster context for {args.environment}", file=sys.stderr)
479
+ sys.exit(2)
480
+
481
+ # Collect health data
482
+ print("Collecting health data...")
483
+ data = collect_health_data(args.namespace)
484
+
485
+ # Create output directory
486
+ output_dir = Path(args.output)
487
+ output_dir.mkdir(parents=True, exist_ok=True)
488
+
489
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
490
+ base_name = f'consul_health_{args.environment}_{timestamp}'
491
+
492
+ # Generate reports
493
+ if args.format in ['pdf', 'both']:
494
+ pdf_path = output_dir / f'{base_name}.pdf'
495
+ print(f"Generating PDF report: {pdf_path}")
496
+ if generate_pdf_report(data, args.environment, str(pdf_path)):
497
+ print(f" ✓ PDF saved: {pdf_path}")
498
+ else:
499
+ print(f" ✗ PDF generation failed", file=sys.stderr)
500
+
501
+ if args.format in ['markdown', 'both']:
502
+ md_path = output_dir / f'{base_name}.md'
503
+ print(f"Generating Markdown report: {md_path}")
504
+ if generate_markdown_report(data, args.environment, str(md_path)):
505
+ print(f" ✓ Markdown saved: {md_path}")
506
+ else:
507
+ print(f" ✗ Markdown generation failed", file=sys.stderr)
508
+
509
+ # Also save raw data as JSON
510
+ json_path = output_dir / f'{base_name}.json'
511
+ with open(json_path, 'w') as f:
512
+ json.dump(data, f, indent=2, default=str)
513
+ print(f" ✓ Raw data saved: {json_path}")
514
+
515
+ print("\nDone!")
516
+
517
+ # Print summary
518
+ is_healthy = data.get('autopilot', {}).get('healthy', False)
519
+ print(f"\n{'='*50}")
520
+ print(f"Cluster Status: {'✓ HEALTHY' if is_healthy else '✗ ISSUES DETECTED'}")
521
+ print(f"Members: {len(data.get('members', []))}")
522
+ print(f"Services: {len(data.get('services', []))}")
523
+ print(f"Warnings: {len(data.get('warnings', []))}")
524
+ print(f"{'='*50}")
525
+
526
+ sys.exit(0)
527
+
528
+
529
+ if __name__ == '__main__':
530
+ main()