clawsql 0.2.0 → 0.2.2
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/README.md +25 -5
- package/dist/bin/clawsql.d.ts +1 -0
- package/dist/bin/clawsql.d.ts.map +1 -1
- package/dist/bin/clawsql.js +18 -2
- package/dist/bin/clawsql.js.map +1 -1
- package/dist/cli/agent/handler.d.ts +13 -31
- package/dist/cli/agent/handler.d.ts.map +1 -1
- package/dist/cli/agent/handler.js +107 -149
- package/dist/cli/agent/handler.js.map +1 -1
- package/dist/cli/agent/index.d.ts +2 -1
- package/dist/cli/agent/index.d.ts.map +1 -1
- package/dist/cli/agent/index.js +7 -1
- package/dist/cli/agent/index.js.map +1 -1
- package/dist/cli/agent/openclaw-integration.d.ts +83 -25
- package/dist/cli/agent/openclaw-integration.d.ts.map +1 -1
- package/dist/cli/agent/openclaw-integration.js +305 -194
- package/dist/cli/agent/openclaw-integration.js.map +1 -1
- package/dist/cli/commands/cleanup.d.ts.map +1 -1
- package/dist/cli/commands/cleanup.js +26 -15
- package/dist/cli/commands/cleanup.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +0 -4
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/doctor.js +330 -469
- package/dist/cli/commands/doctor.js.map +1 -1
- package/dist/cli/commands/install.d.ts +13 -0
- package/dist/cli/commands/install.d.ts.map +1 -0
- package/dist/cli/commands/install.js +286 -0
- package/dist/cli/commands/install.js.map +1 -0
- package/dist/cli/commands/openclaw.d.ts +9 -0
- package/dist/cli/commands/openclaw.d.ts.map +1 -0
- package/dist/cli/commands/openclaw.js +236 -0
- package/dist/cli/commands/openclaw.js.map +1 -0
- package/dist/cli/commands/start.d.ts.map +1 -1
- package/dist/cli/commands/start.js +342 -16
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +71 -25
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +4 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/repl.js +1 -1
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/utils/ai-config.d.ts +32 -0
- package/dist/cli/utils/ai-config.d.ts.map +1 -0
- package/dist/cli/utils/ai-config.js +78 -0
- package/dist/cli/utils/ai-config.js.map +1 -0
- package/dist/cli/utils/command-executor.d.ts.map +1 -1
- package/dist/cli/utils/command-executor.js +60 -23
- package/dist/cli/utils/command-executor.js.map +1 -1
- package/dist/cli/utils/docker-files.d.ts.map +1 -1
- package/dist/cli/utils/docker-files.js +2 -0
- package/dist/cli/utils/docker-files.js.map +1 -1
- package/dist/cli/utils/docker-prereq.d.ts +23 -0
- package/dist/cli/utils/docker-prereq.d.ts.map +1 -1
- package/dist/cli/utils/docker-prereq.js +70 -1
- package/dist/cli/utils/docker-prereq.js.map +1 -1
- package/docker/openclaw/entrypoint.sh +102 -0
- package/docker/openclaw/openclaw.json +4 -1
- package/docker/orchestrator/orchestrator-schema.sql +837 -0
- package/docker-compose.yml +22 -14
- package/init/metadata.sql +14 -2
- package/package.json +1 -1
|
@@ -3,158 +3,138 @@
|
|
|
3
3
|
* ClawSQL CLI - Doctor Command
|
|
4
4
|
*
|
|
5
5
|
* Diagnoses system health and suggests fixes for common issues.
|
|
6
|
-
* Similar to `brew doctor` or `npm doctor`.
|
|
7
6
|
*/
|
|
8
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
8
|
exports.doctorCommand = void 0;
|
|
10
9
|
const components_js_1 = require("../ui/components.js");
|
|
11
10
|
const child_process_1 = require("child_process");
|
|
12
11
|
const openclaw_integration_js_1 = require("../agent/openclaw-integration.js");
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const docker_prereq_js_1 = require("../utils/docker-prereq.js");
|
|
13
|
+
const ai_config_js_1 = require("../utils/ai-config.js");
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Command Definition
|
|
16
|
+
// ============================================================================
|
|
16
17
|
exports.doctorCommand = {
|
|
17
18
|
name: 'doctor',
|
|
18
19
|
description: 'Diagnose system issues and suggest fixes',
|
|
19
20
|
usage: '/doctor [--fix]',
|
|
20
|
-
handler: async (
|
|
21
|
-
|
|
22
|
-
const shouldFix = args.includes('--fix');
|
|
23
|
-
console.log(formatter.header('ClawSQL Doctor'));
|
|
21
|
+
handler: async (_args, ctx) => {
|
|
22
|
+
console.log(ctx.formatter.header('ClawSQL Doctor'));
|
|
24
23
|
console.log(components_js_1.theme.muted('Running diagnostics...\n'));
|
|
25
24
|
const results = [];
|
|
26
|
-
// Run all diagnostic checks
|
|
27
25
|
await runDiagnostics(ctx, results);
|
|
28
|
-
// Display results
|
|
29
26
|
displayResults(results);
|
|
30
|
-
|
|
31
|
-
const errors = results.filter(r => r.severity === 'error');
|
|
32
|
-
const warnings = results.filter(r => r.severity === 'warning');
|
|
33
|
-
console.log();
|
|
34
|
-
if (errors.length === 0 && warnings.length === 0) {
|
|
35
|
-
console.log(components_js_1.theme.success(`${components_js_1.indicators.check} All systems healthy!`));
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
console.log(components_js_1.theme.warning(`Found ${errors.length} error(s) and ${warnings.length} warning(s)`));
|
|
39
|
-
if (!shouldFix && (errors.length > 0 || warnings.length > 0)) {
|
|
40
|
-
console.log(components_js_1.theme.muted('\nSome issues may have automatic fixes available.'));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
27
|
+
printSummary(results);
|
|
43
28
|
},
|
|
44
29
|
};
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Diagnostic Runner
|
|
32
|
+
// ============================================================================
|
|
48
33
|
async function runDiagnostics(ctx, results) {
|
|
49
|
-
|
|
50
|
-
await
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
34
|
+
const runtime = await checkContainerRuntime(results);
|
|
35
|
+
await Promise.all([
|
|
36
|
+
checkDockerImages(runtime, results),
|
|
37
|
+
checkClawSQLAPI(ctx, results),
|
|
38
|
+
checkOrchestrator(ctx, results),
|
|
39
|
+
checkProxySQL(ctx, results),
|
|
40
|
+
checkPrometheus(ctx, results),
|
|
41
|
+
checkOpenClaw(ctx, results),
|
|
42
|
+
]);
|
|
57
43
|
checkConfiguration(ctx, results);
|
|
58
|
-
// MySQL checks
|
|
59
44
|
await checkMySQLInstances(ctx, results);
|
|
60
45
|
await checkReplicationTopology(ctx, results);
|
|
61
|
-
// Sync check
|
|
62
46
|
await checkProxySQLSync(ctx, results);
|
|
63
47
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
48
|
+
// ============================================================================
|
|
49
|
+
// Individual Checks
|
|
50
|
+
// ============================================================================
|
|
67
51
|
async function checkContainerRuntime(results) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
for (const runtime of runtimes) {
|
|
71
|
-
try {
|
|
72
|
-
const result = await execCommand([runtime, 'info'], true);
|
|
73
|
-
if (result.success) {
|
|
74
|
-
foundRuntime = runtime;
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
catch {
|
|
79
|
-
// Continue to next runtime
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
if (foundRuntime) {
|
|
52
|
+
const runtime = await (0, docker_prereq_js_1.detectRuntime)();
|
|
53
|
+
if (!runtime) {
|
|
83
54
|
results.push({
|
|
84
55
|
name: 'Container Runtime',
|
|
85
|
-
severity: '
|
|
86
|
-
message:
|
|
56
|
+
severity: 'error',
|
|
57
|
+
message: 'No container runtime found (docker or podman required)',
|
|
58
|
+
fix: 'Install Docker from https://docs.docker.com/get-docker/',
|
|
59
|
+
});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
results.push({
|
|
63
|
+
name: 'Container Runtime',
|
|
64
|
+
severity: 'ok',
|
|
65
|
+
message: `${runtime} is installed and running`,
|
|
66
|
+
});
|
|
67
|
+
const psResult = await execCommand([runtime, 'ps', '--filter', 'name=clawsql', '--filter', 'name=orchestrator', '--filter', 'name=proxysql', '-q'], true);
|
|
68
|
+
const containerCount = psResult.stdout.trim().split('\n').filter(Boolean).length;
|
|
69
|
+
if (containerCount === 0) {
|
|
70
|
+
results.push({
|
|
71
|
+
name: 'Platform Containers',
|
|
72
|
+
severity: 'warning',
|
|
73
|
+
message: 'No ClawSQL containers are running',
|
|
74
|
+
fix: 'Start the platform with: /start',
|
|
75
|
+
fixCommand: '/start',
|
|
87
76
|
});
|
|
88
|
-
// Check if containers are running
|
|
89
|
-
try {
|
|
90
|
-
const psResult = await execCommand([foundRuntime, 'ps', '--filter', 'name=clawsql', '--filter', 'name=orchestrator',
|
|
91
|
-
'--filter', 'name=proxysql', '-q'], true);
|
|
92
|
-
const containerCount = psResult.stdout.trim().split('\n').filter(Boolean).length;
|
|
93
|
-
if (containerCount === 0) {
|
|
94
|
-
results.push({
|
|
95
|
-
name: 'Platform Containers',
|
|
96
|
-
severity: 'warning',
|
|
97
|
-
message: 'No ClawSQL containers are running',
|
|
98
|
-
fix: 'Start the platform with: /start',
|
|
99
|
-
fixCommand: '/start',
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
else {
|
|
103
|
-
results.push({
|
|
104
|
-
name: 'Platform Containers',
|
|
105
|
-
severity: 'ok',
|
|
106
|
-
message: `${containerCount} container(s) running`,
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
results.push({
|
|
112
|
-
name: 'Platform Containers',
|
|
113
|
-
severity: 'warning',
|
|
114
|
-
message: 'Could not check container status',
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
77
|
}
|
|
118
78
|
else {
|
|
119
79
|
results.push({
|
|
120
|
-
name: '
|
|
80
|
+
name: 'Platform Containers',
|
|
81
|
+
severity: 'ok',
|
|
82
|
+
message: `${containerCount} container(s) running`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return runtime;
|
|
86
|
+
}
|
|
87
|
+
async function checkDockerImages(runtime, results) {
|
|
88
|
+
if (!runtime)
|
|
89
|
+
return;
|
|
90
|
+
const status = await (0, docker_prereq_js_1.checkImagesInstalled)(runtime);
|
|
91
|
+
if (status.installed.length === status.total) {
|
|
92
|
+
results.push({ name: 'Docker Images', severity: 'ok', message: `All ${status.total} images installed` });
|
|
93
|
+
}
|
|
94
|
+
else if (status.installed.length === 0) {
|
|
95
|
+
results.push({
|
|
96
|
+
name: 'Docker Images',
|
|
121
97
|
severity: 'error',
|
|
122
|
-
message: 'No
|
|
123
|
-
fix: '
|
|
98
|
+
message: 'No Docker images installed',
|
|
99
|
+
fix: 'Pull images with: /install',
|
|
100
|
+
fixCommand: '/install',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const missing = status.missing.slice(0, 3).map(i => i.split('/').pop()).join(', ');
|
|
105
|
+
results.push({
|
|
106
|
+
name: 'Docker Images',
|
|
107
|
+
severity: 'warning',
|
|
108
|
+
message: `${status.installed.length}/${status.total} images installed`,
|
|
109
|
+
detail: `Missing: ${missing}${status.missing.length > 3 ? '...' : ''}`,
|
|
110
|
+
fix: 'Pull missing images with: /install',
|
|
111
|
+
fixCommand: '/install',
|
|
124
112
|
});
|
|
125
113
|
}
|
|
126
114
|
}
|
|
127
|
-
/**
|
|
128
|
-
* Check ClawSQL API health
|
|
129
|
-
*/
|
|
130
115
|
async function checkClawSQLAPI(ctx, results) {
|
|
131
116
|
try {
|
|
132
117
|
const response = await fetch(`http://localhost:${ctx.settings.api.port}/health`, {
|
|
133
118
|
signal: AbortSignal.timeout(5000),
|
|
134
119
|
});
|
|
135
|
-
if (response.ok) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
severity: 'error',
|
|
148
|
-
message: `API returned status: ${data.status}`,
|
|
149
|
-
fix: 'Check logs: docker logs clawsql',
|
|
150
|
-
});
|
|
151
|
-
}
|
|
120
|
+
if (!response.ok) {
|
|
121
|
+
results.push({
|
|
122
|
+
name: 'ClawSQL API',
|
|
123
|
+
severity: 'error',
|
|
124
|
+
message: `API returned HTTP ${response.status}`,
|
|
125
|
+
fix: 'Check logs: docker logs clawsql',
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const data = await response.json();
|
|
130
|
+
if (data.status === 'healthy') {
|
|
131
|
+
results.push({ name: 'ClawSQL API', severity: 'ok', message: `Running on port ${ctx.settings.api.port}` });
|
|
152
132
|
}
|
|
153
133
|
else {
|
|
154
134
|
results.push({
|
|
155
135
|
name: 'ClawSQL API',
|
|
156
136
|
severity: 'error',
|
|
157
|
-
message: `API returned
|
|
137
|
+
message: `API returned status: ${data.status}`,
|
|
158
138
|
fix: 'Check logs: docker logs clawsql',
|
|
159
139
|
});
|
|
160
140
|
}
|
|
@@ -170,53 +150,18 @@ async function checkClawSQLAPI(ctx, results) {
|
|
|
170
150
|
});
|
|
171
151
|
}
|
|
172
152
|
}
|
|
173
|
-
/**
|
|
174
|
-
* Check Orchestrator health
|
|
175
|
-
*/
|
|
176
153
|
async function checkOrchestrator(ctx, results) {
|
|
154
|
+
// Health check first
|
|
177
155
|
try {
|
|
178
156
|
const isHealthy = await ctx.orchestrator.healthCheck();
|
|
179
|
-
if (isHealthy) {
|
|
180
|
-
results.push({
|
|
181
|
-
name: 'Orchestrator',
|
|
182
|
-
severity: 'ok',
|
|
183
|
-
message: `Running at ${ctx.settings.orchestrator.url}`,
|
|
184
|
-
});
|
|
185
|
-
// Check if instances are discovered
|
|
186
|
-
try {
|
|
187
|
-
const clusters = await ctx.orchestrator.getClusters();
|
|
188
|
-
if (clusters.length === 0) {
|
|
189
|
-
results.push({
|
|
190
|
-
name: 'MySQL Topology',
|
|
191
|
-
severity: 'warning',
|
|
192
|
-
message: 'No MySQL instances discovered in Orchestrator',
|
|
193
|
-
fix: 'Register instances with: /instances register <host>',
|
|
194
|
-
fixCommand: '/instances register <mysql-host>',
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
else {
|
|
198
|
-
results.push({
|
|
199
|
-
name: 'MySQL Topology',
|
|
200
|
-
severity: 'ok',
|
|
201
|
-
message: `${clusters.length} cluster(s) discovered`,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
results.push({
|
|
207
|
-
name: 'MySQL Topology',
|
|
208
|
-
severity: 'warning',
|
|
209
|
-
message: 'Could not retrieve topology information',
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
else {
|
|
157
|
+
if (!isHealthy) {
|
|
214
158
|
results.push({
|
|
215
159
|
name: 'Orchestrator',
|
|
216
160
|
severity: 'error',
|
|
217
161
|
message: 'Health check failed',
|
|
218
162
|
fix: 'Check Orchestrator container: docker logs orchestrator',
|
|
219
163
|
});
|
|
164
|
+
return;
|
|
220
165
|
}
|
|
221
166
|
}
|
|
222
167
|
catch {
|
|
@@ -225,92 +170,89 @@ async function checkOrchestrator(ctx, results) {
|
|
|
225
170
|
severity: 'error',
|
|
226
171
|
message: 'Cannot connect to Orchestrator',
|
|
227
172
|
detail: `Expected at ${ctx.settings.orchestrator.url}`,
|
|
228
|
-
fix: 'Ensure
|
|
173
|
+
fix: 'Ensure container is running: docker ps | grep orchestrator',
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Orchestrator is healthy, add ok result
|
|
178
|
+
results.push({ name: 'Orchestrator', severity: 'ok', message: `Running at ${ctx.settings.orchestrator.url}` });
|
|
179
|
+
// Check for discovered clusters (separate try-catch to avoid duplicate error)
|
|
180
|
+
try {
|
|
181
|
+
const clusters = await ctx.orchestrator.getClusters();
|
|
182
|
+
if (clusters.length === 0) {
|
|
183
|
+
results.push({
|
|
184
|
+
name: 'MySQL Topology',
|
|
185
|
+
severity: 'warning',
|
|
186
|
+
message: 'No MySQL instances discovered in Orchestrator',
|
|
187
|
+
fix: 'Register instances with: /instances register <host>',
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
results.push({ name: 'MySQL Topology', severity: 'ok', message: `${clusters.length} cluster(s) discovered` });
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
results.push({
|
|
196
|
+
name: 'MySQL Topology',
|
|
197
|
+
severity: 'warning',
|
|
198
|
+
message: 'Could not query Orchestrator for clusters',
|
|
199
|
+
fix: 'Check Orchestrator logs: docker logs orchestrator',
|
|
229
200
|
});
|
|
230
201
|
}
|
|
231
202
|
}
|
|
232
|
-
/**
|
|
233
|
-
* Check ProxySQL health
|
|
234
|
-
*/
|
|
235
203
|
async function checkProxySQL(ctx, results) {
|
|
236
204
|
try {
|
|
237
205
|
await ctx.proxysql.connect();
|
|
238
|
-
results.push({
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
206
|
+
results.push({ name: 'ProxySQL', severity: 'ok', message: `Admin interface on port ${ctx.settings.proxysql.adminPort}` });
|
|
207
|
+
const servers = await ctx.proxysql.getServers();
|
|
208
|
+
if (servers.length === 0) {
|
|
209
|
+
results.push({
|
|
210
|
+
name: 'ProxySQL Servers',
|
|
211
|
+
severity: 'warning',
|
|
212
|
+
message: 'No MySQL servers configured',
|
|
213
|
+
fix: 'Register instances, then sync with: /clusters sync',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
const online = servers.filter(s => s.status === 'ONLINE').length;
|
|
218
|
+
if (online === servers.length) {
|
|
219
|
+
results.push({ name: 'ProxySQL Servers', severity: 'ok', message: `${online}/${servers.length} servers online` });
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
247
222
|
results.push({
|
|
248
223
|
name: 'ProxySQL Servers',
|
|
249
224
|
severity: 'warning',
|
|
250
|
-
message:
|
|
251
|
-
fix: '
|
|
225
|
+
message: `${online}/${servers.length} servers online`,
|
|
226
|
+
fix: 'Check MySQL server connectivity and credentials',
|
|
252
227
|
});
|
|
253
228
|
}
|
|
254
|
-
else {
|
|
255
|
-
const onlineServers = servers.filter(s => s.status === 'ONLINE');
|
|
256
|
-
if (onlineServers.length === servers.length) {
|
|
257
|
-
results.push({
|
|
258
|
-
name: 'ProxySQL Servers',
|
|
259
|
-
severity: 'ok',
|
|
260
|
-
message: `${onlineServers.length}/${servers.length} servers online`,
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
results.push({
|
|
265
|
-
name: 'ProxySQL Servers',
|
|
266
|
-
severity: 'warning',
|
|
267
|
-
message: `${onlineServers.length}/${servers.length} servers online`,
|
|
268
|
-
fix: 'Check MySQL server connectivity and credentials',
|
|
269
|
-
});
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
results.push({
|
|
275
|
-
name: 'ProxySQL Servers',
|
|
276
|
-
severity: 'warning',
|
|
277
|
-
message: 'Could not retrieve server configuration',
|
|
278
|
-
});
|
|
279
229
|
}
|
|
280
230
|
await ctx.proxysql.close();
|
|
281
231
|
}
|
|
282
232
|
catch (error) {
|
|
283
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
284
233
|
results.push({
|
|
285
234
|
name: 'ProxySQL',
|
|
286
235
|
severity: 'error',
|
|
287
236
|
message: 'Cannot connect to ProxySQL admin interface',
|
|
288
|
-
detail: message,
|
|
289
|
-
fix: 'Ensure
|
|
237
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
238
|
+
fix: 'Ensure container is running: docker ps | grep proxysql',
|
|
290
239
|
});
|
|
291
240
|
}
|
|
292
241
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Check Prometheus health
|
|
295
|
-
*/
|
|
296
242
|
async function checkPrometheus(ctx, results) {
|
|
297
243
|
try {
|
|
298
244
|
const response = await fetch(`${ctx.settings.prometheus.url}/-/healthy`, {
|
|
299
245
|
signal: AbortSignal.timeout(5000),
|
|
300
246
|
});
|
|
301
247
|
if (response.ok) {
|
|
302
|
-
results.push({
|
|
303
|
-
name: 'Prometheus',
|
|
304
|
-
severity: 'ok',
|
|
305
|
-
message: `Running at ${ctx.settings.prometheus.url}`,
|
|
306
|
-
});
|
|
248
|
+
results.push({ name: 'Prometheus', severity: 'ok', message: `Running at ${ctx.settings.prometheus.url}` });
|
|
307
249
|
}
|
|
308
250
|
else {
|
|
309
251
|
results.push({
|
|
310
252
|
name: 'Prometheus',
|
|
311
253
|
severity: 'warning',
|
|
312
254
|
message: `Health check returned status ${response.status}`,
|
|
313
|
-
fix: 'Check
|
|
255
|
+
fix: 'Check container: docker logs prometheus',
|
|
314
256
|
});
|
|
315
257
|
}
|
|
316
258
|
}
|
|
@@ -324,127 +266,115 @@ async function checkPrometheus(ctx, results) {
|
|
|
324
266
|
});
|
|
325
267
|
}
|
|
326
268
|
}
|
|
327
|
-
/**
|
|
328
|
-
* Check OpenClaw AI Gateway health
|
|
329
|
-
*/
|
|
330
269
|
async function checkOpenClaw(_ctx, results) {
|
|
331
270
|
try {
|
|
332
|
-
const status = await (0, openclaw_integration_js_1.
|
|
333
|
-
if (status.available) {
|
|
334
|
-
// Check
|
|
335
|
-
const
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
severity: 'ok',
|
|
341
|
-
message: 'Running in Docker container (ws://localhost:18789)',
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
else if (status.isLocal) {
|
|
271
|
+
const status = await (0, openclaw_integration_js_1.getDetailedOpenClawStatus)();
|
|
272
|
+
if (!status.available) {
|
|
273
|
+
// Check if container exists but stopped
|
|
274
|
+
const runtime = await (0, docker_prereq_js_1.detectRuntime)();
|
|
275
|
+
if (runtime) {
|
|
276
|
+
const psResult = await execCommand([runtime, 'ps', '-a', '--filter', 'name=openclaw', '--format', '{{.Status}}'], true);
|
|
277
|
+
const containerStatus = psResult.stdout.trim();
|
|
278
|
+
if (containerStatus && !containerStatus.toLowerCase().includes('up')) {
|
|
345
279
|
results.push({
|
|
346
280
|
name: 'OpenClaw Gateway',
|
|
347
|
-
severity: '
|
|
348
|
-
message: '
|
|
281
|
+
severity: 'warning',
|
|
282
|
+
message: 'OpenClaw container exists but is not running',
|
|
283
|
+
detail: `Status: ${containerStatus}`,
|
|
284
|
+
fix: 'Restart the platform with: /start',
|
|
285
|
+
fixCommand: '/start',
|
|
349
286
|
});
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
else {
|
|
353
|
-
results.push({
|
|
354
|
-
name: 'OpenClaw Gateway',
|
|
355
|
-
severity: 'warning',
|
|
356
|
-
message: 'Gateway is not responding',
|
|
357
|
-
detail: 'Container is running but gateway health check failed',
|
|
358
|
-
fix: 'Check logs: docker logs openclaw',
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
// Check if Docker OpenClaw container exists but is not running
|
|
364
|
-
const runtime = await detectRuntime();
|
|
365
|
-
if (runtime) {
|
|
366
|
-
try {
|
|
367
|
-
const psResult = await execCommand([runtime, 'ps', '-a', '--filter', 'name=openclaw', '--format', '{{.Status}}'], true);
|
|
368
|
-
const containerStatus = psResult.stdout.trim();
|
|
369
|
-
if (containerStatus && !containerStatus.toLowerCase().includes('up')) {
|
|
370
|
-
results.push({
|
|
371
|
-
name: 'OpenClaw Gateway',
|
|
372
|
-
severity: 'warning',
|
|
373
|
-
message: 'OpenClaw container exists but is not running',
|
|
374
|
-
detail: `Container status: ${containerStatus}`,
|
|
375
|
-
fix: 'Restart the platform with: /start',
|
|
376
|
-
fixCommand: '/start',
|
|
377
|
-
});
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
catch {
|
|
382
|
-
// Continue to default message
|
|
287
|
+
return;
|
|
383
288
|
}
|
|
384
289
|
}
|
|
385
290
|
results.push({
|
|
386
291
|
name: 'OpenClaw Gateway',
|
|
387
292
|
severity: 'warning',
|
|
388
293
|
message: 'OpenClaw gateway is not available',
|
|
389
|
-
detail: 'AI
|
|
390
|
-
fix: 'Start
|
|
294
|
+
detail: 'AI features will not work without OpenClaw',
|
|
295
|
+
fix: 'Start with: /start',
|
|
391
296
|
fixCommand: '/start',
|
|
392
297
|
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!status.gatewayHealthy) {
|
|
301
|
+
results.push({
|
|
302
|
+
name: 'OpenClaw Gateway',
|
|
303
|
+
severity: 'warning',
|
|
304
|
+
message: 'Gateway is not responding',
|
|
305
|
+
fix: 'Check logs: docker logs openclaw',
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const modeDisplay = status.mode === 'docker' ? 'Running in Docker' : 'Using local installation';
|
|
310
|
+
results.push({ name: 'OpenClaw Gateway', severity: 'ok', message: `${modeDisplay} (ws://localhost:18789)` });
|
|
311
|
+
// Check for auto-detected AI config from environment
|
|
312
|
+
const aiConfig = (0, ai_config_js_1.detectAIConfigFromEnv)();
|
|
313
|
+
// Model info
|
|
314
|
+
if (status.modelInfo.configured && status.modelInfo.model) {
|
|
315
|
+
results.push({
|
|
316
|
+
name: 'OpenClaw Model',
|
|
317
|
+
severity: 'ok',
|
|
318
|
+
message: `${status.modelInfo.provider || 'custom'}: ${status.modelInfo.model}`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
else if (aiConfig.provider !== 'none') {
|
|
322
|
+
results.push({
|
|
323
|
+
name: 'OpenClaw Model',
|
|
324
|
+
severity: 'ok',
|
|
325
|
+
message: `${aiConfig.provider}${aiConfig.model ? '/' + aiConfig.model : ''} (auto-detected)`,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
results.push({
|
|
330
|
+
name: 'OpenClaw Model',
|
|
331
|
+
severity: 'info',
|
|
332
|
+
message: 'Using bundled qwen model (limited)',
|
|
333
|
+
fix: 'Set ANTHROPIC_API_KEY or OPENAI_API_KEY for better AI',
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
// Quick AI test
|
|
337
|
+
const testResult = await (0, openclaw_integration_js_1.testOpenClawConnection)('ping');
|
|
338
|
+
if (testResult.success) {
|
|
339
|
+
results.push({ name: 'OpenClaw AI Test', severity: 'ok', message: `AI responding (${testResult.latencyMs}ms)` });
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
results.push({
|
|
343
|
+
name: 'OpenClaw AI Test',
|
|
344
|
+
severity: 'warning',
|
|
345
|
+
message: 'AI test failed',
|
|
346
|
+
detail: testResult.error,
|
|
347
|
+
fix: 'Check logs: docker logs openclaw',
|
|
348
|
+
});
|
|
393
349
|
}
|
|
394
350
|
}
|
|
395
351
|
catch (error) {
|
|
396
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
397
352
|
results.push({
|
|
398
353
|
name: 'OpenClaw Gateway',
|
|
399
354
|
severity: 'warning',
|
|
400
|
-
message: 'Could not check
|
|
401
|
-
detail: message,
|
|
402
|
-
fix: 'Ensure
|
|
355
|
+
message: 'Could not check status',
|
|
356
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
357
|
+
fix: 'Ensure container is running: docker ps | grep openclaw',
|
|
403
358
|
});
|
|
404
359
|
}
|
|
405
360
|
}
|
|
406
|
-
/**
|
|
407
|
-
* Detect container runtime
|
|
408
|
-
*/
|
|
409
|
-
async function detectRuntime() {
|
|
410
|
-
const runtimes = ['docker', 'podman'];
|
|
411
|
-
for (const runtime of runtimes) {
|
|
412
|
-
try {
|
|
413
|
-
const result = await execCommand([runtime, 'info'], true);
|
|
414
|
-
if (result.success) {
|
|
415
|
-
return runtime;
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
catch {
|
|
419
|
-
// Continue
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
return null;
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Check configuration issues
|
|
426
|
-
*/
|
|
427
361
|
function checkConfiguration(ctx, results) {
|
|
428
|
-
// Check API token secret
|
|
429
362
|
if (ctx.settings.api.tokenSecret === 'change-me-in-production') {
|
|
430
363
|
results.push({
|
|
431
364
|
name: 'API Token Secret',
|
|
432
365
|
severity: 'warning',
|
|
433
366
|
message: 'Using default token secret (not secure for production)',
|
|
434
|
-
fix: 'Set
|
|
367
|
+
fix: 'Set: API_TOKEN_SECRET=<your-secret>',
|
|
435
368
|
});
|
|
436
369
|
}
|
|
437
|
-
// Check MySQL credentials
|
|
438
370
|
if (!ctx.settings.mysql.adminPassword) {
|
|
439
371
|
results.push({
|
|
440
372
|
name: 'MySQL Credentials',
|
|
441
373
|
severity: 'warning',
|
|
442
374
|
message: 'MySQL admin password not configured',
|
|
443
|
-
|
|
444
|
-
fix: 'Set environment variable: MYSQL_ADMIN_PASSWORD=<password>',
|
|
375
|
+
fix: 'Set: MYSQL_ADMIN_PASSWORD=<password>',
|
|
445
376
|
});
|
|
446
377
|
}
|
|
447
|
-
// Check auto-failover
|
|
448
378
|
if (!ctx.settings.failover.autoFailoverEnabled) {
|
|
449
379
|
results.push({
|
|
450
380
|
name: 'Auto Failover',
|
|
@@ -453,18 +383,10 @@ function checkConfiguration(ctx, results) {
|
|
|
453
383
|
fix: 'Enable with: AUTO_FAILOVER_ENABLED=true',
|
|
454
384
|
});
|
|
455
385
|
}
|
|
456
|
-
// Check metadata database
|
|
457
386
|
if (!ctx.settings.metadataDb.host) {
|
|
458
|
-
results.push({
|
|
459
|
-
name: 'Metadata Database',
|
|
460
|
-
severity: 'ok',
|
|
461
|
-
message: 'Using auto-provisioned metadata-mysql container',
|
|
462
|
-
});
|
|
387
|
+
results.push({ name: 'Metadata Database', severity: 'ok', message: 'Using auto-provisioned container' });
|
|
463
388
|
}
|
|
464
389
|
}
|
|
465
|
-
/**
|
|
466
|
-
* Check MySQL instances health
|
|
467
|
-
*/
|
|
468
390
|
async function checkMySQLInstances(ctx, results) {
|
|
469
391
|
try {
|
|
470
392
|
const clusters = await ctx.orchestrator.getClusters();
|
|
@@ -472,74 +394,67 @@ async function checkMySQLInstances(ctx, results) {
|
|
|
472
394
|
const topology = await ctx.orchestrator.getTopology(clusterName);
|
|
473
395
|
if (!topology)
|
|
474
396
|
continue;
|
|
397
|
+
const label = topology.name || clusterName;
|
|
475
398
|
// Check primary
|
|
476
399
|
if (topology.primary) {
|
|
477
400
|
if (topology.primary.state !== 'online') {
|
|
478
401
|
results.push({
|
|
479
|
-
name: `Primary [${
|
|
402
|
+
name: `Primary [${label}]`,
|
|
480
403
|
severity: 'error',
|
|
481
|
-
message:
|
|
482
|
-
fix: 'Check MySQL instance status
|
|
404
|
+
message: `${topology.primary.host}:${topology.primary.port} is ${topology.primary.state}`,
|
|
405
|
+
fix: 'Check MySQL instance status',
|
|
483
406
|
});
|
|
484
407
|
}
|
|
485
408
|
else {
|
|
486
|
-
results.push({
|
|
487
|
-
name: `Primary [${topology.name || clusterName}]`,
|
|
488
|
-
severity: 'ok',
|
|
489
|
-
message: `${topology.primary.host}:${topology.primary.port} is online`,
|
|
490
|
-
});
|
|
409
|
+
results.push({ name: `Primary [${label}]`, severity: 'ok', message: `${topology.primary.host}:${topology.primary.port} is online` });
|
|
491
410
|
}
|
|
492
411
|
}
|
|
493
412
|
else {
|
|
494
413
|
results.push({
|
|
495
|
-
name: `Primary [${
|
|
414
|
+
name: `Primary [${label}]`,
|
|
496
415
|
severity: 'error',
|
|
497
|
-
message: 'No primary found
|
|
498
|
-
fix: 'Check replication
|
|
416
|
+
message: 'No primary found',
|
|
417
|
+
fix: 'Check replication or promote: /failover switchover',
|
|
499
418
|
});
|
|
500
419
|
}
|
|
501
420
|
// Check replicas
|
|
502
421
|
for (const replica of topology.replicas) {
|
|
503
422
|
if (replica.state !== 'online') {
|
|
504
423
|
results.push({
|
|
505
|
-
name: `Replica [${
|
|
424
|
+
name: `Replica [${label}]`,
|
|
506
425
|
severity: 'warning',
|
|
507
|
-
message:
|
|
508
|
-
fix: 'Check replica
|
|
426
|
+
message: `${replica.host}:${replica.port} is ${replica.state}`,
|
|
427
|
+
fix: 'Check replica status',
|
|
509
428
|
});
|
|
510
429
|
}
|
|
511
430
|
else if (replica.replicationLag !== undefined && replica.replicationLag > 60) {
|
|
512
431
|
results.push({
|
|
513
|
-
name: `Replica Lag [${
|
|
432
|
+
name: `Replica Lag [${label}]`,
|
|
514
433
|
severity: 'warning',
|
|
515
|
-
message:
|
|
516
|
-
fix: 'Check replica performance
|
|
434
|
+
message: `${replica.host}:${replica.port} lag: ${replica.replicationLag}s`,
|
|
435
|
+
fix: 'Check replica performance',
|
|
517
436
|
});
|
|
518
437
|
}
|
|
519
438
|
}
|
|
520
439
|
}
|
|
521
440
|
}
|
|
522
441
|
catch {
|
|
523
|
-
//
|
|
442
|
+
// Handled in Orchestrator check
|
|
524
443
|
}
|
|
525
444
|
}
|
|
526
|
-
/**
|
|
527
|
-
* Check replication topology issues
|
|
528
|
-
*/
|
|
529
445
|
async function checkReplicationTopology(ctx, results) {
|
|
530
446
|
try {
|
|
531
447
|
const analysis = await ctx.orchestrator.getReplicationAnalysis();
|
|
532
448
|
for (const issue of analysis) {
|
|
533
449
|
const analysisType = issue.Analysis;
|
|
534
|
-
const description = issue.Description;
|
|
535
|
-
const affectedHost = issue.Key?.Hostname;
|
|
536
450
|
if (analysisType && !analysisType.includes('NoProblem')) {
|
|
451
|
+
const host = issue.Key?.Hostname;
|
|
537
452
|
results.push({
|
|
538
453
|
name: 'Replication Analysis',
|
|
539
454
|
severity: 'warning',
|
|
540
|
-
message: `${analysisType}: ${
|
|
541
|
-
detail:
|
|
542
|
-
fix: 'Review Orchestrator UI
|
|
455
|
+
message: `${analysisType}: ${host || 'unknown'}`,
|
|
456
|
+
detail: issue.Description,
|
|
457
|
+
fix: 'Review Orchestrator UI: http://localhost:3000',
|
|
543
458
|
});
|
|
544
459
|
}
|
|
545
460
|
}
|
|
@@ -548,238 +463,184 @@ async function checkReplicationTopology(ctx, results) {
|
|
|
548
463
|
// Orchestrator may not be available
|
|
549
464
|
}
|
|
550
465
|
}
|
|
551
|
-
/**
|
|
552
|
-
* Check ProxySQL sync with Orchestrator topology
|
|
553
|
-
*/
|
|
554
466
|
async function checkProxySQLSync(ctx, results) {
|
|
555
467
|
try {
|
|
556
|
-
// Get topology from Orchestrator
|
|
557
468
|
const clusters = await ctx.orchestrator.getClusters();
|
|
558
|
-
if (clusters.length === 0)
|
|
559
|
-
return;
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
let proxysqlServers = [];
|
|
469
|
+
if (clusters.length === 0)
|
|
470
|
+
return;
|
|
471
|
+
// Get ProxySQL data
|
|
472
|
+
let servers = [];
|
|
563
473
|
let hostgroups = [];
|
|
564
474
|
try {
|
|
565
475
|
await ctx.proxysql.connect();
|
|
566
|
-
|
|
476
|
+
servers = await ctx.proxysql.getServers();
|
|
567
477
|
hostgroups = await ctx.proxysql.getReplicationHostgroups();
|
|
568
478
|
await ctx.proxysql.close();
|
|
569
479
|
}
|
|
570
480
|
catch {
|
|
571
|
-
// ProxySQL not available - already reported in checkProxySQL
|
|
572
481
|
return;
|
|
573
482
|
}
|
|
574
483
|
// Build expected instances from Orchestrator
|
|
575
|
-
const
|
|
484
|
+
const expected = new Map();
|
|
576
485
|
for (const clusterName of clusters) {
|
|
577
486
|
const topology = await ctx.orchestrator.getTopology(clusterName);
|
|
578
487
|
if (!topology)
|
|
579
488
|
continue;
|
|
580
489
|
if (topology.primary) {
|
|
581
|
-
|
|
582
|
-
expectedInstances.set(key, {
|
|
490
|
+
expected.set(`${topology.primary.host}:${topology.primary.port}`, {
|
|
583
491
|
host: topology.primary.host,
|
|
584
492
|
port: topology.primary.port,
|
|
585
493
|
role: 'primary',
|
|
586
|
-
cluster: topology.name || clusterName,
|
|
587
494
|
});
|
|
588
495
|
}
|
|
589
496
|
for (const replica of topology.replicas) {
|
|
590
|
-
|
|
591
|
-
expectedInstances.set(key, {
|
|
497
|
+
expected.set(`${replica.host}:${replica.port}`, {
|
|
592
498
|
host: replica.host,
|
|
593
499
|
port: replica.port,
|
|
594
500
|
role: 'replica',
|
|
595
|
-
cluster: topology.name || clusterName,
|
|
596
501
|
});
|
|
597
502
|
}
|
|
598
503
|
}
|
|
599
|
-
// Build actual ProxySQL
|
|
600
|
-
const
|
|
601
|
-
for (const
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
const missingInProxySQL = [];
|
|
615
|
-
const wrongHostgroup = [];
|
|
616
|
-
for (const [key, instance] of expectedInstances) {
|
|
617
|
-
const proxysqlInstance = proxysqlInstances.get(key);
|
|
618
|
-
if (!proxysqlInstance) {
|
|
619
|
-
missingInProxySQL.push(`${key} (${instance.role})`);
|
|
504
|
+
// Build actual from ProxySQL
|
|
505
|
+
const actual = new Map();
|
|
506
|
+
for (const s of servers) {
|
|
507
|
+
actual.set(`${s.hostname}:${s.port}`, { hostgroup: s.hostgroupId });
|
|
508
|
+
}
|
|
509
|
+
const writerHG = hostgroups.length > 0 ? Number(hostgroups[0].writerHostgroup) : 10;
|
|
510
|
+
const readerHG = hostgroups.length > 0 ? Number(hostgroups[0].readerHostgroup) : 20;
|
|
511
|
+
// Check for issues
|
|
512
|
+
const missing = [];
|
|
513
|
+
const wrongHG = [];
|
|
514
|
+
const orphans = [];
|
|
515
|
+
for (const [key, inst] of expected) {
|
|
516
|
+
const proxy = actual.get(key);
|
|
517
|
+
if (!proxy) {
|
|
518
|
+
missing.push(`${key} (${inst.role})`);
|
|
620
519
|
}
|
|
621
520
|
else {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
// Skip if both are NaN or if they match
|
|
626
|
-
if (!isNaN(actualHG) && !isNaN(expectedHG) && actualHG !== expectedHG) {
|
|
627
|
-
wrongHostgroup.push(`${key} is in hg:${actualHG}, expected hg:${expectedHG}`);
|
|
521
|
+
const expectedHG = inst.role === 'primary' ? writerHG : readerHG;
|
|
522
|
+
if (!isNaN(proxy.hostgroup) && !isNaN(expectedHG) && proxy.hostgroup !== expectedHG) {
|
|
523
|
+
wrongHG.push(`${key} in hg:${proxy.hostgroup}, expected hg:${expectedHG}`);
|
|
628
524
|
}
|
|
629
525
|
}
|
|
630
526
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
if (!expectedInstances.has(key)) {
|
|
635
|
-
orphanInstances.push(`${key} (hg:${instance.hostgroup})`);
|
|
527
|
+
for (const [key, inst] of actual) {
|
|
528
|
+
if (!expected.has(key)) {
|
|
529
|
+
orphans.push(`${key} (hg:${inst.hostgroup})`);
|
|
636
530
|
}
|
|
637
531
|
}
|
|
638
|
-
// Report
|
|
639
|
-
if (
|
|
532
|
+
// Report
|
|
533
|
+
if (missing.length > 0) {
|
|
640
534
|
results.push({
|
|
641
535
|
name: 'ProxySQL Sync',
|
|
642
536
|
severity: 'warning',
|
|
643
|
-
message: `${
|
|
644
|
-
detail:
|
|
645
|
-
fix: 'Sync
|
|
537
|
+
message: `${missing.length} instance(s) missing in ProxySQL`,
|
|
538
|
+
detail: missing.slice(0, 5).join(', '),
|
|
539
|
+
fix: 'Sync with: /clusters sync',
|
|
646
540
|
fixCommand: '/clusters sync',
|
|
647
541
|
});
|
|
648
542
|
}
|
|
649
|
-
if (
|
|
543
|
+
if (wrongHG.length > 0) {
|
|
650
544
|
results.push({
|
|
651
545
|
name: 'ProxySQL Hostgroups',
|
|
652
546
|
severity: 'warning',
|
|
653
|
-
message: `${
|
|
654
|
-
detail:
|
|
655
|
-
fix: 'Re-sync
|
|
656
|
-
fixCommand: '/clusters sync',
|
|
547
|
+
message: `${wrongHG.length} instance(s) in wrong hostgroup`,
|
|
548
|
+
detail: wrongHG.slice(0, 3).join('; '),
|
|
549
|
+
fix: 'Re-sync with: /clusters sync',
|
|
657
550
|
});
|
|
658
551
|
}
|
|
659
|
-
if (
|
|
552
|
+
if (orphans.length > 0) {
|
|
660
553
|
results.push({
|
|
661
554
|
name: 'ProxySQL Orphans',
|
|
662
555
|
severity: 'warning',
|
|
663
|
-
message: `${
|
|
664
|
-
detail:
|
|
665
|
-
fix: 'Remove
|
|
556
|
+
message: `${orphans.length} orphan instance(s) in ProxySQL`,
|
|
557
|
+
detail: orphans.slice(0, 3).join(', '),
|
|
558
|
+
fix: 'Remove orphans or register in Orchestrator',
|
|
666
559
|
});
|
|
667
560
|
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
results.push({
|
|
671
|
-
name: 'ProxySQL Sync',
|
|
672
|
-
severity: 'ok',
|
|
673
|
-
message: `All ${expectedInstances.size} instances properly synced`,
|
|
674
|
-
});
|
|
561
|
+
if (missing.length === 0 && wrongHG.length === 0 && orphans.length === 0 && expected.size > 0) {
|
|
562
|
+
results.push({ name: 'ProxySQL Sync', severity: 'ok', message: `All ${expected.size} instances synced` });
|
|
675
563
|
}
|
|
676
564
|
}
|
|
677
|
-
catch
|
|
678
|
-
|
|
679
|
-
results.push({
|
|
680
|
-
name: 'ProxySQL Sync',
|
|
681
|
-
severity: 'warning',
|
|
682
|
-
message: 'Could not verify ProxySQL sync',
|
|
683
|
-
detail: message,
|
|
684
|
-
});
|
|
565
|
+
catch {
|
|
566
|
+
// Already reported in other checks
|
|
685
567
|
}
|
|
686
568
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
569
|
+
// ============================================================================
|
|
570
|
+
// Display Utilities
|
|
571
|
+
// ============================================================================
|
|
690
572
|
function displayResults(results) {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
for (const result of errors) {
|
|
700
|
-
displayResult(result);
|
|
701
|
-
}
|
|
573
|
+
const groups = {
|
|
574
|
+
error: [],
|
|
575
|
+
warning: [],
|
|
576
|
+
info: [],
|
|
577
|
+
ok: [],
|
|
578
|
+
};
|
|
579
|
+
for (const r of results) {
|
|
580
|
+
groups[r.severity].push(r);
|
|
702
581
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
for (const result of warnings) {
|
|
707
|
-
displayResult(result);
|
|
708
|
-
}
|
|
582
|
+
if (groups.error.length > 0) {
|
|
583
|
+
console.log(components_js_1.theme.error.bold('✗ Errors:'));
|
|
584
|
+
groups.error.forEach(displayResult);
|
|
709
585
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
for (const result of info) {
|
|
714
|
-
displayResult(result);
|
|
715
|
-
}
|
|
586
|
+
if (groups.warning.length > 0) {
|
|
587
|
+
console.log(components_js_1.theme.warning.bold('◆ Warnings:'));
|
|
588
|
+
groups.warning.forEach(displayResult);
|
|
716
589
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
590
|
+
if (groups.info.length > 0) {
|
|
591
|
+
console.log(components_js_1.theme.info.bold('○ Information:'));
|
|
592
|
+
groups.info.forEach(displayResult);
|
|
593
|
+
}
|
|
594
|
+
if (groups.ok.length > 0) {
|
|
595
|
+
console.log(components_js_1.theme.success.bold('✓ Healthy:'));
|
|
596
|
+
groups.ok.forEach(r => console.log(components_js_1.theme.muted(` ${r.name}: `) + components_js_1.theme.success(r.message)));
|
|
723
597
|
}
|
|
724
598
|
}
|
|
725
|
-
/**
|
|
726
|
-
* Display a single diagnostic result
|
|
727
|
-
*/
|
|
728
599
|
function displayResult(result) {
|
|
729
|
-
const
|
|
730
|
-
error: components_js_1.theme.error,
|
|
731
|
-
warning: components_js_1.theme.warning,
|
|
732
|
-
ok: components_js_1.theme.success,
|
|
733
|
-
info: components_js_1.theme.info,
|
|
734
|
-
};
|
|
735
|
-
const severityIcons = {
|
|
600
|
+
const icons = {
|
|
736
601
|
error: components_js_1.theme.error(components_js_1.indicators.cross),
|
|
737
602
|
warning: components_js_1.theme.warning(components_js_1.indicators.warning),
|
|
738
|
-
ok: components_js_1.theme.success(components_js_1.indicators.check),
|
|
739
603
|
info: components_js_1.theme.info(components_js_1.indicators.info),
|
|
604
|
+
ok: components_js_1.theme.success(components_js_1.indicators.check),
|
|
740
605
|
};
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
606
|
+
const colorFn = {
|
|
607
|
+
error: components_js_1.theme.error,
|
|
608
|
+
warning: components_js_1.theme.warning,
|
|
609
|
+
info: components_js_1.theme.info,
|
|
610
|
+
ok: components_js_1.theme.success,
|
|
611
|
+
}[result.severity];
|
|
612
|
+
console.log(` ${icons[result.severity]} ${colorFn.bold(result.name)}: ${result.message}`);
|
|
613
|
+
if (result.detail)
|
|
745
614
|
console.log(components_js_1.theme.muted(` ${result.detail}`));
|
|
746
|
-
|
|
747
|
-
if (result.fix) {
|
|
615
|
+
if (result.fix)
|
|
748
616
|
console.log(components_js_1.theme.primary(` Fix: ${result.fix}`));
|
|
749
|
-
|
|
750
|
-
if (result.fixCommand) {
|
|
617
|
+
if (result.fixCommand)
|
|
751
618
|
console.log(components_js_1.theme.muted(` Command: ${result.fixCommand}`));
|
|
619
|
+
}
|
|
620
|
+
function printSummary(results) {
|
|
621
|
+
const errors = results.filter(r => r.severity === 'error').length;
|
|
622
|
+
const warnings = results.filter(r => r.severity === 'warning').length;
|
|
623
|
+
if (errors === 0 && warnings === 0) {
|
|
624
|
+
console.log(components_js_1.theme.success(`${components_js_1.indicators.check} All systems healthy!`));
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
console.log(components_js_1.theme.warning(`Found ${errors} error(s) and ${warnings} warning(s)`));
|
|
752
628
|
}
|
|
753
|
-
console.log();
|
|
754
629
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
630
|
+
// ============================================================================
|
|
631
|
+
// Process Execution
|
|
632
|
+
// ============================================================================
|
|
758
633
|
function execCommand(cmd, silent = false) {
|
|
759
634
|
return new Promise((resolve) => {
|
|
760
|
-
const proc = (0, child_process_1.spawn)(cmd[0], cmd.slice(1), {
|
|
761
|
-
stdio: silent ? 'pipe' : 'inherit',
|
|
762
|
-
});
|
|
635
|
+
const proc = (0, child_process_1.spawn)(cmd[0], cmd.slice(1), { stdio: silent ? 'pipe' : 'inherit' });
|
|
763
636
|
let stdout = '';
|
|
764
637
|
let stderr = '';
|
|
765
638
|
if (silent) {
|
|
766
639
|
proc.stdout?.on('data', (data) => { stdout += data; });
|
|
767
640
|
proc.stderr?.on('data', (data) => { stderr += data; });
|
|
768
641
|
}
|
|
769
|
-
proc.on('close', (code) => {
|
|
770
|
-
|
|
771
|
-
success: code === 0,
|
|
772
|
-
stdout,
|
|
773
|
-
stderr,
|
|
774
|
-
});
|
|
775
|
-
});
|
|
776
|
-
proc.on('error', () => {
|
|
777
|
-
resolve({
|
|
778
|
-
success: false,
|
|
779
|
-
stdout: '',
|
|
780
|
-
stderr: 'Failed to execute command',
|
|
781
|
-
});
|
|
782
|
-
});
|
|
642
|
+
proc.on('close', (code) => resolve({ success: code === 0, stdout, stderr }));
|
|
643
|
+
proc.on('error', () => resolve({ success: false, stdout: '', stderr: 'Failed to execute' }));
|
|
783
644
|
});
|
|
784
645
|
}
|
|
785
646
|
exports.default = exports.doctorCommand;
|