@better-openclaw/core 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/bare-metal-partition.test.mjs +1 -1
  2. package/dist/composer.snapshot.test.mjs +1 -1
  3. package/dist/composer.test.mjs +1 -1
  4. package/dist/generate.d.mts.map +1 -1
  5. package/dist/generate.mjs +7 -1
  6. package/dist/generate.mjs.map +1 -1
  7. package/dist/generate.test.mjs +1 -1
  8. package/dist/generators/bare-metal-install.test.mjs +1 -1
  9. package/dist/generators/caddy.test.d.mts +1 -0
  10. package/dist/generators/caddy.test.mjs +45 -0
  11. package/dist/generators/caddy.test.mjs.map +1 -0
  12. package/dist/generators/env.test.d.mts +1 -0
  13. package/dist/generators/env.test.mjs +60 -0
  14. package/dist/generators/env.test.mjs.map +1 -0
  15. package/dist/generators/health-check.d.mts +18 -0
  16. package/dist/generators/health-check.d.mts.map +1 -0
  17. package/dist/generators/health-check.mjs +705 -0
  18. package/dist/generators/health-check.mjs.map +1 -0
  19. package/dist/generators/health-check.test.d.mts +1 -0
  20. package/dist/generators/health-check.test.mjs +85 -0
  21. package/dist/generators/health-check.test.mjs.map +1 -0
  22. package/dist/generators/scripts.test.d.mts +1 -0
  23. package/dist/generators/scripts.test.mjs +52 -0
  24. package/dist/generators/scripts.test.mjs.map +1 -0
  25. package/dist/generators/traefik.test.mjs +1 -1
  26. package/dist/generators/traefik.test.mjs.map +1 -1
  27. package/dist/index.d.mts +4 -2
  28. package/dist/index.mjs +3 -1
  29. package/dist/migrations.d.mts.map +1 -1
  30. package/dist/migrations.mjs.map +1 -1
  31. package/dist/migrations.test.mjs +1 -1
  32. package/dist/migrations.test.mjs.map +1 -1
  33. package/dist/presets/registry.test.mjs +1 -1
  34. package/dist/resolver.test.mjs +1 -1
  35. package/dist/schema.test.mjs +1 -1
  36. package/dist/services/definitions/convex.mjs.map +1 -1
  37. package/dist/services/definitions/desktop-environment.mjs.map +1 -1
  38. package/dist/services/definitions/index.d.mts +2 -2
  39. package/dist/services/definitions/index.mjs +3 -3
  40. package/dist/services/definitions/index.mjs.map +1 -1
  41. package/dist/services/definitions/mission-control.mjs.map +1 -1
  42. package/dist/services/definitions/stream-gateway.mjs.map +1 -1
  43. package/dist/services/registry.test.mjs +1 -1
  44. package/dist/skills/registry.d.mts.map +1 -1
  45. package/dist/skills/registry.mjs +498 -6
  46. package/dist/skills/registry.mjs.map +1 -1
  47. package/dist/skills/skill-manifest.d.mts +20 -0
  48. package/dist/skills/skill-manifest.d.mts.map +1 -0
  49. package/dist/skills/skill-manifest.mjs +28 -0
  50. package/dist/skills/skill-manifest.mjs.map +1 -0
  51. package/dist/validator.test.mjs +1 -1
  52. package/dist/version-manager.test.mjs +1 -1
  53. package/dist/version-manager.test.mjs.map +1 -1
  54. package/dist/{vi.2VT5v0um-Qk6MgAnK.mjs → vi.2VT5v0um-YSByewHe.mjs} +5 -5
  55. package/dist/{vi.2VT5v0um-Qk6MgAnK.mjs.map → vi.2VT5v0um-YSByewHe.mjs.map} +1 -1
  56. package/package.json +1 -1
  57. package/src/generate.ts +15 -3
  58. package/src/generators/caddy.test.ts +56 -0
  59. package/src/generators/env.test.ts +73 -0
  60. package/src/generators/health-check.test.ts +118 -0
  61. package/src/generators/health-check.ts +774 -0
  62. package/src/generators/scripts.test.ts +60 -0
  63. package/src/generators/traefik.test.ts +9 -17
  64. package/src/index.ts +12 -6
  65. package/src/migrations.test.ts +1 -1
  66. package/src/migrations.ts +1 -4
  67. package/src/services/definitions/convex.ts +2 -4
  68. package/src/services/definitions/desktop-environment.ts +1 -9
  69. package/src/services/definitions/index.ts +6 -6
  70. package/src/services/definitions/mission-control.ts +2 -4
  71. package/src/services/definitions/stream-gateway.ts +1 -2
  72. package/src/skills/registry.ts +336 -6
  73. package/src/skills/skill-manifest.ts +52 -0
  74. package/src/version-manager.test.ts +15 -4
  75. package/tsdown.config.ts +6 -6
@@ -0,0 +1,705 @@
1
+ //#region src/generators/health-check.ts
2
+ /**
3
+ * Generates stack-specific health check and verification scripts.
4
+ *
5
+ * Each generated script is dynamic — it contains the exact service names,
6
+ * ports, and health check commands from the resolved stack so users get
7
+ * a precise, zero-configuration verifier.
8
+ */
9
+ function generateHealthCheck(resolved, options) {
10
+ const files = {};
11
+ files["scripts/health-check.sh"] = generateBashScript(resolved, options);
12
+ files["scripts/health-check.ps1"] = generatePowerShellScript(resolved, options);
13
+ return files;
14
+ }
15
+ function extractServiceChecks(resolved) {
16
+ return resolved.services.map((svc) => ({
17
+ id: svc.definition.id,
18
+ name: svc.definition.name,
19
+ icon: svc.definition.icon,
20
+ ports: svc.definition.ports.map((p) => ({
21
+ host: p.host,
22
+ container: p.container,
23
+ description: p.description,
24
+ exposed: p.exposed
25
+ })),
26
+ healthCheckCmd: svc.definition.healthcheck?.test ?? null
27
+ }));
28
+ }
29
+ function escapeShell(s) {
30
+ return s.replace(/'/g, "'\\''").replace(/"/g, "\\\"");
31
+ }
32
+ function L(...parts) {
33
+ return parts.join("");
34
+ }
35
+ function generateBashScript(resolved, options) {
36
+ const checks = extractServiceChecks(resolved);
37
+ const name = options.projectName;
38
+ const total = checks.length;
39
+ const dockerChecks = [];
40
+ for (const svc of checks) {
41
+ dockerChecks.push(L(" # ── ", svc.icon, " ", svc.name, " ──"));
42
+ dockerChecks.push(L(" check_container \"", svc.id, "\" \"", svc.name, "\" \"", svc.icon, "\""));
43
+ for (const p of svc.ports.filter((pp) => pp.exposed)) dockerChecks.push(L(" check_port \"", svc.id, "\" ", String(p.host), " \"", p.description, "\""));
44
+ if (svc.healthCheckCmd) dockerChecks.push(L(" check_health_cmd \"", svc.id, "\" \"", escapeShell(svc.healthCheckCmd), "\""));
45
+ else dockerChecks.push(" # No healthcheck defined — skipping exec check");
46
+ dockerChecks.push("");
47
+ }
48
+ const bmChecks = [];
49
+ for (const svc of checks) {
50
+ bmChecks.push(L(" # ── ", svc.icon, " ", svc.name, " (bare-metal) ──"));
51
+ bmChecks.push(L(" check_process \"", svc.id, "\" \"", svc.name, "\" \"", svc.icon, "\""));
52
+ for (const p of svc.ports.filter((pp) => pp.exposed)) bmChecks.push(L(" check_port \"", svc.id, "\" ", String(p.host), " \"", p.description, "\""));
53
+ bmChecks.push("");
54
+ }
55
+ const svcIdList = checks.map((s) => "\"" + s.id + "\"").join(" ");
56
+ return [
57
+ "#!/usr/bin/env bash",
58
+ "set -uo pipefail",
59
+ "",
60
+ "# ═══════════════════════════════════════════════════════════════════════",
61
+ L("# 🐾 OpenClaw Stack Health Check — ", name),
62
+ "# ═══════════════════════════════════════════════════════════════════════",
63
+ "#",
64
+ "# Auto-generated verification script for your stack.",
65
+ L("# Checks ", String(total), " services across 6 phases:"),
66
+ "# 1. Environment — prerequisites, .env, secrets",
67
+ "# 2. Container — running state, health status, restart count",
68
+ "# 3. Port — TCP reachability for exposed ports",
69
+ "# 4. Health Command — execute each service's health check",
70
+ "# 5. Resources — memory/CPU warnings",
71
+ "# 6. Logs — scan for ERROR/FATAL/panic patterns",
72
+ "#",
73
+ "# Usage:",
74
+ "# ./scripts/health-check.sh # Full check",
75
+ "# ./scripts/health-check.sh --quick # Skip log scan",
76
+ "# ./scripts/health-check.sh --json # Output as JSON",
77
+ "#",
78
+ "# Exit codes: 0 = all healthy, 1 = some issues, 2 = critical failure",
79
+ "# ═══════════════════════════════════════════════════════════════════════",
80
+ "",
81
+ "SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"",
82
+ "PROJECT_DIR=\"$(dirname \"$SCRIPT_DIR\")\"",
83
+ "cd \"$PROJECT_DIR\"",
84
+ "",
85
+ "# ── Arguments ────────────────────────────────────────────────────────",
86
+ "QUICK_MODE=false",
87
+ "JSON_MODE=false",
88
+ "VERBOSE=false",
89
+ "for arg in \"$@\"; do",
90
+ " case \"$arg\" in",
91
+ " --quick) QUICK_MODE=true ;;",
92
+ " --json) JSON_MODE=true ;;",
93
+ " --verbose|-v) VERBOSE=true ;;",
94
+ " esac",
95
+ "done",
96
+ "",
97
+ "# ── Colour helpers ───────────────────────────────────────────────────",
98
+ "if [ -t 1 ] && [ \"$JSON_MODE\" = false ]; then",
99
+ " RED='\\033[0;31m'; GREEN='\\033[0;32m'; YELLOW='\\033[1;33m'",
100
+ " CYAN='\\033[0;36m'; DIM='\\033[2m'; BOLD='\\033[1m'; NC='\\033[0m'",
101
+ "else",
102
+ " RED=''; GREEN=''; YELLOW=''; CYAN=''; DIM=''; BOLD=''; NC=''",
103
+ "fi",
104
+ "",
105
+ "# ── Counters ─────────────────────────────────────────────────────────",
106
+ L("TOTAL=", String(total)),
107
+ "HEALTHY=0",
108
+ "WARNING=0",
109
+ "FAILED=0",
110
+ "ERRORS=()",
111
+ "WARNINGS_LIST=()",
112
+ "SERVICE_RESULTS=()",
113
+ "",
114
+ "# ── Utility functions ────────────────────────────────────────────────",
115
+ "pass() { echo -e \" ${GREEN}✅ $*${NC}\"; HEALTHY=$((HEALTHY + 1)); }",
116
+ "warn_svc() { echo -e \" ${YELLOW}⚠️ $*${NC}\"; WARNING=$((WARNING + 1)); WARNINGS_LIST+=(\"$*\"); }",
117
+ "fail() { echo -e \" ${RED}❌ $*${NC}\"; FAILED=$((FAILED + 1)); ERRORS+=(\"$*\"); }",
118
+ "info() { echo -e \" ${CYAN}ℹ $*${NC}\"; }",
119
+ "dim() { echo -e \" ${DIM}$*${NC}\"; }",
120
+ "",
121
+ "pad_name() {",
122
+ " local name=\"$1\"",
123
+ " local pad=25",
124
+ " printf \"%-${pad}s\" \"$name\"",
125
+ "}",
126
+ "",
127
+ "# ═══════════════════════════════════════════════════════════════════════",
128
+ "# Phase 1: Environment",
129
+ "# ═══════════════════════════════════════════════════════════════════════",
130
+ "phase_environment() {",
131
+ " if [ \"$JSON_MODE\" = false ]; then",
132
+ " echo \"\"",
133
+ " echo -e \"${BOLD}── Phase 1: Environment ────────────────────────────────────${NC}\"",
134
+ " echo \"\"",
135
+ " fi",
136
+ "",
137
+ " # Docker",
138
+ " if command -v docker &>/dev/null; then",
139
+ " if docker info &>/dev/null 2>&1; then",
140
+ " pass \"Docker daemon is running\"",
141
+ " DOCKER_AVAILABLE=true",
142
+ " else",
143
+ " fail \"Docker daemon is NOT running\"",
144
+ " DOCKER_AVAILABLE=false",
145
+ " fi",
146
+ " else",
147
+ " warn_svc \"Docker is not installed — running bare-metal checks only\"",
148
+ " DOCKER_AVAILABLE=false",
149
+ " fi",
150
+ "",
151
+ " # Docker Compose",
152
+ " if [ \"$DOCKER_AVAILABLE\" = true ]; then",
153
+ " if docker compose version &>/dev/null 2>&1; then",
154
+ " pass \"Docker Compose v2 available\"",
155
+ " else",
156
+ " warn_svc \"Docker Compose v2 not found\"",
157
+ " fi",
158
+ " fi",
159
+ "",
160
+ " # .env",
161
+ " if [ -f \".env\" ]; then",
162
+ " pass \".env file exists\"",
163
+ " EMPTY_SECRETS=0",
164
+ " while IFS='=' read -r key value; do",
165
+ " [[ \"$key\" =~ ^#.*$ ]] && continue",
166
+ " [[ -z \"$key\" ]] && continue",
167
+ " if [[ \"$key\" =~ (PASSWORD|TOKEN|SECRET|SESSION_KEY|COOKIE|API_KEY) ]] && [[ -z \"${value:-}\" ]]; then",
168
+ " if [ \"$VERBOSE\" = true ]; then",
169
+ " warn_svc \"Empty secret: $key\"",
170
+ " fi",
171
+ " EMPTY_SECRETS=$((EMPTY_SECRETS + 1))",
172
+ " fi",
173
+ " done < .env 2>/dev/null || true",
174
+ " if [ \"$EMPTY_SECRETS\" -gt 0 ]; then",
175
+ " warn_svc \"$EMPTY_SECRETS secret(s) are empty in .env\"",
176
+ " else",
177
+ " pass \"All secrets are populated\"",
178
+ " fi",
179
+ " else",
180
+ " warn_svc \".env file not found\"",
181
+ " fi",
182
+ "",
183
+ " # Disk space",
184
+ " AVAIL_KB=$(df -k . 2>/dev/null | awk 'NR==2{print $4}' || echo \"0\")",
185
+ " AVAIL_GB=$((AVAIL_KB / 1024 / 1024))",
186
+ " if [ \"$AVAIL_GB\" -lt 2 ]; then",
187
+ " warn_svc \"Low disk space: ${AVAIL_GB}GB available\"",
188
+ " else",
189
+ " pass \"Disk space: ${AVAIL_GB}GB available\"",
190
+ " fi",
191
+ "}",
192
+ "",
193
+ "# ═══════════════════════════════════════════════════════════════════════",
194
+ "# Phase 2: Container Status",
195
+ "# ═══════════════════════════════════════════════════════════════════════",
196
+ "check_container() {",
197
+ " local id=\"$1\" name=\"$2\" icon=\"$3\"",
198
+ "",
199
+ " local status restarts",
200
+ " status=$(docker compose ps \"$id\" --format json 2>/dev/null | head -1)",
201
+ "",
202
+ " if [ -z \"$status\" ]; then",
203
+ " fail \"$icon $(pad_name \"$name\") — Container not found\"",
204
+ " return",
205
+ " fi",
206
+ "",
207
+ " local state health_status",
208
+ " state=$(echo \"$status\" | grep -o '\"State\":\"[^\"]*\"' | cut -d'\"' -f4 || echo \"unknown\")",
209
+ " health_status=$(echo \"$status\" | grep -o '\"Health\":\"[^\"]*\"' | cut -d'\"' -f4 || echo \"none\")",
210
+ "",
211
+ " restarts=$(docker inspect --format=\"{{.RestartCount}}\" \"$(docker compose ps -q \"$id\" 2>/dev/null | head -1)\" 2>/dev/null || echo \"0\")",
212
+ "",
213
+ " if [ \"$state\" = \"running\" ]; then",
214
+ " if [ \"$health_status\" = \"healthy\" ]; then",
215
+ " pass \"$icon $(pad_name \"$name\") Running (healthy) restarts: $restarts\"",
216
+ " elif [ \"$health_status\" = \"starting\" ]; then",
217
+ " warn_svc \"$icon $(pad_name \"$name\") Running (starting) restarts: $restarts\"",
218
+ " elif [ \"$health_status\" = \"unhealthy\" ]; then",
219
+ " fail \"$icon $(pad_name \"$name\") Running (UNHEALTHY) restarts: $restarts\"",
220
+ " else",
221
+ " pass \"$icon $(pad_name \"$name\") Running restarts: $restarts\"",
222
+ " fi",
223
+ " if [ \"$restarts\" -gt 5 ] 2>/dev/null; then",
224
+ " warn_svc \" └─ $name has restarted $restarts times (possible crash loop)\"",
225
+ " fi",
226
+ " elif [ \"$state\" = \"exited\" ]; then",
227
+ " local exit_code",
228
+ " exit_code=$(docker inspect --format=\"{{.State.ExitCode}}\" \"$(docker compose ps -q \"$id\" 2>/dev/null | head -1)\" 2>/dev/null || echo \"?\")",
229
+ " fail \"$icon $(pad_name \"$name\") Exited (code $exit_code)\"",
230
+ " if [ \"$exit_code\" = \"137\" ]; then",
231
+ " ERRORS+=(\" └─ $name: OOM killed (exit 137). Increase memory limits.\")",
232
+ " elif [ \"$exit_code\" = \"1\" ]; then",
233
+ " ERRORS+=(\" └─ $name: Application error (exit 1). Check logs: docker compose logs $id\")",
234
+ " fi",
235
+ " else",
236
+ " fail \"$icon $(pad_name \"$name\") State: $state\"",
237
+ " fi",
238
+ "}",
239
+ "",
240
+ "# ═══════════════════════════════════════════════════════════════════════",
241
+ "# Phase 3: Port Reachability",
242
+ "# ═══════════════════════════════════════════════════════════════════════",
243
+ "check_port() {",
244
+ " local id=\"$1\" port=\"$2\" desc=\"$3\"",
245
+ " if command -v nc &>/dev/null; then",
246
+ " if nc -z -w 2 localhost \"$port\" 2>/dev/null; then",
247
+ " dim \" Port $port ($desc) — reachable\"",
248
+ " else",
249
+ " warn_svc \" └─ $id: Port $port ($desc) — NOT reachable\"",
250
+ " fi",
251
+ " elif command -v curl &>/dev/null; then",
252
+ " if curl -sf --max-time 2 \"http://localhost:$port/\" &>/dev/null; then",
253
+ " dim \" Port $port ($desc) — reachable\"",
254
+ " else",
255
+ " warn_svc \" └─ $id: Port $port ($desc) — NOT reachable\"",
256
+ " fi",
257
+ " else",
258
+ " dim \" Port $port ($desc) — skipped (no nc or curl)\"",
259
+ " fi",
260
+ "}",
261
+ "",
262
+ "# ═══════════════════════════════════════════════════════════════════════",
263
+ "# Phase 4: Health Check Commands",
264
+ "# ═══════════════════════════════════════════════════════════════════════",
265
+ "check_health_cmd() {",
266
+ " local id=\"$1\" cmd=\"$2\"",
267
+ " local cid=$(docker compose ps -q \"$id\" 2>/dev/null | head -1)",
268
+ " [ -z \"$cid\" ] && return",
269
+ " if docker exec \"$cid\" sh -c \"$cmd\" &>/dev/null; then",
270
+ " dim \" Health command passed\"",
271
+ " else",
272
+ " warn_svc \" └─ $id: Health check command failed\"",
273
+ " if [ \"$VERBOSE\" = true ]; then dim \" Command: $cmd\"; fi",
274
+ " fi",
275
+ "}",
276
+ "",
277
+ "# ═══════════════════════════════════════════════════════════════════════",
278
+ "# Phase 5: Resource Usage",
279
+ "# ═══════════════════════════════════════════════════════════════════════",
280
+ "phase_resources() {",
281
+ " if [ \"$JSON_MODE\" = false ]; then",
282
+ " echo \"\"",
283
+ " echo -e \"${BOLD}── Phase 5: Resource Usage ─────────────────────────────────${NC}\"",
284
+ " echo \"\"",
285
+ " fi",
286
+ " docker stats --no-stream --format \"table {{.Name}}\\t{{.CPUPerc}}\\t{{.MemUsage}}\\t{{.MemPerc}}\" 2>/dev/null || true",
287
+ " if [ \"$VERBOSE\" = false ] && [ \"$JSON_MODE\" = false ]; then",
288
+ " info \"Use --verbose to see warnings for high memory usage\"",
289
+ " fi",
290
+ "}",
291
+ "",
292
+ "# ═══════════════════════════════════════════════════════════════════════",
293
+ "# Phase 6: Log Scan",
294
+ "# ═══════════════════════════════════════════════════════════════════════",
295
+ "phase_logs() {",
296
+ " if [ \"$QUICK_MODE\" = true ]; then",
297
+ " if [ \"$JSON_MODE\" = false ]; then echo \"\"; info \"Skipping log scan (--quick mode)\"; fi",
298
+ " return",
299
+ " fi",
300
+ " if [ \"$JSON_MODE\" = false ]; then",
301
+ " echo \"\"",
302
+ " echo -e \"${BOLD}── Phase 6: Log Scan ───────────────────────────────────────${NC}\"",
303
+ " echo \"\"",
304
+ " fi",
305
+ " local has_errors=false",
306
+ L(" for svc_id in ", svcIdList, "; do"),
307
+ " local error_lines",
308
+ " error_lines=$(docker compose logs --tail=50 \"$svc_id\" 2>/dev/null | grep -iE \"(error|fatal|panic|exception|segfault|killed|oom)\" | tail -5 || true)",
309
+ " if [ -n \"$error_lines\" ]; then",
310
+ " has_errors=true",
311
+ " warn_svc \"$svc_id — found error patterns in logs:\"",
312
+ " echo \"$error_lines\" | while IFS= read -r eline; do",
313
+ " dim \" | $eline\"",
314
+ " done",
315
+ " fi",
316
+ " done",
317
+ " if [ \"$has_errors\" = false ] && [ \"$JSON_MODE\" = false ]; then",
318
+ " pass \"No error patterns found in recent logs\"",
319
+ " fi",
320
+ "}",
321
+ "",
322
+ "# ── Bare-metal process check ────────────────────────────────────────",
323
+ "check_process() {",
324
+ " local id=\"$1\" name=\"$2\" icon=\"$3\"",
325
+ " if command -v systemctl &>/dev/null; then",
326
+ " if systemctl is-active --quiet \"$id\" 2>/dev/null; then",
327
+ " pass \"$icon $(pad_name \"$name\") Active (systemd)\"",
328
+ " return",
329
+ " fi",
330
+ " fi",
331
+ " if pgrep -f \"$id\" &>/dev/null; then",
332
+ " pass \"$icon $(pad_name \"$name\") Running (process)\"",
333
+ " else",
334
+ " fail \"$icon $(pad_name \"$name\") NOT running\"",
335
+ " fi",
336
+ "}",
337
+ "",
338
+ "# ═══════════════════════════════════════════════════════════════════════",
339
+ "# Main",
340
+ "# ═══════════════════════════════════════════════════════════════════════",
341
+ "",
342
+ "DOCKER_AVAILABLE=false",
343
+ "",
344
+ "if [ \"$JSON_MODE\" = false ]; then",
345
+ " echo \"\"",
346
+ " echo \"═══════════════════════════════════════════════════════════════════════\"",
347
+ L(" echo \" 🐾 OpenClaw Stack Health Report — ", name, "\""),
348
+ " echo \"═══════════════════════════════════════════════════════════════════════\"",
349
+ "fi",
350
+ "",
351
+ "phase_environment",
352
+ "",
353
+ "if [ \"$DOCKER_AVAILABLE\" = true ]; then",
354
+ " if [ \"$JSON_MODE\" = false ]; then",
355
+ " echo \"\"",
356
+ " echo -e \"${BOLD}── Phase 2–4: Service Checks ──────────────────────────────${NC}\"",
357
+ " echo \"\"",
358
+ " fi",
359
+ "",
360
+ ...dockerChecks,
361
+ "",
362
+ " phase_resources",
363
+ " phase_logs",
364
+ "",
365
+ "else",
366
+ " if [ \"$JSON_MODE\" = false ]; then",
367
+ " echo \"\"",
368
+ " echo -e \"${BOLD}── Bare-Metal Service Checks ──────────────────────────────${NC}\"",
369
+ " echo \"\"",
370
+ " fi",
371
+ "",
372
+ ...bmChecks,
373
+ "fi",
374
+ "",
375
+ "# ── Summary ──────────────────────────────────────────────────────────",
376
+ "echo \"\"",
377
+ "echo \"═══════════════════════════════════════════════════════════════════════\"",
378
+ "echo \"\"",
379
+ "echo -e \" ${BOLD}Summary${NC}: $TOTAL services checked\"",
380
+ "echo -e \" ${GREEN}Healthy: $HEALTHY${NC} | ${YELLOW}Warnings: $WARNING${NC} | ${RED}Failed: $FAILED${NC}\"",
381
+ "",
382
+ "if [ ${#ERRORS[@]} -gt 0 ]; then",
383
+ " echo \"\"",
384
+ " echo -e \" ${BOLD}${RED}── Errors ─────────────────────────────────────────────────${NC}\"",
385
+ " for e in \"${ERRORS[@]}\"; do",
386
+ " echo -e \" ${RED}$e${NC}\"",
387
+ " done",
388
+ "fi",
389
+ "",
390
+ "echo \"\"",
391
+ "echo \"═══════════════════════════════════════════════════════════════════════\"",
392
+ "echo \"\"",
393
+ "",
394
+ "if [ \"$FAILED\" -gt 0 ]; then",
395
+ " echo -e \" ${RED}Some services need attention. Run with --verbose for details.${NC}\"",
396
+ " exit 1",
397
+ "else",
398
+ " echo -e \" ${GREEN}🎉 All services are running!${NC}\"",
399
+ " exit 0",
400
+ "fi"
401
+ ].join("\n") + "\n";
402
+ }
403
+ function generatePowerShellScript(resolved, options) {
404
+ const checks = extractServiceChecks(resolved);
405
+ const name = options.projectName;
406
+ const total = checks.length;
407
+ const dockerChecks = [];
408
+ for (const svc of checks) {
409
+ dockerChecks.push(L(" # ", svc.icon, " ", svc.name));
410
+ dockerChecks.push(L(" Test-Container -ServiceId \"", svc.id, "\" -ServiceName \"", svc.name, "\" -Icon \"", svc.icon, "\""));
411
+ for (const p of svc.ports.filter((pp) => pp.exposed)) dockerChecks.push(L(" Test-Port -ServiceId \"", svc.id, "\" -Port ", String(p.host), " -Description \"", p.description, "\""));
412
+ dockerChecks.push("");
413
+ }
414
+ const bmChecks = [];
415
+ for (const svc of checks) {
416
+ bmChecks.push(L(" # ", svc.icon, " ", svc.name));
417
+ bmChecks.push(L(" Test-ProcessRunning -ServiceId \"", svc.id, "\" -ServiceName \"", svc.name, "\" -Icon \"", svc.icon, "\""));
418
+ for (const p of svc.ports.filter((pp) => pp.exposed)) bmChecks.push(L(" Test-Port -ServiceId \"", svc.id, "\" -Port ", String(p.host), " -Description \"", p.description, "\""));
419
+ bmChecks.push("");
420
+ }
421
+ const svcIdList = checks.map((s) => "\"" + s.id + "\"").join(", ");
422
+ return [
423
+ "#Requires -Version 5.1",
424
+ "<#",
425
+ ".SYNOPSIS",
426
+ L(" OpenClaw Stack Health Check — ", name),
427
+ "",
428
+ ".DESCRIPTION",
429
+ " Auto-generated verification script for your stack.",
430
+ L(" Checks ", String(total), " services: container status, port reachability, and log errors."),
431
+ "",
432
+ ".PARAMETER Quick",
433
+ " Skip log scanning for faster results.",
434
+ "",
435
+ ".PARAMETER Json",
436
+ " Output results as JSON.",
437
+ "",
438
+ ".EXAMPLE",
439
+ " .\\scripts\\health-check.ps1",
440
+ " .\\scripts\\health-check.ps1 -Quick",
441
+ " .\\scripts\\health-check.ps1 -Json",
442
+ "#>",
443
+ "param(",
444
+ " [switch]$Quick,",
445
+ " [switch]$Json,",
446
+ " [switch]$Detailed",
447
+ ")",
448
+ "",
449
+ "$ErrorActionPreference = \"Continue\"",
450
+ "",
451
+ "# ── Counters ─────────────────────────────────────────────────────────",
452
+ L("$script:Total = ", String(total)),
453
+ "$script:Healthy = 0",
454
+ "$script:Warnings = 0",
455
+ "$script:Failed = 0",
456
+ "$script:Errors = @()",
457
+ "$script:ServiceResults = @()",
458
+ "$script:DockerAvailable = $false",
459
+ "",
460
+ "# ── Helpers ──────────────────────────────────────────────────────────",
461
+ "",
462
+ "function Write-Pass($msg) { Write-Host \" ✅ $msg\" -ForegroundColor Green; $script:Healthy++ }",
463
+ "function Write-Warn($msg) { Write-Host \" ⚠️ $msg\" -ForegroundColor Yellow; $script:Warnings++ }",
464
+ "function Write-Fail($msg) { Write-Host \" ❌ $msg\" -ForegroundColor Red; $script:Failed++; $script:Errors += $msg }",
465
+ "function Write-Info($msg) { Write-Host \" ℹ $msg\" -ForegroundColor Cyan }",
466
+ "function Write-Dim($msg) { Write-Host \" $msg\" -ForegroundColor DarkGray }",
467
+ "",
468
+ "function Pad-Name([string]$n) { return $n.PadRight(25) }",
469
+ "",
470
+ "# ═════════════════════════════════════════════════════════════════════",
471
+ "# Phase 1: Environment",
472
+ "# ═════════════════════════════════════════════════════════════════════",
473
+ "function Test-Environment {",
474
+ " if (-not $Json) {",
475
+ " Write-Host \"\"",
476
+ " Write-Host \"── Phase 1: Environment ────────────────────────────────────\" -ForegroundColor White",
477
+ " Write-Host \"\"",
478
+ " }",
479
+ " try {",
480
+ " $null = docker info 2>&1",
481
+ " if ($LASTEXITCODE -eq 0) {",
482
+ " Write-Pass \"Docker daemon is running\"",
483
+ " $script:DockerAvailable = $true",
484
+ " } else {",
485
+ " Write-Fail \"Docker daemon is NOT running\"",
486
+ " }",
487
+ " } catch {",
488
+ " Write-Warn \"Docker is not installed — running bare-metal checks only\"",
489
+ " }",
490
+ " if ($script:DockerAvailable) {",
491
+ " try {",
492
+ " $null = docker compose version 2>&1",
493
+ " if ($LASTEXITCODE -eq 0) {",
494
+ " Write-Pass \"Docker Compose v2 available\"",
495
+ " } else {",
496
+ " Write-Warn \"Docker Compose v2 not found\"",
497
+ " }",
498
+ " } catch {",
499
+ " Write-Warn \"Docker Compose not available\"",
500
+ " }",
501
+ " }",
502
+ " $envPath = Join-Path $PSScriptRoot \"..\\.env\"",
503
+ " if (Test-Path $envPath) {",
504
+ " Write-Pass \".env file exists\"",
505
+ " $emptySecrets = 0",
506
+ " Get-Content $envPath | ForEach-Object {",
507
+ " if ($_ -match \"(PASSWORD|TOKEN|SECRET|SESSION_KEY|COOKIE|API_KEY).*=\\s*$\") { $emptySecrets++ }",
508
+ " }",
509
+ " if ($emptySecrets -gt 0) {",
510
+ " Write-Warn \"$emptySecrets secret(s) are empty in .env\"",
511
+ " } else {",
512
+ " Write-Pass \"All secrets are populated\"",
513
+ " }",
514
+ " } else {",
515
+ " Write-Warn \".env file not found\"",
516
+ " }",
517
+ " $drive = (Get-Item $PSScriptRoot).PSDrive",
518
+ " $freeGB = [math]::Round($drive.Free / 1GB, 1)",
519
+ " if ($freeGB -lt 2) {",
520
+ " Write-Warn \"Low disk space: ${freeGB}GB available\"",
521
+ " } else {",
522
+ " Write-Pass \"Disk space: ${freeGB}GB available\"",
523
+ " }",
524
+ "}",
525
+ "",
526
+ "# ═════════════════════════════════════════════════════════════════════",
527
+ "# Phase 2: Container Check",
528
+ "# ═════════════════════════════════════════════════════════════════════",
529
+ "function Test-Container {",
530
+ " param([string]$ServiceId, [string]$ServiceName, [string]$Icon)",
531
+ " try {",
532
+ " $status = docker compose ps $ServiceId --format json 2>&1 | ConvertFrom-Json",
533
+ " } catch {",
534
+ " Write-Fail \"$Icon $(Pad-Name $ServiceName) Container not found\"",
535
+ " return",
536
+ " }",
537
+ " if (-not $status) {",
538
+ " Write-Fail \"$Icon $(Pad-Name $ServiceName) Container not found\"",
539
+ " return",
540
+ " }",
541
+ " $state = $status.State",
542
+ " $health = $status.Health",
543
+ " if ($state -eq \"running\") {",
544
+ " if ($health -eq \"healthy\") {",
545
+ " Write-Pass \"$Icon $(Pad-Name $ServiceName) Running (healthy)\"",
546
+ " } elseif ($health -eq \"starting\") {",
547
+ " Write-Warn \"$Icon $(Pad-Name $ServiceName) Running (starting)\"",
548
+ " } elseif ($health -eq \"unhealthy\") {",
549
+ " Write-Fail \"$Icon $(Pad-Name $ServiceName) Running (UNHEALTHY)\"",
550
+ " } else {",
551
+ " Write-Pass \"$Icon $(Pad-Name $ServiceName) Running\"",
552
+ " }",
553
+ " } elseif ($state -eq \"exited\") {",
554
+ " Write-Fail \"$Icon $(Pad-Name $ServiceName) Exited\"",
555
+ " } else {",
556
+ " Write-Fail \"$Icon $(Pad-Name $ServiceName) State: $state\"",
557
+ " }",
558
+ "}",
559
+ "",
560
+ "# ═════════════════════════════════════════════════════════════════════",
561
+ "# Phase 3: Port Check",
562
+ "# ═════════════════════════════════════════════════════════════════════",
563
+ "function Test-Port {",
564
+ " param([string]$ServiceId, [int]$Port, [string]$Description)",
565
+ " try {",
566
+ " $tcp = New-Object System.Net.Sockets.TcpClient",
567
+ " $tcp.ConnectAsync(\"localhost\", $Port).Wait(2000) | Out-Null",
568
+ " if ($tcp.Connected) {",
569
+ " Write-Dim \" Port $Port ($Description) — reachable\"",
570
+ " $tcp.Close()",
571
+ " } else {",
572
+ " Write-Warn \" └─ ${ServiceId}: Port $Port ($Description) — NOT reachable\"",
573
+ " }",
574
+ " } catch {",
575
+ " Write-Warn \" └─ ${ServiceId}: Port $Port ($Description) — NOT reachable\"",
576
+ " }",
577
+ "}",
578
+ "",
579
+ "# ── Bare-metal process check ────────────────────────────────────────",
580
+ "function Test-ProcessRunning {",
581
+ " param([string]$ServiceId, [string]$ServiceName, [string]$Icon)",
582
+ " $svc = Get-Service -Name $ServiceId -ErrorAction SilentlyContinue",
583
+ " if ($svc) {",
584
+ " if ($svc.Status -eq \"Running\") {",
585
+ " Write-Pass \"$Icon $(Pad-Name $ServiceName) Active (service)\"",
586
+ " } else {",
587
+ " Write-Fail \"$Icon $(Pad-Name $ServiceName) $($svc.Status)\"",
588
+ " }",
589
+ " return",
590
+ " }",
591
+ " $proc = Get-Process -Name $ServiceId -ErrorAction SilentlyContinue",
592
+ " if ($proc) {",
593
+ " Write-Pass \"$Icon $(Pad-Name $ServiceName) Running (process)\"",
594
+ " } else {",
595
+ " Write-Fail \"$Icon $(Pad-Name $ServiceName) NOT running\"",
596
+ " }",
597
+ "}",
598
+ "",
599
+ "# ═════════════════════════════════════════════════════════════════════",
600
+ "# Phase 6: Log Scan",
601
+ "# ═════════════════════════════════════════════════════════════════════",
602
+ "function Test-Logs {",
603
+ " if ($Quick) {",
604
+ " if (-not $Json) { Write-Info \"Skipping log scan (-Quick mode)\" }",
605
+ " return",
606
+ " }",
607
+ " if (-not $Json) {",
608
+ " Write-Host \"\"",
609
+ " Write-Host \"── Phase 6: Log Scan ───────────────────────────────────────\" -ForegroundColor White",
610
+ " Write-Host \"\"",
611
+ " }",
612
+ " $hasErrors = $false",
613
+ L(" $svcIds = @(", svcIdList, ")"),
614
+ " foreach ($svcId in $svcIds) {",
615
+ " try {",
616
+ " $logs = docker compose logs --tail=50 $svcId 2>&1 | Out-String",
617
+ " $errorLines = $logs -split \"`n\" | Where-Object { $_ -match \"(error|fatal|panic|exception|segfault|killed|oom)\" } | Select-Object -Last 5",
618
+ " if ($errorLines) {",
619
+ " $hasErrors = $true",
620
+ " Write-Warn \"$svcId — found error patterns in logs:\"",
621
+ " $errorLines | ForEach-Object { Write-Dim \" | $_\" }",
622
+ " }",
623
+ " } catch {}",
624
+ " }",
625
+ " if (-not $hasErrors -and -not $Json) {",
626
+ " Write-Pass \"No error patterns found in recent logs\"",
627
+ " }",
628
+ "}",
629
+ "",
630
+ "# ═════════════════════════════════════════════════════════════════════",
631
+ "# Main",
632
+ "# ═════════════════════════════════════════════════════════════════════",
633
+ "",
634
+ "if (-not $Json) {",
635
+ " Write-Host \"\"",
636
+ " Write-Host \"═══════════════════════════════════════════════════════════════════════\" ",
637
+ L(" Write-Host \" 🐾 OpenClaw Stack Health Report — ", name, "\""),
638
+ " Write-Host \"═══════════════════════════════════════════════════════════════════════\"",
639
+ "}",
640
+ "",
641
+ "Test-Environment",
642
+ "",
643
+ "if ($script:DockerAvailable) {",
644
+ " if (-not $Json) {",
645
+ " Write-Host \"\"",
646
+ " Write-Host \"── Phase 2–4: Service Checks ──────────────────────────────\" -ForegroundColor White",
647
+ " Write-Host \"\"",
648
+ " }",
649
+ "",
650
+ ...dockerChecks,
651
+ " Test-Logs",
652
+ "} else {",
653
+ " if (-not $Json) {",
654
+ " Write-Host \"\"",
655
+ " Write-Host \"── Bare-Metal Service Checks ──────────────────────────────\" -ForegroundColor White",
656
+ " Write-Host \"\"",
657
+ " }",
658
+ "",
659
+ ...bmChecks,
660
+ "}",
661
+ "",
662
+ "# ── Summary ──────────────────────────────────────────────────────────",
663
+ "if ($Json) {",
664
+ " $result = @{",
665
+ L(" project = \"", name, "\""),
666
+ " timestamp = (Get-Date -Format \"o\")",
667
+ " summary = @{",
668
+ " total = $script:Total",
669
+ " healthy = $script:Healthy",
670
+ " warnings = $script:Warnings",
671
+ " failed = $script:Failed",
672
+ " }",
673
+ " errors = $script:Errors",
674
+ " }",
675
+ " $result | ConvertTo-Json -Depth 5",
676
+ "} else {",
677
+ " Write-Host \"\"",
678
+ " Write-Host \"═══════════════════════════════════════════════════════════════════════\"",
679
+ " Write-Host \"\"",
680
+ " Write-Host \" Summary: $($script:Total) services checked\"",
681
+ " Write-Host \" Healthy: $($script:Healthy)\" -ForegroundColor Green -NoNewline",
682
+ " Write-Host \" | Warnings: $($script:Warnings)\" -ForegroundColor Yellow -NoNewline",
683
+ " Write-Host \" | Failed: $($script:Failed)\" -ForegroundColor Red",
684
+ " if ($script:Errors.Count -gt 0) {",
685
+ " Write-Host \"\"",
686
+ " Write-Host \" ── Errors ──────────────────────────────────────────────────\" -ForegroundColor Red",
687
+ " $script:Errors | ForEach-Object { Write-Host \" $_\" -ForegroundColor Red }",
688
+ " }",
689
+ " Write-Host \"\"",
690
+ " Write-Host \"═══════════════════════════════════════════════════════════════════════\"",
691
+ " Write-Host \"\"",
692
+ " if ($script:Failed -gt 0) {",
693
+ " Write-Host \" Some services need attention. Run with -Detailed for more.\" -ForegroundColor Red",
694
+ " exit 1",
695
+ " } else {",
696
+ " Write-Host \" 🎉 All services are running!\" -ForegroundColor Green",
697
+ " exit 0",
698
+ " }",
699
+ "}"
700
+ ].join("\n") + "\n";
701
+ }
702
+
703
+ //#endregion
704
+ export { generateHealthCheck };
705
+ //# sourceMappingURL=health-check.mjs.map