onex-cli 1.9.4__tar.gz → 1.9.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {onex_cli-1.9.4 → onex_cli-1.9.6}/CHANGELOG.md +24 -0
  2. {onex_cli-1.9.4/onex_cli.egg-info → onex_cli-1.9.6}/PKG-INFO +1 -1
  3. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/__init__.py +1 -1
  4. onex_cli-1.9.6/onex/commands/es.py +488 -0
  5. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/mcp/server.py +245 -0
  6. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/schema/service_descriptor.py +248 -0
  7. {onex_cli-1.9.4 → onex_cli-1.9.6/onex_cli.egg-info}/PKG-INFO +1 -1
  8. onex_cli-1.9.4/onex/commands/es.py +0 -140
  9. {onex_cli-1.9.4 → onex_cli-1.9.6}/LICENSE +0 -0
  10. {onex_cli-1.9.4 → onex_cli-1.9.6}/MANIFEST.in +0 -0
  11. {onex_cli-1.9.4 → onex_cli-1.9.6}/README.md +0 -0
  12. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/__main__.py +0 -0
  13. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/__init__.py +0 -0
  14. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/create.py +0 -0
  15. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/create_e2e.py +0 -0
  16. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/debug.py +0 -0
  17. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/deploy.py +0 -0
  18. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/dev.py +0 -0
  19. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/env.py +0 -0
  20. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/init.py +0 -0
  21. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/invoke.py +0 -0
  22. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/login.py +0 -0
  23. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/logout.py +0 -0
  24. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/logs.py +0 -0
  25. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/mcp.py +0 -0
  26. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/platform.py +0 -0
  27. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/provision.py +0 -0
  28. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/replay.py +0 -0
  29. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/status.py +0 -0
  30. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/switch.py +0 -0
  31. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/test.py +0 -0
  32. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/trace.py +0 -0
  33. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/undeploy.py +0 -0
  34. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/validate.py +0 -0
  35. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/commands/vpn.py +0 -0
  36. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/config.py +0 -0
  37. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/mcp/__init__.py +0 -0
  38. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/mcp/tools/__init__.py +0 -0
  39. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/runtime/__init__.py +0 -0
  40. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/runtime/local_runtime.py +0 -0
  41. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/schema/__init__.py +0 -0
  42. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/schema/validator.py +0 -0
  43. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/README.md.j2 +0 -0
  44. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/__init__.py.j2 +0 -0
  45. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/service.yml.j2 +0 -0
  46. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/__init__.py.j2 +0 -0
  47. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/apis/__init__.py.j2 +0 -0
  48. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/models/__init__.py.j2 +0 -0
  49. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/models/schemas.py.j2 +0 -0
  50. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/repositories/__init__.py.j2 +0 -0
  51. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/crud-service/src/services/__init__.py.j2 +0 -0
  52. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/e2e/data_service.yml.j2 +0 -0
  53. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/e2e/test_service_e2e.py.j2 +0 -0
  54. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/README.md.j2 +0 -0
  55. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/__init__.py.j2 +0 -0
  56. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/service.yml.j2 +0 -0
  57. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/src/__init__.py.j2 +0 -0
  58. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/src/triggers/__init__.py.j2 +0 -0
  59. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/src/triggers/events.py.j2 +0 -0
  60. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/event-driven/src/triggers/schedules.py.j2 +0 -0
  61. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/README.md.j2 +0 -0
  62. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/__init__.py.j2 +0 -0
  63. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/service.yml.j2 +0 -0
  64. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/src/__init__.py.j2 +0 -0
  65. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/src/apis/__init__.py.j2 +0 -0
  66. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/minimal/src/apis/hello.py.j2 +0 -0
  67. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/README.md.j2 +0 -0
  68. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/__init__.py.j2 +0 -0
  69. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/service.yml.j2 +0 -0
  70. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/src/__init__.py.j2 +0 -0
  71. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/src/apis/__init__.py.j2 +0 -0
  72. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/templates/rest-api/src/apis/handlers.py.j2 +0 -0
  73. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/utils/__init__.py +0 -0
  74. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/utils/auth.py +0 -0
  75. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/utils/crypto.py +0 -0
  76. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/utils/email.py +0 -0
  77. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/utils/helpers.py +0 -0
  78. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/vpn/__init__.py +0 -0
  79. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/vpn/platform_detector.py +0 -0
  80. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/vpn/setup_vpn.py +0 -0
  81. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex/vpn/wireguard_manager.py +0 -0
  82. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex_cli.egg-info/SOURCES.txt +0 -0
  83. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex_cli.egg-info/dependency_links.txt +0 -0
  84. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex_cli.egg-info/entry_points.txt +0 -0
  85. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex_cli.egg-info/requires.txt +0 -0
  86. {onex_cli-1.9.4 → onex_cli-1.9.6}/onex_cli.egg-info/top_level.txt +0 -0
  87. {onex_cli-1.9.4 → onex_cli-1.9.6}/setup.cfg +0 -0
  88. {onex_cli-1.9.4 → onex_cli-1.9.6}/setup.py +0 -0
  89. {onex_cli-1.9.4 → onex_cli-1.9.6}/tests/test_mcp_logs_e2e.py +0 -0
@@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.9.6] - 2026-03-24
9
+
10
+ ### Added
11
+ - **`onex es status`** - View es_sync trigger status, ES index health, and execution stats
12
+ - Shows active/paused/error triggers with sync ratio (ES vs MongoDB doc counts)
13
+ - Execution stats, failure counts, last error, recent execution logs
14
+ - Filter by service name, JSON output support
15
+ - MCP tool: `onex_es_status`
16
+ - **`onex es test`** - End-to-end es_sync verification
17
+ - Inserts test doc into MongoDB, verifies sync to ES within 10s, cleans up
18
+ - Per-trigger pass/fail with step-by-step details
19
+ - MCP tool: `onex_es_test`
20
+
21
+ ## [1.9.5] - 2026-03-24
22
+
23
+ ### Added
24
+ - **Schema: `docs` field on HTTP triggers** for OpenAPI/Swagger documentation
25
+ - `request_model` / `response_model`: reference Pydantic models for request/response body schemas
26
+ - `query_params`: document query parameters in Swagger UI
27
+ - `error_responses`: document additional error responses by status code
28
+ - `summary`, `description`, `tags`, `status_code`: standard OpenAPI fields
29
+ - Validation: `module:ClassName` format enforced (e.g., `doc_models:RegisterRequest`)
30
+ - New schema models: `DocsConfig`, `QueryParamSpec`, `ErrorResponseSpec`
31
+
8
32
  ## [1.7.9] - 2026-03-02
9
33
 
10
34
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onex-cli
3
- Version: 1.9.4
3
+ Version: 1.9.6
4
4
  Summary: Official CLI for deploying and managing services on OneXEOS Platform (Windows, macOS, Linux)
5
5
  Home-page: https://github.com/onexeos/onex-cli
6
6
  Author: OneXEOS Platform Team
@@ -1,2 +1,2 @@
1
1
  # OneXEOS Services CLI
2
- __version__ = "1.9.4"
2
+ __version__ = "1.9.6"
@@ -0,0 +1,488 @@
1
+ """
2
+ ES command group - Elasticsearch management commands
3
+
4
+ Commands:
5
+ status - View es_sync trigger status, ES index health, and execution stats
6
+ test - Test es_sync by inserting a doc, verifying sync, then cleaning up
7
+ reindex - Reindex ES from MongoDB for a service's es_sync collections
8
+ """
9
+
10
+ import click
11
+ import json
12
+ import requests
13
+ from onex.config import get_config, get_access_token, get_active_environment
14
+ from onex.utils import print_error, print_info, print_success, print_warning
15
+ from onex.utils.auth import auto_refresh_token
16
+
17
+
18
+ @click.group()
19
+ def es():
20
+ """Elasticsearch management commands."""
21
+ pass
22
+
23
+
24
+ def _build_headers(env):
25
+ """Build auth headers for the given environment."""
26
+ access_token = get_access_token(env)
27
+ headers = {}
28
+ if access_token:
29
+ headers['Authorization'] = f'Bearer {access_token}'
30
+ return headers
31
+
32
+
33
+ def _handle_auth_retry(env, headers, make_request):
34
+ """Handle 401 with token refresh. Returns response or None."""
35
+ if env != "local":
36
+ print_warning("Token expired, refreshing...")
37
+ if auto_refresh_token(env, silent=False):
38
+ access_token = get_access_token(env)
39
+ headers['Authorization'] = f'Bearer {access_token}'
40
+ return make_request(headers)
41
+ else:
42
+ print_error("Authentication failed - could not refresh token")
43
+ print_info(f"Please login again: onex login --env {env}")
44
+ return None
45
+
46
+
47
+ @es.command()
48
+ @click.argument('service_name', required=False, default=None)
49
+ @click.option('--env', default=None, help='Environment (local/dev/staging/prod, defaults to active)')
50
+ @click.option('--platform-url', help='Override platform URL')
51
+ @click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
52
+ def status(service_name, env, platform_url, output_json):
53
+ """View es_sync trigger status, ES index health, and execution stats.
54
+
55
+ Shows all es_sync triggers with their current status, ES index document counts,
56
+ MongoDB source counts, execution stats, and recent execution logs.
57
+
58
+ \b
59
+ Examples:
60
+ # View all es_sync triggers across all services
61
+ onex es status
62
+
63
+ # View es_sync triggers for a specific service
64
+ onex es status crm
65
+
66
+ # Output as JSON
67
+ onex es status crm --json
68
+ """
69
+ if env is None:
70
+ env = get_active_environment()
71
+
72
+ config = get_config()
73
+ base_url = platform_url or config.get_platform_url(env)
74
+ headers = _build_headers(env)
75
+
76
+ click.echo(f"🔍 ES Sync Status ({env})...")
77
+
78
+ params = {}
79
+ if service_name:
80
+ params['service_name'] = service_name
81
+
82
+ try:
83
+ response = requests.get(
84
+ f"{base_url}/_internal/es-sync",
85
+ params=params,
86
+ headers=headers,
87
+ timeout=30,
88
+ )
89
+
90
+ if response.status_code == 401 and env != "local":
91
+ retry_resp = _handle_auth_retry(env, headers, lambda h: requests.get(
92
+ f"{base_url}/_internal/es-sync", params=params, headers=h, timeout=30))
93
+ if retry_resp:
94
+ response = retry_resp
95
+ else:
96
+ return
97
+
98
+ if response.status_code == 200:
99
+ data = response.json()
100
+
101
+ if output_json:
102
+ click.echo(json.dumps(data, indent=2))
103
+ return
104
+
105
+ total = data.get('total', 0)
106
+ summary = data.get('summary', {})
107
+
108
+ if total == 0:
109
+ if service_name:
110
+ print_info(f"No es_sync triggers found for service '{service_name}'.")
111
+ else:
112
+ print_info("No es_sync triggers registered.")
113
+ print_info("Deploy a service with es_sync in service.yml to register triggers.")
114
+ return
115
+
116
+ # Summary header
117
+ click.echo()
118
+ active = summary.get('active', 0)
119
+ paused = summary.get('paused', 0)
120
+ error = summary.get('error', 0)
121
+ total_exec = summary.get('total_executions', 0)
122
+ total_fail = summary.get('total_failures', 0)
123
+
124
+ click.echo(f" Total: {total} trigger(s) | "
125
+ f"{click.style(f'{active} active', fg='green')} | "
126
+ f"{click.style(f'{paused} paused', fg='yellow') if paused else f'{paused} paused'} | "
127
+ f"{click.style(f'{error} error', fg='red') if error else f'{error} error'}")
128
+ click.echo(f" Executions: {total_exec:,} total | {total_fail:,} failures")
129
+ click.echo()
130
+
131
+ # Per-service details
132
+ for svc in data.get('services', []):
133
+ svc_name = svc.get('service_name', '?')
134
+ triggers = svc.get('triggers', [])
135
+ click.echo(click.style(f" ┌─ {svc_name} ({len(triggers)} trigger(s))", fg='cyan', bold=True))
136
+
137
+ for i, t in enumerate(triggers):
138
+ is_last = (i == len(triggers) - 1)
139
+ prefix = " └──" if is_last else " ├──"
140
+ indent = " " if is_last else " │ "
141
+
142
+ # Status indicator
143
+ t_status = t.get('status', 'unknown')
144
+ if t_status == 'active':
145
+ status_str = click.style('● ACTIVE', fg='green')
146
+ elif t_status == 'paused':
147
+ status_str = click.style('○ PAUSED', fg='yellow')
148
+ else:
149
+ status_str = click.style('✖ ERROR', fg='red')
150
+
151
+ collection = t.get('collection', '?')
152
+ index = t.get('index', '?')
153
+ click.echo(f"{prefix} {collection} → {index} {status_str}")
154
+
155
+ # ES index info
156
+ es_info = t.get('es_index', {})
157
+ mongo_count = t.get('mongo_doc_count', 0)
158
+ es_count = es_info.get('doc_count', 0)
159
+ es_size = es_info.get('size', 'N/A')
160
+ es_exists = es_info.get('exists', False)
161
+
162
+ if es_exists:
163
+ # Show sync ratio
164
+ sync_indicator = ''
165
+ if mongo_count > 0 and es_count > 0:
166
+ ratio = es_count / mongo_count * 100
167
+ if ratio >= 99:
168
+ sync_indicator = click.style(' (in sync)', fg='green')
169
+ elif ratio >= 80:
170
+ sync_indicator = click.style(f' ({ratio:.0f}% synced)', fg='yellow')
171
+ else:
172
+ sync_indicator = click.style(f' ({ratio:.0f}% synced)', fg='red')
173
+ click.echo(f"{indent}MongoDB: {mongo_count:,} docs | ES: {es_count:,} docs ({es_size}){sync_indicator}")
174
+ else:
175
+ click.echo(f"{indent}MongoDB: {mongo_count:,} docs | " +
176
+ click.style("ES index missing!", fg='red'))
177
+
178
+ # Execution stats
179
+ exec_count = t.get('execution_count', 0)
180
+ fail_count = t.get('failure_count', 0)
181
+ last_exec = t.get('last_execution')
182
+ last_error = t.get('last_error')
183
+
184
+ exec_line = f"{indent}Executions: {exec_count:,}"
185
+ if fail_count > 0:
186
+ exec_line += f" | " + click.style(f"Failures: {fail_count:,}", fg='red')
187
+ if last_exec:
188
+ exec_line += f" | Last: {last_exec}"
189
+ click.echo(exec_line)
190
+
191
+ if last_error:
192
+ click.echo(f"{indent}" + click.style(f"Last error: {last_error}", fg='red'))
193
+
194
+ # Operations & field mapping
195
+ ops = t.get('operations', [])
196
+ if ops:
197
+ click.echo(f"{indent}Operations: {', '.join(ops)}")
198
+ fm = t.get('field_mapping')
199
+ if fm:
200
+ click.echo(f"{indent}Field mapping: {len(fm)} field(s)")
201
+
202
+ # Recent logs (compact)
203
+ recent = t.get('recent_logs', [])
204
+ if recent:
205
+ click.echo(f"{indent}Recent:")
206
+ for log in recent[:3]:
207
+ op = log.get('operation', '?')
208
+ ok = '✓' if log.get('success') else '✗'
209
+ ms = log.get('execution_time_ms', 0)
210
+ ts = log.get('executed_at', '')
211
+ if ts and len(ts) > 19:
212
+ ts = ts[:19]
213
+ color = 'green' if log.get('success') else 'red'
214
+ click.echo(f"{indent} {click.style(ok, fg=color)} {op} ({ms}ms) {ts}")
215
+
216
+ click.echo()
217
+
218
+ elif response.status_code == 503:
219
+ print_error("Stream Trigger Service unavailable")
220
+ print_info("Make sure platform is running: ./scripts/deploy-dev.sh status")
221
+
222
+ else:
223
+ print_error(f"Failed: HTTP {response.status_code}")
224
+ try:
225
+ detail = response.json().get('detail', response.text)
226
+ print_error(f" {detail}")
227
+ except Exception:
228
+ pass
229
+
230
+ except requests.exceptions.ConnectionError:
231
+ print_error(f"Cannot connect to platform at {base_url}")
232
+ print_info("Make sure platform is running")
233
+
234
+ except Exception as e:
235
+ print_error(f"Error: {e}")
236
+
237
+
238
+ @es.command()
239
+ @click.argument('service_name')
240
+ @click.option('--collection', '-c', default=None, help='Specific collection to test (all if omitted)')
241
+ @click.option('--env', default=None, help='Environment (local/dev/staging/prod, defaults to active)')
242
+ @click.option('--platform-url', help='Override platform URL')
243
+ @click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
244
+ def test(service_name, collection, env, platform_url, output_json):
245
+ """Test es_sync by inserting a doc, verifying sync, then cleaning up.
246
+
247
+ For each es_sync trigger, this command:
248
+ 1. Checks ES index exists
249
+ 2. Inserts a test document into MongoDB
250
+ 3. Waits up to 10s for it to appear in ES via change stream
251
+ 4. Cleans up the test document from both MongoDB and ES
252
+
253
+ \b
254
+ Examples:
255
+ # Test all es_sync triggers for crm service
256
+ onex es test crm
257
+
258
+ # Test a specific collection
259
+ onex es test crm --collection adminService
260
+
261
+ # Test on dev environment
262
+ onex es test crm --env dev
263
+ """
264
+ if env is None:
265
+ env = get_active_environment()
266
+
267
+ config = get_config()
268
+ base_url = platform_url or config.get_platform_url(env)
269
+ headers = _build_headers(env)
270
+
271
+ click.echo(f"🧪 Testing ES Sync for {service_name} ({env})...")
272
+ click.echo()
273
+
274
+ body = {'service_name': service_name}
275
+ if collection:
276
+ body['collection'] = collection
277
+
278
+ try:
279
+ response = requests.post(
280
+ f"{base_url}/_internal/es-sync/test",
281
+ json=body,
282
+ headers=headers,
283
+ timeout=120,
284
+ )
285
+
286
+ if response.status_code == 401 and env != "local":
287
+ retry_resp = _handle_auth_retry(env, headers, lambda h: requests.post(
288
+ f"{base_url}/_internal/es-sync/test", json=body, headers=h, timeout=120))
289
+ if retry_resp:
290
+ response = retry_resp
291
+ else:
292
+ return
293
+
294
+ if response.status_code == 200:
295
+ data = response.json()
296
+
297
+ if output_json:
298
+ click.echo(json.dumps(data, indent=2))
299
+ return
300
+
301
+ results = data.get('results', [])
302
+ summary = data.get('summary', {})
303
+ error = data.get('error')
304
+
305
+ if error and not results:
306
+ print_error(error)
307
+ print_info("Deploy a service with es_sync in service.yml first.")
308
+ return
309
+
310
+ for r in results:
311
+ coll = r.get('collection', '?')
312
+ index = r.get('index', '?')
313
+ passed = r.get('passed', False)
314
+ duration = r.get('duration_ms', 0)
315
+ steps = r.get('steps', [])
316
+
317
+ if passed:
318
+ click.echo(click.style(
319
+ f" ✅ {coll} → {index}: PASSED ({duration}ms)", fg='green'))
320
+ else:
321
+ click.echo(click.style(
322
+ f" ❌ {coll} → {index}: FAILED ({duration}ms)", fg='red'))
323
+
324
+ for step in steps:
325
+ if step.startswith('OK:'):
326
+ click.echo(f" {click.style('✓', fg='green')} {step[4:]}")
327
+ elif step.startswith('FAIL:'):
328
+ click.echo(f" {click.style('✗', fg='red')} {step[6:]}")
329
+ elif step.startswith('WARN:'):
330
+ click.echo(f" {click.style('!', fg='yellow')} {step[6:]}")
331
+ elif step.startswith('SKIP:'):
332
+ click.echo(f" {click.style('○', fg='yellow')} {step[6:]}")
333
+ else:
334
+ click.echo(f" {step}")
335
+
336
+ click.echo()
337
+ total = summary.get('total', 0)
338
+ passed_count = summary.get('passed', 0)
339
+ failed_count = summary.get('failed', 0)
340
+
341
+ if failed_count == 0:
342
+ print_success(f"All {total} es_sync trigger(s) passed")
343
+ else:
344
+ print_warning(f"{passed_count}/{total} passed, {failed_count} failed")
345
+
346
+ elif response.status_code == 503:
347
+ print_error("Stream Trigger Service unavailable")
348
+ print_info("Make sure platform is running: ./scripts/deploy-dev.sh status")
349
+
350
+ else:
351
+ print_error(f"Test failed: HTTP {response.status_code}")
352
+ try:
353
+ detail = response.json().get('detail', response.text)
354
+ print_error(f" {detail}")
355
+ except Exception:
356
+ pass
357
+
358
+ except requests.exceptions.ConnectionError:
359
+ print_error(f"Cannot connect to platform at {base_url}")
360
+ print_info("Make sure platform is running")
361
+
362
+ except requests.exceptions.Timeout:
363
+ print_error("Test timed out (>2 minutes)")
364
+ print_info("This can happen if change streams are slow. Check stream-trigger logs.")
365
+
366
+ except Exception as e:
367
+ print_error(f"Error: {e}")
368
+
369
+
370
+ @es.command()
371
+ @click.argument('service_name')
372
+ @click.option('--collection', '-c', default=None, help='Specific collection to reindex (all if omitted)')
373
+ @click.option('--env', default=None, help='Environment (local/dev/staging/prod, defaults to active)')
374
+ @click.option('--platform-url', help='Override platform URL')
375
+ @click.option('--json', 'output_json', is_flag=True, help='Output as JSON')
376
+ def reindex(service_name, collection, env, platform_url, output_json):
377
+ """Reindex Elasticsearch from MongoDB for a service.
378
+
379
+ Reads all documents from MongoDB collections that have es_sync triggers
380
+ registered and bulk-indexes them into Elasticsearch.
381
+
382
+ \b
383
+ Examples:
384
+ # Reindex all ES collections for crm service
385
+ onex es reindex crm
386
+
387
+ # Reindex a specific collection
388
+ onex es reindex crm --collection adminService
389
+
390
+ # Reindex on dev environment
391
+ onex es reindex crm --env dev
392
+ """
393
+ if env is None:
394
+ env = get_active_environment()
395
+
396
+ config = get_config()
397
+ base_url = platform_url or config.get_platform_url(env)
398
+
399
+ # Build auth headers
400
+ access_token = get_access_token(env)
401
+ headers = {}
402
+ if access_token:
403
+ headers['Authorization'] = f'Bearer {access_token}'
404
+
405
+ click.echo(f"🔄 Reindexing ES for {service_name} ({env})...")
406
+
407
+ body = {'service_name': service_name}
408
+ if collection:
409
+ body['collection'] = collection
410
+
411
+ try:
412
+ response = requests.post(
413
+ f"{base_url}/_internal/reindex",
414
+ json=body,
415
+ headers=headers,
416
+ timeout=300,
417
+ )
418
+
419
+ if response.status_code == 401 and env != "local":
420
+ print_warning("Token expired, refreshing...")
421
+ if auto_refresh_token(env, silent=False):
422
+ access_token = get_access_token(env)
423
+ headers['Authorization'] = f'Bearer {access_token}'
424
+ response = requests.post(
425
+ f"{base_url}/_internal/reindex",
426
+ json=body,
427
+ headers=headers,
428
+ timeout=300,
429
+ )
430
+ else:
431
+ print_error("Authentication failed - could not refresh token")
432
+ print_info(f"Please login again: onex login --env {env}")
433
+ return
434
+
435
+ if response.status_code == 200:
436
+ data = response.json()
437
+
438
+ if output_json:
439
+ click.echo(json.dumps(data, indent=2))
440
+ return
441
+
442
+ collections = data.get('collections', [])
443
+ total = data.get('total_docs_synced', 0)
444
+
445
+ if not collections:
446
+ print_info("No es_sync collections found for this service.")
447
+ return
448
+
449
+ click.echo()
450
+ for col in collections:
451
+ name = col.get('collection', '?')
452
+ index = col.get('index', '?')
453
+ docs = col.get('docs_synced', 0)
454
+ duration = col.get('duration_ms', 0)
455
+ error = col.get('error')
456
+
457
+ if error:
458
+ click.echo(click.style(
459
+ f" ❌ {name} → {index}: {error}", fg='red'))
460
+ else:
461
+ click.echo(
462
+ f" ✅ {name} → {index}: {docs} docs ({duration}ms)")
463
+
464
+ click.echo()
465
+ print_success(f"Total: {total} docs synced across {len(collections)} collection(s)")
466
+
467
+ elif response.status_code == 404:
468
+ print_error(response.json().get('detail', 'No es_sync triggers found'))
469
+ print_info("Make sure the service has es_sync configured in service.yml and is deployed.")
470
+
471
+ else:
472
+ print_error(f"Reindex failed: HTTP {response.status_code}")
473
+ try:
474
+ detail = response.json().get('detail', response.text)
475
+ print_error(f" {detail}")
476
+ except Exception:
477
+ pass
478
+
479
+ except requests.exceptions.ConnectionError:
480
+ print_error(f"Cannot connect to platform at {base_url}")
481
+ print_info("Make sure platform is running")
482
+
483
+ except requests.exceptions.Timeout:
484
+ print_error("Reindex timed out (>5 minutes)")
485
+ print_info("This can happen with very large collections. Try reindexing one collection at a time with --collection.")
486
+
487
+ except Exception as e:
488
+ print_error(f"Error: {e}")