@calltelemetry/cli 0.4.26 → 0.5.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.
- package/dist/commands/diag.d.ts.map +1 -1
- package/dist/commands/diag.js +29 -0
- package/dist/commands/diag.js.map +1 -1
- package/dist/commands/k8s.d.ts +3 -0
- package/dist/commands/k8s.d.ts.map +1 -0
- package/dist/commands/k8s.js +192 -0
- package/dist/commands/k8s.js.map +1 -0
- package/dist/commands/network.d.ts +3 -0
- package/dist/commands/network.d.ts.map +1 -0
- package/dist/commands/network.js +135 -0
- package/dist/commands/network.js.map +1 -0
- package/dist/commands/shell.d.ts +3 -0
- package/dist/commands/shell.d.ts.map +1 -0
- package/dist/commands/shell.js +10 -0
- package/dist/commands/shell.js.map +1 -0
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/backend.d.ts +85 -0
- package/dist/lib/backend.d.ts.map +1 -0
- package/dist/lib/backend.js +6 -0
- package/dist/lib/backend.js.map +1 -0
- package/dist/lib/compose-backend.d.ts +19 -0
- package/dist/lib/compose-backend.d.ts.map +1 -0
- package/dist/lib/compose-backend.js +78 -0
- package/dist/lib/compose-backend.js.map +1 -0
- package/dist/lib/deployment.d.ts +6 -0
- package/dist/lib/deployment.d.ts.map +1 -0
- package/dist/lib/deployment.js +51 -0
- package/dist/lib/deployment.js.map +1 -0
- package/dist/lib/doctor.d.ts +13 -0
- package/dist/lib/doctor.d.ts.map +1 -0
- package/dist/lib/doctor.js +82 -0
- package/dist/lib/doctor.js.map +1 -0
- package/dist/lib/helmfile.d.ts +14 -0
- package/dist/lib/helmfile.d.ts.map +1 -0
- package/dist/lib/helmfile.js +78 -0
- package/dist/lib/helmfile.js.map +1 -0
- package/dist/lib/k8s-backend.d.ts +36 -0
- package/dist/lib/k8s-backend.d.ts.map +1 -0
- package/dist/lib/k8s-backend.js +301 -0
- package/dist/lib/k8s-backend.js.map +1 -0
- package/dist/lib/k8s-client.d.ts +14 -0
- package/dist/lib/k8s-client.d.ts.map +1 -0
- package/dist/lib/k8s-client.js +42 -0
- package/dist/lib/k8s-client.js.map +1 -0
- package/dist/lib/network.d.ts +8 -0
- package/dist/lib/network.d.ts.map +1 -1
- package/dist/lib/network.js +17 -0
- package/dist/lib/network.js.map +1 -1
- package/dist/lib/prefs.d.ts +3 -0
- package/dist/lib/prefs.d.ts.map +1 -1
- package/dist/lib/prefs.js +9 -1
- package/dist/lib/prefs.js.map +1 -1
- package/dist/shell/commands/config.d.ts +11 -0
- package/dist/shell/commands/config.d.ts.map +1 -0
- package/dist/shell/commands/config.js +193 -0
- package/dist/shell/commands/config.js.map +1 -0
- package/dist/shell/commands/connect.d.ts +18 -0
- package/dist/shell/commands/connect.d.ts.map +1 -0
- package/dist/shell/commands/connect.js +99 -0
- package/dist/shell/commands/connect.js.map +1 -0
- package/dist/shell/commands/db.d.ts +11 -0
- package/dist/shell/commands/db.d.ts.map +1 -0
- package/dist/shell/commands/db.js +237 -0
- package/dist/shell/commands/db.js.map +1 -0
- package/dist/shell/commands/debug.d.ts +15 -0
- package/dist/shell/commands/debug.d.ts.map +1 -0
- package/dist/shell/commands/debug.js +310 -0
- package/dist/shell/commands/debug.js.map +1 -0
- package/dist/shell/commands/diag.d.ts +16 -0
- package/dist/shell/commands/diag.d.ts.map +1 -0
- package/dist/shell/commands/diag.js +607 -0
- package/dist/shell/commands/diag.js.map +1 -0
- package/dist/shell/commands/registry.d.ts +7 -0
- package/dist/shell/commands/registry.d.ts.map +1 -0
- package/dist/shell/commands/registry.js +391 -0
- package/dist/shell/commands/registry.js.map +1 -0
- package/dist/shell/commands/secrets.d.ts +18 -0
- package/dist/shell/commands/secrets.d.ts.map +1 -0
- package/dist/shell/commands/secrets.js +100 -0
- package/dist/shell/commands/secrets.js.map +1 -0
- package/dist/shell/commands/services.d.ts +7 -0
- package/dist/shell/commands/services.d.ts.map +1 -0
- package/dist/shell/commands/services.js +115 -0
- package/dist/shell/commands/services.js.map +1 -0
- package/dist/shell/commands/show.d.ts +26 -0
- package/dist/shell/commands/show.d.ts.map +1 -0
- package/dist/shell/commands/show.js +897 -0
- package/dist/shell/commands/show.js.map +1 -0
- package/dist/shell/commands/system.d.ts +13 -0
- package/dist/shell/commands/system.d.ts.map +1 -0
- package/dist/shell/commands/system.js +270 -0
- package/dist/shell/commands/system.js.map +1 -0
- package/dist/shell/commands/test.d.ts +12 -0
- package/dist/shell/commands/test.d.ts.map +1 -0
- package/dist/shell/commands/test.js +197 -0
- package/dist/shell/commands/test.js.map +1 -0
- package/dist/shell/commands/users.d.ts +14 -0
- package/dist/shell/commands/users.d.ts.map +1 -0
- package/dist/shell/commands/users.js +86 -0
- package/dist/shell/commands/users.js.map +1 -0
- package/dist/shell/completer.d.ts +17 -0
- package/dist/shell/completer.d.ts.map +1 -0
- package/dist/shell/completer.js +151 -0
- package/dist/shell/completer.js.map +1 -0
- package/dist/shell/context.d.ts +12 -0
- package/dist/shell/context.d.ts.map +1 -0
- package/dist/shell/context.js +2 -0
- package/dist/shell/context.js.map +1 -0
- package/dist/shell/footer.d.ts +8 -0
- package/dist/shell/footer.d.ts.map +1 -0
- package/dist/shell/footer.js +47 -0
- package/dist/shell/footer.js.map +1 -0
- package/dist/shell/formatter.d.ts +47 -0
- package/dist/shell/formatter.d.ts.map +1 -0
- package/dist/shell/formatter.js +135 -0
- package/dist/shell/formatter.js.map +1 -0
- package/dist/shell/mcp-bridge.d.ts +13 -0
- package/dist/shell/mcp-bridge.d.ts.map +1 -0
- package/dist/shell/mcp-bridge.js +23 -0
- package/dist/shell/mcp-bridge.js.map +1 -0
- package/dist/shell/parser.d.ts +36 -0
- package/dist/shell/parser.d.ts.map +1 -0
- package/dist/shell/parser.js +110 -0
- package/dist/shell/parser.js.map +1 -0
- package/dist/shell/repl.d.ts +5 -0
- package/dist/shell/repl.d.ts.map +1 -0
- package/dist/shell/repl.js +273 -0
- package/dist/shell/repl.js.map +1 -0
- package/dist/shell/sparkline.d.ts +16 -0
- package/dist/shell/sparkline.d.ts.map +1 -0
- package/dist/shell/sparkline.js +56 -0
- package/dist/shell/sparkline.js.map +1 -0
- package/dist/shell/theme.d.ts +72 -0
- package/dist/shell/theme.d.ts.map +1 -0
- package/dist/shell/theme.js +60 -0
- package/dist/shell/theme.js.map +1 -0
- package/dist/ui/views/DbHealthView.d.ts +6 -0
- package/dist/ui/views/DbHealthView.d.ts.map +1 -0
- package/dist/ui/views/DbHealthView.js +180 -0
- package/dist/ui/views/DbHealthView.js.map +1 -0
- package/dist/ui/views/K8sDeployView.d.ts +8 -0
- package/dist/ui/views/K8sDeployView.d.ts.map +1 -0
- package/dist/ui/views/K8sDeployView.js +81 -0
- package/dist/ui/views/K8sDeployView.js.map +1 -0
- package/dist/ui/views/K8sRunbookView.d.ts +8 -0
- package/dist/ui/views/K8sRunbookView.d.ts.map +1 -0
- package/dist/ui/views/K8sRunbookView.js +181 -0
- package/dist/ui/views/K8sRunbookView.js.map +1 -0
- package/dist/ui/views/K8sServicesView.d.ts +8 -0
- package/dist/ui/views/K8sServicesView.d.ts.map +1 -0
- package/dist/ui/views/K8sServicesView.js +106 -0
- package/dist/ui/views/K8sServicesView.js.map +1 -0
- package/dist/ui/views/K8sStatusView.d.ts +8 -0
- package/dist/ui/views/K8sStatusView.d.ts.map +1 -0
- package/dist/ui/views/K8sStatusView.js +135 -0
- package/dist/ui/views/K8sStatusView.js.map +1 -0
- package/dist/ui/views/MainMenu.d.ts.map +1 -1
- package/dist/ui/views/MainMenu.js +54 -24
- package/dist/ui/views/MainMenu.js.map +1 -1
- package/dist/ui/views/SettingsView.d.ts.map +1 -1
- package/dist/ui/views/SettingsView.js +19 -1
- package/dist/ui/views/SettingsView.js.map +1 -1
- package/dist/ui/views/SetupWizardView.d.ts +7 -0
- package/dist/ui/views/SetupWizardView.d.ts.map +1 -0
- package/dist/ui/views/SetupWizardView.js +244 -0
- package/dist/ui/views/SetupWizardView.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { table, kv, statusLine, banner } from '../formatter.js';
|
|
6
|
+
import { getAppHealth } from '../../lib/rpc.js';
|
|
7
|
+
import { rpc } from '../../lib/rpc.js';
|
|
8
|
+
import { getServiceList } from '../../lib/services.js';
|
|
9
|
+
import { compose, composeExec } from '../../lib/compose.js';
|
|
10
|
+
import { getSystemStats, getDockerVersion, formatBytes } from '../../lib/system.js';
|
|
11
|
+
import { DB_USER, DB_NAME, PG_ENV } from '../../lib/db.js';
|
|
12
|
+
import { getPaths } from '../../lib/paths.js';
|
|
13
|
+
import { getIpv6Status } from '../../lib/ipv6.js';
|
|
14
|
+
import { isOtelEnabled } from '../../lib/otel.js';
|
|
15
|
+
import { getPostgresVersion } from '../../lib/postgres.js';
|
|
16
|
+
import { getSecrets, SECRET_CATALOG, SECRET_GROUPS, countOverrides } from '../../lib/secrets.js';
|
|
17
|
+
import { isJtapiEnabled, getJtapiHealthCheck } from '../../lib/jtapi.js';
|
|
18
|
+
import { Status, thresholdColor, visibleLength } from '../theme.js';
|
|
19
|
+
// ── Helpers ──
|
|
20
|
+
async function psqlTerse(query) {
|
|
21
|
+
const result = await composeExec('db', ['psql', '-U', DB_USER, '-d', DB_NAME, '-t', '-c', query], { env: PG_ENV, pipe: true });
|
|
22
|
+
return (result.stdout ?? '').trim();
|
|
23
|
+
}
|
|
24
|
+
/** Inline meter bar: ████████░░░░░░░░ 12/100 (12%) */
|
|
25
|
+
function meterBar(current, max, width = 20) {
|
|
26
|
+
const pct = max > 0 ? Math.min(100, Math.round((current / max) * 100)) : 0;
|
|
27
|
+
const filled = Math.round((pct / 100) * width);
|
|
28
|
+
const empty = width - filled;
|
|
29
|
+
const color = thresholdColor(pct);
|
|
30
|
+
const bar = color('\u2588'.repeat(filled)) + chalk.dim('\u2591'.repeat(empty));
|
|
31
|
+
return `${bar} ${current.toLocaleString()}/${max.toLocaleString()} (${pct}%)`;
|
|
32
|
+
}
|
|
33
|
+
/** Size bar for table rows — proportional to max value */
|
|
34
|
+
function sizeBar(bytes, maxBytes, width = 10) {
|
|
35
|
+
if (maxBytes <= 0)
|
|
36
|
+
return '\u2591'.repeat(width);
|
|
37
|
+
const pct = Math.min(100, Math.round((bytes / maxBytes) * 100));
|
|
38
|
+
const filled = Math.max(1, Math.round((pct / 100) * width));
|
|
39
|
+
const empty = width - filled;
|
|
40
|
+
return chalk.cyan('\u2588'.repeat(filled)) + chalk.dim('\u2591'.repeat(empty));
|
|
41
|
+
}
|
|
42
|
+
/** Loading spinner — writes to stdout and returns a clear function */
|
|
43
|
+
function showSpinner(label) {
|
|
44
|
+
const text = ` ${Status.running.color(Status.running.symbol)} ${chalk.cyan(label)}`;
|
|
45
|
+
process.stdout.write(text + '\r');
|
|
46
|
+
return () => {
|
|
47
|
+
process.stdout.write(' '.repeat(visibleLength(text) + 2) + '\r');
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/** Format a service state with colored dot */
|
|
51
|
+
function serviceState(state, health) {
|
|
52
|
+
const s = state.toLowerCase();
|
|
53
|
+
const h = (health ?? '').toLowerCase();
|
|
54
|
+
if (s === 'running' && h === 'healthy') {
|
|
55
|
+
return `${Status.online.color(Status.online.symbol)} Running`;
|
|
56
|
+
}
|
|
57
|
+
if (s === 'running' && h === 'unhealthy') {
|
|
58
|
+
return `${Status.degraded.color(Status.degraded.symbol)} Degraded`;
|
|
59
|
+
}
|
|
60
|
+
if (s === 'running') {
|
|
61
|
+
return `${Status.online.color(Status.online.symbol)} Running`;
|
|
62
|
+
}
|
|
63
|
+
if (s === 'exited' || s === 'stopped') {
|
|
64
|
+
return `${Status.offline.color(Status.offline.symbol)} Stopped`;
|
|
65
|
+
}
|
|
66
|
+
if (s === 'restarting') {
|
|
67
|
+
return `${Status.warn.color(Status.warn.symbol)} Restarting`;
|
|
68
|
+
}
|
|
69
|
+
return `${Status.waiting.color(Status.waiting.symbol)} ${state}`;
|
|
70
|
+
}
|
|
71
|
+
/** Parse size string like "1.8 GB", "256 MB", "48 kB" into bytes for comparison */
|
|
72
|
+
function parseSizeToBytes(sizeStr) {
|
|
73
|
+
const match = sizeStr.trim().match(/^([\d.]+)\s*(bytes|kB|MB|GB|TB)/i);
|
|
74
|
+
if (!match)
|
|
75
|
+
return 0;
|
|
76
|
+
const num = parseFloat(match[1]);
|
|
77
|
+
const unit = match[2].toLowerCase();
|
|
78
|
+
const multipliers = {
|
|
79
|
+
bytes: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3, tb: 1024 ** 4,
|
|
80
|
+
};
|
|
81
|
+
return num * (multipliers[unit] ?? 0);
|
|
82
|
+
}
|
|
83
|
+
// ── show health ──
|
|
84
|
+
export async function showHealth(_args, _opts, _ctx) {
|
|
85
|
+
const clear = showSpinner('Checking system health...');
|
|
86
|
+
try {
|
|
87
|
+
const h = await getAppHealth();
|
|
88
|
+
clear();
|
|
89
|
+
console.log(banner('System Health'));
|
|
90
|
+
// Build check list with severity
|
|
91
|
+
const checks = [
|
|
92
|
+
{ label: 'Admin API', ok: h.api, detail: h.apiError },
|
|
93
|
+
{ label: 'CURRI API', ok: h.curri, detail: h.curriError },
|
|
94
|
+
{ label: 'RPC', ok: h.rpc, detail: h.rpcError },
|
|
95
|
+
{ label: 'DB Pool', ok: h.dbConnected, detail: h.dbError },
|
|
96
|
+
{ label: 'DB Direct', ok: h.dbDirect, detail: h.dbDirectError },
|
|
97
|
+
{ label: 'CURRI Proxy', ok: h.curriProxy, detail: h.curriProxyError },
|
|
98
|
+
{ label: 'Admin Proxy', ok: h.adminProxy, detail: h.adminProxyError },
|
|
99
|
+
{ label: 'SFTP', ok: h.sftp, detail: h.sftpError },
|
|
100
|
+
];
|
|
101
|
+
const failures = checks.filter(c => !c.ok);
|
|
102
|
+
const healthy = checks.filter(c => c.ok);
|
|
103
|
+
// Show failures first (critical)
|
|
104
|
+
if (failures.length > 0) {
|
|
105
|
+
console.log(chalk.red.bold('\n CRITICAL'));
|
|
106
|
+
for (const f of failures) {
|
|
107
|
+
console.log(` ${statusLine(f.label, 'fail', f.detail)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Show healthy checks
|
|
111
|
+
if (healthy.length > 0) {
|
|
112
|
+
console.log(chalk.green.bold('\n HEALTHY'));
|
|
113
|
+
for (const h of healthy) {
|
|
114
|
+
console.log(` ${statusLine(h.label, 'ok')}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// Overall verdict
|
|
118
|
+
const overallStatus = h.overall === 'healthy' ? 'ok' : h.overall === 'degraded' ? 'warn' : 'fail';
|
|
119
|
+
const overallLabel = h.overall === 'healthy'
|
|
120
|
+
? `All ${checks.length} checks passing`
|
|
121
|
+
: h.overall === 'degraded'
|
|
122
|
+
? `${failures.length} of ${checks.length} checks failing`
|
|
123
|
+
: `${failures.length} of ${checks.length} checks failing`;
|
|
124
|
+
console.log(`\n ${statusLine('Overall', overallStatus, overallLabel)}`);
|
|
125
|
+
console.log();
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
clear();
|
|
129
|
+
console.log(banner('System Health'));
|
|
130
|
+
console.log(statusLine('Health Check', 'fail', err.message));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// ── show version ──
|
|
134
|
+
export async function showVersion(_args, _opts, _ctx) {
|
|
135
|
+
const clear = showSpinner('Gathering version info...');
|
|
136
|
+
const pairs = [];
|
|
137
|
+
// CLI version from package
|
|
138
|
+
try {
|
|
139
|
+
const pkg = await import('../../../package.json', { with: { type: 'json' } });
|
|
140
|
+
pairs.push(['CLI Version', chalk.bold(pkg.default?.version ?? 'unknown')]);
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
pairs.push(['CLI Version', chalk.dim('unknown')]);
|
|
144
|
+
}
|
|
145
|
+
// App version via RPC
|
|
146
|
+
try {
|
|
147
|
+
const out = await rpc(':application.loaded_applications() |> Enum.find(fn {app, _, _} -> app == :cdrcisco end) |> elem(2) |> to_string() |> IO.puts()');
|
|
148
|
+
pairs.push(['App Version', chalk.bold(out.trim() || 'unknown')]);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
pairs.push(['App Version', chalk.dim('unavailable')]);
|
|
152
|
+
}
|
|
153
|
+
// Docker version
|
|
154
|
+
try {
|
|
155
|
+
const dv = await getDockerVersion();
|
|
156
|
+
pairs.push(['Docker', dv ? `${dv}` : chalk.dim('unavailable')]);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
pairs.push(['Docker', chalk.dim('unavailable')]);
|
|
160
|
+
}
|
|
161
|
+
// System stats
|
|
162
|
+
try {
|
|
163
|
+
const stats = await getSystemStats();
|
|
164
|
+
pairs.push(['System', stats]);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
pairs.push(['System', chalk.dim('unavailable')]);
|
|
168
|
+
}
|
|
169
|
+
clear();
|
|
170
|
+
console.log(banner('Version Information'));
|
|
171
|
+
console.log(kv(pairs));
|
|
172
|
+
}
|
|
173
|
+
// ── show services ──
|
|
174
|
+
export async function showServices(_args, _opts, _ctx) {
|
|
175
|
+
const clear = showSpinner('Querying services...');
|
|
176
|
+
try {
|
|
177
|
+
const services = await getServiceList();
|
|
178
|
+
clear();
|
|
179
|
+
if (services.length === 0) {
|
|
180
|
+
console.log(banner('Services'));
|
|
181
|
+
console.log(statusLine('Services', 'warn', 'No services found. Is Docker Compose running?'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// Count by state for summary
|
|
185
|
+
const running = services.filter(s => s.state.toLowerCase() === 'running').length;
|
|
186
|
+
const stopped = services.length - running;
|
|
187
|
+
console.log(banner('Services'));
|
|
188
|
+
console.log(chalk.dim(` ${running} running, ${stopped} stopped, ${services.length} total\n`));
|
|
189
|
+
const rows = services.map(s => [
|
|
190
|
+
s.service,
|
|
191
|
+
serviceState(s.state, s.health),
|
|
192
|
+
s.status || chalk.dim('-'),
|
|
193
|
+
s.ports || chalk.dim('-'),
|
|
194
|
+
]);
|
|
195
|
+
console.log(table(['Name', 'State', 'Status', 'Ports'], rows));
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
clear();
|
|
199
|
+
console.log(banner('Services'));
|
|
200
|
+
console.log(statusLine('Services', 'fail', err.message));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// ── show connections ──
|
|
204
|
+
export async function showConnections(_args, _opts, ctx) {
|
|
205
|
+
console.log(banner('MCP Connections'));
|
|
206
|
+
if (ctx.connections.size === 0) {
|
|
207
|
+
console.log(statusLine('Connections', 'info', 'No active connections'));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const rows = [];
|
|
211
|
+
for (const [name, conn] of ctx.connections) {
|
|
212
|
+
rows.push([name, conn.type, `${Status.online.color(Status.online.symbol)} connected`]);
|
|
213
|
+
}
|
|
214
|
+
console.log(table(['Name', 'Type', 'Status'], rows));
|
|
215
|
+
}
|
|
216
|
+
// ── show db status ──
|
|
217
|
+
export async function showDbStatus(_args, _opts, _ctx) {
|
|
218
|
+
const clear = showSpinner('Checking database...');
|
|
219
|
+
try {
|
|
220
|
+
// Run checks in parallel
|
|
221
|
+
const [, size, connInfo] = await Promise.all([
|
|
222
|
+
composeExec('db', ['pg_isready', '-U', DB_USER, '-d', DB_NAME], { pipe: true }),
|
|
223
|
+
psqlTerse(`SELECT pg_size_pretty(pg_database_size('${DB_NAME}'));`),
|
|
224
|
+
psqlTerse(`SELECT count(*) FROM pg_stat_activity WHERE datname = '${DB_NAME}';`).catch(() => null),
|
|
225
|
+
]);
|
|
226
|
+
// Try to get max connections
|
|
227
|
+
let maxConns = null;
|
|
228
|
+
try {
|
|
229
|
+
maxConns = await psqlTerse(`SHOW max_connections;`);
|
|
230
|
+
}
|
|
231
|
+
catch { /* ignore */ }
|
|
232
|
+
clear();
|
|
233
|
+
console.log(banner('Database Status'));
|
|
234
|
+
console.log(statusLine('PostgreSQL', 'ok', 'accepting connections'));
|
|
235
|
+
console.log(statusLine('Database Size', 'info', size));
|
|
236
|
+
// Connection pool meter bar
|
|
237
|
+
if (connInfo && maxConns) {
|
|
238
|
+
const current = parseInt(connInfo, 10) || 0;
|
|
239
|
+
const max = parseInt(maxConns, 10) || 100;
|
|
240
|
+
console.log(`\n ${chalk.bold('Connections')}: ${meterBar(current, max)}`);
|
|
241
|
+
}
|
|
242
|
+
console.log();
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
clear();
|
|
246
|
+
console.log(banner('Database Status'));
|
|
247
|
+
console.log(statusLine('PostgreSQL', 'fail', err.message));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// ── show db tables ──
|
|
251
|
+
export async function showDbTables(args, _opts, _ctx) {
|
|
252
|
+
const clear = showSpinner('Querying tables...');
|
|
253
|
+
try {
|
|
254
|
+
const nameFilter = args[0];
|
|
255
|
+
let query = `SELECT tablename, pg_size_pretty(pg_total_relation_size(quote_ident(tablename))) AS size, pg_total_relation_size(quote_ident(tablename)) AS raw_size FROM pg_tables WHERE schemaname = 'public'`;
|
|
256
|
+
if (nameFilter) {
|
|
257
|
+
query += ` AND tablename LIKE '%${nameFilter}%'`;
|
|
258
|
+
}
|
|
259
|
+
query += ' ORDER BY pg_total_relation_size(quote_ident(tablename)) DESC LIMIT 30;';
|
|
260
|
+
// Also get row counts for each table
|
|
261
|
+
const countQuery = nameFilter
|
|
262
|
+
? `SELECT relname, n_live_tup FROM pg_stat_user_tables WHERE relname LIKE '%${nameFilter}%' ORDER BY n_live_tup DESC LIMIT 30;`
|
|
263
|
+
: `SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY n_live_tup DESC LIMIT 30;`;
|
|
264
|
+
const [result, countResult] = await Promise.all([
|
|
265
|
+
psqlTerse(query),
|
|
266
|
+
psqlTerse(countQuery).catch(() => ''),
|
|
267
|
+
]);
|
|
268
|
+
clear();
|
|
269
|
+
if (!result) {
|
|
270
|
+
console.log(banner('Database Tables'));
|
|
271
|
+
console.log(statusLine('Tables', 'info', 'No tables found'));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
// Parse row counts into a map
|
|
275
|
+
const rowCounts = new Map();
|
|
276
|
+
if (countResult) {
|
|
277
|
+
for (const line of countResult.split('\n')) {
|
|
278
|
+
if (!line.includes('|'))
|
|
279
|
+
continue;
|
|
280
|
+
const [name, count] = line.split('|').map(c => c.trim());
|
|
281
|
+
if (name && count)
|
|
282
|
+
rowCounts.set(name, parseInt(count, 10) || 0);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Parse table data
|
|
286
|
+
const parsed = result.split('\n')
|
|
287
|
+
.filter(line => line.includes('|'))
|
|
288
|
+
.map(line => {
|
|
289
|
+
const cols = line.split('|').map(c => c.trim());
|
|
290
|
+
return {
|
|
291
|
+
name: cols[0] ?? '',
|
|
292
|
+
size: cols[1] ?? '',
|
|
293
|
+
rawSize: parseInt(cols[2] ?? '0', 10) || 0,
|
|
294
|
+
};
|
|
295
|
+
});
|
|
296
|
+
const maxSize = parsed.length > 0 ? parsed[0].rawSize : 0;
|
|
297
|
+
console.log(banner('Database Tables'));
|
|
298
|
+
console.log(chalk.dim(` ${parsed.length} tables, sorted by size\n`));
|
|
299
|
+
const rows = parsed.map(t => {
|
|
300
|
+
const bar = sizeBar(t.rawSize, maxSize, 10);
|
|
301
|
+
const count = rowCounts.get(t.name);
|
|
302
|
+
const countStr = count !== undefined ? `${count.toLocaleString()} rows` : chalk.dim('-');
|
|
303
|
+
return [t.name, `${bar} ${t.size}`, countStr];
|
|
304
|
+
});
|
|
305
|
+
console.log(table(['Table', 'Size', 'Rows'], rows));
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
clear();
|
|
309
|
+
console.log(banner('Database Tables'));
|
|
310
|
+
console.log(statusLine('Tables', 'fail', err.message));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// ── show db size ──
|
|
314
|
+
export async function showDbSize(_args, _opts, _ctx) {
|
|
315
|
+
const clear = showSpinner('Calculating database size...');
|
|
316
|
+
try {
|
|
317
|
+
const [totalSize, tableCount, top] = await Promise.all([
|
|
318
|
+
psqlTerse(`SELECT pg_size_pretty(pg_database_size('${DB_NAME}'));`),
|
|
319
|
+
psqlTerse(`SELECT count(*) FROM pg_tables WHERE schemaname = 'public';`),
|
|
320
|
+
psqlTerse(`SELECT tablename, pg_size_pretty(pg_total_relation_size(quote_ident(tablename))), pg_total_relation_size(quote_ident(tablename)) FROM pg_tables WHERE schemaname = 'public' ORDER BY pg_total_relation_size(quote_ident(tablename)) DESC LIMIT 5;`),
|
|
321
|
+
]);
|
|
322
|
+
clear();
|
|
323
|
+
console.log(banner('Database Size'));
|
|
324
|
+
console.log(kv([
|
|
325
|
+
['Total Size', chalk.bold(totalSize)],
|
|
326
|
+
['Table Count', tableCount],
|
|
327
|
+
]));
|
|
328
|
+
if (top) {
|
|
329
|
+
const parsed = top.split('\n')
|
|
330
|
+
.filter(line => line.includes('|'))
|
|
331
|
+
.map(line => {
|
|
332
|
+
const cols = line.split('|').map(c => c.trim());
|
|
333
|
+
return { name: cols[0] ?? '', size: cols[1] ?? '', rawSize: parseInt(cols[2] ?? '0', 10) || 0 };
|
|
334
|
+
});
|
|
335
|
+
const maxSize = parsed.length > 0 ? parsed[0].rawSize : 0;
|
|
336
|
+
console.log(banner('Top 5 Tables'));
|
|
337
|
+
const rows = parsed.map(t => [
|
|
338
|
+
t.name,
|
|
339
|
+
`${sizeBar(t.rawSize, maxSize, 12)} ${t.size}`,
|
|
340
|
+
]);
|
|
341
|
+
console.log(table(['Table', 'Size'], rows));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
clear();
|
|
346
|
+
console.log(banner('Database Size'));
|
|
347
|
+
console.log(statusLine('DB Size', 'fail', err.message));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// ── show db backups ──
|
|
351
|
+
export async function showDbBackups(_args, _opts, _ctx) {
|
|
352
|
+
const { backupDir, dbDumpsDir } = getPaths();
|
|
353
|
+
console.log(banner('Database Backups'));
|
|
354
|
+
for (const [label, dir] of [['Backups', backupDir], ['DB Dumps', dbDumpsDir]]) {
|
|
355
|
+
if (!existsSync(dir)) {
|
|
356
|
+
console.log(statusLine(label, 'info', `${dir} does not exist`));
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.sql') || f.endsWith('.gz') || f.endsWith('.dump'));
|
|
361
|
+
if (files.length === 0) {
|
|
362
|
+
console.log(statusLine(label, 'info', 'no backups found'));
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
console.log(statusLine(label, 'ok', `${files.length} file(s)`));
|
|
366
|
+
// Show recent files with sizes
|
|
367
|
+
const recent = files.slice(-10);
|
|
368
|
+
for (const f of recent) {
|
|
369
|
+
try {
|
|
370
|
+
const st = statSync(join(dir, f));
|
|
371
|
+
const size = formatBytes(st.size);
|
|
372
|
+
const date = st.mtime.toLocaleDateString();
|
|
373
|
+
console.log(` ${chalk.dim(date)} ${f} ${chalk.dim(size)}`);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
console.log(` ${f}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
console.log(statusLine(label, 'fail', err.message));
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
// ── show migrate status ──
|
|
387
|
+
export async function showMigrateStatus(_args, _opts, _ctx) {
|
|
388
|
+
const clear = showSpinner('Checking migrations...');
|
|
389
|
+
try {
|
|
390
|
+
const out = await rpc('Ecto.Migrator.migrations(Cdrcisco.Repo) |> Enum.map(fn {status, version, name} -> "#{status}|#{version}|#{name}" end) |> Enum.join("\\n") |> IO.puts()');
|
|
391
|
+
clear();
|
|
392
|
+
console.log(banner('Migration Status'));
|
|
393
|
+
const lines = out.trim().split('\n').filter(l => l.includes('|'));
|
|
394
|
+
if (lines.length === 0) {
|
|
395
|
+
console.log(statusLine('Migrations', 'info', 'No migrations found'));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const upCount = lines.filter(l => l.startsWith('up')).length;
|
|
399
|
+
const downCount = lines.filter(l => l.startsWith('down')).length;
|
|
400
|
+
console.log(chalk.dim(` ${upCount} applied, ${downCount} pending\n`));
|
|
401
|
+
const rows = lines.map(line => {
|
|
402
|
+
const [status, version, name] = line.split('|').map(c => c.trim());
|
|
403
|
+
const isUp = status === 'up';
|
|
404
|
+
const stateStr = isUp
|
|
405
|
+
? `${Status.ok.color(Status.ok.symbol)} up`
|
|
406
|
+
: `${Status.warn.color(Status.warn.symbol)} down`;
|
|
407
|
+
return [stateStr, version ?? '', name ?? ''];
|
|
408
|
+
});
|
|
409
|
+
console.log(table(['Status', 'Version', 'Name'], rows));
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
clear();
|
|
413
|
+
console.log(banner('Migration Status'));
|
|
414
|
+
console.log(statusLine('Migrations', 'fail', err.message));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
// ── show migrate history ──
|
|
418
|
+
export async function showMigrateHistory(_args, _opts, _ctx) {
|
|
419
|
+
const clear = showSpinner('Loading migration history...');
|
|
420
|
+
try {
|
|
421
|
+
const result = await psqlTerse(`SELECT version, inserted_at FROM schema_migrations ORDER BY version DESC LIMIT 20;`);
|
|
422
|
+
clear();
|
|
423
|
+
if (!result) {
|
|
424
|
+
console.log(banner('Migration History'));
|
|
425
|
+
console.log(statusLine('History', 'info', 'No migration history found'));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
console.log(banner('Migration History'));
|
|
429
|
+
const rows = result.split('\n')
|
|
430
|
+
.filter(line => line.includes('|'))
|
|
431
|
+
.map(line => {
|
|
432
|
+
const cols = line.split('|').map(c => c.trim());
|
|
433
|
+
return [cols[0] ?? '', cols[1] ?? ''];
|
|
434
|
+
});
|
|
435
|
+
console.log(table(['Version', 'Applied At'], rows));
|
|
436
|
+
}
|
|
437
|
+
catch (err) {
|
|
438
|
+
clear();
|
|
439
|
+
console.log(banner('Migration History'));
|
|
440
|
+
console.log(statusLine('Migration History', 'fail', err.message));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// ── show docker status ──
|
|
444
|
+
export async function showDockerStatus(_args, _opts, _ctx) {
|
|
445
|
+
const clear = showSpinner('Querying containers...');
|
|
446
|
+
try {
|
|
447
|
+
const result = await compose(['ps', '-a', '--format', 'json'], { pipe: true });
|
|
448
|
+
const raw = (result.stdout ?? '').trim();
|
|
449
|
+
clear();
|
|
450
|
+
if (!raw) {
|
|
451
|
+
console.log(banner('Docker Containers'));
|
|
452
|
+
console.log(statusLine('Containers', 'info', 'No containers found'));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const containers = raw.split('\n')
|
|
456
|
+
.filter(line => line.trim())
|
|
457
|
+
.map(line => JSON.parse(line));
|
|
458
|
+
const running = containers.filter(c => (c.State ?? '').toLowerCase() === 'running').length;
|
|
459
|
+
console.log(banner('Docker Containers'));
|
|
460
|
+
console.log(chalk.dim(` ${running} running, ${containers.length - running} stopped, ${containers.length} total\n`));
|
|
461
|
+
const rows = containers.map(obj => [
|
|
462
|
+
obj.Name ?? obj.Names ?? '',
|
|
463
|
+
serviceState(obj.State ?? '', obj.Health ?? ''),
|
|
464
|
+
obj.Status ?? chalk.dim('-'),
|
|
465
|
+
obj.Ports ?? chalk.dim('-'),
|
|
466
|
+
]);
|
|
467
|
+
console.log(table(['Name', 'State', 'Status', 'Ports'], rows));
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
clear();
|
|
471
|
+
console.log(banner('Docker Containers'));
|
|
472
|
+
console.log(statusLine('Docker', 'fail', err.message));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
// ── show docker network ──
|
|
476
|
+
export async function showDockerNetwork(_args, _opts, _ctx) {
|
|
477
|
+
const clear = showSpinner('Querying networks...');
|
|
478
|
+
try {
|
|
479
|
+
const { stdout } = await execa('docker', ['network', 'ls', '--format', 'json']);
|
|
480
|
+
clear();
|
|
481
|
+
console.log(banner('Docker Networks'));
|
|
482
|
+
const lines = stdout.trim().split('\n').filter(l => l.trim());
|
|
483
|
+
if (lines.length === 0) {
|
|
484
|
+
console.log(statusLine('Networks', 'info', 'No networks found'));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
const rows = lines.map(line => {
|
|
488
|
+
try {
|
|
489
|
+
const obj = JSON.parse(line);
|
|
490
|
+
return [
|
|
491
|
+
obj.Name ?? '',
|
|
492
|
+
obj.Driver ?? '',
|
|
493
|
+
obj.Scope ?? '',
|
|
494
|
+
];
|
|
495
|
+
}
|
|
496
|
+
catch {
|
|
497
|
+
return [line, '', ''];
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
console.log(table(['Name', 'Driver', 'Scope'], rows));
|
|
501
|
+
}
|
|
502
|
+
catch (err) {
|
|
503
|
+
clear();
|
|
504
|
+
console.log(banner('Docker Networks'));
|
|
505
|
+
console.log(statusLine('Docker Networks', 'fail', err.message));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
// ── show config certs ──
|
|
509
|
+
export async function showConfigCerts(_args, _opts, _ctx) {
|
|
510
|
+
const { certsDir } = getPaths();
|
|
511
|
+
console.log(banner('TLS Certificates'));
|
|
512
|
+
const certFile = join(certsDir, 'appliance.crt');
|
|
513
|
+
if (!existsSync(certFile)) {
|
|
514
|
+
console.log(statusLine('Certificate', 'warn', 'no certificate found'));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
try {
|
|
518
|
+
const { stdout } = await execa('openssl', ['x509', '-in', certFile, '-noout', '-dates', '-subject']);
|
|
519
|
+
const lines = stdout.split('\n');
|
|
520
|
+
const pairs = [];
|
|
521
|
+
for (const l of lines) {
|
|
522
|
+
if (!l.includes('='))
|
|
523
|
+
continue;
|
|
524
|
+
const [k, ...rest] = l.split('=');
|
|
525
|
+
const key = k.trim();
|
|
526
|
+
const value = rest.join('=').trim();
|
|
527
|
+
// Highlight expiry dates
|
|
528
|
+
if (key === 'notAfter') {
|
|
529
|
+
const expiry = new Date(value);
|
|
530
|
+
const now = new Date();
|
|
531
|
+
const daysLeft = Math.floor((expiry.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
|
|
532
|
+
const color = daysLeft < 30 ? chalk.red : daysLeft < 90 ? chalk.yellow : chalk.green;
|
|
533
|
+
pairs.push([key, `${value} ${color(`(${daysLeft} days remaining)`)}`]);
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
pairs.push([key, value]);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
console.log(kv(pairs));
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
console.log(statusLine('Certificate', 'fail', err.message));
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// ── show config logging ──
|
|
546
|
+
export async function showConfigLogging(_args, _opts, _ctx) {
|
|
547
|
+
console.log(banner('Logging Configuration'));
|
|
548
|
+
try {
|
|
549
|
+
const out = await rpc('Logger.level() |> IO.inspect()');
|
|
550
|
+
const level = out.trim().replace(/^:/, '');
|
|
551
|
+
const levelColor = level === 'debug' ? chalk.cyan : level === 'info' ? chalk.green : level === 'warning' ? chalk.yellow : level === 'error' ? chalk.red : chalk.white;
|
|
552
|
+
console.log(kv([['Log Level', levelColor(level)]]));
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
console.log(kv([['Log Level', chalk.dim('unavailable (app not running)')]]));
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
// ── show config ipv6 ──
|
|
559
|
+
export async function showConfigIpv6(_args, _opts, _ctx) {
|
|
560
|
+
const status = getIpv6Status();
|
|
561
|
+
console.log(banner('IPv6 Configuration'));
|
|
562
|
+
const statusMap = {
|
|
563
|
+
enabled: 'ok', disabled: 'info', unknown: 'warn',
|
|
564
|
+
};
|
|
565
|
+
console.log(statusLine('IPv6', statusMap[status] ?? 'info', status));
|
|
566
|
+
}
|
|
567
|
+
// ── show config otel ──
|
|
568
|
+
export async function showConfigOtel(_args, _opts, _ctx) {
|
|
569
|
+
const enabled = isOtelEnabled();
|
|
570
|
+
console.log(banner('OpenTelemetry'));
|
|
571
|
+
console.log(statusLine('OTel', enabled ? 'ok' : 'info', enabled ? 'enabled' : 'disabled'));
|
|
572
|
+
if (enabled) {
|
|
573
|
+
console.log(chalk.dim(' Traces and metrics exported via OTLP'));
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
// ── show config postgres ──
|
|
577
|
+
export async function showConfigPostgres(_args, _opts, _ctx) {
|
|
578
|
+
const version = getPostgresVersion();
|
|
579
|
+
console.log(banner('PostgreSQL Configuration'));
|
|
580
|
+
console.log(kv([
|
|
581
|
+
['Configured Version', chalk.bold(version)],
|
|
582
|
+
['Database', DB_NAME],
|
|
583
|
+
['User', DB_USER],
|
|
584
|
+
]));
|
|
585
|
+
}
|
|
586
|
+
// ── show config secrets ──
|
|
587
|
+
export async function showConfigSecrets(_args, _opts, _ctx) {
|
|
588
|
+
const overrides = countOverrides();
|
|
589
|
+
const secrets = getSecrets();
|
|
590
|
+
console.log(banner('Secrets'));
|
|
591
|
+
const overrideStatus = overrides > 0 ? 'ok' : 'warn';
|
|
592
|
+
const overrideDetail = overrides > 0
|
|
593
|
+
? `${overrides} of ${SECRET_CATALOG.length} customized from defaults`
|
|
594
|
+
: `All ${SECRET_CATALOG.length} secrets using defaults ${chalk.yellow('(insecure)')}`;
|
|
595
|
+
console.log(statusLine('Customized', overrideStatus, overrideDetail));
|
|
596
|
+
// Group by category
|
|
597
|
+
for (const group of SECRET_GROUPS) {
|
|
598
|
+
const groupSecrets = SECRET_CATALOG.filter(d => d.group === group);
|
|
599
|
+
if (groupSecrets.length === 0)
|
|
600
|
+
continue;
|
|
601
|
+
console.log(chalk.bold(`\n ${group}`));
|
|
602
|
+
for (const def of groupSecrets) {
|
|
603
|
+
const val = secrets[def.key] ?? def.defaultValue;
|
|
604
|
+
const isDefault = val === def.defaultValue;
|
|
605
|
+
let display;
|
|
606
|
+
if (def.sensitive) {
|
|
607
|
+
if (isDefault) {
|
|
608
|
+
display = chalk.yellow('(default)');
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
// Masked with stars, show first/last char hint
|
|
612
|
+
display = chalk.green('*'.repeat(8));
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
display = isDefault ? chalk.dim(val) : chalk.white(val);
|
|
617
|
+
}
|
|
618
|
+
const customBadge = !isDefault ? chalk.green(' [custom]') : '';
|
|
619
|
+
console.log(` ${chalk.dim(def.label.padEnd(20))} ${display}${customBadge}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
console.log();
|
|
623
|
+
}
|
|
624
|
+
// ── show license ──
|
|
625
|
+
export async function showLicense(_args, _opts, _ctx) {
|
|
626
|
+
const clear = showSpinner('Checking license...');
|
|
627
|
+
try {
|
|
628
|
+
const out = await rpc('case Cdrcisco.License.status() do ' +
|
|
629
|
+
'{:ok, info} -> info |> Jason.encode!() |> IO.puts(); ' +
|
|
630
|
+
'{:error, reason} -> IO.puts("error:#{reason}") ' +
|
|
631
|
+
'end');
|
|
632
|
+
const trimmed = out.trim();
|
|
633
|
+
clear();
|
|
634
|
+
console.log(banner('License'));
|
|
635
|
+
if (trimmed.startsWith('error:')) {
|
|
636
|
+
console.log(statusLine('License', 'warn', trimmed.replace('error:', '')));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
const info = JSON.parse(trimmed);
|
|
641
|
+
const pairs = Object.entries(info).map(([k, v]) => {
|
|
642
|
+
const strVal = String(v);
|
|
643
|
+
// Highlight key license fields
|
|
644
|
+
if (k.toLowerCase().includes('expire') || k.toLowerCase().includes('valid')) {
|
|
645
|
+
const isValid = strVal.toLowerCase() === 'true' || strVal.toLowerCase() === 'valid';
|
|
646
|
+
return [k, isValid ? chalk.green(strVal) : chalk.red(strVal)];
|
|
647
|
+
}
|
|
648
|
+
return [k, strVal];
|
|
649
|
+
});
|
|
650
|
+
console.log(kv(pairs));
|
|
651
|
+
}
|
|
652
|
+
catch {
|
|
653
|
+
console.log(trimmed);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
catch (err) {
|
|
657
|
+
clear();
|
|
658
|
+
console.log(banner('License'));
|
|
659
|
+
console.log(statusLine('License', 'fail', err.message));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// ── show jtapi status ──
|
|
663
|
+
export async function showJtapiStatus(_args, _opts, _ctx) {
|
|
664
|
+
const enabled = isJtapiEnabled();
|
|
665
|
+
console.log(banner('JTAPI Status'));
|
|
666
|
+
console.log(statusLine('JTAPI', enabled ? 'ok' : 'info', enabled ? 'enabled' : 'disabled'));
|
|
667
|
+
if (enabled) {
|
|
668
|
+
const clear = showSpinner('Checking sidecar health...');
|
|
669
|
+
try {
|
|
670
|
+
const health = await getJtapiHealthCheck();
|
|
671
|
+
clear();
|
|
672
|
+
console.log(statusLine('Sidecar', health.reachable ? 'ok' : 'fail', health.status ?? (health.reachable ? 'reachable' : health.detail ?? 'unreachable')));
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
clear();
|
|
676
|
+
console.log(statusLine('Sidecar', 'fail', err.message));
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// ── show users ──
|
|
681
|
+
export async function showUsers(_args, _opts, _ctx) {
|
|
682
|
+
const clear = showSpinner('Loading users...');
|
|
683
|
+
try {
|
|
684
|
+
const out = await rpc('Cdrcisco.Accounts.list_users() |> Enum.map(fn u -> "#{u.email}|#{u.role || "user"}|#{u.inserted_at}" end) |> Enum.join("\\n") |> IO.puts()');
|
|
685
|
+
const trimmed = out.trim();
|
|
686
|
+
clear();
|
|
687
|
+
if (!trimmed) {
|
|
688
|
+
console.log(banner('Users'));
|
|
689
|
+
console.log(statusLine('Users', 'info', 'No users found'));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
console.log(banner('Users'));
|
|
693
|
+
const rows = trimmed.split('\n').map(line => {
|
|
694
|
+
const [email, role, created] = line.split('|');
|
|
695
|
+
const roleBadge = role === 'admin'
|
|
696
|
+
? chalk.magenta(role ?? '')
|
|
697
|
+
: chalk.dim(role ?? 'user');
|
|
698
|
+
return [email ?? '', roleBadge, created ?? ''];
|
|
699
|
+
});
|
|
700
|
+
console.log(chalk.dim(` ${rows.length} user(s)\n`));
|
|
701
|
+
console.log(table(['Email', 'Role', 'Created'], rows));
|
|
702
|
+
}
|
|
703
|
+
catch (err) {
|
|
704
|
+
clear();
|
|
705
|
+
console.log(banner('Users'));
|
|
706
|
+
console.log(statusLine('Users', 'fail', err.message));
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// ── show network ──
|
|
710
|
+
export async function showNetwork(_args, _opts, _ctx) {
|
|
711
|
+
const clear = showSpinner('Scanning network interfaces...');
|
|
712
|
+
console.log(banner('Network Configuration'));
|
|
713
|
+
// Try ip addr first (Linux), fall back to ifconfig (macOS)
|
|
714
|
+
try {
|
|
715
|
+
const { stdout } = await execa('ip', ['-br', 'addr']);
|
|
716
|
+
clear();
|
|
717
|
+
const lines = stdout.trim().split('\n');
|
|
718
|
+
const rows = lines.map(line => {
|
|
719
|
+
const parts = line.trim().split(/\s+/);
|
|
720
|
+
const iface = parts[0] ?? '';
|
|
721
|
+
const state = (parts[1] ?? '').toUpperCase();
|
|
722
|
+
const addrs = parts.slice(2).join(', ') || chalk.dim('none');
|
|
723
|
+
let stateStr;
|
|
724
|
+
if (state === 'UP') {
|
|
725
|
+
stateStr = `${Status.online.color(Status.online.symbol)} UP`;
|
|
726
|
+
}
|
|
727
|
+
else if (state === 'DOWN') {
|
|
728
|
+
stateStr = `${Status.offline.color(Status.offline.symbol)} DOWN`;
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
stateStr = `${Status.waiting.color(Status.waiting.symbol)} ${state}`;
|
|
732
|
+
}
|
|
733
|
+
return [iface, stateStr, addrs];
|
|
734
|
+
});
|
|
735
|
+
console.log(table(['Interface', 'State', 'Addresses'], rows));
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
// ip not available, try ifconfig (macOS)
|
|
740
|
+
}
|
|
741
|
+
try {
|
|
742
|
+
const { stdout } = await execa('ifconfig');
|
|
743
|
+
clear();
|
|
744
|
+
// Parse macOS ifconfig into structured data
|
|
745
|
+
const interfaces = [];
|
|
746
|
+
let current = null;
|
|
747
|
+
for (const line of stdout.split('\n')) {
|
|
748
|
+
const ifMatch = line.match(/^(\w+):\s+flags=\d+<([^>]*)>/);
|
|
749
|
+
if (ifMatch) {
|
|
750
|
+
if (current)
|
|
751
|
+
interfaces.push(current);
|
|
752
|
+
current = { name: ifMatch[1], state: ifMatch[2].includes('UP') ? 'UP' : 'DOWN', addresses: [] };
|
|
753
|
+
}
|
|
754
|
+
const inetMatch = line.match(/inet\s+(\S+)/);
|
|
755
|
+
if (inetMatch && current) {
|
|
756
|
+
current.addresses.push(inetMatch[1]);
|
|
757
|
+
}
|
|
758
|
+
const inet6Match = line.match(/inet6\s+(\S+)/);
|
|
759
|
+
if (inet6Match && current) {
|
|
760
|
+
current.addresses.push(inet6Match[1]);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (current)
|
|
764
|
+
interfaces.push(current);
|
|
765
|
+
const rows = interfaces.map(iface => {
|
|
766
|
+
const stateStr = iface.state === 'UP'
|
|
767
|
+
? `${Status.online.color(Status.online.symbol)} UP`
|
|
768
|
+
: `${Status.offline.color(Status.offline.symbol)} DOWN`;
|
|
769
|
+
const addrs = iface.addresses.length > 0 ? iface.addresses.join(', ') : chalk.dim('none');
|
|
770
|
+
return [iface.name, stateStr, addrs];
|
|
771
|
+
});
|
|
772
|
+
console.log(table(['Interface', 'State', 'Addresses'], rows));
|
|
773
|
+
}
|
|
774
|
+
catch {
|
|
775
|
+
clear();
|
|
776
|
+
console.log(statusLine('Network', 'fail', 'Unable to retrieve network information'));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// ── show os-updates status ──
|
|
780
|
+
export async function showOsUpdatesStatus(_args, _opts, _ctx) {
|
|
781
|
+
console.log(banner('OS Update Status'));
|
|
782
|
+
// Try dnf-automatic (RHEL/AlmaLinux/Fedora)
|
|
783
|
+
try {
|
|
784
|
+
const { stdout } = await execa('systemctl', ['show', 'dnf-automatic-install.timer', '--property=ActiveState,SubState,NextElapseUSecRealtime']);
|
|
785
|
+
const props = {};
|
|
786
|
+
for (const line of stdout.split('\n')) {
|
|
787
|
+
const [k, ...rest] = line.split('=');
|
|
788
|
+
if (k)
|
|
789
|
+
props[k.trim()] = rest.join('=').trim();
|
|
790
|
+
}
|
|
791
|
+
const active = props.ActiveState === 'active';
|
|
792
|
+
const nextRun = props.NextElapseUSecRealtime;
|
|
793
|
+
console.log(statusLine('Auto-Update Timer', active ? 'ok' : 'warn', active ? 'active (dnf-automatic)' : 'inactive'));
|
|
794
|
+
if (nextRun && nextRun !== 'n/a') {
|
|
795
|
+
// Parse systemd timestamp
|
|
796
|
+
const nextDate = new Date(nextRun);
|
|
797
|
+
if (!isNaN(nextDate.getTime())) {
|
|
798
|
+
const now = new Date();
|
|
799
|
+
const hoursUntil = Math.round((nextDate.getTime() - now.getTime()) / (1000 * 60 * 60));
|
|
800
|
+
console.log(statusLine('Next Run', 'info', `${nextDate.toLocaleString()} (in ~${hoursUntil}h)`));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
console.log(statusLine('Service', 'info', `${props.SubState ?? 'unknown'}`));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
catch {
|
|
807
|
+
// Not dnf-based
|
|
808
|
+
}
|
|
809
|
+
// Try apt-daily (Debian/Ubuntu)
|
|
810
|
+
try {
|
|
811
|
+
const { stdout } = await execa('systemctl', ['show', 'apt-daily-upgrade.timer', '--property=ActiveState,SubState,NextElapseUSecRealtime']);
|
|
812
|
+
const props = {};
|
|
813
|
+
for (const line of stdout.split('\n')) {
|
|
814
|
+
const [k, ...rest] = line.split('=');
|
|
815
|
+
if (k)
|
|
816
|
+
props[k.trim()] = rest.join('=').trim();
|
|
817
|
+
}
|
|
818
|
+
const active = props.ActiveState === 'active';
|
|
819
|
+
console.log(statusLine('Auto-Update Timer', active ? 'ok' : 'warn', active ? 'active (apt-daily)' : 'inactive'));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
catch {
|
|
823
|
+
// No systemd timers found
|
|
824
|
+
}
|
|
825
|
+
console.log(statusLine('OS Updates', 'info', 'automatic update timer not found'));
|
|
826
|
+
}
|
|
827
|
+
// ── show k8s status ──
|
|
828
|
+
export async function showK8sStatus(_args, _opts, ctx) {
|
|
829
|
+
if (ctx.backend !== 'k8s') {
|
|
830
|
+
console.log(banner('Kubernetes Status'));
|
|
831
|
+
console.log(statusLine('Kubernetes', 'info', 'not using k8s backend'));
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
const clear = showSpinner('Querying Kubernetes cluster...');
|
|
835
|
+
console.log(banner('Kubernetes Status'));
|
|
836
|
+
try {
|
|
837
|
+
const [{ stdout: nodeOut }, { stdout: podOut }] = await Promise.all([
|
|
838
|
+
execa('kubectl', ['get', 'nodes', '-o', 'json']),
|
|
839
|
+
execa('kubectl', ['get', 'pods', '-o', 'json']),
|
|
840
|
+
]);
|
|
841
|
+
clear();
|
|
842
|
+
// Parse nodes
|
|
843
|
+
const nodes = JSON.parse(nodeOut);
|
|
844
|
+
if (nodes.items?.length > 0) {
|
|
845
|
+
console.log(chalk.bold('\n Nodes'));
|
|
846
|
+
const nodeRows = nodes.items.map((n) => {
|
|
847
|
+
const name = n.metadata?.name ?? '';
|
|
848
|
+
const conditions = n.status?.conditions ?? [];
|
|
849
|
+
const ready = conditions.find((c) => c.type === 'Ready');
|
|
850
|
+
const isReady = ready?.status === 'True';
|
|
851
|
+
const stateStr = isReady
|
|
852
|
+
? `${Status.online.color(Status.online.symbol)} Ready`
|
|
853
|
+
: `${Status.fail.color(Status.fail.symbol)} NotReady`;
|
|
854
|
+
const version = n.status?.nodeInfo?.kubeletVersion ?? '';
|
|
855
|
+
const os = n.status?.nodeInfo?.osImage ?? '';
|
|
856
|
+
return [name, stateStr, version, os];
|
|
857
|
+
});
|
|
858
|
+
console.log(table(['Name', 'Status', 'Version', 'OS'], nodeRows));
|
|
859
|
+
}
|
|
860
|
+
// Parse pods
|
|
861
|
+
const pods = JSON.parse(podOut);
|
|
862
|
+
if (pods.items?.length > 0) {
|
|
863
|
+
console.log(chalk.bold('\n Pods'));
|
|
864
|
+
const podRows = pods.items.map((p) => {
|
|
865
|
+
const name = p.metadata?.name ?? '';
|
|
866
|
+
const phase = p.status?.phase ?? '';
|
|
867
|
+
let stateStr;
|
|
868
|
+
if (phase === 'Running') {
|
|
869
|
+
stateStr = `${Status.online.color(Status.online.symbol)} Running`;
|
|
870
|
+
}
|
|
871
|
+
else if (phase === 'Succeeded') {
|
|
872
|
+
stateStr = `${Status.ok.color(Status.ok.symbol)} Completed`;
|
|
873
|
+
}
|
|
874
|
+
else if (phase === 'Failed') {
|
|
875
|
+
stateStr = `${Status.fail.color(Status.fail.symbol)} Failed`;
|
|
876
|
+
}
|
|
877
|
+
else if (phase === 'Pending') {
|
|
878
|
+
stateStr = `${Status.waiting.color(Status.waiting.symbol)} Pending`;
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
stateStr = `${Status.info.color(Status.info.symbol)} ${phase}`;
|
|
882
|
+
}
|
|
883
|
+
const restarts = (p.status?.containerStatuses ?? [])
|
|
884
|
+
.reduce((sum, c) => sum + (c.restartCount ?? 0), 0);
|
|
885
|
+
const restartsStr = restarts > 0 ? chalk.yellow(`${restarts}`) : chalk.dim('0');
|
|
886
|
+
const ns = p.metadata?.namespace ?? '';
|
|
887
|
+
return [name, ns, stateStr, restartsStr];
|
|
888
|
+
});
|
|
889
|
+
console.log(table(['Pod', 'Namespace', 'Status', 'Restarts'], podRows));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
catch (err) {
|
|
893
|
+
clear();
|
|
894
|
+
console.log(statusLine('Kubernetes', 'fail', err.message));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
//# sourceMappingURL=show.js.map
|