@better-openclaw/core 1.0.11 → 1.0.13

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 (78) hide show
  1. package/README.md +70 -0
  2. package/dist/bare-metal-partition.test.mjs +1 -1
  3. package/dist/composer.d.mts.map +1 -1
  4. package/dist/composer.mjs +5 -3
  5. package/dist/composer.mjs.map +1 -1
  6. package/dist/composer.snapshot.test.mjs +1 -1
  7. package/dist/composer.test.mjs +1 -1
  8. package/dist/generate.test.mjs +1 -1
  9. package/dist/generators/bare-metal-install.test.mjs +1 -1
  10. package/dist/generators/caddy.test.mjs +1 -1
  11. package/dist/generators/env.test.mjs +1 -1
  12. package/dist/generators/health-check.mjs.map +1 -1
  13. package/dist/generators/health-check.test.mjs +1 -1
  14. package/dist/generators/scripts.test.mjs +1 -1
  15. package/dist/generators/traefik.test.mjs +1 -1
  16. package/dist/{magic-string.es-1uTtupLe.mjs → magic-string.es-CfFonO_S.mjs} +1 -1
  17. package/dist/{magic-string.es-1uTtupLe.mjs.map → magic-string.es-CfFonO_S.mjs.map} +1 -1
  18. package/dist/manifest-BjK4ALvr.mjs +1158 -0
  19. package/dist/manifest-BjK4ALvr.mjs.map +1 -0
  20. package/dist/migrations.d.mts.map +1 -1
  21. package/dist/migrations.mjs.map +1 -1
  22. package/dist/migrations.test.mjs +1 -1
  23. package/dist/presets/registry.test.mjs +1 -1
  24. package/dist/resolver.test.mjs +1 -1
  25. package/dist/schema.d.mts +4 -0
  26. package/dist/schema.d.mts.map +1 -1
  27. package/dist/schema.mjs +2 -1
  28. package/dist/schema.mjs.map +1 -1
  29. package/dist/schema.test.mjs +1 -1
  30. package/dist/services/definitions/hexstrike.d.mts +7 -0
  31. package/dist/services/definitions/hexstrike.d.mts.map +1 -0
  32. package/dist/services/definitions/hexstrike.mjs +44 -0
  33. package/dist/services/definitions/hexstrike.mjs.map +1 -0
  34. package/dist/services/definitions/index.d.mts +6 -1
  35. package/dist/services/definitions/index.d.mts.map +1 -1
  36. package/dist/services/definitions/index.mjs +12 -2
  37. package/dist/services/definitions/index.mjs.map +1 -1
  38. package/dist/services/definitions/pentagi.d.mts +7 -0
  39. package/dist/services/definitions/pentagi.d.mts.map +1 -0
  40. package/dist/services/definitions/pentagi.mjs +44 -0
  41. package/dist/services/definitions/pentagi.mjs.map +1 -0
  42. package/dist/services/definitions/pentestagent.d.mts +7 -0
  43. package/dist/services/definitions/pentestagent.d.mts.map +1 -0
  44. package/dist/services/definitions/pentestagent.mjs +39 -0
  45. package/dist/services/definitions/pentestagent.mjs.map +1 -0
  46. package/dist/services/definitions/scrapling.d.mts +7 -0
  47. package/dist/services/definitions/scrapling.d.mts.map +1 -0
  48. package/dist/services/definitions/scrapling.mjs +44 -0
  49. package/dist/services/definitions/scrapling.mjs.map +1 -0
  50. package/dist/services/definitions/solidityguard.d.mts +7 -0
  51. package/dist/services/definitions/solidityguard.d.mts.map +1 -0
  52. package/dist/services/definitions/solidityguard.mjs +49 -0
  53. package/dist/services/definitions/solidityguard.mjs.map +1 -0
  54. package/dist/services/registry.test.mjs +1 -1
  55. package/dist/skills/skill-manifest.d.mts.map +1 -1
  56. package/dist/skills/skill-manifest.mjs +5 -13
  57. package/dist/skills/skill-manifest.mjs.map +1 -1
  58. package/dist/types.mjs +5 -0
  59. package/dist/types.mjs.map +1 -1
  60. package/dist/validator.test.mjs +1 -1
  61. package/dist/version-manager.test.mjs +1 -1
  62. package/dist/{vi.2VT5v0um-YSByewHe.mjs → vi.2VT5v0um-BgmKutxR.mjs} +2 -2
  63. package/dist/{vi.2VT5v0um-YSByewHe.mjs.map → vi.2VT5v0um-BgmKutxR.mjs.map} +1 -1
  64. package/package.json +1 -1
  65. package/src/composer.ts +12 -3
  66. package/src/generators/health-check.ts +46 -8
  67. package/src/migrations.ts +0 -1
  68. package/src/schema.ts +1 -0
  69. package/src/services/definitions/hexstrike.ts +40 -0
  70. package/src/services/definitions/index.ts +15 -0
  71. package/src/services/definitions/pentagi.ts +40 -0
  72. package/src/services/definitions/pentestagent.ts +33 -0
  73. package/src/services/definitions/scrapling.ts +40 -0
  74. package/src/services/definitions/solidityguard.ts +46 -0
  75. package/src/skills/manifest.json +1391 -0
  76. package/src/skills/manifest.schema.json +51 -0
  77. package/src/skills/skill-manifest.ts +27 -22
  78. package/src/types.ts +1 -0
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # @better-openclaw/core
2
+
3
+ The core engine responsible for parsing configurations, resolving dependencies, formatting outputs, and generating production-ready OpenClaw Docker Compose stacks.
4
+
5
+ ## Features
6
+
7
+ - **Service Registry:** A unified, expandable catalog of pre-configured Docker services (e.g., Traefik, PostgreSQL, Qdrant, Ollama, N8N, SearXNG, Scrapling, etc.) categorized by function (databases, models, scrapers, tools).
8
+ - **Dependency Resolution Engine:** Automatically detects and resolves required services. If you select a Postgres-dependent service, Postgres is automatically injected into the generation plan.
9
+ - **Skill Injection (`SKILL.md`):** Deep integration with AI agent workflows. Packages specialized `SKILL.md` instructions into volume mounts for AI tools like the `browser` integration or `tinyfish`.
10
+ - **Intelligent Networking & Proxies:** Fully integrated reverse proxy generation (Caddy and Traefik) and auto-SSL domain generation.
11
+ - **Cross-Platform & Heterogeneous Topologies:** Supports generating stacks for `local` (Docker Desktop), `vps` (cloud), and `homelab` deployments. It explicitly supports a hybrid native-docker model via `deploymentType: "bare-metal"`.
12
+ - **GPU Passthrough Support:** Automatically injects NVIDIA or AMD runtime flags to AI services if the `gpuRequired` flag is detected on the requested service and enabled by the user.
13
+
14
+ ## Programmatic API
15
+
16
+ You can use the generation engine programmatically within any Node.js or TypeScript application:
17
+
18
+ ```typescript
19
+ import { generate, type GenerationInput } from "@better-openclaw/core";
20
+
21
+ const input: GenerationInput = {
22
+ projectName: "my-openclaw-stack",
23
+ services: ["postgres-database", "ollama-local-llm", "n8n-workflow"],
24
+ skillPacks: ["ollama-local-llm", "n8n-workflows"],
25
+ proxy: "caddy",
26
+ domain: "my-ai.example.com",
27
+ gpu: true,
28
+ platform: "linux/amd64",
29
+ deployment: "vps",
30
+ deploymentType: "docker", // or "bare-metal"
31
+ generateSecrets: true,
32
+ openclawVersion: "latest",
33
+ monitoring: true,
34
+ };
35
+
36
+ // Generates the Compose YAML, configs, skills, and .env securely.
37
+ const result = generate(input);
38
+
39
+ console.log(result.files["docker-compose.yaml"]);
40
+ console.log(result.metadata.estimatedMemoryMB);
41
+ ```
42
+
43
+ ## Service Definition Format
44
+
45
+ The Core reads from `src/services/definitions/`. New services should expose a standardized `ServiceDefinition`:
46
+
47
+ ```typescript
48
+ export const myCoolService: ServiceDefinition = {
49
+ id: "my-cool-service",
50
+ name: "Cool AI Service",
51
+ description: "Provides an API for cool operations.",
52
+ category: "tools",
53
+ image: "cool/service:latest",
54
+ ports: [{ port: 8080, public: true }],
55
+ environment: { API_KEY: "${SECRET_KEY}" },
56
+ dependsOn: ["postgres-database"],
57
+ };
58
+ ```
59
+
60
+ ## Adding Skills
61
+
62
+ Skills are markdown instructions or code bundles mapped to specific tools. They are defined in `skills/manifest.json`. During generation, if a `SkillPack` is explicitly selected or implicitly included via an auto-installing service, the Core locates the corresponding files and mounts them into the generated stack's Volume pathways.
63
+
64
+ ## Development
65
+
66
+ ```bash
67
+ pnpm build # Compiles TypeScript via tsdown
68
+ pnpm test # Executes integration tests verifying generating valid stacks
69
+ pnpm lint # Executes Biome linting rules
70
+ ```
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "./vi.2VT5v0um-YSByewHe.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "./vi.2VT5v0um-BgmKutxR.mjs";
2
2
  import { partitionBareMetal, platformToNativePlatform, resolvedWithOnlyServices } from "./bare-metal-partition.mjs";
3
3
  import { resolve } from "./resolver.mjs";
4
4
 
@@ -1 +1 @@
1
- {"version":3,"file":"composer.d.mts","names":[],"sources":["../src/composer.ts"],"mappings":";;;UAKiB,aAAA;EAChB,KAAA,EAAO,MAAA;EACP,QAAA;EACA,QAAA;AAAA;;;;;iBAmQe,OAAA,CAAQ,QAAA,EAAU,cAAA,EAAgB,OAAA,EAAS,cAAA;;;AAA3D;;iBAkDgB,gBAAA,CAAiB,QAAA,EAAU,cAAA,EAAgB,OAAA,EAAS,cAAA,GAAiB,aAAA"}
1
+ {"version":3,"file":"composer.d.mts","names":[],"sources":["../src/composer.ts"],"mappings":";;;UAKiB,aAAA;EAChB,KAAA,EAAO,MAAA;EACP,QAAA;EACA,QAAA;AAAA;;;;;iBA4Qe,OAAA,CAAQ,QAAA,EAAU,cAAA,EAAgB,OAAA,EAAS,cAAA;;;AAA3D;;iBAkDgB,gBAAA,CAAiB,QAAA,EAAU,cAAA,EAAgB,OAAA,EAAS,cAAA,GAAiB,aAAA"}
package/dist/composer.mjs CHANGED
@@ -70,7 +70,7 @@ function buildGatewayServices(resolved, options, dependsOn) {
70
70
  for (const { definition: def } of resolved.services) {
71
71
  for (const env of def.openclawEnvVars) gatewayEnv[env.key] = env.secret ? `\${${env.key}}` : env.defaultValue;
72
72
  if (def.openclawVolumeMounts) for (const vol of def.openclawVolumeMounts) {
73
- allVolumes.add(vol.name);
73
+ if (!(vol.name.startsWith("./") || vol.name.startsWith("/") || vol.name.startsWith("~"))) allVolumes.add(vol.name);
74
74
  gatewayVolumes.push(`${vol.name}:${vol.containerPath}`);
75
75
  }
76
76
  }
@@ -136,8 +136,10 @@ function buildCompanionService(def, resolved, options, allVolumes) {
136
136
  });
137
137
  }
138
138
  if (def.volumes.length > 0) svc.volumes = def.volumes.map((v) => {
139
- allVolumes.add(v.name);
140
- volumeNames.push(v.name);
139
+ if (!(v.name.startsWith("./") || v.name.startsWith("/") || v.name.startsWith("~"))) {
140
+ allVolumes.add(v.name);
141
+ volumeNames.push(v.name);
142
+ }
141
143
  return `${v.name}:${v.containerPath}`;
142
144
  });
143
145
  if (def.id === "postgresql") {
@@ -1 +1 @@
1
- {"version":3,"file":"composer.mjs","names":[],"sources":["../src/composer.ts"],"sourcesContent":["import { stringify } from \"yaml\";\nimport type { ComposeOptions, ResolverOutput, ServiceCategory } from \"./types.js\";\n\n// ── Public Types ────────────────────────────────────────────────────────────\n\nexport interface ComposeResult {\n\tfiles: Record<string, string>; // filename -> YAML content\n\tmainFile: string; // \"docker-compose.yml\"\n\tprofiles: string[]; // list of profiles used\n}\n\n// ── Category → Profile Mapping ──────────────────────────────────────────────\n\nconst CATEGORY_PROFILE_MAP: Partial<Record<ServiceCategory, { file: string; profile: string }>> = {\n\tai: { file: \"docker-compose.ai.yml\", profile: \"ai\" },\n\t\"ai-platform\": { file: \"docker-compose.ai.yml\", profile: \"ai\" },\n\tmedia: { file: \"docker-compose.media.yml\", profile: \"media\" },\n\tmonitoring: { file: \"docker-compose.monitoring.yml\", profile: \"monitoring\" },\n\tanalytics: { file: \"docker-compose.monitoring.yml\", profile: \"monitoring\" },\n\t\"dev-tools\": { file: \"docker-compose.tools.yml\", profile: \"tools\" },\n\t\"coding-agent\": { file: \"docker-compose.tools.yml\", profile: \"tools\" },\n\t\"social-media\": { file: \"docker-compose.social.yml\", profile: \"social\" },\n\tknowledge: { file: \"docker-compose.knowledge.yml\", profile: \"knowledge\" },\n\tcommunication: { file: \"docker-compose.communication.yml\", profile: \"communication\" },\n};\n\nconst YAML_OPTIONS = { lineWidth: 120, nullStr: \"\" };\n\n// ── Shared Gateway Builder ──────────────────────────────────────────────────\n\ninterface GatewayBuildResult {\n\tgatewayService: Record<string, unknown>;\n\tcliService: Record<string, unknown>;\n\tallVolumes: Set<string>;\n}\n\n/**\n * Builds the OpenClaw gateway and CLI service entries.\n * Matches the real OpenClaw docker-compose.yml structure:\n * - Bridge port 18790 for ACP/WebSocket\n * - Bind-mount volumes (not named volumes)\n * - Claude web-provider session env vars\n * - Gateway bind mode (--bind lan)\n * - CLI companion service with stdin/tty\n */\nfunction buildGatewayServices(\n\tresolved: ResolverOutput,\n\toptions: ComposeOptions,\n\tdependsOn?: Record<string, { condition: string }>,\n): GatewayBuildResult {\n\tconst allVolumes = new Set<string>();\n\n\t// Gateway environment\n\tconst gatewayEnv: Record<string, string> = {\n\t\tHOME: \"/home/node\",\n\t\tTERM: \"xterm-256color\",\n\t\tOPENCLAW_GATEWAY_TOKEN: \"${OPENCLAW_GATEWAY_TOKEN}\",\n\t\t// Claude web-provider session vars (optional, user fills in .env)\n\t\tCLAUDE_AI_SESSION_KEY: \"${CLAUDE_AI_SESSION_KEY}\",\n\t\tCLAUDE_WEB_SESSION_KEY: \"${CLAUDE_WEB_SESSION_KEY}\",\n\t\tCLAUDE_WEB_COOKIE: \"${CLAUDE_WEB_COOKIE}\",\n\t};\n\n\t// Gateway volumes (bind-mount style matching real docker-setup.sh)\n\tconst gatewayVolumes: string[] = [\n\t\t\"${OPENCLAW_CONFIG_DIR:-./openclaw/config}:/home/node/.openclaw\",\n\t\t\"${OPENCLAW_WORKSPACE_DIR:-./openclaw/workspace}:/home/node/.openclaw/workspace\",\n\t];\n\n\t// Collect env vars and volume mounts from companion services\n\tfor (const { definition: def } of resolved.services) {\n\t\tfor (const env of def.openclawEnvVars) {\n\t\t\tgatewayEnv[env.key] = env.secret ? `\\${${env.key}}` : env.defaultValue;\n\t\t}\n\t\tif (def.openclawVolumeMounts) {\n\t\t\tfor (const vol of def.openclawVolumeMounts) {\n\t\t\t\tallVolumes.add(vol.name);\n\t\t\t\tgatewayVolumes.push(`${vol.name}:${vol.containerPath}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Gateway service\n\tconst gateway: Record<string, unknown> = {\n\t\timage: `\\${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:${options.openclawVersion}}`,\n\t\tenvironment: gatewayEnv,\n\t\tvolumes: gatewayVolumes,\n\t\tports: [\"${OPENCLAW_GATEWAY_PORT:-18789}:18789\", \"${OPENCLAW_BRIDGE_PORT:-18790}:18790\"],\n\t\tnetworks: [\"openclaw-network\"],\n\t\tinit: true,\n\t\trestart: \"unless-stopped\",\n\t\tcommand: [\n\t\t\t\"node\",\n\t\t\t\"dist/index.js\",\n\t\t\t\"gateway\",\n\t\t\t\"--bind\",\n\t\t\t\"${OPENCLAW_GATEWAY_BIND:-lan}\",\n\t\t\t\"--port\",\n\t\t\t\"18789\",\n\t\t],\n\t};\n\n\t// Traefik labels for the gateway\n\tconst gwTraefikLabels = options.traefikLabels?.get(\"openclaw-gateway\");\n\tif (gwTraefikLabels) {\n\t\tgateway.labels = gwTraefikLabels;\n\t}\n\n\tif (options.bareMetalNativeHost) {\n\t\tgateway.extra_hosts = [\"host.docker.internal:host-gateway\"];\n\t}\n\n\tif (dependsOn && Object.keys(dependsOn).length > 0) {\n\t\tgateway.depends_on = dependsOn;\n\t}\n\n\t// CLI companion service (matching real OpenClaw docker-compose.yml)\n\tconst cliService: Record<string, unknown> = {\n\t\timage: `\\${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:${options.openclawVersion}}`,\n\t\tenvironment: {\n\t\t\tHOME: \"/home/node\",\n\t\t\tTERM: \"xterm-256color\",\n\t\t\tOPENCLAW_GATEWAY_TOKEN: \"${OPENCLAW_GATEWAY_TOKEN}\",\n\t\t\tBROWSER: \"echo\",\n\t\t\tCLAUDE_AI_SESSION_KEY: \"${CLAUDE_AI_SESSION_KEY}\",\n\t\t\tCLAUDE_WEB_SESSION_KEY: \"${CLAUDE_WEB_SESSION_KEY}\",\n\t\t\tCLAUDE_WEB_COOKIE: \"${CLAUDE_WEB_COOKIE}\",\n\t\t},\n\t\tvolumes: [\n\t\t\t\"${OPENCLAW_CONFIG_DIR:-./openclaw/config}:/home/node/.openclaw\",\n\t\t\t\"${OPENCLAW_WORKSPACE_DIR:-./openclaw/workspace}:/home/node/.openclaw/workspace\",\n\t\t],\n\t\tstdin_open: true,\n\t\ttty: true,\n\t\tinit: true,\n\t\tnetworks: [\"openclaw-network\"],\n\t\tentrypoint: [\"node\", \"dist/index.js\"],\n\t};\n\n\treturn { gatewayService: gateway, cliService: cliService, allVolumes };\n}\n\n// ── Shared Companion Service Builder ────────────────────────────────────────\n\nfunction buildCompanionService(\n\tdef: ResolverOutput[\"services\"][number][\"definition\"],\n\tresolved: ResolverOutput,\n\toptions: ComposeOptions,\n\tallVolumes: Set<string>,\n): { entry: Record<string, unknown>; volumeNames: string[] } {\n\tconst svc: Record<string, unknown> = {};\n\tconst volumeNames: string[] = [];\n\n\tsvc.image = `${def.image}:${def.imageTag}`;\n\n\tif (def.environment.length > 0) {\n\t\tconst env: Record<string, string> = {};\n\t\tfor (const e of def.environment) {\n\t\t\tenv[e.key] = e.secret ? `\\${${e.key}}` : e.defaultValue;\n\t\t}\n\t\tsvc.environment = env;\n\t}\n\n\tconst exposedPorts = def.ports.filter((p) => p.exposed);\n\tif (exposedPorts.length > 0) {\n\t\tconst prefix = def.id.toUpperCase().replace(/-/g, \"_\");\n\t\tsvc.ports = exposedPorts.map((p, i) => {\n\t\t\tconst suffix = exposedPorts.length > 1 ? `_${i}` : \"\";\n\t\t\treturn `\\${${prefix}_PORT${suffix}:-${p.host}}:${p.container}`;\n\t\t});\n\t}\n\n\tif (def.volumes.length > 0) {\n\t\tsvc.volumes = def.volumes.map((v) => {\n\t\t\tallVolumes.add(v.name);\n\t\t\tvolumeNames.push(v.name);\n\t\t\treturn `${v.name}:${v.containerPath}`;\n\t\t});\n\t}\n\n\t// PostgreSQL: mount the init script for creating per-service databases\n\tif (def.id === \"postgresql\") {\n\t\tif (!svc.volumes) svc.volumes = [];\n\t\t(svc.volumes as string[]).push(\n\t\t\t\"./postgres/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro\",\n\t\t);\n\t}\n\n\tif (def.healthcheck) {\n\t\tconst hc: Record<string, unknown> = {\n\t\t\ttest: [\"CMD-SHELL\", def.healthcheck.test],\n\t\t\tinterval: def.healthcheck.interval,\n\t\t\ttimeout: def.healthcheck.timeout,\n\t\t\tretries: def.healthcheck.retries,\n\t\t};\n\t\tif (def.healthcheck.startPeriod) {\n\t\t\thc.start_period = def.healthcheck.startPeriod;\n\t\t}\n\t\tsvc.healthcheck = hc;\n\t}\n\n\tsvc.restart = def.restartPolicy;\n\tsvc.networks = def.networks;\n\n\tif (def.command) svc.command = def.command;\n\tif (def.entrypoint) svc.entrypoint = def.entrypoint;\n\n\t// Labels: merge static definition labels with dynamic Traefik labels\n\tconst mergedLabels: Record<string, string> = {};\n\tif (def.labels) Object.assign(mergedLabels, def.labels);\n\tconst traefikLabels = options.traefikLabels?.get(def.id);\n\tif (traefikLabels) Object.assign(mergedLabels, traefikLabels);\n\tif (Object.keys(mergedLabels).length > 0) svc.labels = mergedLabels;\n\n\t// Traefik: bind-mount static config and Docker socket\n\tif (def.id === \"traefik\" && options.traefikLabels) {\n\t\tif (!svc.volumes) svc.volumes = [];\n\t\t(svc.volumes as string[]).push(\n\t\t\t\"./traefik/traefik.yml:/etc/traefik/traefik.yml:ro\",\n\t\t\t\"/var/run/docker.sock:/var/run/docker.sock:ro\",\n\t\t);\n\t}\n\n\tlet deploy: Record<string, unknown> | undefined;\n\tif (def.deploy) {\n\t\tdeploy = JSON.parse(JSON.stringify(def.deploy)) as Record<string, unknown>;\n\t}\n\tif (options.gpu && def.gpuRequired) {\n\t\tconst base = deploy ?? {};\n\t\tconst resources = (base.resources ?? {}) as Record<string, unknown>;\n\t\tdeploy = {\n\t\t\t...base,\n\t\t\tresources: {\n\t\t\t\t...resources,\n\t\t\t\treservations: {\n\t\t\t\t\t...((resources.reservations as Record<string, unknown>) ?? {}),\n\t\t\t\t\tdevices: [{ driver: \"nvidia\", count: \"all\", capabilities: [\"gpu\"] }],\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n\tif (deploy) svc.deploy = deploy;\n\n\t// Merge both dependsOn and requires to ensure proper Docker startup ordering\n\tconst depIds = [...new Set([...def.dependsOn, ...def.requires])].filter((id) =>\n\t\tresolved.services.some((s) => s.definition.id === id),\n\t);\n\tif (depIds.length > 0) {\n\t\tconst dependsOn: Record<string, { condition: string }> = {};\n\t\tfor (const depId of depIds) {\n\t\t\tconst dep = resolved.services.find((s) => s.definition.id === depId);\n\t\t\tdependsOn[depId] = {\n\t\t\t\tcondition: dep?.definition.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t\t};\n\t\t}\n\t\tsvc.depends_on = dependsOn;\n\t}\n\n\treturn { entry: svc, volumeNames };\n}\n\n// ── Single-File Compose ─────────────────────────────────────────────────────\n\n/**\n * Generates a single Docker Compose YAML string with ALL services.\n * Backward-compatible signature.\n */\nexport function compose(resolved: ResolverOutput, options: ComposeOptions): string {\n\t// Build depends_on for ALL companion services\n\tconst gatewayDependsOn: Record<string, { condition: string }> = {};\n\tfor (const { definition: def } of resolved.services) {\n\t\tgatewayDependsOn[def.id] = {\n\t\t\tcondition: def.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t};\n\t}\n\n\tconst { gatewayService, cliService, allVolumes } = buildGatewayServices(\n\t\tresolved,\n\t\toptions,\n\t\tgatewayDependsOn,\n\t);\n\n\tconst services: Record<string, Record<string, unknown>> = {\n\t\t\"openclaw-gateway\": gatewayService,\n\t};\n\n\tfor (const { definition: def } of resolved.services) {\n\t\tconst { entry } = buildCompanionService(def, resolved, options, allVolumes);\n\t\tservices[def.id] = entry;\n\t}\n\n\t// Add CLI service\n\tservices[\"openclaw-cli\"] = cliService;\n\n\tconst volumes: Record<string, null> = {};\n\tfor (const v of [...allVolumes].sort()) {\n\t\tvolumes[v] = null;\n\t}\n\n\tconst networks = { \"openclaw-network\": { driver: \"bridge\" } };\n\n\treturn stringify({ services, volumes, networks }, YAML_OPTIONS);\n}\n\n// ── Multi-File Compose ──────────────────────────────────────────────────────\n\ninterface ServiceInfo {\n\tid: string;\n\tcategory: ServiceCategory;\n\tentry: Record<string, unknown>;\n\tvolumeNames: string[];\n}\n\n/**\n * Generates multiple Docker Compose files, splitting services into profile-based\n * override files by category.\n */\nexport function composeMultiFile(resolved: ResolverOutput, options: ComposeOptions): ComposeResult {\n\tconst allVolumes = new Set<string>();\n\n\t// Build all companion service entries & classify by category\n\tconst serviceInfos: ServiceInfo[] = [];\n\n\tfor (const { definition: def } of resolved.services) {\n\t\tconst { entry, volumeNames } = buildCompanionService(def, resolved, options, allVolumes);\n\t\tserviceInfos.push({ id: def.id, category: def.category, entry, volumeNames });\n\t}\n\n\t// Partition services into base vs. profile files\n\tconst baseServiceIds = new Set<string>();\n\tconst profileFileMap: Record<string, { profile: string; services: ServiceInfo[] }> = {};\n\n\tfor (const info of serviceInfos) {\n\t\tconst mapping = CATEGORY_PROFILE_MAP[info.category];\n\t\tif (mapping) {\n\t\t\tif (!profileFileMap[mapping.file]) {\n\t\t\t\tprofileFileMap[mapping.file] = { profile: mapping.profile, services: [] };\n\t\t\t}\n\t\t\tprofileFileMap[mapping.file]!.services.push(info);\n\t\t} else {\n\t\t\tbaseServiceIds.add(info.id);\n\t\t}\n\t}\n\n\t// Gateway depends_on (only base services)\n\tconst gatewayDependsOn: Record<string, { condition: string }> = {};\n\tfor (const { definition: def } of resolved.services) {\n\t\tif (baseServiceIds.has(def.id)) {\n\t\t\tgatewayDependsOn[def.id] = {\n\t\t\t\tcondition: def.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t\t};\n\t\t}\n\t}\n\n\tconst {\n\t\tgatewayService,\n\t\tcliService,\n\t\tallVolumes: gwVolumes,\n\t} = buildGatewayServices(resolved, options, gatewayDependsOn);\n\n\t// Merge gateway volumes into allVolumes\n\tfor (const v of gwVolumes) allVolumes.add(v);\n\n\t// Base file: gateway + CLI + core services + ALL volumes + networks\n\tconst baseServices: Record<string, Record<string, unknown>> = {\n\t\t\"openclaw-gateway\": gatewayService,\n\t};\n\n\tfor (const info of serviceInfos) {\n\t\tif (baseServiceIds.has(info.id)) {\n\t\t\tbaseServices[info.id] = info.entry;\n\t\t}\n\t}\n\n\tbaseServices[\"openclaw-cli\"] = cliService;\n\n\tconst sortedAllVolumes: Record<string, null> = {};\n\tfor (const v of [...allVolumes].sort()) {\n\t\tsortedAllVolumes[v] = null;\n\t}\n\n\tconst networks = { \"openclaw-network\": { driver: \"bridge\" } };\n\n\tconst files: Record<string, string> = {};\n\tfiles[\"docker-compose.yml\"] = stringify(\n\t\t{ services: baseServices, volumes: sortedAllVolumes, networks },\n\t\tYAML_OPTIONS,\n\t);\n\n\t// Profile override files\n\tconst usedProfiles = new Set<string>();\n\n\tfor (const [fileName, { profile, services }] of Object.entries(profileFileMap)) {\n\t\tusedProfiles.add(profile);\n\n\t\tconst profileServices: Record<string, Record<string, unknown>> = {};\n\t\tconst profileVolumes = new Set<string>();\n\n\t\tfor (const info of services) {\n\t\t\tprofileServices[info.id] = { ...info.entry, profiles: [profile] };\n\t\t\tfor (const vName of info.volumeNames) {\n\t\t\t\tprofileVolumes.add(vName);\n\t\t\t}\n\t\t}\n\n\t\tconst fileContent: Record<string, unknown> = { services: profileServices };\n\n\t\tif (profileVolumes.size > 0) {\n\t\t\tconst sortedProfileVolumes: Record<string, null> = {};\n\t\t\tfor (const v of [...profileVolumes].sort()) {\n\t\t\t\tsortedProfileVolumes[v] = null;\n\t\t\t}\n\t\t\tfileContent.volumes = sortedProfileVolumes;\n\t\t}\n\n\t\tfiles[fileName] = stringify(fileContent, YAML_OPTIONS);\n\t}\n\n\treturn {\n\t\tfiles,\n\t\tmainFile: \"docker-compose.yml\",\n\t\tprofiles: [...usedProfiles].sort(),\n\t};\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"composer.mjs","names":[],"sources":["../src/composer.ts"],"sourcesContent":["import { stringify } from \"yaml\";\nimport type { ComposeOptions, ResolverOutput, ServiceCategory } from \"./types.js\";\n\n// ── Public Types ────────────────────────────────────────────────────────────\n\nexport interface ComposeResult {\n\tfiles: Record<string, string>; // filename -> YAML content\n\tmainFile: string; // \"docker-compose.yml\"\n\tprofiles: string[]; // list of profiles used\n}\n\n// ── Category → Profile Mapping ──────────────────────────────────────────────\n\nconst CATEGORY_PROFILE_MAP: Partial<Record<ServiceCategory, { file: string; profile: string }>> = {\n\tai: { file: \"docker-compose.ai.yml\", profile: \"ai\" },\n\t\"ai-platform\": { file: \"docker-compose.ai.yml\", profile: \"ai\" },\n\tmedia: { file: \"docker-compose.media.yml\", profile: \"media\" },\n\tmonitoring: { file: \"docker-compose.monitoring.yml\", profile: \"monitoring\" },\n\tanalytics: { file: \"docker-compose.monitoring.yml\", profile: \"monitoring\" },\n\t\"dev-tools\": { file: \"docker-compose.tools.yml\", profile: \"tools\" },\n\t\"coding-agent\": { file: \"docker-compose.tools.yml\", profile: \"tools\" },\n\t\"social-media\": { file: \"docker-compose.social.yml\", profile: \"social\" },\n\tknowledge: { file: \"docker-compose.knowledge.yml\", profile: \"knowledge\" },\n\tcommunication: { file: \"docker-compose.communication.yml\", profile: \"communication\" },\n};\n\nconst YAML_OPTIONS = { lineWidth: 120, nullStr: \"\" };\n\n// ── Shared Gateway Builder ──────────────────────────────────────────────────\n\ninterface GatewayBuildResult {\n\tgatewayService: Record<string, unknown>;\n\tcliService: Record<string, unknown>;\n\tallVolumes: Set<string>;\n}\n\n/**\n * Builds the OpenClaw gateway and CLI service entries.\n * Matches the real OpenClaw docker-compose.yml structure:\n * - Bridge port 18790 for ACP/WebSocket\n * - Bind-mount volumes (not named volumes)\n * - Claude web-provider session env vars\n * - Gateway bind mode (--bind lan)\n * - CLI companion service with stdin/tty\n */\nfunction buildGatewayServices(\n\tresolved: ResolverOutput,\n\toptions: ComposeOptions,\n\tdependsOn?: Record<string, { condition: string }>,\n): GatewayBuildResult {\n\tconst allVolumes = new Set<string>();\n\n\t// Gateway environment\n\tconst gatewayEnv: Record<string, string> = {\n\t\tHOME: \"/home/node\",\n\t\tTERM: \"xterm-256color\",\n\t\tOPENCLAW_GATEWAY_TOKEN: \"${OPENCLAW_GATEWAY_TOKEN}\",\n\t\t// Claude web-provider session vars (optional, user fills in .env)\n\t\tCLAUDE_AI_SESSION_KEY: \"${CLAUDE_AI_SESSION_KEY}\",\n\t\tCLAUDE_WEB_SESSION_KEY: \"${CLAUDE_WEB_SESSION_KEY}\",\n\t\tCLAUDE_WEB_COOKIE: \"${CLAUDE_WEB_COOKIE}\",\n\t};\n\n\t// Gateway volumes (bind-mount style matching real docker-setup.sh)\n\tconst gatewayVolumes: string[] = [\n\t\t\"${OPENCLAW_CONFIG_DIR:-./openclaw/config}:/home/node/.openclaw\",\n\t\t\"${OPENCLAW_WORKSPACE_DIR:-./openclaw/workspace}:/home/node/.openclaw/workspace\",\n\t];\n\n\t// Collect env vars and volume mounts from companion services\n\tfor (const { definition: def } of resolved.services) {\n\t\tfor (const env of def.openclawEnvVars) {\n\t\t\tgatewayEnv[env.key] = env.secret ? `\\${${env.key}}` : env.defaultValue;\n\t\t}\n\t\tif (def.openclawVolumeMounts) {\n\t\t\tfor (const vol of def.openclawVolumeMounts) {\n\t\t\t\tconst isBindMount =\n\t\t\t\t\tvol.name.startsWith(\"./\") || vol.name.startsWith(\"/\") || vol.name.startsWith(\"~\");\n\t\t\t\tif (!isBindMount) {\n\t\t\t\t\tallVolumes.add(vol.name);\n\t\t\t\t}\n\t\t\t\tgatewayVolumes.push(`${vol.name}:${vol.containerPath}`);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Gateway service\n\tconst gateway: Record<string, unknown> = {\n\t\timage: `\\${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:${options.openclawVersion}}`,\n\t\tenvironment: gatewayEnv,\n\t\tvolumes: gatewayVolumes,\n\t\tports: [\"${OPENCLAW_GATEWAY_PORT:-18789}:18789\", \"${OPENCLAW_BRIDGE_PORT:-18790}:18790\"],\n\t\tnetworks: [\"openclaw-network\"],\n\t\tinit: true,\n\t\trestart: \"unless-stopped\",\n\t\tcommand: [\n\t\t\t\"node\",\n\t\t\t\"dist/index.js\",\n\t\t\t\"gateway\",\n\t\t\t\"--bind\",\n\t\t\t\"${OPENCLAW_GATEWAY_BIND:-lan}\",\n\t\t\t\"--port\",\n\t\t\t\"18789\",\n\t\t],\n\t};\n\n\t// Traefik labels for the gateway\n\tconst gwTraefikLabels = options.traefikLabels?.get(\"openclaw-gateway\");\n\tif (gwTraefikLabels) {\n\t\tgateway.labels = gwTraefikLabels;\n\t}\n\n\tif (options.bareMetalNativeHost) {\n\t\tgateway.extra_hosts = [\"host.docker.internal:host-gateway\"];\n\t}\n\n\tif (dependsOn && Object.keys(dependsOn).length > 0) {\n\t\tgateway.depends_on = dependsOn;\n\t}\n\n\t// CLI companion service (matching real OpenClaw docker-compose.yml)\n\tconst cliService: Record<string, unknown> = {\n\t\timage: `\\${OPENCLAW_IMAGE:-ghcr.io/openclaw/openclaw:${options.openclawVersion}}`,\n\t\tenvironment: {\n\t\t\tHOME: \"/home/node\",\n\t\t\tTERM: \"xterm-256color\",\n\t\t\tOPENCLAW_GATEWAY_TOKEN: \"${OPENCLAW_GATEWAY_TOKEN}\",\n\t\t\tBROWSER: \"echo\",\n\t\t\tCLAUDE_AI_SESSION_KEY: \"${CLAUDE_AI_SESSION_KEY}\",\n\t\t\tCLAUDE_WEB_SESSION_KEY: \"${CLAUDE_WEB_SESSION_KEY}\",\n\t\t\tCLAUDE_WEB_COOKIE: \"${CLAUDE_WEB_COOKIE}\",\n\t\t},\n\t\tvolumes: [\n\t\t\t\"${OPENCLAW_CONFIG_DIR:-./openclaw/config}:/home/node/.openclaw\",\n\t\t\t\"${OPENCLAW_WORKSPACE_DIR:-./openclaw/workspace}:/home/node/.openclaw/workspace\",\n\t\t],\n\t\tstdin_open: true,\n\t\ttty: true,\n\t\tinit: true,\n\t\tnetworks: [\"openclaw-network\"],\n\t\tentrypoint: [\"node\", \"dist/index.js\"],\n\t};\n\n\treturn { gatewayService: gateway, cliService: cliService, allVolumes };\n}\n\n// ── Shared Companion Service Builder ────────────────────────────────────────\n\nfunction buildCompanionService(\n\tdef: ResolverOutput[\"services\"][number][\"definition\"],\n\tresolved: ResolverOutput,\n\toptions: ComposeOptions,\n\tallVolumes: Set<string>,\n): { entry: Record<string, unknown>; volumeNames: string[] } {\n\tconst svc: Record<string, unknown> = {};\n\tconst volumeNames: string[] = [];\n\n\tsvc.image = `${def.image}:${def.imageTag}`;\n\n\tif (def.environment.length > 0) {\n\t\tconst env: Record<string, string> = {};\n\t\tfor (const e of def.environment) {\n\t\t\tenv[e.key] = e.secret ? `\\${${e.key}}` : e.defaultValue;\n\t\t}\n\t\tsvc.environment = env;\n\t}\n\n\tconst exposedPorts = def.ports.filter((p) => p.exposed);\n\tif (exposedPorts.length > 0) {\n\t\tconst prefix = def.id.toUpperCase().replace(/-/g, \"_\");\n\t\tsvc.ports = exposedPorts.map((p, i) => {\n\t\t\tconst suffix = exposedPorts.length > 1 ? `_${i}` : \"\";\n\t\t\treturn `\\${${prefix}_PORT${suffix}:-${p.host}}:${p.container}`;\n\t\t});\n\t}\n\n\tif (def.volumes.length > 0) {\n\t\tsvc.volumes = def.volumes.map((v) => {\n\t\t\tconst isBindMount =\n\t\t\t\tv.name.startsWith(\"./\") || v.name.startsWith(\"/\") || v.name.startsWith(\"~\");\n\t\t\t\n\t\t\tif (!isBindMount) {\n\t\t\t\tallVolumes.add(v.name);\n\t\t\t\tvolumeNames.push(v.name);\n\t\t\t}\n\t\t\treturn `${v.name}:${v.containerPath}`;\n\t\t});\n\t}\n\n\t// PostgreSQL: mount the init script for creating per-service databases\n\tif (def.id === \"postgresql\") {\n\t\tif (!svc.volumes) svc.volumes = [];\n\t\t(svc.volumes as string[]).push(\n\t\t\t\"./postgres/init-databases.sh:/docker-entrypoint-initdb.d/init-databases.sh:ro\",\n\t\t);\n\t}\n\n\tif (def.healthcheck) {\n\t\tconst hc: Record<string, unknown> = {\n\t\t\ttest: [\"CMD-SHELL\", def.healthcheck.test],\n\t\t\tinterval: def.healthcheck.interval,\n\t\t\ttimeout: def.healthcheck.timeout,\n\t\t\tretries: def.healthcheck.retries,\n\t\t};\n\t\tif (def.healthcheck.startPeriod) {\n\t\t\thc.start_period = def.healthcheck.startPeriod;\n\t\t}\n\t\tsvc.healthcheck = hc;\n\t}\n\n\tsvc.restart = def.restartPolicy;\n\tsvc.networks = def.networks;\n\n\tif (def.command) svc.command = def.command;\n\tif (def.entrypoint) svc.entrypoint = def.entrypoint;\n\n\t// Labels: merge static definition labels with dynamic Traefik labels\n\tconst mergedLabels: Record<string, string> = {};\n\tif (def.labels) Object.assign(mergedLabels, def.labels);\n\tconst traefikLabels = options.traefikLabels?.get(def.id);\n\tif (traefikLabels) Object.assign(mergedLabels, traefikLabels);\n\tif (Object.keys(mergedLabels).length > 0) svc.labels = mergedLabels;\n\n\t// Traefik: bind-mount static config and Docker socket\n\tif (def.id === \"traefik\" && options.traefikLabels) {\n\t\tif (!svc.volumes) svc.volumes = [];\n\t\t(svc.volumes as string[]).push(\n\t\t\t\"./traefik/traefik.yml:/etc/traefik/traefik.yml:ro\",\n\t\t\t\"/var/run/docker.sock:/var/run/docker.sock:ro\",\n\t\t);\n\t}\n\n\tlet deploy: Record<string, unknown> | undefined;\n\tif (def.deploy) {\n\t\tdeploy = JSON.parse(JSON.stringify(def.deploy)) as Record<string, unknown>;\n\t}\n\tif (options.gpu && def.gpuRequired) {\n\t\tconst base = deploy ?? {};\n\t\tconst resources = (base.resources ?? {}) as Record<string, unknown>;\n\t\tdeploy = {\n\t\t\t...base,\n\t\t\tresources: {\n\t\t\t\t...resources,\n\t\t\t\treservations: {\n\t\t\t\t\t...((resources.reservations as Record<string, unknown>) ?? {}),\n\t\t\t\t\tdevices: [{ driver: \"nvidia\", count: \"all\", capabilities: [\"gpu\"] }],\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t}\n\tif (deploy) svc.deploy = deploy;\n\n\t// Merge both dependsOn and requires to ensure proper Docker startup ordering\n\tconst depIds = [...new Set([...def.dependsOn, ...def.requires])].filter((id) =>\n\t\tresolved.services.some((s) => s.definition.id === id),\n\t);\n\tif (depIds.length > 0) {\n\t\tconst dependsOn: Record<string, { condition: string }> = {};\n\t\tfor (const depId of depIds) {\n\t\t\tconst dep = resolved.services.find((s) => s.definition.id === depId);\n\t\t\tdependsOn[depId] = {\n\t\t\t\tcondition: dep?.definition.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t\t};\n\t\t}\n\t\tsvc.depends_on = dependsOn;\n\t}\n\n\treturn { entry: svc, volumeNames };\n}\n\n// ── Single-File Compose ─────────────────────────────────────────────────────\n\n/**\n * Generates a single Docker Compose YAML string with ALL services.\n * Backward-compatible signature.\n */\nexport function compose(resolved: ResolverOutput, options: ComposeOptions): string {\n\t// Build depends_on for ALL companion services\n\tconst gatewayDependsOn: Record<string, { condition: string }> = {};\n\tfor (const { definition: def } of resolved.services) {\n\t\tgatewayDependsOn[def.id] = {\n\t\t\tcondition: def.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t};\n\t}\n\n\tconst { gatewayService, cliService, allVolumes } = buildGatewayServices(\n\t\tresolved,\n\t\toptions,\n\t\tgatewayDependsOn,\n\t);\n\n\tconst services: Record<string, Record<string, unknown>> = {\n\t\t\"openclaw-gateway\": gatewayService,\n\t};\n\n\tfor (const { definition: def } of resolved.services) {\n\t\tconst { entry } = buildCompanionService(def, resolved, options, allVolumes);\n\t\tservices[def.id] = entry;\n\t}\n\n\t// Add CLI service\n\tservices[\"openclaw-cli\"] = cliService;\n\n\tconst volumes: Record<string, null> = {};\n\tfor (const v of [...allVolumes].sort()) {\n\t\tvolumes[v] = null;\n\t}\n\n\tconst networks = { \"openclaw-network\": { driver: \"bridge\" } };\n\n\treturn stringify({ services, volumes, networks }, YAML_OPTIONS);\n}\n\n// ── Multi-File Compose ──────────────────────────────────────────────────────\n\ninterface ServiceInfo {\n\tid: string;\n\tcategory: ServiceCategory;\n\tentry: Record<string, unknown>;\n\tvolumeNames: string[];\n}\n\n/**\n * Generates multiple Docker Compose files, splitting services into profile-based\n * override files by category.\n */\nexport function composeMultiFile(resolved: ResolverOutput, options: ComposeOptions): ComposeResult {\n\tconst allVolumes = new Set<string>();\n\n\t// Build all companion service entries & classify by category\n\tconst serviceInfos: ServiceInfo[] = [];\n\n\tfor (const { definition: def } of resolved.services) {\n\t\tconst { entry, volumeNames } = buildCompanionService(def, resolved, options, allVolumes);\n\t\tserviceInfos.push({ id: def.id, category: def.category, entry, volumeNames });\n\t}\n\n\t// Partition services into base vs. profile files\n\tconst baseServiceIds = new Set<string>();\n\tconst profileFileMap: Record<string, { profile: string; services: ServiceInfo[] }> = {};\n\n\tfor (const info of serviceInfos) {\n\t\tconst mapping = CATEGORY_PROFILE_MAP[info.category];\n\t\tif (mapping) {\n\t\t\tif (!profileFileMap[mapping.file]) {\n\t\t\t\tprofileFileMap[mapping.file] = { profile: mapping.profile, services: [] };\n\t\t\t}\n\t\t\tprofileFileMap[mapping.file]!.services.push(info);\n\t\t} else {\n\t\t\tbaseServiceIds.add(info.id);\n\t\t}\n\t}\n\n\t// Gateway depends_on (only base services)\n\tconst gatewayDependsOn: Record<string, { condition: string }> = {};\n\tfor (const { definition: def } of resolved.services) {\n\t\tif (baseServiceIds.has(def.id)) {\n\t\t\tgatewayDependsOn[def.id] = {\n\t\t\t\tcondition: def.healthcheck ? \"service_healthy\" : \"service_started\",\n\t\t\t};\n\t\t}\n\t}\n\n\tconst {\n\t\tgatewayService,\n\t\tcliService,\n\t\tallVolumes: gwVolumes,\n\t} = buildGatewayServices(resolved, options, gatewayDependsOn);\n\n\t// Merge gateway volumes into allVolumes\n\tfor (const v of gwVolumes) allVolumes.add(v);\n\n\t// Base file: gateway + CLI + core services + ALL volumes + networks\n\tconst baseServices: Record<string, Record<string, unknown>> = {\n\t\t\"openclaw-gateway\": gatewayService,\n\t};\n\n\tfor (const info of serviceInfos) {\n\t\tif (baseServiceIds.has(info.id)) {\n\t\t\tbaseServices[info.id] = info.entry;\n\t\t}\n\t}\n\n\tbaseServices[\"openclaw-cli\"] = cliService;\n\n\tconst sortedAllVolumes: Record<string, null> = {};\n\tfor (const v of [...allVolumes].sort()) {\n\t\tsortedAllVolumes[v] = null;\n\t}\n\n\tconst networks = { \"openclaw-network\": { driver: \"bridge\" } };\n\n\tconst files: Record<string, string> = {};\n\tfiles[\"docker-compose.yml\"] = stringify(\n\t\t{ services: baseServices, volumes: sortedAllVolumes, networks },\n\t\tYAML_OPTIONS,\n\t);\n\n\t// Profile override files\n\tconst usedProfiles = new Set<string>();\n\n\tfor (const [fileName, { profile, services }] of Object.entries(profileFileMap)) {\n\t\tusedProfiles.add(profile);\n\n\t\tconst profileServices: Record<string, Record<string, unknown>> = {};\n\t\tconst profileVolumes = new Set<string>();\n\n\t\tfor (const info of services) {\n\t\t\tprofileServices[info.id] = { ...info.entry, profiles: [profile] };\n\t\t\tfor (const vName of info.volumeNames) {\n\t\t\t\tprofileVolumes.add(vName);\n\t\t\t}\n\t\t}\n\n\t\tconst fileContent: Record<string, unknown> = { services: profileServices };\n\n\t\tif (profileVolumes.size > 0) {\n\t\t\tconst sortedProfileVolumes: Record<string, null> = {};\n\t\t\tfor (const v of [...profileVolumes].sort()) {\n\t\t\t\tsortedProfileVolumes[v] = null;\n\t\t\t}\n\t\t\tfileContent.volumes = sortedProfileVolumes;\n\t\t}\n\n\t\tfiles[fileName] = stringify(fileContent, YAML_OPTIONS);\n\t}\n\n\treturn {\n\t\tfiles,\n\t\tmainFile: \"docker-compose.yml\",\n\t\tprofiles: [...usedProfiles].sort(),\n\t};\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "./vi.2VT5v0um-YSByewHe.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "./vi.2VT5v0um-BgmKutxR.mjs";
2
2
  import { resolve } from "./resolver.mjs";
3
3
  import { composeMultiFile } from "./composer.mjs";
4
4
 
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "./vi.2VT5v0um-YSByewHe.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "./vi.2VT5v0um-BgmKutxR.mjs";
2
2
  import { resolve } from "./resolver.mjs";
3
3
  import { compose } from "./composer.mjs";
4
4
  import { parse } from "yaml";
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "./vi.2VT5v0um-YSByewHe.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "./vi.2VT5v0um-BgmKutxR.mjs";
2
2
  import { generate } from "./generate.mjs";
3
3
  import { parse } from "yaml";
4
4
 
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-YSByewHe.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-BgmKutxR.mjs";
2
2
  import { generateBareMetalInstall } from "./bare-metal-install.mjs";
3
3
 
4
4
  //#region src/generators/bare-metal-install.test.ts
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-YSByewHe.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-BgmKutxR.mjs";
2
2
  import { generate } from "../generate.mjs";
3
3
 
4
4
  //#region src/generators/caddy.test.ts
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-YSByewHe.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-BgmKutxR.mjs";
2
2
  import { generate } from "../generate.mjs";
3
3
 
4
4
  //#region src/generators/env.test.ts
@@ -1 +1 @@
1
- {"version":3,"file":"health-check.mjs","names":[],"sources":["../../src/generators/health-check.ts"],"sourcesContent":["import type { ResolverOutput } from \"../types.js\";\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\ninterface HealthCheckOptions {\n\tprojectName: string;\n\tdeploymentType?: \"docker\" | \"bare-metal\";\n}\n\n// ─── Generator ──────────────────────────────────────────────────────────────\n\n/**\n * Generates stack-specific health check and verification scripts.\n *\n * Each generated script is dynamic — it contains the exact service names,\n * ports, and health check commands from the resolved stack so users get\n * a precise, zero-configuration verifier.\n */\nexport function generateHealthCheck(\n\tresolved: ResolverOutput,\n\toptions: HealthCheckOptions,\n): Record<string, string> {\n\tconst files: Record<string, string> = {};\n\n\tfiles[\"scripts/health-check.sh\"] = generateBashScript(resolved, options);\n\tfiles[\"scripts/health-check.ps1\"] = generatePowerShellScript(resolved, options);\n\n\treturn files;\n}\n\n// ─── Service metadata extraction ────────────────────────────────────────────\n\ninterface ServiceCheck {\n\tid: string;\n\tname: string;\n\ticon: string;\n\tports: Array<{ host: number; container: number; description: string; exposed: boolean }>;\n\thealthCheckCmd: string | null;\n}\n\nfunction extractServiceChecks(resolved: ResolverOutput): ServiceCheck[] {\n\treturn resolved.services.map((svc) => ({\n\t\tid: svc.definition.id,\n\t\tname: svc.definition.name,\n\t\ticon: svc.definition.icon,\n\t\tports: svc.definition.ports.map((p) => ({\n\t\t\thost: p.host,\n\t\t\tcontainer: p.container,\n\t\t\tdescription: p.description,\n\t\t\texposed: p.exposed,\n\t\t})),\n\t\thealthCheckCmd: svc.definition.healthcheck?.test ?? null,\n\t}));\n}\n\nfunction escapeShell(s: string): string {\n\treturn s.replace(/'/g, \"'\\\\''\").replace(/\"/g, '\\\\\"');\n}\n\n// Use L() to build lines safely — avoids template literal issues with shell syntax\nfunction L(...parts: string[]): string {\n\treturn parts.join(\"\");\n}\n\n// ─── Bash Script ────────────────────────────────────────────────────────────\n\nfunction generateBashScript(resolved: ResolverOutput, options: HealthCheckOptions): string {\n\tconst checks = extractServiceChecks(resolved);\n\tconst name = options.projectName;\n\tconst total = checks.length;\n\n\t// Build per-service docker check calls\n\tconst dockerChecks: string[] = [];\n\tfor (const svc of checks) {\n\t\tdockerChecks.push(L(\" # ── \", svc.icon, \" \", svc.name, \" ──\"));\n\t\tdockerChecks.push(L(' check_container \"', svc.id, '\" \"', svc.name, '\" \"', svc.icon, '\"'));\n\t\tfor (const p of svc.ports.filter((pp) => pp.exposed)) {\n\t\t\tdockerChecks.push(L(' check_port \"', svc.id, '\" ', String(p.host), ' \"', p.description, '\"'));\n\t\t}\n\t\tif (svc.healthCheckCmd) {\n\t\t\tdockerChecks.push(\n\t\t\t\tL(' check_health_cmd \"', svc.id, '\" \"', escapeShell(svc.healthCheckCmd), '\"'),\n\t\t\t);\n\t\t} else {\n\t\t\tdockerChecks.push(\" # No healthcheck defined — skipping exec check\");\n\t\t}\n\t\tdockerChecks.push(\"\");\n\t}\n\n\t// Build per-service bare-metal check calls\n\tconst bmChecks: string[] = [];\n\tfor (const svc of checks) {\n\t\tbmChecks.push(L(\" # ── \", svc.icon, \" \", svc.name, \" (bare-metal) ──\"));\n\t\tbmChecks.push(L(' check_process \"', svc.id, '\" \"', svc.name, '\" \"', svc.icon, '\"'));\n\t\tfor (const p of svc.ports.filter((pp) => pp.exposed)) {\n\t\t\tbmChecks.push(L(' check_port \"', svc.id, '\" ', String(p.host), ' \"', p.description, '\"'));\n\t\t}\n\t\tbmChecks.push(\"\");\n\t}\n\n\t// Build the log-scan service list\n\tconst svcIdList = checks.map((s) => '\"' + s.id + '\"').join(\" \");\n\n\tconst lines: string[] = [\n\t\t\"#!/usr/bin/env bash\",\n\t\t\"set -uo pipefail\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\tL(\"# 🐾 OpenClaw Stack Health Check — \", name),\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"#\",\n\t\t\"# Auto-generated verification script for your stack.\",\n\t\tL(\"# Checks \", String(total), \" services across 6 phases:\"),\n\t\t\"# 1. Environment — prerequisites, .env, secrets\",\n\t\t\"# 2. Container — running state, health status, restart count\",\n\t\t\"# 3. Port — TCP reachability for exposed ports\",\n\t\t\"# 4. Health Command — execute each service's health check\",\n\t\t\"# 5. Resources — memory/CPU warnings\",\n\t\t\"# 6. Logs — scan for ERROR/FATAL/panic patterns\",\n\t\t\"#\",\n\t\t\"# Usage:\",\n\t\t\"# ./scripts/health-check.sh # Full check\",\n\t\t\"# ./scripts/health-check.sh --quick # Skip log scan\",\n\t\t\"# ./scripts/health-check.sh --json # Output as JSON\",\n\t\t\"#\",\n\t\t\"# Exit codes: 0 = all healthy, 1 = some issues, 2 = critical failure\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"\",\n\t\t'SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"',\n\t\t'PROJECT_DIR=\"$(dirname \"$SCRIPT_DIR\")\"',\n\t\t'cd \"$PROJECT_DIR\"',\n\t\t\"\",\n\t\t\"# ── Arguments ────────────────────────────────────────────────────────\",\n\t\t\"QUICK_MODE=false\",\n\t\t\"JSON_MODE=false\",\n\t\t\"VERBOSE=false\",\n\t\t'for arg in \"$@\"; do',\n\t\t' case \"$arg\" in',\n\t\t\" --quick) QUICK_MODE=true ;;\",\n\t\t\" --json) JSON_MODE=true ;;\",\n\t\t\" --verbose|-v) VERBOSE=true ;;\",\n\t\t\" esac\",\n\t\t\"done\",\n\t\t\"\",\n\t\t\"# ── Colour helpers ───────────────────────────────────────────────────\",\n\t\t'if [ -t 1 ] && [ \"$JSON_MODE\" = false ]; then',\n\t\t\" RED='\\\\033[0;31m'; GREEN='\\\\033[0;32m'; YELLOW='\\\\033[1;33m'\",\n\t\t\" CYAN='\\\\033[0;36m'; DIM='\\\\033[2m'; BOLD='\\\\033[1m'; NC='\\\\033[0m'\",\n\t\t\"else\",\n\t\t\" RED=''; GREEN=''; YELLOW=''; CYAN=''; DIM=''; BOLD=''; NC=''\",\n\t\t\"fi\",\n\t\t\"\",\n\t\t\"# ── Counters ─────────────────────────────────────────────────────────\",\n\t\tL(\"TOTAL=\", String(total)),\n\t\t\"HEALTHY=0\",\n\t\t\"WARNING=0\",\n\t\t\"FAILED=0\",\n\t\t\"ERRORS=()\",\n\t\t\"WARNINGS_LIST=()\",\n\t\t\"SERVICE_RESULTS=()\",\n\t\t\"\",\n\t\t\"# ── Utility functions ────────────────────────────────────────────────\",\n\t\t'pass() { echo -e \" ${GREEN}✅ $*${NC}\"; HEALTHY=$((HEALTHY + 1)); }',\n\t\t'warn_svc() { echo -e \" ${YELLOW}⚠️ $*${NC}\"; WARNING=$((WARNING + 1)); WARNINGS_LIST+=(\"$*\"); }',\n\t\t'fail() { echo -e \" ${RED}❌ $*${NC}\"; FAILED=$((FAILED + 1)); ERRORS+=(\"$*\"); }',\n\t\t'info() { echo -e \" ${CYAN}ℹ $*${NC}\"; }',\n\t\t'dim() { echo -e \" ${DIM}$*${NC}\"; }',\n\t\t\"\",\n\t\t\"pad_name() {\",\n\t\t' local name=\"$1\"',\n\t\t\" local pad=25\",\n\t\t' printf \"%-${pad}s\" \"$name\"',\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 1: Environment\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"phase_environment() {\",\n\t\t' if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo -e \"${BOLD}── Phase 1: Environment ────────────────────────────────────${NC}\"',\n\t\t' echo \"\"',\n\t\t\" fi\",\n\t\t\"\",\n\t\t\" # Docker\",\n\t\t\" if command -v docker &>/dev/null; then\",\n\t\t\" if docker info &>/dev/null 2>&1; then\",\n\t\t' pass \"Docker daemon is running\"',\n\t\t\" DOCKER_AVAILABLE=true\",\n\t\t\" else\",\n\t\t' fail \"Docker daemon is NOT running\"',\n\t\t\" DOCKER_AVAILABLE=false\",\n\t\t\" fi\",\n\t\t\" else\",\n\t\t' warn_svc \"Docker is not installed — running bare-metal checks only\"',\n\t\t\" DOCKER_AVAILABLE=false\",\n\t\t\" fi\",\n\t\t\"\",\n\t\t\" # Docker Compose\",\n\t\t' if [ \"$DOCKER_AVAILABLE\" = true ]; then',\n\t\t\" if docker compose version &>/dev/null 2>&1; then\",\n\t\t' pass \"Docker Compose v2 available\"',\n\t\t\" else\",\n\t\t' warn_svc \"Docker Compose v2 not found\"',\n\t\t\" fi\",\n\t\t\" fi\",\n\t\t\"\",\n\t\t\" # .env\",\n\t\t' if [ -f \".env\" ]; then',\n\t\t' pass \".env file exists\"',\n\t\t\" EMPTY_SECRETS=0\",\n\t\t\" while IFS='=' read -r key value; do\",\n\t\t' [[ \"$key\" =~ ^#.*$ ]] && continue',\n\t\t' [[ -z \"$key\" ]] && continue',\n\t\t' if [[ \"$key\" =~ (PASSWORD|TOKEN|SECRET|SESSION_KEY|COOKIE|API_KEY) ]] && [[ -z \"${value:-}\" ]]; then',\n\t\t' if [ \"$VERBOSE\" = true ]; then',\n\t\t' warn_svc \"Empty secret: $key\"',\n\t\t\" fi\",\n\t\t\" EMPTY_SECRETS=$((EMPTY_SECRETS + 1))\",\n\t\t\" fi\",\n\t\t\" done < .env 2>/dev/null || true\",\n\t\t' if [ \"$EMPTY_SECRETS\" -gt 0 ]; then',\n\t\t' warn_svc \"$EMPTY_SECRETS secret(s) are empty in .env\"',\n\t\t\" else\",\n\t\t' pass \"All secrets are populated\"',\n\t\t\" fi\",\n\t\t\" else\",\n\t\t' warn_svc \".env file not found\"',\n\t\t\" fi\",\n\t\t\"\",\n\t\t\" # Disk space\",\n\t\t\" AVAIL_KB=$(df -k . 2>/dev/null | awk 'NR==2{print $4}' || echo \\\"0\\\")\",\n\t\t\" AVAIL_GB=$((AVAIL_KB / 1024 / 1024))\",\n\t\t' if [ \"$AVAIL_GB\" -lt 2 ]; then',\n\t\t' warn_svc \"Low disk space: ${AVAIL_GB}GB available\"',\n\t\t\" else\",\n\t\t' pass \"Disk space: ${AVAIL_GB}GB available\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 2: Container Status\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"check_container() {\",\n\t\t' local id=\"$1\" name=\"$2\" icon=\"$3\"',\n\t\t\"\",\n\t\t\" local status restarts\",\n\t\t' status=$(docker compose ps \"$id\" --format json 2>/dev/null | head -1)',\n\t\t\"\",\n\t\t' if [ -z \"$status\" ]; then',\n\t\t' fail \"$icon $(pad_name \"$name\") — Container not found\"',\n\t\t\" return\",\n\t\t\" fi\",\n\t\t\"\",\n\t\t\" local state health_status\",\n\t\t\" state=$(echo \\\"$status\\\" | grep -o '\\\"State\\\":\\\"[^\\\"]*\\\"' | cut -d'\\\"' -f4 || echo \\\"unknown\\\")\",\n\t\t\" health_status=$(echo \\\"$status\\\" | grep -o '\\\"Health\\\":\\\"[^\\\"]*\\\"' | cut -d'\\\"' -f4 || echo \\\"none\\\")\",\n\t\t\"\",\n\t\t' restarts=$(docker inspect --format=\"{{.RestartCount}}\" \"$(docker compose ps -q \"$id\" 2>/dev/null | head -1)\" 2>/dev/null || echo \"0\")',\n\t\t\"\",\n\t\t' if [ \"$state\" = \"running\" ]; then',\n\t\t' if [ \"$health_status\" = \"healthy\" ]; then',\n\t\t' pass \"$icon $(pad_name \"$name\") Running (healthy) restarts: $restarts\"',\n\t\t' elif [ \"$health_status\" = \"starting\" ]; then',\n\t\t' warn_svc \"$icon $(pad_name \"$name\") Running (starting) restarts: $restarts\"',\n\t\t' elif [ \"$health_status\" = \"unhealthy\" ]; then',\n\t\t' fail \"$icon $(pad_name \"$name\") Running (UNHEALTHY) restarts: $restarts\"',\n\t\t\" else\",\n\t\t' pass \"$icon $(pad_name \"$name\") Running restarts: $restarts\"',\n\t\t\" fi\",\n\t\t' if [ \"$restarts\" -gt 5 ] 2>/dev/null; then',\n\t\t' warn_svc \" └─ $name has restarted $restarts times (possible crash loop)\"',\n\t\t\" fi\",\n\t\t' elif [ \"$state\" = \"exited\" ]; then',\n\t\t\" local exit_code\",\n\t\t' exit_code=$(docker inspect --format=\"{{.State.ExitCode}}\" \"$(docker compose ps -q \"$id\" 2>/dev/null | head -1)\" 2>/dev/null || echo \"?\")',\n\t\t' fail \"$icon $(pad_name \"$name\") Exited (code $exit_code)\"',\n\t\t' if [ \"$exit_code\" = \"137\" ]; then',\n\t\t' ERRORS+=(\" └─ $name: OOM killed (exit 137). Increase memory limits.\")',\n\t\t' elif [ \"$exit_code\" = \"1\" ]; then',\n\t\t' ERRORS+=(\" └─ $name: Application error (exit 1). Check logs: docker compose logs $id\")',\n\t\t\" fi\",\n\t\t\" else\",\n\t\t' fail \"$icon $(pad_name \"$name\") State: $state\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 3: Port Reachability\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"check_port() {\",\n\t\t' local id=\"$1\" port=\"$2\" desc=\"$3\"',\n\t\t\" if command -v nc &>/dev/null; then\",\n\t\t' if nc -z -w 2 localhost \"$port\" 2>/dev/null; then',\n\t\t' dim \" Port $port ($desc) — reachable\"',\n\t\t\" else\",\n\t\t' warn_svc \" └─ $id: Port $port ($desc) — NOT reachable\"',\n\t\t\" fi\",\n\t\t\" elif command -v curl &>/dev/null; then\",\n\t\t' if curl -sf --max-time 2 \"http://localhost:$port/\" &>/dev/null; then',\n\t\t' dim \" Port $port ($desc) — reachable\"',\n\t\t\" else\",\n\t\t' warn_svc \" └─ $id: Port $port ($desc) — NOT reachable\"',\n\t\t\" fi\",\n\t\t\" else\",\n\t\t' dim \" Port $port ($desc) — skipped (no nc or curl)\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 4: Health Check Commands\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"check_health_cmd() {\",\n\t\t' local id=\"$1\" cmd=\"$2\"',\n\t\t' local cid=$(docker compose ps -q \"$id\" 2>/dev/null | head -1)',\n\t\t' [ -z \"$cid\" ] && return',\n\t\t' if docker exec \"$cid\" sh -c \"$cmd\" &>/dev/null; then',\n\t\t' dim \" Health command passed\"',\n\t\t\" else\",\n\t\t' warn_svc \" └─ $id: Health check command failed\"',\n\t\t' if [ \"$VERBOSE\" = true ]; then dim \" Command: $cmd\"; fi',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 5: Resource Usage\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"phase_resources() {\",\n\t\t' if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo -e \"${BOLD}── Phase 5: Resource Usage ─────────────────────────────────${NC}\"',\n\t\t' echo \"\"',\n\t\t\" fi\",\n\t\t' docker stats --no-stream --format \"table {{.Name}}\\\\t{{.CPUPerc}}\\\\t{{.MemUsage}}\\\\t{{.MemPerc}}\" 2>/dev/null || true',\n\t\t' if [ \"$VERBOSE\" = false ] && [ \"$JSON_MODE\" = false ]; then',\n\t\t' info \"Use --verbose to see warnings for high memory usage\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 6: Log Scan\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"phase_logs() {\",\n\t\t' if [ \"$QUICK_MODE\" = true ]; then',\n\t\t' if [ \"$JSON_MODE\" = false ]; then echo \"\"; info \"Skipping log scan (--quick mode)\"; fi',\n\t\t\" return\",\n\t\t\" fi\",\n\t\t' if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo -e \"${BOLD}── Phase 6: Log Scan ───────────────────────────────────────${NC}\"',\n\t\t' echo \"\"',\n\t\t\" fi\",\n\t\t\" local has_errors=false\",\n\t\tL(\" for svc_id in \", svcIdList, \"; do\"),\n\t\t\" local error_lines\",\n\t\t' error_lines=$(docker compose logs --tail=50 \"$svc_id\" 2>/dev/null | grep -iE \"(error|fatal|panic|exception|segfault|killed|oom)\" | tail -5 || true)',\n\t\t' if [ -n \"$error_lines\" ]; then',\n\t\t\" has_errors=true\",\n\t\t' warn_svc \"$svc_id — found error patterns in logs:\"',\n\t\t' echo \"$error_lines\" | while IFS= read -r eline; do',\n\t\t' dim \" | $eline\"',\n\t\t\" done\",\n\t\t\" fi\",\n\t\t\" done\",\n\t\t' if [ \"$has_errors\" = false ] && [ \"$JSON_MODE\" = false ]; then',\n\t\t' pass \"No error patterns found in recent logs\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ── Bare-metal process check ────────────────────────────────────────\",\n\t\t\"check_process() {\",\n\t\t' local id=\"$1\" name=\"$2\" icon=\"$3\"',\n\t\t\" if command -v systemctl &>/dev/null; then\",\n\t\t' if systemctl is-active --quiet \"$id\" 2>/dev/null; then',\n\t\t' pass \"$icon $(pad_name \"$name\") Active (systemd)\"',\n\t\t\" return\",\n\t\t\" fi\",\n\t\t\" fi\",\n\t\t' if pgrep -f \"$id\" &>/dev/null; then',\n\t\t' pass \"$icon $(pad_name \"$name\") Running (process)\"',\n\t\t\" else\",\n\t\t' fail \"$icon $(pad_name \"$name\") NOT running\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Main\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"\",\n\t\t\"DOCKER_AVAILABLE=false\",\n\t\t\"\",\n\t\t'if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo \"═══════════════════════════════════════════════════════════════════════\"',\n\t\tL(' echo \" 🐾 OpenClaw Stack Health Report — ', name, '\"'),\n\t\t' echo \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t\"fi\",\n\t\t\"\",\n\t\t\"phase_environment\",\n\t\t\"\",\n\t\t'if [ \"$DOCKER_AVAILABLE\" = true ]; then',\n\t\t' if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo -e \"${BOLD}── Phase 2–4: Service Checks ──────────────────────────────${NC}\"',\n\t\t' echo \"\"',\n\t\t\" fi\",\n\t\t\"\",\n\t\t...dockerChecks,\n\t\t\"\",\n\t\t\" phase_resources\",\n\t\t\" phase_logs\",\n\t\t\"\",\n\t\t\"else\",\n\t\t' if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo -e \"${BOLD}── Bare-Metal Service Checks ──────────────────────────────${NC}\"',\n\t\t' echo \"\"',\n\t\t\" fi\",\n\t\t\"\",\n\t\t...bmChecks,\n\t\t\"fi\",\n\t\t\"\",\n\t\t\"# ── Summary ──────────────────────────────────────────────────────────\",\n\t\t'echo \"\"',\n\t\t'echo \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t'echo \"\"',\n\t\t'echo -e \" ${BOLD}Summary${NC}: $TOTAL services checked\"',\n\t\t'echo -e \" ${GREEN}Healthy: $HEALTHY${NC} | ${YELLOW}Warnings: $WARNING${NC} | ${RED}Failed: $FAILED${NC}\"',\n\t\t\"\",\n\t\t\"if [ ${#ERRORS[@]} -gt 0 ]; then\",\n\t\t' echo \"\"',\n\t\t' echo -e \" ${BOLD}${RED}── Errors ─────────────────────────────────────────────────${NC}\"',\n\t\t' for e in \"${ERRORS[@]}\"; do',\n\t\t' echo -e \" ${RED}$e${NC}\"',\n\t\t\" done\",\n\t\t\"fi\",\n\t\t\"\",\n\t\t'echo \"\"',\n\t\t'echo \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t'echo \"\"',\n\t\t\"\",\n\t\t'if [ \"$FAILED\" -gt 0 ]; then',\n\t\t' echo -e \" ${RED}Some services need attention. Run with --verbose for details.${NC}\"',\n\t\t\" exit 1\",\n\t\t\"else\",\n\t\t' echo -e \" ${GREEN}🎉 All services are running!${NC}\"',\n\t\t\" exit 0\",\n\t\t\"fi\",\n\t];\n\n\treturn lines.join(\"\\n\") + \"\\n\";\n}\n\n// ─── PowerShell Script ──────────────────────────────────────────────────────\n\nfunction generatePowerShellScript(resolved: ResolverOutput, options: HealthCheckOptions): string {\n\tconst checks = extractServiceChecks(resolved);\n\tconst name = options.projectName;\n\tconst total = checks.length;\n\n\t// Build per-service docker check calls\n\tconst dockerChecks: string[] = [];\n\tfor (const svc of checks) {\n\t\tdockerChecks.push(L(\" # \", svc.icon, \" \", svc.name));\n\t\tdockerChecks.push(\n\t\t\tL(' Test-Container -ServiceId \"', svc.id, '\" -ServiceName \"', svc.name, '\" -Icon \"', svc.icon, '\"'),\n\t\t);\n\t\tfor (const p of svc.ports.filter((pp) => pp.exposed)) {\n\t\t\tdockerChecks.push(\n\t\t\t\tL(' Test-Port -ServiceId \"', svc.id, '\" -Port ', String(p.host), ' -Description \"', p.description, '\"'),\n\t\t\t);\n\t\t}\n\t\tdockerChecks.push(\"\");\n\t}\n\n\t// Build per-service bare-metal check calls\n\tconst bmChecks: string[] = [];\n\tfor (const svc of checks) {\n\t\tbmChecks.push(L(\" # \", svc.icon, \" \", svc.name));\n\t\tbmChecks.push(\n\t\t\tL(' Test-ProcessRunning -ServiceId \"', svc.id, '\" -ServiceName \"', svc.name, '\" -Icon \"', svc.icon, '\"'),\n\t\t);\n\t\tfor (const p of svc.ports.filter((pp) => pp.exposed)) {\n\t\t\tbmChecks.push(\n\t\t\t\tL(' Test-Port -ServiceId \"', svc.id, '\" -Port ', String(p.host), ' -Description \"', p.description, '\"'),\n\t\t\t);\n\t\t}\n\t\tbmChecks.push(\"\");\n\t}\n\n\tconst svcIdList = checks.map((s) => '\"' + s.id + '\"').join(\", \");\n\n\tconst lines: string[] = [\n\t\t\"#Requires -Version 5.1\",\n\t\t\"<#\",\n\t\t\".SYNOPSIS\",\n\t\tL(\" OpenClaw Stack Health Check — \", name),\n\t\t\"\",\n\t\t\".DESCRIPTION\",\n\t\t\" Auto-generated verification script for your stack.\",\n\t\tL(\" Checks \", String(total), \" services: container status, port reachability, and log errors.\"),\n\t\t\"\",\n\t\t\".PARAMETER Quick\",\n\t\t\" Skip log scanning for faster results.\",\n\t\t\"\",\n\t\t\".PARAMETER Json\",\n\t\t\" Output results as JSON.\",\n\t\t\"\",\n\t\t\".EXAMPLE\",\n\t\t\" .\\\\scripts\\\\health-check.ps1\",\n\t\t\" .\\\\scripts\\\\health-check.ps1 -Quick\",\n\t\t\" .\\\\scripts\\\\health-check.ps1 -Json\",\n\t\t\"#>\",\n\t\t\"param(\",\n\t\t\" [switch]$Quick,\",\n\t\t\" [switch]$Json,\",\n\t\t\" [switch]$Detailed\",\n\t\t\")\",\n\t\t\"\",\n\t\t'$ErrorActionPreference = \"Continue\"',\n\t\t\"\",\n\t\t\"# ── Counters ─────────────────────────────────────────────────────────\",\n\t\tL(\"$script:Total = \", String(total)),\n\t\t\"$script:Healthy = 0\",\n\t\t\"$script:Warnings = 0\",\n\t\t\"$script:Failed = 0\",\n\t\t\"$script:Errors = @()\",\n\t\t\"$script:ServiceResults = @()\",\n\t\t\"$script:DockerAvailable = $false\",\n\t\t\"\",\n\t\t\"# ── Helpers ──────────────────────────────────────────────────────────\",\n\t\t\"\",\n\t\t'function Write-Pass($msg) { Write-Host \" ✅ $msg\" -ForegroundColor Green; $script:Healthy++ }',\n\t\t'function Write-Warn($msg) { Write-Host \" ⚠️ $msg\" -ForegroundColor Yellow; $script:Warnings++ }',\n\t\t'function Write-Fail($msg) { Write-Host \" ❌ $msg\" -ForegroundColor Red; $script:Failed++; $script:Errors += $msg }',\n\t\t'function Write-Info($msg) { Write-Host \" ℹ $msg\" -ForegroundColor Cyan }',\n\t\t'function Write-Dim($msg) { Write-Host \" $msg\" -ForegroundColor DarkGray }',\n\t\t\"\",\n\t\t\"function Pad-Name([string]$n) { return $n.PadRight(25) }\",\n\t\t\"\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 1: Environment\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"function Test-Environment {\",\n\t\t\" if (-not $Json) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"── Phase 1: Environment ────────────────────────────────────\" -ForegroundColor White',\n\t\t' Write-Host \"\"',\n\t\t\" }\",\n\t\t\" try {\",\n\t\t\" $null = docker info 2>&1\",\n\t\t\" if ($LASTEXITCODE -eq 0) {\",\n\t\t' Write-Pass \"Docker daemon is running\"',\n\t\t\" $script:DockerAvailable = $true\",\n\t\t\" } else {\",\n\t\t' Write-Fail \"Docker daemon is NOT running\"',\n\t\t\" }\",\n\t\t\" } catch {\",\n\t\t' Write-Warn \"Docker is not installed — running bare-metal checks only\"',\n\t\t\" }\",\n\t\t\" if ($script:DockerAvailable) {\",\n\t\t\" try {\",\n\t\t\" $null = docker compose version 2>&1\",\n\t\t\" if ($LASTEXITCODE -eq 0) {\",\n\t\t' Write-Pass \"Docker Compose v2 available\"',\n\t\t\" } else {\",\n\t\t' Write-Warn \"Docker Compose v2 not found\"',\n\t\t\" }\",\n\t\t\" } catch {\",\n\t\t' Write-Warn \"Docker Compose not available\"',\n\t\t\" }\",\n\t\t\" }\",\n\t\t' $envPath = Join-Path $PSScriptRoot \"..\\\\.env\"',\n\t\t\" if (Test-Path $envPath) {\",\n\t\t' Write-Pass \".env file exists\"',\n\t\t\" $emptySecrets = 0\",\n\t\t\" Get-Content $envPath | ForEach-Object {\",\n\t\t' if ($_ -match \"(PASSWORD|TOKEN|SECRET|SESSION_KEY|COOKIE|API_KEY).*=\\\\s*$\") { $emptySecrets++ }',\n\t\t\" }\",\n\t\t\" if ($emptySecrets -gt 0) {\",\n\t\t' Write-Warn \"$emptySecrets secret(s) are empty in .env\"',\n\t\t\" } else {\",\n\t\t' Write-Pass \"All secrets are populated\"',\n\t\t\" }\",\n\t\t\" } else {\",\n\t\t' Write-Warn \".env file not found\"',\n\t\t\" }\",\n\t\t\" $drive = (Get-Item $PSScriptRoot).PSDrive\",\n\t\t\" $freeGB = [math]::Round($drive.Free / 1GB, 1)\",\n\t\t\" if ($freeGB -lt 2) {\",\n\t\t' Write-Warn \"Low disk space: ${freeGB}GB available\"',\n\t\t\" } else {\",\n\t\t' Write-Pass \"Disk space: ${freeGB}GB available\"',\n\t\t\" }\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 2: Container Check\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"function Test-Container {\",\n\t\t\" param([string]$ServiceId, [string]$ServiceName, [string]$Icon)\",\n\t\t\" try {\",\n\t\t\" $status = docker compose ps $ServiceId --format json 2>&1 | ConvertFrom-Json\",\n\t\t\" } catch {\",\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) Container not found\"',\n\t\t\" return\",\n\t\t\" }\",\n\t\t\" if (-not $status) {\",\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) Container not found\"',\n\t\t\" return\",\n\t\t\" }\",\n\t\t\" $state = $status.State\",\n\t\t\" $health = $status.Health\",\n\t\t' if ($state -eq \"running\") {',\n\t\t' if ($health -eq \"healthy\") {',\n\t\t' Write-Pass \"$Icon $(Pad-Name $ServiceName) Running (healthy)\"',\n\t\t' } elseif ($health -eq \"starting\") {',\n\t\t' Write-Warn \"$Icon $(Pad-Name $ServiceName) Running (starting)\"',\n\t\t' } elseif ($health -eq \"unhealthy\") {',\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) Running (UNHEALTHY)\"',\n\t\t\" } else {\",\n\t\t' Write-Pass \"$Icon $(Pad-Name $ServiceName) Running\"',\n\t\t\" }\",\n\t\t' } elseif ($state -eq \"exited\") {',\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) Exited\"',\n\t\t\" } else {\",\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) State: $state\"',\n\t\t\" }\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 3: Port Check\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"function Test-Port {\",\n\t\t\" param([string]$ServiceId, [int]$Port, [string]$Description)\",\n\t\t\" try {\",\n\t\t\" $tcp = New-Object System.Net.Sockets.TcpClient\",\n\t\t' $tcp.ConnectAsync(\"localhost\", $Port).Wait(2000) | Out-Null',\n\t\t\" if ($tcp.Connected) {\",\n\t\t' Write-Dim \" Port $Port ($Description) — reachable\"',\n\t\t\" $tcp.Close()\",\n\t\t\" } else {\",\n\t\t' Write-Warn \" └─ ${ServiceId}: Port $Port ($Description) — NOT reachable\"',\n\t\t\" }\",\n\t\t\" } catch {\",\n\t\t' Write-Warn \" └─ ${ServiceId}: Port $Port ($Description) — NOT reachable\"',\n\t\t\" }\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ── Bare-metal process check ────────────────────────────────────────\",\n\t\t\"function Test-ProcessRunning {\",\n\t\t\" param([string]$ServiceId, [string]$ServiceName, [string]$Icon)\",\n\t\t\" $svc = Get-Service -Name $ServiceId -ErrorAction SilentlyContinue\",\n\t\t\" if ($svc) {\",\n\t\t' if ($svc.Status -eq \"Running\") {',\n\t\t' Write-Pass \"$Icon $(Pad-Name $ServiceName) Active (service)\"',\n\t\t\" } else {\",\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) $($svc.Status)\"',\n\t\t\" }\",\n\t\t\" return\",\n\t\t\" }\",\n\t\t\" $proc = Get-Process -Name $ServiceId -ErrorAction SilentlyContinue\",\n\t\t\" if ($proc) {\",\n\t\t' Write-Pass \"$Icon $(Pad-Name $ServiceName) Running (process)\"',\n\t\t\" } else {\",\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) NOT running\"',\n\t\t\" }\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 6: Log Scan\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"function Test-Logs {\",\n\t\t\" if ($Quick) {\",\n\t\t' if (-not $Json) { Write-Info \"Skipping log scan (-Quick mode)\" }',\n\t\t\" return\",\n\t\t\" }\",\n\t\t\" if (-not $Json) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"── Phase 6: Log Scan ───────────────────────────────────────\" -ForegroundColor White',\n\t\t' Write-Host \"\"',\n\t\t\" }\",\n\t\t\" $hasErrors = $false\",\n\t\tL(\" $svcIds = @(\", svcIdList, \")\"),\n\t\t\" foreach ($svcId in $svcIds) {\",\n\t\t\" try {\",\n\t\t\" $logs = docker compose logs --tail=50 $svcId 2>&1 | Out-String\",\n\t\t' $errorLines = $logs -split \"`n\" | Where-Object { $_ -match \"(error|fatal|panic|exception|segfault|killed|oom)\" } | Select-Object -Last 5',\n\t\t\" if ($errorLines) {\",\n\t\t\" $hasErrors = $true\",\n\t\t' Write-Warn \"$svcId — found error patterns in logs:\"',\n\t\t' $errorLines | ForEach-Object { Write-Dim \" | $_\" }',\n\t\t\" }\",\n\t\t\" } catch {}\",\n\t\t\" }\",\n\t\t\" if (-not $hasErrors -and -not $Json) {\",\n\t\t' Write-Pass \"No error patterns found in recent logs\"',\n\t\t\" }\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"# Main\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"\",\n\t\t\"if (-not $Json) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"═══════════════════════════════════════════════════════════════════════\" ',\n\t\tL(' Write-Host \" 🐾 OpenClaw Stack Health Report — ', name, '\"'),\n\t\t' Write-Host \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t\"}\",\n\t\t\"\",\n\t\t\"Test-Environment\",\n\t\t\"\",\n\t\t\"if ($script:DockerAvailable) {\",\n\t\t\" if (-not $Json) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"── Phase 2–4: Service Checks ──────────────────────────────\" -ForegroundColor White',\n\t\t' Write-Host \"\"',\n\t\t\" }\",\n\t\t\"\",\n\t\t...dockerChecks,\n\t\t\" Test-Logs\",\n\t\t\"} else {\",\n\t\t\" if (-not $Json) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"── Bare-Metal Service Checks ──────────────────────────────\" -ForegroundColor White',\n\t\t' Write-Host \"\"',\n\t\t\" }\",\n\t\t\"\",\n\t\t...bmChecks,\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ── Summary ──────────────────────────────────────────────────────────\",\n\t\t\"if ($Json) {\",\n\t\t\" $result = @{\",\n\t\tL(' project = \"', name, '\"'),\n\t\t' timestamp = (Get-Date -Format \"o\")',\n\t\t\" summary = @{\",\n\t\t\" total = $script:Total\",\n\t\t\" healthy = $script:Healthy\",\n\t\t\" warnings = $script:Warnings\",\n\t\t\" failed = $script:Failed\",\n\t\t\" }\",\n\t\t\" errors = $script:Errors\",\n\t\t\" }\",\n\t\t\" $result | ConvertTo-Json -Depth 5\",\n\t\t\"} else {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \" Summary: $($script:Total) services checked\"',\n\t\t' Write-Host \" Healthy: $($script:Healthy)\" -ForegroundColor Green -NoNewline',\n\t\t' Write-Host \" | Warnings: $($script:Warnings)\" -ForegroundColor Yellow -NoNewline',\n\t\t' Write-Host \" | Failed: $($script:Failed)\" -ForegroundColor Red',\n\t\t\" if ($script:Errors.Count -gt 0) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \" ── Errors ──────────────────────────────────────────────────\" -ForegroundColor Red',\n\t\t' $script:Errors | ForEach-Object { Write-Host \" $_\" -ForegroundColor Red }',\n\t\t\" }\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t' Write-Host \"\"',\n\t\t\" if ($script:Failed -gt 0) {\",\n\t\t' Write-Host \" Some services need attention. Run with -Detailed for more.\" -ForegroundColor Red',\n\t\t\" exit 1\",\n\t\t\" } else {\",\n\t\t' Write-Host \" 🎉 All services are running!\" -ForegroundColor Green',\n\t\t\" exit 0\",\n\t\t\" }\",\n\t\t\"}\",\n\t];\n\n\treturn lines.join(\"\\n\") + \"\\n\";\n}\n"],"mappings":";;;;;;;;AAkBA,SAAgB,oBACf,UACA,SACyB;CACzB,MAAM,QAAgC,EAAE;AAExC,OAAM,6BAA6B,mBAAmB,UAAU,QAAQ;AACxE,OAAM,8BAA8B,yBAAyB,UAAU,QAAQ;AAE/E,QAAO;;AAaR,SAAS,qBAAqB,UAA0C;AACvE,QAAO,SAAS,SAAS,KAAK,SAAS;EACtC,IAAI,IAAI,WAAW;EACnB,MAAM,IAAI,WAAW;EACrB,MAAM,IAAI,WAAW;EACrB,OAAO,IAAI,WAAW,MAAM,KAAK,OAAO;GACvC,MAAM,EAAE;GACR,WAAW,EAAE;GACb,aAAa,EAAE;GACf,SAAS,EAAE;GACX,EAAE;EACH,gBAAgB,IAAI,WAAW,aAAa,QAAQ;EACpD,EAAE;;AAGJ,SAAS,YAAY,GAAmB;AACvC,QAAO,EAAE,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,OAAM;;AAIrD,SAAS,EAAE,GAAG,OAAyB;AACtC,QAAO,MAAM,KAAK,GAAG;;AAKtB,SAAS,mBAAmB,UAA0B,SAAqC;CAC1F,MAAM,SAAS,qBAAqB,SAAS;CAC7C,MAAM,OAAO,QAAQ;CACrB,MAAM,QAAQ,OAAO;CAGrB,MAAM,eAAyB,EAAE;AACjC,MAAK,MAAM,OAAO,QAAQ;AACzB,eAAa,KAAK,EAAE,WAAW,IAAI,MAAM,KAAK,IAAI,MAAM,MAAM,CAAC;AAC/D,eAAa,KAAK,EAAE,wBAAuB,IAAI,IAAI,SAAO,IAAI,MAAM,SAAO,IAAI,MAAM,KAAI,CAAC;AAC1F,OAAK,MAAM,KAAK,IAAI,MAAM,QAAQ,OAAO,GAAG,QAAQ,CACnD,cAAa,KAAK,EAAE,mBAAkB,IAAI,IAAI,OAAM,OAAO,EAAE,KAAK,EAAE,OAAM,EAAE,aAAa,KAAI,CAAC;AAE/F,MAAI,IAAI,eACP,cAAa,KACZ,EAAE,yBAAwB,IAAI,IAAI,SAAO,YAAY,IAAI,eAAe,EAAE,KAAI,CAC9E;MAED,cAAa,KAAK,mDAAmD;AAEtE,eAAa,KAAK,GAAG;;CAItB,MAAM,WAAqB,EAAE;AAC7B,MAAK,MAAM,OAAO,QAAQ;AACzB,WAAS,KAAK,EAAE,WAAW,IAAI,MAAM,KAAK,IAAI,MAAM,mBAAmB,CAAC;AACxE,WAAS,KAAK,EAAE,sBAAqB,IAAI,IAAI,SAAO,IAAI,MAAM,SAAO,IAAI,MAAM,KAAI,CAAC;AACpF,OAAK,MAAM,KAAK,IAAI,MAAM,QAAQ,OAAO,GAAG,QAAQ,CACnD,UAAS,KAAK,EAAE,mBAAkB,IAAI,IAAI,OAAM,OAAO,EAAE,KAAK,EAAE,OAAM,EAAE,aAAa,KAAI,CAAC;AAE3F,WAAS,KAAK,GAAG;;CAIlB,MAAM,YAAY,OAAO,KAAK,MAAM,OAAM,EAAE,KAAK,KAAI,CAAC,KAAK,IAAI;AA6V/D,QA3VwB;EACvB;EACA;EACA;EACA;EACA,EAAE,uCAAuC,KAAK;EAC9C;EACA;EACA;EACA,EAAE,aAAa,OAAO,MAAM,EAAE,6BAA6B;EAC3D;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,UAAU,OAAO,MAAM,CAAC;EAC1B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,oBAAoB,WAAW,OAAO;EACxC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,gDAA+C,MAAM,KAAI;EAC3D;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,CAEY,KAAK,KAAK,GAAG;;AAK3B,SAAS,yBAAyB,UAA0B,SAAqC;CAChG,MAAM,SAAS,qBAAqB,SAAS;CAC7C,MAAM,OAAO,QAAQ;CACrB,MAAM,QAAQ,OAAO;CAGrB,MAAM,eAAyB,EAAE;AACjC,MAAK,MAAM,OAAO,QAAQ;AACzB,eAAa,KAAK,EAAE,UAAU,IAAI,MAAM,KAAK,IAAI,KAAK,CAAC;AACvD,eAAa,KACZ,EAAE,oCAAmC,IAAI,IAAI,sBAAoB,IAAI,MAAM,eAAa,IAAI,MAAM,KAAI,CACtG;AACD,OAAK,MAAM,KAAK,IAAI,MAAM,QAAQ,OAAO,GAAG,QAAQ,CACnD,cAAa,KACZ,EAAE,+BAA8B,IAAI,IAAI,aAAY,OAAO,EAAE,KAAK,EAAE,oBAAmB,EAAE,aAAa,KAAI,CAC1G;AAEF,eAAa,KAAK,GAAG;;CAItB,MAAM,WAAqB,EAAE;AAC7B,MAAK,MAAM,OAAO,QAAQ;AACzB,WAAS,KAAK,EAAE,UAAU,IAAI,MAAM,KAAK,IAAI,KAAK,CAAC;AACnD,WAAS,KACR,EAAE,yCAAwC,IAAI,IAAI,sBAAoB,IAAI,MAAM,eAAa,IAAI,MAAM,KAAI,CAC3G;AACD,OAAK,MAAM,KAAK,IAAI,MAAM,QAAQ,OAAO,GAAG,QAAQ,CACnD,UAAS,KACR,EAAE,+BAA8B,IAAI,IAAI,aAAY,OAAO,EAAE,KAAK,EAAE,oBAAmB,EAAE,aAAa,KAAI,CAC1G;AAEF,WAAS,KAAK,GAAG;;CAGlB,MAAM,YAAY,OAAO,KAAK,MAAM,OAAM,EAAE,KAAK,KAAI,CAAC,KAAK,KAAK;AA0RhE,QAxRwB;EACvB;EACA;EACA;EACA,EAAE,sCAAsC,KAAK;EAC7C;EACA;EACA;EACA,EAAE,eAAe,OAAO,MAAM,EAAE,kEAAkE;EAClG;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,oBAAoB,OAAO,MAAM,CAAC;EACpC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,oBAAoB,WAAW,IAAI;EACrC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,wDAAuD,MAAM,KAAI;EACnE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACA;EACA;EACA,EAAE,wBAAuB,MAAM,KAAI;EACnC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,CAEY,KAAK,KAAK,GAAG"}
1
+ {"version":3,"file":"health-check.mjs","names":[],"sources":["../../src/generators/health-check.ts"],"sourcesContent":["import type { ResolverOutput } from \"../types.js\";\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\ninterface HealthCheckOptions {\n\tprojectName: string;\n\tdeploymentType?: \"docker\" | \"bare-metal\";\n}\n\n// ─── Generator ──────────────────────────────────────────────────────────────\n\n/**\n * Generates stack-specific health check and verification scripts.\n *\n * Each generated script is dynamic — it contains the exact service names,\n * ports, and health check commands from the resolved stack so users get\n * a precise, zero-configuration verifier.\n */\nexport function generateHealthCheck(\n\tresolved: ResolverOutput,\n\toptions: HealthCheckOptions,\n): Record<string, string> {\n\tconst files: Record<string, string> = {};\n\n\tfiles[\"scripts/health-check.sh\"] = generateBashScript(resolved, options);\n\tfiles[\"scripts/health-check.ps1\"] = generatePowerShellScript(resolved, options);\n\n\treturn files;\n}\n\n// ─── Service metadata extraction ────────────────────────────────────────────\n\ninterface ServiceCheck {\n\tid: string;\n\tname: string;\n\ticon: string;\n\tports: Array<{ host: number; container: number; description: string; exposed: boolean }>;\n\thealthCheckCmd: string | null;\n}\n\nfunction extractServiceChecks(resolved: ResolverOutput): ServiceCheck[] {\n\treturn resolved.services.map((svc) => ({\n\t\tid: svc.definition.id,\n\t\tname: svc.definition.name,\n\t\ticon: svc.definition.icon,\n\t\tports: svc.definition.ports.map((p) => ({\n\t\t\thost: p.host,\n\t\t\tcontainer: p.container,\n\t\t\tdescription: p.description,\n\t\t\texposed: p.exposed,\n\t\t})),\n\t\thealthCheckCmd: svc.definition.healthcheck?.test ?? null,\n\t}));\n}\n\nfunction escapeShell(s: string): string {\n\treturn s.replace(/'/g, \"'\\\\''\").replace(/\"/g, '\\\\\"');\n}\n\n// Use L() to build lines safely — avoids template literal issues with shell syntax\nfunction L(...parts: string[]): string {\n\treturn parts.join(\"\");\n}\n\n// ─── Bash Script ────────────────────────────────────────────────────────────\n\nfunction generateBashScript(resolved: ResolverOutput, options: HealthCheckOptions): string {\n\tconst checks = extractServiceChecks(resolved);\n\tconst name = options.projectName;\n\tconst total = checks.length;\n\n\t// Build per-service docker check calls\n\tconst dockerChecks: string[] = [];\n\tfor (const svc of checks) {\n\t\tdockerChecks.push(L(\" # ── \", svc.icon, \" \", svc.name, \" ──\"));\n\t\tdockerChecks.push(L(' check_container \"', svc.id, '\" \"', svc.name, '\" \"', svc.icon, '\"'));\n\t\tfor (const p of svc.ports.filter((pp) => pp.exposed)) {\n\t\t\tdockerChecks.push(\n\t\t\t\tL(' check_port \"', svc.id, '\" ', String(p.host), ' \"', p.description, '\"'),\n\t\t\t);\n\t\t}\n\t\tif (svc.healthCheckCmd) {\n\t\t\tdockerChecks.push(\n\t\t\t\tL(' check_health_cmd \"', svc.id, '\" \"', escapeShell(svc.healthCheckCmd), '\"'),\n\t\t\t);\n\t\t} else {\n\t\t\tdockerChecks.push(\" # No healthcheck defined — skipping exec check\");\n\t\t}\n\t\tdockerChecks.push(\"\");\n\t}\n\n\t// Build per-service bare-metal check calls\n\tconst bmChecks: string[] = [];\n\tfor (const svc of checks) {\n\t\tbmChecks.push(L(\" # ── \", svc.icon, \" \", svc.name, \" (bare-metal) ──\"));\n\t\tbmChecks.push(L(' check_process \"', svc.id, '\" \"', svc.name, '\" \"', svc.icon, '\"'));\n\t\tfor (const p of svc.ports.filter((pp) => pp.exposed)) {\n\t\t\tbmChecks.push(L(' check_port \"', svc.id, '\" ', String(p.host), ' \"', p.description, '\"'));\n\t\t}\n\t\tbmChecks.push(\"\");\n\t}\n\n\t// Build the log-scan service list\n\tconst svcIdList = checks.map((s) => '\"' + s.id + '\"').join(\" \");\n\n\tconst lines: string[] = [\n\t\t\"#!/usr/bin/env bash\",\n\t\t\"set -uo pipefail\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\tL(\"# 🐾 OpenClaw Stack Health Check — \", name),\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"#\",\n\t\t\"# Auto-generated verification script for your stack.\",\n\t\tL(\"# Checks \", String(total), \" services across 6 phases:\"),\n\t\t\"# 1. Environment — prerequisites, .env, secrets\",\n\t\t\"# 2. Container — running state, health status, restart count\",\n\t\t\"# 3. Port — TCP reachability for exposed ports\",\n\t\t\"# 4. Health Command — execute each service's health check\",\n\t\t\"# 5. Resources — memory/CPU warnings\",\n\t\t\"# 6. Logs — scan for ERROR/FATAL/panic patterns\",\n\t\t\"#\",\n\t\t\"# Usage:\",\n\t\t\"# ./scripts/health-check.sh # Full check\",\n\t\t\"# ./scripts/health-check.sh --quick # Skip log scan\",\n\t\t\"# ./scripts/health-check.sh --json # Output as JSON\",\n\t\t\"#\",\n\t\t\"# Exit codes: 0 = all healthy, 1 = some issues, 2 = critical failure\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"\",\n\t\t'SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"',\n\t\t'PROJECT_DIR=\"$(dirname \"$SCRIPT_DIR\")\"',\n\t\t'cd \"$PROJECT_DIR\"',\n\t\t\"\",\n\t\t\"# ── Arguments ────────────────────────────────────────────────────────\",\n\t\t\"QUICK_MODE=false\",\n\t\t\"JSON_MODE=false\",\n\t\t\"VERBOSE=false\",\n\t\t'for arg in \"$@\"; do',\n\t\t' case \"$arg\" in',\n\t\t\" --quick) QUICK_MODE=true ;;\",\n\t\t\" --json) JSON_MODE=true ;;\",\n\t\t\" --verbose|-v) VERBOSE=true ;;\",\n\t\t\" esac\",\n\t\t\"done\",\n\t\t\"\",\n\t\t\"# ── Colour helpers ───────────────────────────────────────────────────\",\n\t\t'if [ -t 1 ] && [ \"$JSON_MODE\" = false ]; then',\n\t\t\" RED='\\\\033[0;31m'; GREEN='\\\\033[0;32m'; YELLOW='\\\\033[1;33m'\",\n\t\t\" CYAN='\\\\033[0;36m'; DIM='\\\\033[2m'; BOLD='\\\\033[1m'; NC='\\\\033[0m'\",\n\t\t\"else\",\n\t\t\" RED=''; GREEN=''; YELLOW=''; CYAN=''; DIM=''; BOLD=''; NC=''\",\n\t\t\"fi\",\n\t\t\"\",\n\t\t\"# ── Counters ─────────────────────────────────────────────────────────\",\n\t\tL(\"TOTAL=\", String(total)),\n\t\t\"HEALTHY=0\",\n\t\t\"WARNING=0\",\n\t\t\"FAILED=0\",\n\t\t\"ERRORS=()\",\n\t\t\"WARNINGS_LIST=()\",\n\t\t\"SERVICE_RESULTS=()\",\n\t\t\"\",\n\t\t\"# ── Utility functions ────────────────────────────────────────────────\",\n\t\t'pass() { echo -e \" ${GREEN}✅ $*${NC}\"; HEALTHY=$((HEALTHY + 1)); }',\n\t\t'warn_svc() { echo -e \" ${YELLOW}⚠️ $*${NC}\"; WARNING=$((WARNING + 1)); WARNINGS_LIST+=(\"$*\"); }',\n\t\t'fail() { echo -e \" ${RED}❌ $*${NC}\"; FAILED=$((FAILED + 1)); ERRORS+=(\"$*\"); }',\n\t\t'info() { echo -e \" ${CYAN}ℹ $*${NC}\"; }',\n\t\t'dim() { echo -e \" ${DIM}$*${NC}\"; }',\n\t\t\"\",\n\t\t\"pad_name() {\",\n\t\t' local name=\"$1\"',\n\t\t\" local pad=25\",\n\t\t' printf \"%-${pad}s\" \"$name\"',\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 1: Environment\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"phase_environment() {\",\n\t\t' if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo -e \"${BOLD}── Phase 1: Environment ────────────────────────────────────${NC}\"',\n\t\t' echo \"\"',\n\t\t\" fi\",\n\t\t\"\",\n\t\t\" # Docker\",\n\t\t\" if command -v docker &>/dev/null; then\",\n\t\t\" if docker info &>/dev/null 2>&1; then\",\n\t\t' pass \"Docker daemon is running\"',\n\t\t\" DOCKER_AVAILABLE=true\",\n\t\t\" else\",\n\t\t' fail \"Docker daemon is NOT running\"',\n\t\t\" DOCKER_AVAILABLE=false\",\n\t\t\" fi\",\n\t\t\" else\",\n\t\t' warn_svc \"Docker is not installed — running bare-metal checks only\"',\n\t\t\" DOCKER_AVAILABLE=false\",\n\t\t\" fi\",\n\t\t\"\",\n\t\t\" # Docker Compose\",\n\t\t' if [ \"$DOCKER_AVAILABLE\" = true ]; then',\n\t\t\" if docker compose version &>/dev/null 2>&1; then\",\n\t\t' pass \"Docker Compose v2 available\"',\n\t\t\" else\",\n\t\t' warn_svc \"Docker Compose v2 not found\"',\n\t\t\" fi\",\n\t\t\" fi\",\n\t\t\"\",\n\t\t\" # .env\",\n\t\t' if [ -f \".env\" ]; then',\n\t\t' pass \".env file exists\"',\n\t\t\" EMPTY_SECRETS=0\",\n\t\t\" while IFS='=' read -r key value; do\",\n\t\t' [[ \"$key\" =~ ^#.*$ ]] && continue',\n\t\t' [[ -z \"$key\" ]] && continue',\n\t\t' if [[ \"$key\" =~ (PASSWORD|TOKEN|SECRET|SESSION_KEY|COOKIE|API_KEY) ]] && [[ -z \"${value:-}\" ]]; then',\n\t\t' if [ \"$VERBOSE\" = true ]; then',\n\t\t' warn_svc \"Empty secret: $key\"',\n\t\t\" fi\",\n\t\t\" EMPTY_SECRETS=$((EMPTY_SECRETS + 1))\",\n\t\t\" fi\",\n\t\t\" done < .env 2>/dev/null || true\",\n\t\t' if [ \"$EMPTY_SECRETS\" -gt 0 ]; then',\n\t\t' warn_svc \"$EMPTY_SECRETS secret(s) are empty in .env\"',\n\t\t\" else\",\n\t\t' pass \"All secrets are populated\"',\n\t\t\" fi\",\n\t\t\" else\",\n\t\t' warn_svc \".env file not found\"',\n\t\t\" fi\",\n\t\t\"\",\n\t\t\" # Disk space\",\n\t\t\" AVAIL_KB=$(df -k . 2>/dev/null | awk 'NR==2{print $4}' || echo \\\"0\\\")\",\n\t\t\" AVAIL_GB=$((AVAIL_KB / 1024 / 1024))\",\n\t\t' if [ \"$AVAIL_GB\" -lt 2 ]; then',\n\t\t' warn_svc \"Low disk space: ${AVAIL_GB}GB available\"',\n\t\t\" else\",\n\t\t' pass \"Disk space: ${AVAIL_GB}GB available\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 2: Container Status\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"check_container() {\",\n\t\t' local id=\"$1\" name=\"$2\" icon=\"$3\"',\n\t\t\"\",\n\t\t\" local status restarts\",\n\t\t' status=$(docker compose ps \"$id\" --format json 2>/dev/null | head -1)',\n\t\t\"\",\n\t\t' if [ -z \"$status\" ]; then',\n\t\t' fail \"$icon $(pad_name \"$name\") — Container not found\"',\n\t\t\" return\",\n\t\t\" fi\",\n\t\t\"\",\n\t\t\" local state health_status\",\n\t\t' state=$(echo \"$status\" | grep -o \\'\"State\":\"[^\"]*\"\\' | cut -d\\'\"\\' -f4 || echo \"unknown\")',\n\t\t' health_status=$(echo \"$status\" | grep -o \\'\"Health\":\"[^\"]*\"\\' | cut -d\\'\"\\' -f4 || echo \"none\")',\n\t\t\"\",\n\t\t' restarts=$(docker inspect --format=\"{{.RestartCount}}\" \"$(docker compose ps -q \"$id\" 2>/dev/null | head -1)\" 2>/dev/null || echo \"0\")',\n\t\t\"\",\n\t\t' if [ \"$state\" = \"running\" ]; then',\n\t\t' if [ \"$health_status\" = \"healthy\" ]; then',\n\t\t' pass \"$icon $(pad_name \"$name\") Running (healthy) restarts: $restarts\"',\n\t\t' elif [ \"$health_status\" = \"starting\" ]; then',\n\t\t' warn_svc \"$icon $(pad_name \"$name\") Running (starting) restarts: $restarts\"',\n\t\t' elif [ \"$health_status\" = \"unhealthy\" ]; then',\n\t\t' fail \"$icon $(pad_name \"$name\") Running (UNHEALTHY) restarts: $restarts\"',\n\t\t\" else\",\n\t\t' pass \"$icon $(pad_name \"$name\") Running restarts: $restarts\"',\n\t\t\" fi\",\n\t\t' if [ \"$restarts\" -gt 5 ] 2>/dev/null; then',\n\t\t' warn_svc \" └─ $name has restarted $restarts times (possible crash loop)\"',\n\t\t\" fi\",\n\t\t' elif [ \"$state\" = \"exited\" ]; then',\n\t\t\" local exit_code\",\n\t\t' exit_code=$(docker inspect --format=\"{{.State.ExitCode}}\" \"$(docker compose ps -q \"$id\" 2>/dev/null | head -1)\" 2>/dev/null || echo \"?\")',\n\t\t' fail \"$icon $(pad_name \"$name\") Exited (code $exit_code)\"',\n\t\t' if [ \"$exit_code\" = \"137\" ]; then',\n\t\t' ERRORS+=(\" └─ $name: OOM killed (exit 137). Increase memory limits.\")',\n\t\t' elif [ \"$exit_code\" = \"1\" ]; then',\n\t\t' ERRORS+=(\" └─ $name: Application error (exit 1). Check logs: docker compose logs $id\")',\n\t\t\" fi\",\n\t\t\" else\",\n\t\t' fail \"$icon $(pad_name \"$name\") State: $state\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 3: Port Reachability\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"check_port() {\",\n\t\t' local id=\"$1\" port=\"$2\" desc=\"$3\"',\n\t\t\" if command -v nc &>/dev/null; then\",\n\t\t' if nc -z -w 2 localhost \"$port\" 2>/dev/null; then',\n\t\t' dim \" Port $port ($desc) — reachable\"',\n\t\t\" else\",\n\t\t' warn_svc \" └─ $id: Port $port ($desc) — NOT reachable\"',\n\t\t\" fi\",\n\t\t\" elif command -v curl &>/dev/null; then\",\n\t\t' if curl -sf --max-time 2 \"http://localhost:$port/\" &>/dev/null; then',\n\t\t' dim \" Port $port ($desc) — reachable\"',\n\t\t\" else\",\n\t\t' warn_svc \" └─ $id: Port $port ($desc) — NOT reachable\"',\n\t\t\" fi\",\n\t\t\" else\",\n\t\t' dim \" Port $port ($desc) — skipped (no nc or curl)\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 4: Health Check Commands\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"check_health_cmd() {\",\n\t\t' local id=\"$1\" cmd=\"$2\"',\n\t\t' local cid=$(docker compose ps -q \"$id\" 2>/dev/null | head -1)',\n\t\t' [ -z \"$cid\" ] && return',\n\t\t' if docker exec \"$cid\" sh -c \"$cmd\" &>/dev/null; then',\n\t\t' dim \" Health command passed\"',\n\t\t\" else\",\n\t\t' warn_svc \" └─ $id: Health check command failed\"',\n\t\t' if [ \"$VERBOSE\" = true ]; then dim \" Command: $cmd\"; fi',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 5: Resource Usage\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"phase_resources() {\",\n\t\t' if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo -e \"${BOLD}── Phase 5: Resource Usage ─────────────────────────────────${NC}\"',\n\t\t' echo \"\"',\n\t\t\" fi\",\n\t\t' docker stats --no-stream --format \"table {{.Name}}\\\\t{{.CPUPerc}}\\\\t{{.MemUsage}}\\\\t{{.MemPerc}}\" 2>/dev/null || true',\n\t\t' if [ \"$VERBOSE\" = false ] && [ \"$JSON_MODE\" = false ]; then',\n\t\t' info \"Use --verbose to see warnings for high memory usage\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 6: Log Scan\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"phase_logs() {\",\n\t\t' if [ \"$QUICK_MODE\" = true ]; then',\n\t\t' if [ \"$JSON_MODE\" = false ]; then echo \"\"; info \"Skipping log scan (--quick mode)\"; fi',\n\t\t\" return\",\n\t\t\" fi\",\n\t\t' if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo -e \"${BOLD}── Phase 6: Log Scan ───────────────────────────────────────${NC}\"',\n\t\t' echo \"\"',\n\t\t\" fi\",\n\t\t\" local has_errors=false\",\n\t\tL(\" for svc_id in \", svcIdList, \"; do\"),\n\t\t\" local error_lines\",\n\t\t' error_lines=$(docker compose logs --tail=50 \"$svc_id\" 2>/dev/null | grep -iE \"(error|fatal|panic|exception|segfault|killed|oom)\" | tail -5 || true)',\n\t\t' if [ -n \"$error_lines\" ]; then',\n\t\t\" has_errors=true\",\n\t\t' warn_svc \"$svc_id — found error patterns in logs:\"',\n\t\t' echo \"$error_lines\" | while IFS= read -r eline; do',\n\t\t' dim \" | $eline\"',\n\t\t\" done\",\n\t\t\" fi\",\n\t\t\" done\",\n\t\t' if [ \"$has_errors\" = false ] && [ \"$JSON_MODE\" = false ]; then',\n\t\t' pass \"No error patterns found in recent logs\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ── Bare-metal process check ────────────────────────────────────────\",\n\t\t\"check_process() {\",\n\t\t' local id=\"$1\" name=\"$2\" icon=\"$3\"',\n\t\t\" if command -v systemctl &>/dev/null; then\",\n\t\t' if systemctl is-active --quiet \"$id\" 2>/dev/null; then',\n\t\t' pass \"$icon $(pad_name \"$name\") Active (systemd)\"',\n\t\t\" return\",\n\t\t\" fi\",\n\t\t\" fi\",\n\t\t' if pgrep -f \"$id\" &>/dev/null; then',\n\t\t' pass \"$icon $(pad_name \"$name\") Running (process)\"',\n\t\t\" else\",\n\t\t' fail \"$icon $(pad_name \"$name\") NOT running\"',\n\t\t\" fi\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"# Main\",\n\t\t\"# ═══════════════════════════════════════════════════════════════════════\",\n\t\t\"\",\n\t\t\"DOCKER_AVAILABLE=false\",\n\t\t\"\",\n\t\t'if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo \"═══════════════════════════════════════════════════════════════════════\"',\n\t\tL(' echo \" 🐾 OpenClaw Stack Health Report — ', name, '\"'),\n\t\t' echo \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t\"fi\",\n\t\t\"\",\n\t\t\"phase_environment\",\n\t\t\"\",\n\t\t'if [ \"$DOCKER_AVAILABLE\" = true ]; then',\n\t\t' if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo -e \"${BOLD}── Phase 2–4: Service Checks ──────────────────────────────${NC}\"',\n\t\t' echo \"\"',\n\t\t\" fi\",\n\t\t\"\",\n\t\t...dockerChecks,\n\t\t\"\",\n\t\t\" phase_resources\",\n\t\t\" phase_logs\",\n\t\t\"\",\n\t\t\"else\",\n\t\t' if [ \"$JSON_MODE\" = false ]; then',\n\t\t' echo \"\"',\n\t\t' echo -e \"${BOLD}── Bare-Metal Service Checks ──────────────────────────────${NC}\"',\n\t\t' echo \"\"',\n\t\t\" fi\",\n\t\t\"\",\n\t\t...bmChecks,\n\t\t\"fi\",\n\t\t\"\",\n\t\t\"# ── Summary ──────────────────────────────────────────────────────────\",\n\t\t'echo \"\"',\n\t\t'echo \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t'echo \"\"',\n\t\t'echo -e \" ${BOLD}Summary${NC}: $TOTAL services checked\"',\n\t\t'echo -e \" ${GREEN}Healthy: $HEALTHY${NC} | ${YELLOW}Warnings: $WARNING${NC} | ${RED}Failed: $FAILED${NC}\"',\n\t\t\"\",\n\t\t\"if [ ${#ERRORS[@]} -gt 0 ]; then\",\n\t\t' echo \"\"',\n\t\t' echo -e \" ${BOLD}${RED}── Errors ─────────────────────────────────────────────────${NC}\"',\n\t\t' for e in \"${ERRORS[@]}\"; do',\n\t\t' echo -e \" ${RED}$e${NC}\"',\n\t\t\" done\",\n\t\t\"fi\",\n\t\t\"\",\n\t\t'echo \"\"',\n\t\t'echo \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t'echo \"\"',\n\t\t\"\",\n\t\t'if [ \"$FAILED\" -gt 0 ]; then',\n\t\t' echo -e \" ${RED}Some services need attention. Run with --verbose for details.${NC}\"',\n\t\t\" exit 1\",\n\t\t\"else\",\n\t\t' echo -e \" ${GREEN}🎉 All services are running!${NC}\"',\n\t\t\" exit 0\",\n\t\t\"fi\",\n\t];\n\n\treturn lines.join(\"\\n\") + \"\\n\";\n}\n\n// ─── PowerShell Script ──────────────────────────────────────────────────────\n\nfunction generatePowerShellScript(resolved: ResolverOutput, options: HealthCheckOptions): string {\n\tconst checks = extractServiceChecks(resolved);\n\tconst name = options.projectName;\n\tconst total = checks.length;\n\n\t// Build per-service docker check calls\n\tconst dockerChecks: string[] = [];\n\tfor (const svc of checks) {\n\t\tdockerChecks.push(L(\" # \", svc.icon, \" \", svc.name));\n\t\tdockerChecks.push(\n\t\t\tL(\n\t\t\t\t' Test-Container -ServiceId \"',\n\t\t\t\tsvc.id,\n\t\t\t\t'\" -ServiceName \"',\n\t\t\t\tsvc.name,\n\t\t\t\t'\" -Icon \"',\n\t\t\t\tsvc.icon,\n\t\t\t\t'\"',\n\t\t\t),\n\t\t);\n\t\tfor (const p of svc.ports.filter((pp) => pp.exposed)) {\n\t\t\tdockerChecks.push(\n\t\t\t\tL(\n\t\t\t\t\t' Test-Port -ServiceId \"',\n\t\t\t\t\tsvc.id,\n\t\t\t\t\t'\" -Port ',\n\t\t\t\t\tString(p.host),\n\t\t\t\t\t' -Description \"',\n\t\t\t\t\tp.description,\n\t\t\t\t\t'\"',\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\tdockerChecks.push(\"\");\n\t}\n\n\t// Build per-service bare-metal check calls\n\tconst bmChecks: string[] = [];\n\tfor (const svc of checks) {\n\t\tbmChecks.push(L(\" # \", svc.icon, \" \", svc.name));\n\t\tbmChecks.push(\n\t\t\tL(\n\t\t\t\t' Test-ProcessRunning -ServiceId \"',\n\t\t\t\tsvc.id,\n\t\t\t\t'\" -ServiceName \"',\n\t\t\t\tsvc.name,\n\t\t\t\t'\" -Icon \"',\n\t\t\t\tsvc.icon,\n\t\t\t\t'\"',\n\t\t\t),\n\t\t);\n\t\tfor (const p of svc.ports.filter((pp) => pp.exposed)) {\n\t\t\tbmChecks.push(\n\t\t\t\tL(\n\t\t\t\t\t' Test-Port -ServiceId \"',\n\t\t\t\t\tsvc.id,\n\t\t\t\t\t'\" -Port ',\n\t\t\t\t\tString(p.host),\n\t\t\t\t\t' -Description \"',\n\t\t\t\t\tp.description,\n\t\t\t\t\t'\"',\n\t\t\t\t),\n\t\t\t);\n\t\t}\n\t\tbmChecks.push(\"\");\n\t}\n\n\tconst svcIdList = checks.map((s) => '\"' + s.id + '\"').join(\", \");\n\n\tconst lines: string[] = [\n\t\t\"#Requires -Version 5.1\",\n\t\t\"<#\",\n\t\t\".SYNOPSIS\",\n\t\tL(\" OpenClaw Stack Health Check — \", name),\n\t\t\"\",\n\t\t\".DESCRIPTION\",\n\t\t\" Auto-generated verification script for your stack.\",\n\t\tL(\n\t\t\t\" Checks \",\n\t\t\tString(total),\n\t\t\t\" services: container status, port reachability, and log errors.\",\n\t\t),\n\t\t\"\",\n\t\t\".PARAMETER Quick\",\n\t\t\" Skip log scanning for faster results.\",\n\t\t\"\",\n\t\t\".PARAMETER Json\",\n\t\t\" Output results as JSON.\",\n\t\t\"\",\n\t\t\".EXAMPLE\",\n\t\t\" .\\\\scripts\\\\health-check.ps1\",\n\t\t\" .\\\\scripts\\\\health-check.ps1 -Quick\",\n\t\t\" .\\\\scripts\\\\health-check.ps1 -Json\",\n\t\t\"#>\",\n\t\t\"param(\",\n\t\t\" [switch]$Quick,\",\n\t\t\" [switch]$Json,\",\n\t\t\" [switch]$Detailed\",\n\t\t\")\",\n\t\t\"\",\n\t\t'$ErrorActionPreference = \"Continue\"',\n\t\t\"\",\n\t\t\"# ── Counters ─────────────────────────────────────────────────────────\",\n\t\tL(\"$script:Total = \", String(total)),\n\t\t\"$script:Healthy = 0\",\n\t\t\"$script:Warnings = 0\",\n\t\t\"$script:Failed = 0\",\n\t\t\"$script:Errors = @()\",\n\t\t\"$script:ServiceResults = @()\",\n\t\t\"$script:DockerAvailable = $false\",\n\t\t\"\",\n\t\t\"# ── Helpers ──────────────────────────────────────────────────────────\",\n\t\t\"\",\n\t\t'function Write-Pass($msg) { Write-Host \" ✅ $msg\" -ForegroundColor Green; $script:Healthy++ }',\n\t\t'function Write-Warn($msg) { Write-Host \" ⚠️ $msg\" -ForegroundColor Yellow; $script:Warnings++ }',\n\t\t'function Write-Fail($msg) { Write-Host \" ❌ $msg\" -ForegroundColor Red; $script:Failed++; $script:Errors += $msg }',\n\t\t'function Write-Info($msg) { Write-Host \" ℹ $msg\" -ForegroundColor Cyan }',\n\t\t'function Write-Dim($msg) { Write-Host \" $msg\" -ForegroundColor DarkGray }',\n\t\t\"\",\n\t\t\"function Pad-Name([string]$n) { return $n.PadRight(25) }\",\n\t\t\"\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 1: Environment\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"function Test-Environment {\",\n\t\t\" if (-not $Json) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"── Phase 1: Environment ────────────────────────────────────\" -ForegroundColor White',\n\t\t' Write-Host \"\"',\n\t\t\" }\",\n\t\t\" try {\",\n\t\t\" $null = docker info 2>&1\",\n\t\t\" if ($LASTEXITCODE -eq 0) {\",\n\t\t' Write-Pass \"Docker daemon is running\"',\n\t\t\" $script:DockerAvailable = $true\",\n\t\t\" } else {\",\n\t\t' Write-Fail \"Docker daemon is NOT running\"',\n\t\t\" }\",\n\t\t\" } catch {\",\n\t\t' Write-Warn \"Docker is not installed — running bare-metal checks only\"',\n\t\t\" }\",\n\t\t\" if ($script:DockerAvailable) {\",\n\t\t\" try {\",\n\t\t\" $null = docker compose version 2>&1\",\n\t\t\" if ($LASTEXITCODE -eq 0) {\",\n\t\t' Write-Pass \"Docker Compose v2 available\"',\n\t\t\" } else {\",\n\t\t' Write-Warn \"Docker Compose v2 not found\"',\n\t\t\" }\",\n\t\t\" } catch {\",\n\t\t' Write-Warn \"Docker Compose not available\"',\n\t\t\" }\",\n\t\t\" }\",\n\t\t' $envPath = Join-Path $PSScriptRoot \"..\\\\.env\"',\n\t\t\" if (Test-Path $envPath) {\",\n\t\t' Write-Pass \".env file exists\"',\n\t\t\" $emptySecrets = 0\",\n\t\t\" Get-Content $envPath | ForEach-Object {\",\n\t\t' if ($_ -match \"(PASSWORD|TOKEN|SECRET|SESSION_KEY|COOKIE|API_KEY).*=\\\\s*$\") { $emptySecrets++ }',\n\t\t\" }\",\n\t\t\" if ($emptySecrets -gt 0) {\",\n\t\t' Write-Warn \"$emptySecrets secret(s) are empty in .env\"',\n\t\t\" } else {\",\n\t\t' Write-Pass \"All secrets are populated\"',\n\t\t\" }\",\n\t\t\" } else {\",\n\t\t' Write-Warn \".env file not found\"',\n\t\t\" }\",\n\t\t\" $drive = (Get-Item $PSScriptRoot).PSDrive\",\n\t\t\" $freeGB = [math]::Round($drive.Free / 1GB, 1)\",\n\t\t\" if ($freeGB -lt 2) {\",\n\t\t' Write-Warn \"Low disk space: ${freeGB}GB available\"',\n\t\t\" } else {\",\n\t\t' Write-Pass \"Disk space: ${freeGB}GB available\"',\n\t\t\" }\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 2: Container Check\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"function Test-Container {\",\n\t\t\" param([string]$ServiceId, [string]$ServiceName, [string]$Icon)\",\n\t\t\" try {\",\n\t\t\" $status = docker compose ps $ServiceId --format json 2>&1 | ConvertFrom-Json\",\n\t\t\" } catch {\",\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) Container not found\"',\n\t\t\" return\",\n\t\t\" }\",\n\t\t\" if (-not $status) {\",\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) Container not found\"',\n\t\t\" return\",\n\t\t\" }\",\n\t\t\" $state = $status.State\",\n\t\t\" $health = $status.Health\",\n\t\t' if ($state -eq \"running\") {',\n\t\t' if ($health -eq \"healthy\") {',\n\t\t' Write-Pass \"$Icon $(Pad-Name $ServiceName) Running (healthy)\"',\n\t\t' } elseif ($health -eq \"starting\") {',\n\t\t' Write-Warn \"$Icon $(Pad-Name $ServiceName) Running (starting)\"',\n\t\t' } elseif ($health -eq \"unhealthy\") {',\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) Running (UNHEALTHY)\"',\n\t\t\" } else {\",\n\t\t' Write-Pass \"$Icon $(Pad-Name $ServiceName) Running\"',\n\t\t\" }\",\n\t\t' } elseif ($state -eq \"exited\") {',\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) Exited\"',\n\t\t\" } else {\",\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) State: $state\"',\n\t\t\" }\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 3: Port Check\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"function Test-Port {\",\n\t\t\" param([string]$ServiceId, [int]$Port, [string]$Description)\",\n\t\t\" try {\",\n\t\t\" $tcp = New-Object System.Net.Sockets.TcpClient\",\n\t\t' $tcp.ConnectAsync(\"localhost\", $Port).Wait(2000) | Out-Null',\n\t\t\" if ($tcp.Connected) {\",\n\t\t' Write-Dim \" Port $Port ($Description) — reachable\"',\n\t\t\" $tcp.Close()\",\n\t\t\" } else {\",\n\t\t' Write-Warn \" └─ ${ServiceId}: Port $Port ($Description) — NOT reachable\"',\n\t\t\" }\",\n\t\t\" } catch {\",\n\t\t' Write-Warn \" └─ ${ServiceId}: Port $Port ($Description) — NOT reachable\"',\n\t\t\" }\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ── Bare-metal process check ────────────────────────────────────────\",\n\t\t\"function Test-ProcessRunning {\",\n\t\t\" param([string]$ServiceId, [string]$ServiceName, [string]$Icon)\",\n\t\t\" $svc = Get-Service -Name $ServiceId -ErrorAction SilentlyContinue\",\n\t\t\" if ($svc) {\",\n\t\t' if ($svc.Status -eq \"Running\") {',\n\t\t' Write-Pass \"$Icon $(Pad-Name $ServiceName) Active (service)\"',\n\t\t\" } else {\",\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) $($svc.Status)\"',\n\t\t\" }\",\n\t\t\" return\",\n\t\t\" }\",\n\t\t\" $proc = Get-Process -Name $ServiceId -ErrorAction SilentlyContinue\",\n\t\t\" if ($proc) {\",\n\t\t' Write-Pass \"$Icon $(Pad-Name $ServiceName) Running (process)\"',\n\t\t\" } else {\",\n\t\t' Write-Fail \"$Icon $(Pad-Name $ServiceName) NOT running\"',\n\t\t\" }\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"# Phase 6: Log Scan\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"function Test-Logs {\",\n\t\t\" if ($Quick) {\",\n\t\t' if (-not $Json) { Write-Info \"Skipping log scan (-Quick mode)\" }',\n\t\t\" return\",\n\t\t\" }\",\n\t\t\" if (-not $Json) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"── Phase 6: Log Scan ───────────────────────────────────────\" -ForegroundColor White',\n\t\t' Write-Host \"\"',\n\t\t\" }\",\n\t\t\" $hasErrors = $false\",\n\t\tL(\" $svcIds = @(\", svcIdList, \")\"),\n\t\t\" foreach ($svcId in $svcIds) {\",\n\t\t\" try {\",\n\t\t\" $logs = docker compose logs --tail=50 $svcId 2>&1 | Out-String\",\n\t\t' $errorLines = $logs -split \"`n\" | Where-Object { $_ -match \"(error|fatal|panic|exception|segfault|killed|oom)\" } | Select-Object -Last 5',\n\t\t\" if ($errorLines) {\",\n\t\t\" $hasErrors = $true\",\n\t\t' Write-Warn \"$svcId — found error patterns in logs:\"',\n\t\t' $errorLines | ForEach-Object { Write-Dim \" | $_\" }',\n\t\t\" }\",\n\t\t\" } catch {}\",\n\t\t\" }\",\n\t\t\" if (-not $hasErrors -and -not $Json) {\",\n\t\t' Write-Pass \"No error patterns found in recent logs\"',\n\t\t\" }\",\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"# Main\",\n\t\t\"# ═════════════════════════════════════════════════════════════════════\",\n\t\t\"\",\n\t\t\"if (-not $Json) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"═══════════════════════════════════════════════════════════════════════\" ',\n\t\tL(' Write-Host \" 🐾 OpenClaw Stack Health Report — ', name, '\"'),\n\t\t' Write-Host \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t\"}\",\n\t\t\"\",\n\t\t\"Test-Environment\",\n\t\t\"\",\n\t\t\"if ($script:DockerAvailable) {\",\n\t\t\" if (-not $Json) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"── Phase 2–4: Service Checks ──────────────────────────────\" -ForegroundColor White',\n\t\t' Write-Host \"\"',\n\t\t\" }\",\n\t\t\"\",\n\t\t...dockerChecks,\n\t\t\" Test-Logs\",\n\t\t\"} else {\",\n\t\t\" if (-not $Json) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"── Bare-Metal Service Checks ──────────────────────────────\" -ForegroundColor White',\n\t\t' Write-Host \"\"',\n\t\t\" }\",\n\t\t\"\",\n\t\t...bmChecks,\n\t\t\"}\",\n\t\t\"\",\n\t\t\"# ── Summary ──────────────────────────────────────────────────────────\",\n\t\t\"if ($Json) {\",\n\t\t\" $result = @{\",\n\t\tL(' project = \"', name, '\"'),\n\t\t' timestamp = (Get-Date -Format \"o\")',\n\t\t\" summary = @{\",\n\t\t\" total = $script:Total\",\n\t\t\" healthy = $script:Healthy\",\n\t\t\" warnings = $script:Warnings\",\n\t\t\" failed = $script:Failed\",\n\t\t\" }\",\n\t\t\" errors = $script:Errors\",\n\t\t\" }\",\n\t\t\" $result | ConvertTo-Json -Depth 5\",\n\t\t\"} else {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \" Summary: $($script:Total) services checked\"',\n\t\t' Write-Host \" Healthy: $($script:Healthy)\" -ForegroundColor Green -NoNewline',\n\t\t' Write-Host \" | Warnings: $($script:Warnings)\" -ForegroundColor Yellow -NoNewline',\n\t\t' Write-Host \" | Failed: $($script:Failed)\" -ForegroundColor Red',\n\t\t\" if ($script:Errors.Count -gt 0) {\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \" ── Errors ──────────────────────────────────────────────────\" -ForegroundColor Red',\n\t\t' $script:Errors | ForEach-Object { Write-Host \" $_\" -ForegroundColor Red }',\n\t\t\" }\",\n\t\t' Write-Host \"\"',\n\t\t' Write-Host \"═══════════════════════════════════════════════════════════════════════\"',\n\t\t' Write-Host \"\"',\n\t\t\" if ($script:Failed -gt 0) {\",\n\t\t' Write-Host \" Some services need attention. Run with -Detailed for more.\" -ForegroundColor Red',\n\t\t\" exit 1\",\n\t\t\" } else {\",\n\t\t' Write-Host \" 🎉 All services are running!\" -ForegroundColor Green',\n\t\t\" exit 0\",\n\t\t\" }\",\n\t\t\"}\",\n\t];\n\n\treturn lines.join(\"\\n\") + \"\\n\";\n}\n"],"mappings":";;;;;;;;AAkBA,SAAgB,oBACf,UACA,SACyB;CACzB,MAAM,QAAgC,EAAE;AAExC,OAAM,6BAA6B,mBAAmB,UAAU,QAAQ;AACxE,OAAM,8BAA8B,yBAAyB,UAAU,QAAQ;AAE/E,QAAO;;AAaR,SAAS,qBAAqB,UAA0C;AACvE,QAAO,SAAS,SAAS,KAAK,SAAS;EACtC,IAAI,IAAI,WAAW;EACnB,MAAM,IAAI,WAAW;EACrB,MAAM,IAAI,WAAW;EACrB,OAAO,IAAI,WAAW,MAAM,KAAK,OAAO;GACvC,MAAM,EAAE;GACR,WAAW,EAAE;GACb,aAAa,EAAE;GACf,SAAS,EAAE;GACX,EAAE;EACH,gBAAgB,IAAI,WAAW,aAAa,QAAQ;EACpD,EAAE;;AAGJ,SAAS,YAAY,GAAmB;AACvC,QAAO,EAAE,QAAQ,MAAM,QAAQ,CAAC,QAAQ,MAAM,OAAM;;AAIrD,SAAS,EAAE,GAAG,OAAyB;AACtC,QAAO,MAAM,KAAK,GAAG;;AAKtB,SAAS,mBAAmB,UAA0B,SAAqC;CAC1F,MAAM,SAAS,qBAAqB,SAAS;CAC7C,MAAM,OAAO,QAAQ;CACrB,MAAM,QAAQ,OAAO;CAGrB,MAAM,eAAyB,EAAE;AACjC,MAAK,MAAM,OAAO,QAAQ;AACzB,eAAa,KAAK,EAAE,WAAW,IAAI,MAAM,KAAK,IAAI,MAAM,MAAM,CAAC;AAC/D,eAAa,KAAK,EAAE,wBAAuB,IAAI,IAAI,SAAO,IAAI,MAAM,SAAO,IAAI,MAAM,KAAI,CAAC;AAC1F,OAAK,MAAM,KAAK,IAAI,MAAM,QAAQ,OAAO,GAAG,QAAQ,CACnD,cAAa,KACZ,EAAE,mBAAkB,IAAI,IAAI,OAAM,OAAO,EAAE,KAAK,EAAE,OAAM,EAAE,aAAa,KAAI,CAC3E;AAEF,MAAI,IAAI,eACP,cAAa,KACZ,EAAE,yBAAwB,IAAI,IAAI,SAAO,YAAY,IAAI,eAAe,EAAE,KAAI,CAC9E;MAED,cAAa,KAAK,mDAAmD;AAEtE,eAAa,KAAK,GAAG;;CAItB,MAAM,WAAqB,EAAE;AAC7B,MAAK,MAAM,OAAO,QAAQ;AACzB,WAAS,KAAK,EAAE,WAAW,IAAI,MAAM,KAAK,IAAI,MAAM,mBAAmB,CAAC;AACxE,WAAS,KAAK,EAAE,sBAAqB,IAAI,IAAI,SAAO,IAAI,MAAM,SAAO,IAAI,MAAM,KAAI,CAAC;AACpF,OAAK,MAAM,KAAK,IAAI,MAAM,QAAQ,OAAO,GAAG,QAAQ,CACnD,UAAS,KAAK,EAAE,mBAAkB,IAAI,IAAI,OAAM,OAAO,EAAE,KAAK,EAAE,OAAM,EAAE,aAAa,KAAI,CAAC;AAE3F,WAAS,KAAK,GAAG;;CAIlB,MAAM,YAAY,OAAO,KAAK,MAAM,OAAM,EAAE,KAAK,KAAI,CAAC,KAAK,IAAI;AA6V/D,QA3VwB;EACvB;EACA;EACA;EACA;EACA,EAAE,uCAAuC,KAAK;EAC9C;EACA;EACA;EACA,EAAE,aAAa,OAAO,MAAM,EAAE,6BAA6B;EAC3D;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,UAAU,OAAO,MAAM,CAAC;EAC1B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,oBAAoB,WAAW,OAAO;EACxC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,gDAA+C,MAAM,KAAI;EAC3D;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,CAEY,KAAK,KAAK,GAAG;;AAK3B,SAAS,yBAAyB,UAA0B,SAAqC;CAChG,MAAM,SAAS,qBAAqB,SAAS;CAC7C,MAAM,OAAO,QAAQ;CACrB,MAAM,QAAQ,OAAO;CAGrB,MAAM,eAAyB,EAAE;AACjC,MAAK,MAAM,OAAO,QAAQ;AACzB,eAAa,KAAK,EAAE,UAAU,IAAI,MAAM,KAAK,IAAI,KAAK,CAAC;AACvD,eAAa,KACZ,EACC,oCACA,IAAI,IACJ,sBACA,IAAI,MACJ,eACA,IAAI,MACJ,KACA,CACD;AACD,OAAK,MAAM,KAAK,IAAI,MAAM,QAAQ,OAAO,GAAG,QAAQ,CACnD,cAAa,KACZ,EACC,+BACA,IAAI,IACJ,aACA,OAAO,EAAE,KAAK,EACd,oBACA,EAAE,aACF,KACA,CACD;AAEF,eAAa,KAAK,GAAG;;CAItB,MAAM,WAAqB,EAAE;AAC7B,MAAK,MAAM,OAAO,QAAQ;AACzB,WAAS,KAAK,EAAE,UAAU,IAAI,MAAM,KAAK,IAAI,KAAK,CAAC;AACnD,WAAS,KACR,EACC,yCACA,IAAI,IACJ,sBACA,IAAI,MACJ,eACA,IAAI,MACJ,KACA,CACD;AACD,OAAK,MAAM,KAAK,IAAI,MAAM,QAAQ,OAAO,GAAG,QAAQ,CACnD,UAAS,KACR,EACC,+BACA,IAAI,IACJ,aACA,OAAO,EAAE,KAAK,EACd,oBACA,EAAE,aACF,KACA,CACD;AAEF,WAAS,KAAK,GAAG;;CAGlB,MAAM,YAAY,OAAO,KAAK,MAAM,OAAM,EAAE,KAAK,KAAI,CAAC,KAAK,KAAK;AA8RhE,QA5RwB;EACvB;EACA;EACA;EACA,EAAE,sCAAsC,KAAK;EAC7C;EACA;EACA;EACA,EACC,eACA,OAAO,MAAM,EACb,kEACA;EACD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,oBAAoB,OAAO,MAAM,CAAC;EACpC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,oBAAoB,WAAW,IAAI;EACrC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,EAAE,wDAAuD,MAAM,KAAI;EACnE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,GAAG;EACH;EACA;EACA;EACA;EACA;EACA,EAAE,wBAAuB,MAAM,KAAI;EACnC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,CAEY,KAAK,KAAK,GAAG"}
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-YSByewHe.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-BgmKutxR.mjs";
2
2
  import { generate } from "../generate.mjs";
3
3
 
4
4
  //#region src/generators/health-check.test.ts
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-YSByewHe.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-BgmKutxR.mjs";
2
2
  import { generateScripts } from "./scripts.mjs";
3
3
 
4
4
  //#region src/generators/scripts.test.ts
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-YSByewHe.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-BgmKutxR.mjs";
2
2
  import { resolve } from "../resolver.mjs";
3
3
  import { generateTraefikConfig } from "./traefik.mjs";
4
4
 
@@ -1011,4 +1011,4 @@ var MagicString = class MagicString {
1011
1011
 
1012
1012
  //#endregion
1013
1013
  export { MagicString as default };
1014
- //# sourceMappingURL=magic-string.es-1uTtupLe.mjs.map
1014
+ //# sourceMappingURL=magic-string.es-CfFonO_S.mjs.map