@eve-horizon/cli 0.0.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.
@@ -0,0 +1,457 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleSystem = handleSystem;
4
+ const args_1 = require("../lib/args");
5
+ const client_1 = require("../lib/client");
6
+ const output_1 = require("../lib/output");
7
+ // ============================================================================
8
+ // Main Handler
9
+ // ============================================================================
10
+ /**
11
+ * System administration commands.
12
+ *
13
+ * NOTE: When auth is implemented, these commands should be restricted to
14
+ * system administrators only - not regular org users.
15
+ */
16
+ async function handleSystem(subcommand, positionals, flags, context) {
17
+ const json = Boolean(flags.json);
18
+ switch (subcommand) {
19
+ case 'status':
20
+ return handleStatus(context, json);
21
+ case 'health':
22
+ return handleHealth(context, json);
23
+ case 'jobs':
24
+ return handleJobs(flags, context, json);
25
+ case 'envs':
26
+ return handleEnvs(flags, context, json);
27
+ case 'logs':
28
+ return handleLogs(positionals, flags, context, json);
29
+ case 'pods':
30
+ return handlePods(context, json);
31
+ case 'events':
32
+ return handleEvents(flags, context, json);
33
+ case 'config':
34
+ return handleConfig(context, json);
35
+ default:
36
+ throw new Error('Usage: eve system <status|health|jobs|envs|logs|pods|events|config>');
37
+ }
38
+ }
39
+ // ============================================================================
40
+ // Subcommand Handlers
41
+ // ============================================================================
42
+ /**
43
+ * eve system status
44
+ * Show comprehensive system status via API endpoint.
45
+ *
46
+ * Note: This calls GET /system/status on the API. The API is responsible for
47
+ * aggregating health from internal services (orchestrator, worker, etc.).
48
+ * The CLI only talks to the API - never directly to other services.
49
+ */
50
+ async function handleStatus(context, json) {
51
+ try {
52
+ // Try the full system status endpoint first
53
+ const status = await (0, client_1.requestJson)(context, '/system/status');
54
+ if (json) {
55
+ (0, output_1.outputJson)(status, json);
56
+ }
57
+ else {
58
+ formatStatus(status);
59
+ }
60
+ }
61
+ catch (error) {
62
+ // If /system/status doesn't exist yet, fall back to basic health check
63
+ const err = error;
64
+ if (err.message?.includes('HTTP 404')) {
65
+ console.log('Note: Full system status endpoint not yet implemented.');
66
+ console.log('Falling back to basic health check...');
67
+ console.log('');
68
+ return handleHealth(context, json);
69
+ }
70
+ throw error;
71
+ }
72
+ }
73
+ /**
74
+ * eve system health
75
+ * Quick health check of the API.
76
+ *
77
+ * Note: This only checks the API's health endpoint. The API is the gateway -
78
+ * if the API is healthy and connected to the database, the system is operational.
79
+ */
80
+ async function handleHealth(context, json) {
81
+ try {
82
+ const health = await (0, client_1.requestJson)(context, '/health');
83
+ if (json) {
84
+ (0, output_1.outputJson)(health, json);
85
+ }
86
+ else {
87
+ const isHealthy = health.status === 'ok' || health.status === 'healthy';
88
+ const icon = isHealthy ? '✓' : '✗';
89
+ const status = isHealthy ? 'Healthy' : 'Unhealthy';
90
+ console.log('System Health Check');
91
+ console.log('═══════════════════════════════════════');
92
+ console.log('');
93
+ console.log(` ${icon} API: ${status}`);
94
+ if (health.database) {
95
+ console.log(` Database: ${health.database}`);
96
+ }
97
+ if (health.version) {
98
+ console.log(` Version: ${health.version}`);
99
+ }
100
+ console.log('');
101
+ if (isHealthy) {
102
+ console.log('API is operational.');
103
+ }
104
+ else {
105
+ console.log('API is not healthy. Check server logs.');
106
+ }
107
+ }
108
+ }
109
+ catch (error) {
110
+ const err = error;
111
+ if (json) {
112
+ (0, output_1.outputJson)({
113
+ api: { healthy: false, error: err.message },
114
+ }, json);
115
+ }
116
+ else {
117
+ console.log('System Health Check');
118
+ console.log('═══════════════════════════════════════');
119
+ console.log('');
120
+ console.log(' ✗ API: Unreachable');
121
+ console.log(` ${err.message}`);
122
+ console.log('');
123
+ console.log(`Make sure the API is running and EVE_API_URL is set correctly.`);
124
+ console.log(`Current: ${context.apiUrl}`);
125
+ }
126
+ }
127
+ }
128
+ // ============================================================================
129
+ // Formatting Helpers
130
+ // ============================================================================
131
+ function formatStatus(status) {
132
+ console.log('╭────────────────────────────────────────────────────────────────╮');
133
+ console.log('│ System Status │');
134
+ console.log('╰────────────────────────────────────────────────────────────────╯');
135
+ console.log('');
136
+ // Services
137
+ console.log('Services:');
138
+ formatServiceHealth(' API', status.api.healthy, status.api.version);
139
+ formatServiceHealth(' Orchestrator', status.orchestrator.healthy);
140
+ formatServiceHealth(' Worker', status.worker.healthy);
141
+ console.log('');
142
+ // Queue
143
+ console.log('Job Queue:');
144
+ console.log(` Ready: ${status.queue.ready}`);
145
+ console.log(` Active: ${status.queue.active}`);
146
+ console.log(` Blocked: ${status.queue.blocked}`);
147
+ console.log('');
148
+ // Overall assessment
149
+ const allHealthy = status.api.healthy && status.orchestrator.healthy && status.worker.healthy;
150
+ if (allHealthy) {
151
+ console.log('Status: All systems operational');
152
+ }
153
+ else {
154
+ const unhealthy = [];
155
+ if (!status.api.healthy)
156
+ unhealthy.push('API');
157
+ if (!status.orchestrator.healthy)
158
+ unhealthy.push('Orchestrator');
159
+ if (!status.worker.healthy)
160
+ unhealthy.push('Worker');
161
+ console.log(`Status: Issues detected with: ${unhealthy.join(', ')}`);
162
+ }
163
+ }
164
+ function formatServiceHealth(name, healthy, version) {
165
+ const icon = healthy ? '✓' : '✗';
166
+ const status = healthy ? 'healthy' : 'unhealthy';
167
+ const versionStr = version ? ` (v${version})` : '';
168
+ console.log(`${name}: ${icon} ${status}${versionStr}`);
169
+ }
170
+ /**
171
+ * eve system jobs [--org=X] [--project=X] [--phase=X] [--limit=50] [--offset=0]
172
+ * Admin view: list all jobs across all projects
173
+ */
174
+ async function handleJobs(flags, context, json) {
175
+ const query = buildQuery({
176
+ org_id: (0, args_1.getStringFlag)(flags, ['org']),
177
+ project_id: (0, args_1.getStringFlag)(flags, ['project']),
178
+ phase: (0, args_1.getStringFlag)(flags, ['phase']),
179
+ limit: (0, args_1.getStringFlag)(flags, ['limit']) ?? '50',
180
+ offset: (0, args_1.getStringFlag)(flags, ['offset']),
181
+ });
182
+ const response = await (0, client_1.requestJson)(context, `/jobs${query}`);
183
+ if (json) {
184
+ (0, output_1.outputJson)(response, json);
185
+ }
186
+ else {
187
+ if (response.jobs.length === 0) {
188
+ console.log('No jobs found.');
189
+ return;
190
+ }
191
+ console.log('System Jobs (admin view):');
192
+ console.log('');
193
+ formatJobsTable(response.jobs);
194
+ }
195
+ }
196
+ /**
197
+ * eve system envs [--org=X] [--project=X] [--limit=50] [--offset=0]
198
+ * Admin view: list all environments across all projects
199
+ */
200
+ async function handleEnvs(flags, context, json) {
201
+ const query = buildQuery({
202
+ org_id: (0, args_1.getStringFlag)(flags, ['org']),
203
+ project_id: (0, args_1.getStringFlag)(flags, ['project']),
204
+ limit: (0, args_1.getStringFlag)(flags, ['limit']) ?? '50',
205
+ offset: (0, args_1.getStringFlag)(flags, ['offset']),
206
+ });
207
+ try {
208
+ const response = await (0, client_1.requestJson)(context, `/system/envs${query}`);
209
+ if (json) {
210
+ (0, output_1.outputJson)(response, json);
211
+ }
212
+ else {
213
+ if (response.environments.length === 0) {
214
+ console.log('No environments found.');
215
+ return;
216
+ }
217
+ console.log('System Environments (admin view):');
218
+ console.log('');
219
+ formatEnvsTable(response.environments);
220
+ }
221
+ }
222
+ catch (error) {
223
+ const err = error;
224
+ if (err.message?.includes('HTTP 404')) {
225
+ console.error('Error: The /system/envs endpoint is not yet implemented.');
226
+ console.error('Please use "eve env list" within a project context instead.');
227
+ process.exit(1);
228
+ }
229
+ throw error;
230
+ }
231
+ }
232
+ /**
233
+ * eve system logs <service> [--tail=100]
234
+ * Fetch recent logs for a service via API.
235
+ */
236
+ async function handleLogs(positionals, flags, context, json) {
237
+ const service = positionals[0] ?? (0, args_1.getStringFlag)(flags, ['service']);
238
+ if (!service) {
239
+ throw new Error('Usage: eve system logs <api|orchestrator|worker|postgres> [--tail=<n>]');
240
+ }
241
+ const tail = (0, args_1.getStringFlag)(flags, ['tail']);
242
+ const query = buildQuery({ tail: tail ?? '100' });
243
+ const response = await (0, client_1.requestJson)(context, `/system/logs/${service}${query}`);
244
+ if (json) {
245
+ (0, output_1.outputJson)(response, json);
246
+ return;
247
+ }
248
+ if (response.length === 0) {
249
+ console.log('No logs found.');
250
+ return;
251
+ }
252
+ for (const entry of response) {
253
+ console.log(`[${entry.timestamp}] ${entry.line}`);
254
+ }
255
+ }
256
+ /**
257
+ * eve system pods
258
+ * List pods via API.
259
+ */
260
+ async function handlePods(context, json) {
261
+ const response = await (0, client_1.requestJson)(context, '/system/pods');
262
+ if (json) {
263
+ (0, output_1.outputJson)(response, json);
264
+ return;
265
+ }
266
+ if (response.length === 0) {
267
+ console.log('No pods found.');
268
+ return;
269
+ }
270
+ console.log('Pods:');
271
+ console.log('');
272
+ formatPodsTable(response);
273
+ }
274
+ /**
275
+ * eve system events [--limit=50]
276
+ * List recent cluster events via API.
277
+ */
278
+ async function handleEvents(flags, context, json) {
279
+ const limit = (0, args_1.getStringFlag)(flags, ['limit']);
280
+ const query = buildQuery({ limit: limit ?? '50' });
281
+ const response = await (0, client_1.requestJson)(context, `/system/events${query}`);
282
+ if (json) {
283
+ (0, output_1.outputJson)(response, json);
284
+ return;
285
+ }
286
+ if (response.length === 0) {
287
+ console.log('No events found.');
288
+ return;
289
+ }
290
+ console.log('Recent Events:');
291
+ for (const event of response) {
292
+ const target = `${event.involvedObject.kind}/${event.involvedObject.name}`;
293
+ console.log(`[${event.timestamp}] ${event.type} ${event.reason} ${target} - ${event.message}`);
294
+ }
295
+ }
296
+ /**
297
+ * eve system config
298
+ * Show cluster config summary via API.
299
+ */
300
+ async function handleConfig(context, json) {
301
+ const response = await (0, client_1.requestJson)(context, '/system/config');
302
+ if (json) {
303
+ (0, output_1.outputJson)(response, json);
304
+ return;
305
+ }
306
+ console.log('Cluster Config:');
307
+ console.log(` Namespace: ${response.namespace}`);
308
+ if (response.clusterVersion) {
309
+ console.log(` Cluster Version: ${response.clusterVersion}`);
310
+ }
311
+ if (response.nodeCount !== undefined) {
312
+ console.log(` Node Count: ${response.nodeCount}`);
313
+ }
314
+ if (response.deployments.length > 0) {
315
+ console.log(' Deployments:');
316
+ response.deployments.forEach((deployment) => console.log(` - ${deployment}`));
317
+ }
318
+ }
319
+ // ============================================================================
320
+ // Helper Functions
321
+ // ============================================================================
322
+ function buildQuery(params) {
323
+ const search = new URLSearchParams();
324
+ Object.entries(params).forEach(([key, value]) => {
325
+ if (value === undefined || value === '')
326
+ return;
327
+ search.set(key, String(value));
328
+ });
329
+ const query = search.toString();
330
+ return query ? `?${query}` : '';
331
+ }
332
+ /**
333
+ * Format jobs as a human-readable table
334
+ */
335
+ function formatJobsTable(jobs) {
336
+ if (jobs.length === 0) {
337
+ console.log('No jobs found.');
338
+ return;
339
+ }
340
+ // Calculate column widths
341
+ const idWidth = Math.max(6, ...jobs.map((j) => j.id.length));
342
+ const projectWidth = Math.max(10, ...jobs.map((j) => j.project_id.length));
343
+ const phaseWidth = Math.max(5, ...jobs.map((j) => j.phase.length));
344
+ const titleWidth = Math.min(40, Math.max(5, ...jobs.map((j) => j.title.length)));
345
+ // Header
346
+ const header = [
347
+ padRight('Job ID', idWidth),
348
+ padRight('Project', projectWidth),
349
+ padRight('Phase', phaseWidth),
350
+ padRight('P', 2),
351
+ padRight('Title', titleWidth),
352
+ 'Assignee',
353
+ ].join(' ');
354
+ console.log(header);
355
+ console.log('-'.repeat(header.length));
356
+ // Rows
357
+ for (const job of jobs) {
358
+ const title = job.title.length > titleWidth ? job.title.slice(0, titleWidth - 3) + '...' : job.title;
359
+ const row = [
360
+ padRight(job.id, idWidth),
361
+ padRight(job.project_id, projectWidth),
362
+ padRight(job.phase, phaseWidth),
363
+ padRight(`P${job.priority}`, 2),
364
+ padRight(title, titleWidth),
365
+ job.assignee ?? '-',
366
+ ].join(' ');
367
+ console.log(row);
368
+ }
369
+ console.log('');
370
+ console.log(`Total: ${jobs.length} job(s)`);
371
+ }
372
+ /**
373
+ * Format environments as a human-readable table
374
+ */
375
+ function formatEnvsTable(envs) {
376
+ if (envs.length === 0) {
377
+ console.log('No environments found.');
378
+ return;
379
+ }
380
+ // Calculate column widths
381
+ const projectWidth = Math.max(10, ...envs.map((e) => e.project_id.length));
382
+ const nameWidth = Math.max(12, ...envs.map((e) => e.name.length));
383
+ const typeWidth = Math.max(4, ...envs.map((e) => e.type.length));
384
+ const namespaceWidth = Math.max(9, ...envs.map((e) => (e.namespace ?? '-').length));
385
+ const releaseWidth = Math.max(7, ...envs.map((e) => (e.current_release ?? '-').length));
386
+ // Header
387
+ const header = [
388
+ padRight('Project', projectWidth),
389
+ padRight('Environment', nameWidth),
390
+ padRight('Type', typeWidth),
391
+ padRight('Namespace', namespaceWidth),
392
+ padRight('Release', releaseWidth),
393
+ ].join(' ');
394
+ console.log(header);
395
+ console.log('-'.repeat(header.length));
396
+ // Rows
397
+ for (const env of envs) {
398
+ const row = [
399
+ padRight(env.project_id, projectWidth),
400
+ padRight(env.name, nameWidth),
401
+ padRight(env.type, typeWidth),
402
+ padRight(env.namespace ?? '-', namespaceWidth),
403
+ padRight(env.current_release ?? '-', releaseWidth),
404
+ ].join(' ');
405
+ console.log(row);
406
+ }
407
+ console.log('');
408
+ console.log(`Total: ${envs.length} environment(s)`);
409
+ }
410
+ function formatPodsTable(pods) {
411
+ if (pods.length === 0) {
412
+ console.log('No pods found.');
413
+ return;
414
+ }
415
+ const nameWidth = Math.max(8, ...pods.map((p) => p.name.length));
416
+ const nsWidth = Math.max(8, ...pods.map((p) => p.namespace.length));
417
+ const phaseWidth = Math.max(5, ...pods.map((p) => p.phase.length));
418
+ const readyWidth = 5;
419
+ const restartsWidth = Math.max(8, ...pods.map((p) => String(p.restarts).length));
420
+ const ageWidth = Math.max(3, ...pods.map((p) => p.age.length));
421
+ const header = [
422
+ padRight('Name', nameWidth),
423
+ padRight('Namespace', nsWidth),
424
+ padRight('Phase', phaseWidth),
425
+ padRight('Ready', readyWidth),
426
+ padRight('Restarts', restartsWidth),
427
+ padRight('Age', ageWidth),
428
+ 'Component',
429
+ 'Org',
430
+ 'Project',
431
+ 'Env',
432
+ ].join(' ');
433
+ console.log(header);
434
+ console.log('-'.repeat(header.length));
435
+ for (const pod of pods) {
436
+ console.log([
437
+ padRight(pod.name, nameWidth),
438
+ padRight(pod.namespace, nsWidth),
439
+ padRight(pod.phase, phaseWidth),
440
+ padRight(pod.ready ? 'yes' : 'no', readyWidth),
441
+ padRight(String(pod.restarts), restartsWidth),
442
+ padRight(pod.age, ageWidth),
443
+ padRight(pod.component ?? '-', 10),
444
+ padRight(pod.orgId ?? '-', 10),
445
+ padRight(pod.projectId ?? '-', 10),
446
+ padRight(pod.env ?? '-', 10),
447
+ ].join(' '));
448
+ }
449
+ }
450
+ /**
451
+ * Pad a string to the right with spaces
452
+ */
453
+ function padRight(str, width) {
454
+ if (str.length >= width)
455
+ return str;
456
+ return str + ' '.repeat(width - str.length);
457
+ }