@better-openclaw/core 1.0.23 → 1.0.24

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 (254) hide show
  1. package/dist/bare-metal-partition.test.cjs +3 -4
  2. package/dist/bare-metal-partition.test.cjs.map +1 -1
  3. package/dist/bare-metal-partition.test.mjs +3 -4
  4. package/dist/bare-metal-partition.test.mjs.map +1 -1
  5. package/dist/composer.cjs +13 -1
  6. package/dist/composer.cjs.map +1 -1
  7. package/dist/composer.d.cts.map +1 -1
  8. package/dist/composer.d.mts.map +1 -1
  9. package/dist/composer.mjs +13 -1
  10. package/dist/composer.mjs.map +1 -1
  11. package/dist/composer.snapshot.test.cjs +1 -1
  12. package/dist/composer.snapshot.test.mjs +1 -1
  13. package/dist/composer.test.cjs +3 -2
  14. package/dist/composer.test.cjs.map +1 -1
  15. package/dist/composer.test.mjs +3 -2
  16. package/dist/composer.test.mjs.map +1 -1
  17. package/dist/deployers/strip-host-ports.test.cjs +1 -1
  18. package/dist/deployers/strip-host-ports.test.mjs +1 -1
  19. package/dist/generate.cjs +6 -2
  20. package/dist/generate.cjs.map +1 -1
  21. package/dist/generate.d.cts.map +1 -1
  22. package/dist/generate.d.mts.map +1 -1
  23. package/dist/generate.mjs +6 -2
  24. package/dist/generate.mjs.map +1 -1
  25. package/dist/generate.test.cjs +2 -2
  26. package/dist/generate.test.cjs.map +1 -1
  27. package/dist/generate.test.mjs +2 -2
  28. package/dist/generate.test.mjs.map +1 -1
  29. package/dist/generators/bare-metal-install.test.cjs +1 -1
  30. package/dist/generators/bare-metal-install.test.mjs +1 -1
  31. package/dist/generators/caddy.test.cjs +1 -1
  32. package/dist/generators/caddy.test.mjs +1 -1
  33. package/dist/generators/clone-repos.cjs +140 -0
  34. package/dist/generators/clone-repos.cjs.map +1 -0
  35. package/dist/generators/clone-repos.d.cts +11 -0
  36. package/dist/generators/clone-repos.d.cts.map +1 -0
  37. package/dist/generators/clone-repos.d.mts +11 -0
  38. package/dist/generators/clone-repos.d.mts.map +1 -0
  39. package/dist/generators/clone-repos.mjs +139 -0
  40. package/dist/generators/clone-repos.mjs.map +1 -0
  41. package/dist/generators/clone-repos.test.cjs +140 -0
  42. package/dist/generators/clone-repos.test.cjs.map +1 -0
  43. package/dist/generators/clone-repos.test.d.cts +1 -0
  44. package/dist/generators/clone-repos.test.d.mts +1 -0
  45. package/dist/generators/clone-repos.test.mjs +141 -0
  46. package/dist/generators/clone-repos.test.mjs.map +1 -0
  47. package/dist/generators/env.test.cjs +1 -1
  48. package/dist/generators/env.test.mjs +1 -1
  49. package/dist/generators/health-check.test.cjs +1 -1
  50. package/dist/generators/health-check.test.mjs +1 -1
  51. package/dist/generators/postgres-init.cjs +20 -0
  52. package/dist/generators/postgres-init.cjs.map +1 -1
  53. package/dist/generators/postgres-init.d.cts.map +1 -1
  54. package/dist/generators/postgres-init.d.mts.map +1 -1
  55. package/dist/generators/postgres-init.mjs +20 -0
  56. package/dist/generators/postgres-init.mjs.map +1 -1
  57. package/dist/generators/scripts.cjs +332 -3
  58. package/dist/generators/scripts.cjs.map +1 -1
  59. package/dist/generators/scripts.d.cts +3 -1
  60. package/dist/generators/scripts.d.cts.map +1 -1
  61. package/dist/generators/scripts.d.mts +3 -1
  62. package/dist/generators/scripts.d.mts.map +1 -1
  63. package/dist/generators/scripts.mjs +332 -3
  64. package/dist/generators/scripts.mjs.map +1 -1
  65. package/dist/generators/scripts.test.cjs +39 -5
  66. package/dist/generators/scripts.test.cjs.map +1 -1
  67. package/dist/generators/scripts.test.mjs +39 -5
  68. package/dist/generators/scripts.test.mjs.map +1 -1
  69. package/dist/generators/stack-manifest.cjs +1 -0
  70. package/dist/generators/stack-manifest.cjs.map +1 -1
  71. package/dist/generators/stack-manifest.d.cts +3 -2
  72. package/dist/generators/stack-manifest.d.cts.map +1 -1
  73. package/dist/generators/stack-manifest.d.mts +3 -2
  74. package/dist/generators/stack-manifest.d.mts.map +1 -1
  75. package/dist/generators/stack-manifest.mjs +1 -0
  76. package/dist/generators/stack-manifest.mjs.map +1 -1
  77. package/dist/generators/traefik.test.cjs +1 -1
  78. package/dist/generators/traefik.test.mjs +1 -1
  79. package/dist/index.cjs +8 -1
  80. package/dist/index.d.cts +5 -3
  81. package/dist/index.d.mts +5 -3
  82. package/dist/index.mjs +5 -3
  83. package/dist/migrations.test.cjs +1 -1
  84. package/dist/migrations.test.mjs +1 -1
  85. package/dist/presets/registry.cjs.map +1 -1
  86. package/dist/presets/registry.d.cts.map +1 -1
  87. package/dist/presets/registry.d.mts.map +1 -1
  88. package/dist/presets/registry.mjs.map +1 -1
  89. package/dist/presets/registry.test.cjs +1 -1
  90. package/dist/presets/registry.test.mjs +1 -1
  91. package/dist/resolver.cjs +8 -0
  92. package/dist/resolver.cjs.map +1 -1
  93. package/dist/resolver.mjs +9 -1
  94. package/dist/resolver.mjs.map +1 -1
  95. package/dist/resolver.test.cjs +47 -12
  96. package/dist/resolver.test.cjs.map +1 -1
  97. package/dist/resolver.test.mjs +47 -12
  98. package/dist/resolver.test.mjs.map +1 -1
  99. package/dist/{schema-B4c64P8N.d.cts → schema-eX44HhRp.d.mts} +62 -8
  100. package/dist/schema-eX44HhRp.d.mts.map +1 -0
  101. package/dist/{schema-CXNhYci1.d.mts → schema-tn5RK8CM.d.cts} +62 -8
  102. package/dist/schema-tn5RK8CM.d.cts.map +1 -0
  103. package/dist/schema.cjs +22 -4
  104. package/dist/schema.cjs.map +1 -1
  105. package/dist/schema.d.cts +2 -2
  106. package/dist/schema.d.mts +2 -2
  107. package/dist/schema.mjs +21 -5
  108. package/dist/schema.mjs.map +1 -1
  109. package/dist/schema.test.cjs +1 -1
  110. package/dist/schema.test.mjs +1 -1
  111. package/dist/services/definitions/apptension-saas.cjs +87 -0
  112. package/dist/services/definitions/apptension-saas.cjs.map +1 -0
  113. package/dist/services/definitions/apptension-saas.d.cts +7 -0
  114. package/dist/services/definitions/apptension-saas.d.cts.map +1 -0
  115. package/dist/services/definitions/apptension-saas.d.mts +7 -0
  116. package/dist/services/definitions/apptension-saas.d.mts.map +1 -0
  117. package/dist/services/definitions/apptension-saas.mjs +86 -0
  118. package/dist/services/definitions/apptension-saas.mjs.map +1 -0
  119. package/dist/services/definitions/boxyhq-saas.cjs +88 -0
  120. package/dist/services/definitions/boxyhq-saas.cjs.map +1 -0
  121. package/dist/services/definitions/boxyhq-saas.d.cts +7 -0
  122. package/dist/services/definitions/boxyhq-saas.d.cts.map +1 -0
  123. package/dist/services/definitions/boxyhq-saas.d.mts +7 -0
  124. package/dist/services/definitions/boxyhq-saas.d.mts.map +1 -0
  125. package/dist/services/definitions/boxyhq-saas.mjs +87 -0
  126. package/dist/services/definitions/boxyhq-saas.mjs.map +1 -0
  127. package/dist/services/definitions/cmsaas-starter.cjs +86 -0
  128. package/dist/services/definitions/cmsaas-starter.cjs.map +1 -0
  129. package/dist/services/definitions/cmsaas-starter.d.cts +7 -0
  130. package/dist/services/definitions/cmsaas-starter.d.cts.map +1 -0
  131. package/dist/services/definitions/cmsaas-starter.d.mts +7 -0
  132. package/dist/services/definitions/cmsaas-starter.d.mts.map +1 -0
  133. package/dist/services/definitions/cmsaas-starter.mjs +85 -0
  134. package/dist/services/definitions/cmsaas-starter.mjs.map +1 -0
  135. package/dist/services/definitions/index.cjs +51 -36
  136. package/dist/services/definitions/index.cjs.map +1 -1
  137. package/dist/services/definitions/index.d.cts +30 -25
  138. package/dist/services/definitions/index.d.cts.map +1 -1
  139. package/dist/services/definitions/index.d.mts +30 -25
  140. package/dist/services/definitions/index.d.mts.map +1 -1
  141. package/dist/services/definitions/index.mjs +47 -37
  142. package/dist/services/definitions/index.mjs.map +1 -1
  143. package/dist/services/definitions/ixartz-saas.cjs +88 -0
  144. package/dist/services/definitions/ixartz-saas.cjs.map +1 -0
  145. package/dist/services/definitions/ixartz-saas.d.cts +7 -0
  146. package/dist/services/definitions/ixartz-saas.d.cts.map +1 -0
  147. package/dist/services/definitions/ixartz-saas.d.mts +7 -0
  148. package/dist/services/definitions/ixartz-saas.d.mts.map +1 -0
  149. package/dist/services/definitions/ixartz-saas.mjs +87 -0
  150. package/dist/services/definitions/ixartz-saas.mjs.map +1 -0
  151. package/dist/services/definitions/mission-control.cjs +16 -2
  152. package/dist/services/definitions/mission-control.cjs.map +1 -1
  153. package/dist/services/definitions/mission-control.mjs +16 -2
  154. package/dist/services/definitions/mission-control.mjs.map +1 -1
  155. package/dist/services/definitions/open-saas.cjs +81 -0
  156. package/dist/services/definitions/open-saas.cjs.map +1 -0
  157. package/dist/services/definitions/open-saas.d.cts +7 -0
  158. package/dist/services/definitions/open-saas.d.cts.map +1 -0
  159. package/dist/services/definitions/open-saas.d.mts +7 -0
  160. package/dist/services/definitions/open-saas.d.mts.map +1 -0
  161. package/dist/services/definitions/open-saas.mjs +80 -0
  162. package/dist/services/definitions/open-saas.mjs.map +1 -0
  163. package/dist/services/registry.cjs +3 -0
  164. package/dist/services/registry.cjs.map +1 -1
  165. package/dist/services/registry.d.cts.map +1 -1
  166. package/dist/services/registry.d.mts.map +1 -1
  167. package/dist/services/registry.mjs +3 -0
  168. package/dist/services/registry.mjs.map +1 -1
  169. package/dist/services/registry.test.cjs +8 -1
  170. package/dist/services/registry.test.cjs.map +1 -1
  171. package/dist/services/registry.test.mjs +8 -1
  172. package/dist/services/registry.test.mjs.map +1 -1
  173. package/dist/{skill-manifest-BVUXU0__.mjs → skill-manifest-6XhrhWsG.mjs} +49 -1
  174. package/dist/{skill-manifest--IgY9REK.cjs.map → skill-manifest-6XhrhWsG.mjs.map} +1 -1
  175. package/dist/{skill-manifest--IgY9REK.cjs → skill-manifest-B8znSsym.cjs} +49 -1
  176. package/dist/{skill-manifest-BVUXU0__.mjs.map → skill-manifest-B8znSsym.cjs.map} +1 -1
  177. package/dist/skills/registry.cjs +3 -3
  178. package/dist/skills/registry.cjs.map +1 -1
  179. package/dist/skills/registry.mjs +3 -3
  180. package/dist/skills/registry.mjs.map +1 -1
  181. package/dist/skills/skill-manifest.cjs +1 -1
  182. package/dist/skills/skill-manifest.mjs +1 -1
  183. package/dist/track-analytics.cjs +50 -0
  184. package/dist/track-analytics.cjs.map +1 -0
  185. package/dist/track-analytics.d.cts +34 -0
  186. package/dist/track-analytics.d.cts.map +1 -0
  187. package/dist/track-analytics.d.mts +34 -0
  188. package/dist/track-analytics.d.mts.map +1 -0
  189. package/dist/track-analytics.mjs +48 -0
  190. package/dist/track-analytics.mjs.map +1 -0
  191. package/dist/track-analytics.test.cjs +91 -0
  192. package/dist/track-analytics.test.cjs.map +1 -0
  193. package/dist/track-analytics.test.d.cts +1 -0
  194. package/dist/track-analytics.test.d.mts +1 -0
  195. package/dist/track-analytics.test.mjs +92 -0
  196. package/dist/track-analytics.test.mjs.map +1 -0
  197. package/dist/types.cjs +7 -0
  198. package/dist/types.cjs.map +1 -1
  199. package/dist/types.d.cts +4 -2
  200. package/dist/types.d.cts.map +1 -1
  201. package/dist/types.d.mts +4 -2
  202. package/dist/types.d.mts.map +1 -1
  203. package/dist/types.mjs +7 -0
  204. package/dist/types.mjs.map +1 -1
  205. package/dist/validator.test.cjs +1 -1
  206. package/dist/validator.test.mjs +1 -1
  207. package/dist/version-manager.cjs +1 -1
  208. package/dist/version-manager.cjs.map +1 -1
  209. package/dist/version-manager.mjs +1 -1
  210. package/dist/version-manager.mjs.map +1 -1
  211. package/dist/version-manager.test.cjs +7 -5
  212. package/dist/version-manager.test.cjs.map +1 -1
  213. package/dist/version-manager.test.mjs +7 -5
  214. package/dist/version-manager.test.mjs.map +1 -1
  215. package/dist/{vi.2VT5v0um-DvC3SVNc.mjs → vi.2VT5v0um-C_jmO7m2.mjs} +5 -5
  216. package/dist/{vi.2VT5v0um-DvC3SVNc.mjs.map → vi.2VT5v0um-C_jmO7m2.mjs.map} +1 -1
  217. package/dist/{vi.2VT5v0um-CRqXre87.cjs → vi.2VT5v0um-iVBt6Fyq.cjs} +5 -5
  218. package/dist/{vi.2VT5v0um-CRqXre87.cjs.map → vi.2VT5v0um-iVBt6Fyq.cjs.map} +1 -1
  219. package/package.json +1 -1
  220. package/src/__snapshots__/composer.snapshot.test.ts.snap +155 -0
  221. package/src/bare-metal-partition.test.ts +4 -3
  222. package/src/composer.test.ts +4 -2
  223. package/src/composer.ts +20 -1
  224. package/src/generate.test.ts +2 -1
  225. package/src/generate.ts +10 -1
  226. package/src/generators/clone-repos.test.ts +154 -0
  227. package/src/generators/clone-repos.ts +159 -0
  228. package/src/generators/postgres-init.ts +17 -0
  229. package/src/generators/scripts.test.ts +52 -4
  230. package/src/generators/scripts.ts +351 -3
  231. package/src/generators/stack-manifest.ts +4 -2
  232. package/src/index.ts +8 -0
  233. package/src/presets/registry.ts +241 -329
  234. package/src/resolver.test.ts +53 -15
  235. package/src/resolver.ts +13 -1
  236. package/src/schema.ts +33 -4
  237. package/src/services/definitions/apptension-saas.ts +84 -0
  238. package/src/services/definitions/boxyhq-saas.ts +84 -0
  239. package/src/services/definitions/cmsaas-starter.ts +84 -0
  240. package/src/services/definitions/index.ts +90 -70
  241. package/src/services/definitions/ixartz-saas.ts +84 -0
  242. package/src/services/definitions/mission-control.ts +19 -2
  243. package/src/services/definitions/open-saas.ts +79 -0
  244. package/src/services/registry.test.ts +8 -0
  245. package/src/services/registry.ts +7 -0
  246. package/src/skills/manifest.json +64 -0
  247. package/src/skills/registry.ts +3 -3
  248. package/src/track-analytics.test.ts +82 -0
  249. package/src/track-analytics.ts +76 -0
  250. package/src/types.ts +11 -0
  251. package/src/version-manager.test.ts +10 -5
  252. package/src/version-manager.ts +1 -1
  253. package/dist/schema-B4c64P8N.d.cts.map +0 -1
  254. package/dist/schema-CXNhYci1.d.mts.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"composer.test.cjs","names":["describe","compose","resolve","yaml"],"sources":["../src/composer.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { parse } from \"yaml\";\nimport { compose } from \"./composer.js\";\nimport { resolve } from \"./resolver.js\";\nimport type { ComposeOptions, ResolverOutput } from \"./types.js\";\n\nconst defaultOptions: ComposeOptions = {\n\tprojectName: \"test-project\",\n\tproxy: \"none\",\n\tgpu: false,\n\tplatform: \"linux/amd64\",\n\tdeployment: \"local\",\n\topenclawVersion: \"latest\",\n};\n\ndescribe(\"compose\", () => {\n\tit(\"generates minimal stack with just OpenClaw gateway when no companions\", () => {\n\t\tconst resolved = resolve({ services: [], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should have the gateway service\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].image).toContain(\"ghcr.io/openclaw/openclaw\");\n\n\t\t// Gateway should have core environment\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.HOME).toBe(\"/home/node\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.TERM).toBe(\"xterm-256color\");\n\n\t\t// Gateway uses bind-mount volumes (not named volumes in top-level volumes section)\n\t\tconst gwVolumes = parsed.services[\"openclaw-gateway\"].volumes as string[];\n\t\texpect(gwVolumes.some((v: string) => v.includes(\".openclaw\"))).toBe(true);\n\n\t\t// Should have network\n\t\texpect(parsed.networks).toHaveProperty(\"openclaw-network\");\n\t\texpect(parsed.networks[\"openclaw-network\"].driver).toBe(\"bridge\");\n\n\t\t// Should have CLI companion service\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-cli\");\n\n\t\t// Gateway should have no depends_on (no companions)\n\t\texpect(parsed.services[\"openclaw-gateway\"]).not.toHaveProperty(\"depends_on\");\n\n\t\t// Gateway should have restart policy\n\t\texpect(parsed.services[\"openclaw-gateway\"].restart).toBe(\"unless-stopped\");\n\t});\n\n\tit(\"generates Redis companion with proper gateway depends_on\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should have gateway and redis services\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services).toHaveProperty(\"redis\");\n\n\t\t// Redis service should have correct image\n\t\texpect(parsed.services.redis.image).toBe(\"redis:8-alpine\");\n\n\t\t// Gateway depends_on should include redis with service_healthy (redis has healthcheck)\n\t\texpect(parsed.services[\"openclaw-gateway\"].depends_on).toHaveProperty(\"redis\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].depends_on.redis.condition).toBe(\"service_healthy\");\n\n\t\t// Redis should have a healthcheck\n\t\texpect(parsed.services.redis.healthcheck).toBeDefined();\n\t\texpect(parsed.services.redis.healthcheck.test).toContain(\"redis-cli ping\");\n\n\t\t// Redis should have restart policy\n\t\texpect(parsed.services.redis.restart).toBe(\"unless-stopped\");\n\n\t\t// Redis should be on openclaw-network\n\t\texpect(parsed.services.redis.networks).toContain(\"openclaw-network\");\n\n\t\t// Gateway environment should include redis-related openclawEnvVars\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment).toHaveProperty(\"REDIS_HOST\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.REDIS_HOST).toBe(\"redis\");\n\t});\n\n\tit(\"generates parseable YAML for a multi-service stack\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"n8n\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\n\t\t// Should not throw when parsing\n\t\texpect(() => parse(yaml)).not.toThrow();\n\n\t\tconst parsed = parse(yaml);\n\t\texpect(parsed).toHaveProperty(\"services\");\n\t\texpect(parsed).toHaveProperty(\"volumes\");\n\t\texpect(parsed).toHaveProperty(\"networks\");\n\n\t\t// Should have gateway + redis + n8n + postgresql (n8n requires postgresql)\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services).toHaveProperty(\"redis\");\n\t\texpect(parsed.services).toHaveProperty(\"n8n\");\n\t\texpect(parsed.services).toHaveProperty(\"postgresql\");\n\t});\n\n\tit(\"includes all service volumes in top-level volumes section\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"n8n\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Collect all volume names from all resolved services\n\t\tconst serviceVolumeNames = new Set<string>();\n\t\tfor (const rs of resolved.services) {\n\t\t\tfor (const vol of rs.definition.volumes) {\n\t\t\t\tserviceVolumeNames.add(vol.name);\n\t\t\t}\n\t\t}\n\n\t\t// All service volumes must appear in top-level volumes\n\t\tfor (const volName of serviceVolumeNames) {\n\t\t\texpect(parsed.volumes).toHaveProperty(volName);\n\t\t}\n\n\t\t// Gateway uses bind-mount volumes (not in top-level volumes section)\n\t\tconst gwVols = parsed.services[\"openclaw-gateway\"].volumes as string[];\n\t\texpect(gwVols.some((v: string) => v.includes(\".openclaw\"))).toBe(true);\n\t});\n\n\tit(\"includes GPU passthrough when gpu=true and service requires it\", () => {\n\t\tconst resolved = resolve({ services: [\"ollama\"], skillPacks: [] });\n\n\t\t// Modify resolved output to simulate a GPU-required service\n\t\tconst gpuResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: { ...s.definition, gpuRequired: true },\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(gpuResolved, { ...defaultOptions, gpu: true });\n\t\tconst parsed = parse(yaml);\n\n\t\t// Ollama service should have GPU device reservation\n\t\tconst ollamaService = parsed.services.ollama;\n\t\texpect(ollamaService.deploy).toBeDefined();\n\t\texpect(ollamaService.deploy.resources.reservations.devices).toEqual([\n\t\t\t{\n\t\t\t\tdriver: \"nvidia\",\n\t\t\t\tcount: \"all\",\n\t\t\t\tcapabilities: [\"gpu\"],\n\t\t\t},\n\t\t]);\n\t});\n\n\tit(\"does not include GPU passthrough when gpu=false\", () => {\n\t\tconst resolved = resolve({ services: [\"ollama\"], skillPacks: [] });\n\n\t\t// Even if service requires GPU, gpu option is false\n\t\tconst gpuResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: { ...s.definition, gpuRequired: true },\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(gpuResolved, { ...defaultOptions, gpu: false });\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should NOT have deploy with GPU devices\n\t\tconst ollamaService = parsed.services.ollama;\n\t\texpect(ollamaService.deploy?.resources?.reservations?.devices).toBeUndefined();\n\t});\n\n\tit(\"does not map internal-only ports to host\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\"], skillPacks: [] });\n\n\t\t// Modify redis to have an internal-only port\n\t\tconst modifiedResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: {\n\t\t\t\t\t...s.definition,\n\t\t\t\t\tports: [\n\t\t\t\t\t\t...s.definition.ports,\n\t\t\t\t\t\t{ host: 16379, container: 16379, description: \"Redis cluster bus\", exposed: false },\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(modifiedResolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should only have the exposed port, not the internal one\n\t\tconst redisPorts = parsed.services.redis.ports;\n\t\texpect(redisPorts).toHaveLength(1);\n\t\texpect(redisPorts[0]).toContain(\"6379\");\n\t\texpect(redisPorts[0]).not.toContain(\"16379\");\n\t});\n\n\tit(\"uses service_started condition for services without healthcheck\", () => {\n\t\tconst resolved = resolve({ services: [\"minio\"], skillPacks: [] });\n\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Minio may or may not have a healthcheck – the gateway depends_on\n\t\t// condition should reflect the definition accurately\n\t\tconst gatewayDeps = parsed.services[\"openclaw-gateway\"].depends_on;\n\t\tfor (const [id, dep] of Object.entries(gatewayDeps)) {\n\t\t\tconst svc = resolved.services.find((s) => s.definition.id === id);\n\t\t\tif (svc?.definition.healthcheck) {\n\t\t\t\texpect((dep as { condition: string }).condition).toBe(\"service_healthy\");\n\t\t\t} else {\n\t\t\t\texpect((dep as { condition: string }).condition).toBe(\"service_started\");\n\t\t\t}\n\t\t}\n\t});\n\n\tit(\"gateway is always the first service in the output\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"ollama\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// First key in services should be openclaw-gateway\n\t\tconst serviceKeys = Object.keys(parsed.services);\n\t\texpect(serviceKeys[0]).toBe(\"openclaw-gateway\");\n\t});\n});\n"],"mappings":";;;;;;AAMA,MAAM,iBAAiC;CACtC,aAAa;CACb,OAAO;CACP,KAAK;CACL,UAAU;CACV,YAAY;CACZ,iBAAiB;CACjB;AAEDA,oBAAAA,SAAS,iBAAiB;AACzB,qBAAA,GAAG,+EAA+E;EAGjF,MAAM,UAAA,GAAA,KAAA,OADOC,iBAAAA,QADIC,iBAAAA,QAAQ;GAAE,UAAU,EAAE;GAAE,YAAY,EAAE;GAAE,CAAC,EAC3B,eAAe,CACpB;AAG1B,sBAAA,aAAO,OAAO,SAAS,CAAC,eAAe,mBAAmB;AAC1D,sBAAA,aAAO,OAAO,SAAS,oBAAoB,MAAM,CAAC,UAAU,4BAA4B;AAGxF,sBAAA,aAAO,OAAO,SAAS,oBAAoB,YAAY,KAAK,CAAC,KAAK,aAAa;AAC/E,sBAAA,aAAO,OAAO,SAAS,oBAAoB,YAAY,KAAK,CAAC,KAAK,iBAAiB;EAGnF,MAAM,YAAY,OAAO,SAAS,oBAAoB;AACtD,sBAAA,aAAO,UAAU,MAAM,MAAc,EAAE,SAAS,YAAY,CAAC,CAAC,CAAC,KAAK,KAAK;AAGzE,sBAAA,aAAO,OAAO,SAAS,CAAC,eAAe,mBAAmB;AAC1D,sBAAA,aAAO,OAAO,SAAS,oBAAoB,OAAO,CAAC,KAAK,SAAS;AAGjE,sBAAA,aAAO,OAAO,SAAS,CAAC,eAAe,eAAe;AAGtD,sBAAA,aAAO,OAAO,SAAS,oBAAoB,CAAC,IAAI,eAAe,aAAa;AAG5E,sBAAA,aAAO,OAAO,SAAS,oBAAoB,QAAQ,CAAC,KAAK,iBAAiB;GACzE;AAEF,qBAAA,GAAG,kEAAkE;EAGpE,MAAM,UAAA,GAAA,KAAA,OADOD,iBAAAA,QADIC,iBAAAA,QAAQ;GAAE,UAAU,CAAC,QAAQ;GAAE,YAAY,EAAE;GAAE,CAAC,EAClC,eAAe,CACpB;AAG1B,sBAAA,aAAO,OAAO,SAAS,CAAC,eAAe,mBAAmB;AAC1D,sBAAA,aAAO,OAAO,SAAS,CAAC,eAAe,QAAQ;AAG/C,sBAAA,aAAO,OAAO,SAAS,MAAM,MAAM,CAAC,KAAK,iBAAiB;AAG1D,sBAAA,aAAO,OAAO,SAAS,oBAAoB,WAAW,CAAC,eAAe,QAAQ;AAC9E,sBAAA,aAAO,OAAO,SAAS,oBAAoB,WAAW,MAAM,UAAU,CAAC,KAAK,kBAAkB;AAG9F,sBAAA,aAAO,OAAO,SAAS,MAAM,YAAY,CAAC,aAAa;AACvD,sBAAA,aAAO,OAAO,SAAS,MAAM,YAAY,KAAK,CAAC,UAAU,iBAAiB;AAG1E,sBAAA,aAAO,OAAO,SAAS,MAAM,QAAQ,CAAC,KAAK,iBAAiB;AAG5D,sBAAA,aAAO,OAAO,SAAS,MAAM,SAAS,CAAC,UAAU,mBAAmB;AAGpE,sBAAA,aAAO,OAAO,SAAS,oBAAoB,YAAY,CAAC,eAAe,aAAa;AACpF,sBAAA,aAAO,OAAO,SAAS,oBAAoB,YAAY,WAAW,CAAC,KAAK,QAAQ;GAC/E;AAEF,qBAAA,GAAG,4DAA4D;EAE9D,MAAMC,SAAOF,iBAAAA,QADIC,iBAAAA,QAAQ;GAAE,UAAU,CAAC,SAAS,MAAM;GAAE,YAAY,EAAE;GAAE,CAAC,EACzC,eAAe;AAG9C,sBAAA,oBAAA,GAAA,KAAA,OAAmBC,OAAK,CAAC,CAAC,IAAI,SAAS;EAEvC,MAAM,UAAA,GAAA,KAAA,OAAeA,OAAK;AAC1B,sBAAA,aAAO,OAAO,CAAC,eAAe,WAAW;AACzC,sBAAA,aAAO,OAAO,CAAC,eAAe,UAAU;AACxC,sBAAA,aAAO,OAAO,CAAC,eAAe,WAAW;AAGzC,sBAAA,aAAO,OAAO,SAAS,CAAC,eAAe,mBAAmB;AAC1D,sBAAA,aAAO,OAAO,SAAS,CAAC,eAAe,QAAQ;AAC/C,sBAAA,aAAO,OAAO,SAAS,CAAC,eAAe,MAAM;AAC7C,sBAAA,aAAO,OAAO,SAAS,CAAC,eAAe,aAAa;GACnD;AAEF,qBAAA,GAAG,mEAAmE;EACrE,MAAM,WAAWD,iBAAAA,QAAQ;GAAE,UAAU,CAAC,SAAS,MAAM;GAAE,YAAY,EAAE;GAAE,CAAC;EAExE,MAAM,UAAA,GAAA,KAAA,OADOD,iBAAAA,QAAQ,UAAU,eAAe,CACpB;EAG1B,MAAM,qCAAqB,IAAI,KAAa;AAC5C,OAAK,MAAM,MAAM,SAAS,SACzB,MAAK,MAAM,OAAO,GAAG,WAAW,QAC/B,oBAAmB,IAAI,IAAI,KAAK;AAKlC,OAAK,MAAM,WAAW,mBACrB,qBAAA,aAAO,OAAO,QAAQ,CAAC,eAAe,QAAQ;EAI/C,MAAM,SAAS,OAAO,SAAS,oBAAoB;AACnD,sBAAA,aAAO,OAAO,MAAM,MAAc,EAAE,SAAS,YAAY,CAAC,CAAC,CAAC,KAAK,KAAK;GACrE;AAEF,qBAAA,GAAG,wEAAwE;EAC1E,MAAM,WAAWC,iBAAAA,QAAQ;GAAE,UAAU,CAAC,SAAS;GAAE,YAAY,EAAE;GAAE,CAAC;EAelE,MAAM,iBAAA,GAAA,KAAA,OAJOD,iBAAAA,QARuB;GACnC,GAAG;GACH,UAAU,SAAS,SAAS,KAAK,OAAO;IACvC,GAAG;IACH,YAAY;KAAE,GAAG,EAAE;KAAY,aAAa;KAAM;IAClD,EAAE;GACH,EAEiC;GAAE,GAAG;GAAgB,KAAK;GAAM,CAAC,CACzC,CAGG,SAAS;AACtC,sBAAA,aAAO,cAAc,OAAO,CAAC,aAAa;AAC1C,sBAAA,aAAO,cAAc,OAAO,UAAU,aAAa,QAAQ,CAAC,QAAQ,CACnE;GACC,QAAQ;GACR,OAAO;GACP,cAAc,CAAC,MAAM;GACrB,CACD,CAAC;GACD;AAEF,qBAAA,GAAG,yDAAyD;EAC3D,MAAM,WAAWC,iBAAAA,QAAQ;GAAE,UAAU,CAAC,SAAS;GAAE,YAAY,EAAE;GAAE,CAAC;EAelE,MAAM,iBAAA,GAAA,KAAA,OAJOD,iBAAAA,QARuB;GACnC,GAAG;GACH,UAAU,SAAS,SAAS,KAAK,OAAO;IACvC,GAAG;IACH,YAAY;KAAE,GAAG,EAAE;KAAY,aAAa;KAAM;IAClD,EAAE;GACH,EAEiC;GAAE,GAAG;GAAgB,KAAK;GAAO,CAAC,CAC1C,CAGG,SAAS;AACtC,sBAAA,aAAO,cAAc,QAAQ,WAAW,cAAc,QAAQ,CAAC,eAAe;GAC7E;AAEF,qBAAA,GAAG,kDAAkD;EACpD,MAAM,WAAWC,iBAAAA,QAAQ;GAAE,UAAU,CAAC,QAAQ;GAAE,YAAY,EAAE;GAAE,CAAC;EAqBjE,MAAM,cAAA,GAAA,KAAA,OAJOD,iBAAAA,QAd4B;GACxC,GAAG;GACH,UAAU,SAAS,SAAS,KAAK,OAAO;IACvC,GAAG;IACH,YAAY;KACX,GAAG,EAAE;KACL,OAAO,CACN,GAAG,EAAE,WAAW,OAChB;MAAE,MAAM;MAAO,WAAW;MAAO,aAAa;MAAqB,SAAS;MAAO,CACnF;KACD;IACD,EAAE;GACH,EAEsC,eAAe,CAC5B,CAGA,SAAS,MAAM;AACzC,sBAAA,aAAO,WAAW,CAAC,aAAa,EAAE;AAClC,sBAAA,aAAO,WAAW,GAAG,CAAC,UAAU,OAAO;AACvC,sBAAA,aAAO,WAAW,GAAG,CAAC,IAAI,UAAU,QAAQ;GAC3C;AAEF,qBAAA,GAAG,yEAAyE;EAC3E,MAAM,WAAWC,iBAAAA,QAAQ;GAAE,UAAU,CAAC,QAAQ;GAAE,YAAY,EAAE;GAAE,CAAC;EAOjE,MAAM,eAAA,GAAA,KAAA,OALOD,iBAAAA,QAAQ,UAAU,eAAe,CACpB,CAIC,SAAS,oBAAoB;AACxD,OAAK,MAAM,CAAC,IAAI,QAAQ,OAAO,QAAQ,YAAY,CAElD,KADY,SAAS,SAAS,MAAM,MAAM,EAAE,WAAW,OAAO,GAAG,EACxD,WAAW,YACnB,qBAAA,aAAQ,IAA8B,UAAU,CAAC,KAAK,kBAAkB;MAExE,qBAAA,aAAQ,IAA8B,UAAU,CAAC,KAAK,kBAAkB;GAGzE;AAEF,qBAAA,GAAG,2DAA2D;EAG7D,MAAM,UAAA,GAAA,KAAA,OADOA,iBAAAA,QADIC,iBAAAA,QAAQ;GAAE,UAAU,CAAC,SAAS,SAAS;GAAE,YAAY,EAAE;GAAE,CAAC,EAC5C,eAAe,CACpB;AAI1B,sBAAA,aADoB,OAAO,KAAK,OAAO,SAAS,CAC7B,GAAG,CAAC,KAAK,mBAAmB;GAC9C;EACD"}
1
+ {"version":3,"file":"composer.test.cjs","names":["describe","compose","resolve","yaml"],"sources":["../src/composer.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { parse } from \"yaml\";\nimport { compose } from \"./composer.js\";\nimport { resolve } from \"./resolver.js\";\nimport type { ComposeOptions, ResolverOutput } from \"./types.js\";\n\nconst defaultOptions: ComposeOptions = {\n\tprojectName: \"test-project\",\n\tproxy: \"none\",\n\tgpu: false,\n\tplatform: \"linux/amd64\",\n\tdeployment: \"local\",\n\topenclawVersion: \"latest\",\n};\n\ndescribe(\"compose\", () => {\n\tit(\"generates minimal stack with just OpenClaw gateway when no companions\", () => {\n\t\tconst resolved = resolve({ services: [], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should have the gateway service\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].image).toContain(\"ghcr.io/openclaw/openclaw\");\n\n\t\t// Gateway should have core environment\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.HOME).toBe(\"/home/node\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.TERM).toBe(\"xterm-256color\");\n\n\t\t// Gateway uses bind-mount volumes (not named volumes in top-level volumes section)\n\t\tconst gwVolumes = parsed.services[\"openclaw-gateway\"].volumes as string[];\n\t\texpect(gwVolumes.some((v: string) => v.includes(\".openclaw\"))).toBe(true);\n\n\t\t// Should have network\n\t\texpect(parsed.networks).toHaveProperty(\"openclaw-network\");\n\t\texpect(parsed.networks[\"openclaw-network\"].driver).toBe(\"bridge\");\n\n\t\t// Should have CLI companion service\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-cli\");\n\n\t\t// Gateway should have depends_on for mandatory services (convex, mission-control, tailscale)\n\t\t// Even with no user-selected services, mandatory services are always present\n\t\texpect(parsed.services).toHaveProperty(\"convex\");\n\t\texpect(parsed.services).toHaveProperty(\"mission-control\");\n\n\t\t// Gateway should have restart policy\n\t\texpect(parsed.services[\"openclaw-gateway\"].restart).toBe(\"unless-stopped\");\n\t});\n\n\tit(\"generates Redis companion with proper gateway depends_on\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should have gateway and redis services\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services).toHaveProperty(\"redis\");\n\n\t\t// Redis service should have correct image\n\t\texpect(parsed.services.redis.image).toBe(\"redis:8-alpine\");\n\n\t\t// Gateway depends_on should include redis with service_healthy (redis has healthcheck)\n\t\texpect(parsed.services[\"openclaw-gateway\"].depends_on).toHaveProperty(\"redis\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].depends_on.redis.condition).toBe(\"service_healthy\");\n\n\t\t// Redis should have a healthcheck\n\t\texpect(parsed.services.redis.healthcheck).toBeDefined();\n\t\texpect(parsed.services.redis.healthcheck.test).toContain(\"redis-cli ping\");\n\n\t\t// Redis should have restart policy\n\t\texpect(parsed.services.redis.restart).toBe(\"unless-stopped\");\n\n\t\t// Redis should be on openclaw-network\n\t\texpect(parsed.services.redis.networks).toContain(\"openclaw-network\");\n\n\t\t// Gateway environment should include redis-related openclawEnvVars\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment).toHaveProperty(\"REDIS_HOST\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.REDIS_HOST).toBe(\"redis\");\n\t});\n\n\tit(\"generates parseable YAML for a multi-service stack\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"n8n\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\n\t\t// Should not throw when parsing\n\t\texpect(() => parse(yaml)).not.toThrow();\n\n\t\tconst parsed = parse(yaml);\n\t\texpect(parsed).toHaveProperty(\"services\");\n\t\texpect(parsed).toHaveProperty(\"volumes\");\n\t\texpect(parsed).toHaveProperty(\"networks\");\n\n\t\t// Should have gateway + redis + n8n + postgresql (n8n requires postgresql)\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services).toHaveProperty(\"redis\");\n\t\texpect(parsed.services).toHaveProperty(\"n8n\");\n\t\texpect(parsed.services).toHaveProperty(\"postgresql\");\n\t});\n\n\tit(\"includes all service volumes in top-level volumes section\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"n8n\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Collect all volume names from all resolved services\n\t\tconst serviceVolumeNames = new Set<string>();\n\t\tfor (const rs of resolved.services) {\n\t\t\tfor (const vol of rs.definition.volumes) {\n\t\t\t\tserviceVolumeNames.add(vol.name);\n\t\t\t}\n\t\t}\n\n\t\t// All service volumes must appear in top-level volumes\n\t\tfor (const volName of serviceVolumeNames) {\n\t\t\texpect(parsed.volumes).toHaveProperty(volName);\n\t\t}\n\n\t\t// Gateway uses bind-mount volumes (not in top-level volumes section)\n\t\tconst gwVols = parsed.services[\"openclaw-gateway\"].volumes as string[];\n\t\texpect(gwVols.some((v: string) => v.includes(\".openclaw\"))).toBe(true);\n\t});\n\n\tit(\"includes GPU passthrough when gpu=true and service requires it\", () => {\n\t\tconst resolved = resolve({ services: [\"ollama\"], skillPacks: [] });\n\n\t\t// Modify resolved output to simulate a GPU-required service\n\t\tconst gpuResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: { ...s.definition, gpuRequired: true },\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(gpuResolved, { ...defaultOptions, gpu: true });\n\t\tconst parsed = parse(yaml);\n\n\t\t// Ollama service should have GPU device reservation\n\t\tconst ollamaService = parsed.services.ollama;\n\t\texpect(ollamaService.deploy).toBeDefined();\n\t\texpect(ollamaService.deploy.resources.reservations.devices).toEqual([\n\t\t\t{\n\t\t\t\tdriver: \"nvidia\",\n\t\t\t\tcount: \"all\",\n\t\t\t\tcapabilities: [\"gpu\"],\n\t\t\t},\n\t\t]);\n\t});\n\n\tit(\"does not include GPU passthrough when gpu=false\", () => {\n\t\tconst resolved = resolve({ services: [\"ollama\"], skillPacks: [] });\n\n\t\t// Even if service requires GPU, gpu option is false\n\t\tconst gpuResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: { ...s.definition, gpuRequired: true },\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(gpuResolved, { ...defaultOptions, gpu: false });\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should NOT have deploy with GPU devices\n\t\tconst ollamaService = parsed.services.ollama;\n\t\texpect(ollamaService.deploy?.resources?.reservations?.devices).toBeUndefined();\n\t});\n\n\tit(\"does not map internal-only ports to host\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\"], skillPacks: [] });\n\n\t\t// Modify redis to have an internal-only port\n\t\tconst modifiedResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: {\n\t\t\t\t\t...s.definition,\n\t\t\t\t\tports: [\n\t\t\t\t\t\t...s.definition.ports,\n\t\t\t\t\t\t{ host: 16379, container: 16379, description: \"Redis cluster bus\", exposed: false },\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(modifiedResolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should only have the exposed port, not the internal one\n\t\tconst redisPorts = parsed.services.redis.ports;\n\t\texpect(redisPorts).toHaveLength(1);\n\t\texpect(redisPorts[0]).toContain(\"6379\");\n\t\texpect(redisPorts[0]).not.toContain(\"16379\");\n\t});\n\n\tit(\"uses service_started condition for services without healthcheck\", () => {\n\t\tconst resolved = resolve({ services: [\"minio\"], skillPacks: [] });\n\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Minio may or may not have a healthcheck – the gateway depends_on\n\t\t// condition should reflect the definition accurately\n\t\tconst gatewayDeps = parsed.services[\"openclaw-gateway\"].depends_on;\n\t\tfor (const [id, dep] of Object.entries(gatewayDeps)) {\n\t\t\tconst svc = resolved.services.find((s) => s.definition.id === id);\n\t\t\tif (svc?.definition.healthcheck) {\n\t\t\t\texpect((dep as { condition: string }).condition).toBe(\"service_healthy\");\n\t\t\t} else {\n\t\t\t\texpect((dep as { condition: string }).condition).toBe(\"service_started\");\n\t\t\t}\n\t\t}\n\t});\n\n\tit(\"gateway is always the first service in the output\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"ollama\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// First key in services should be openclaw-gateway\n\t\tconst serviceKeys = Object.keys(parsed.services);\n\t\texpect(serviceKeys[0]).toBe(\"openclaw-gateway\");\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-DvC3SVNc.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "./vi.2VT5v0um-C_jmO7m2.mjs";
2
2
  import { resolve } from "./resolver.mjs";
3
3
  import { compose } from "./composer.mjs";
4
4
  import { parse } from "yaml";
@@ -26,7 +26,8 @@ describe("compose", () => {
26
26
  globalExpect(parsed.networks).toHaveProperty("openclaw-network");
27
27
  globalExpect(parsed.networks["openclaw-network"].driver).toBe("bridge");
28
28
  globalExpect(parsed.services).toHaveProperty("openclaw-cli");
29
- globalExpect(parsed.services["openclaw-gateway"]).not.toHaveProperty("depends_on");
29
+ globalExpect(parsed.services).toHaveProperty("convex");
30
+ globalExpect(parsed.services).toHaveProperty("mission-control");
30
31
  globalExpect(parsed.services["openclaw-gateway"].restart).toBe("unless-stopped");
31
32
  });
32
33
  it("generates Redis companion with proper gateway depends_on", () => {
@@ -1 +1 @@
1
- {"version":3,"file":"composer.test.mjs","names":[],"sources":["../src/composer.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { parse } from \"yaml\";\nimport { compose } from \"./composer.js\";\nimport { resolve } from \"./resolver.js\";\nimport type { ComposeOptions, ResolverOutput } from \"./types.js\";\n\nconst defaultOptions: ComposeOptions = {\n\tprojectName: \"test-project\",\n\tproxy: \"none\",\n\tgpu: false,\n\tplatform: \"linux/amd64\",\n\tdeployment: \"local\",\n\topenclawVersion: \"latest\",\n};\n\ndescribe(\"compose\", () => {\n\tit(\"generates minimal stack with just OpenClaw gateway when no companions\", () => {\n\t\tconst resolved = resolve({ services: [], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should have the gateway service\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].image).toContain(\"ghcr.io/openclaw/openclaw\");\n\n\t\t// Gateway should have core environment\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.HOME).toBe(\"/home/node\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.TERM).toBe(\"xterm-256color\");\n\n\t\t// Gateway uses bind-mount volumes (not named volumes in top-level volumes section)\n\t\tconst gwVolumes = parsed.services[\"openclaw-gateway\"].volumes as string[];\n\t\texpect(gwVolumes.some((v: string) => v.includes(\".openclaw\"))).toBe(true);\n\n\t\t// Should have network\n\t\texpect(parsed.networks).toHaveProperty(\"openclaw-network\");\n\t\texpect(parsed.networks[\"openclaw-network\"].driver).toBe(\"bridge\");\n\n\t\t// Should have CLI companion service\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-cli\");\n\n\t\t// Gateway should have no depends_on (no companions)\n\t\texpect(parsed.services[\"openclaw-gateway\"]).not.toHaveProperty(\"depends_on\");\n\n\t\t// Gateway should have restart policy\n\t\texpect(parsed.services[\"openclaw-gateway\"].restart).toBe(\"unless-stopped\");\n\t});\n\n\tit(\"generates Redis companion with proper gateway depends_on\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should have gateway and redis services\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services).toHaveProperty(\"redis\");\n\n\t\t// Redis service should have correct image\n\t\texpect(parsed.services.redis.image).toBe(\"redis:8-alpine\");\n\n\t\t// Gateway depends_on should include redis with service_healthy (redis has healthcheck)\n\t\texpect(parsed.services[\"openclaw-gateway\"].depends_on).toHaveProperty(\"redis\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].depends_on.redis.condition).toBe(\"service_healthy\");\n\n\t\t// Redis should have a healthcheck\n\t\texpect(parsed.services.redis.healthcheck).toBeDefined();\n\t\texpect(parsed.services.redis.healthcheck.test).toContain(\"redis-cli ping\");\n\n\t\t// Redis should have restart policy\n\t\texpect(parsed.services.redis.restart).toBe(\"unless-stopped\");\n\n\t\t// Redis should be on openclaw-network\n\t\texpect(parsed.services.redis.networks).toContain(\"openclaw-network\");\n\n\t\t// Gateway environment should include redis-related openclawEnvVars\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment).toHaveProperty(\"REDIS_HOST\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.REDIS_HOST).toBe(\"redis\");\n\t});\n\n\tit(\"generates parseable YAML for a multi-service stack\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"n8n\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\n\t\t// Should not throw when parsing\n\t\texpect(() => parse(yaml)).not.toThrow();\n\n\t\tconst parsed = parse(yaml);\n\t\texpect(parsed).toHaveProperty(\"services\");\n\t\texpect(parsed).toHaveProperty(\"volumes\");\n\t\texpect(parsed).toHaveProperty(\"networks\");\n\n\t\t// Should have gateway + redis + n8n + postgresql (n8n requires postgresql)\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services).toHaveProperty(\"redis\");\n\t\texpect(parsed.services).toHaveProperty(\"n8n\");\n\t\texpect(parsed.services).toHaveProperty(\"postgresql\");\n\t});\n\n\tit(\"includes all service volumes in top-level volumes section\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"n8n\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Collect all volume names from all resolved services\n\t\tconst serviceVolumeNames = new Set<string>();\n\t\tfor (const rs of resolved.services) {\n\t\t\tfor (const vol of rs.definition.volumes) {\n\t\t\t\tserviceVolumeNames.add(vol.name);\n\t\t\t}\n\t\t}\n\n\t\t// All service volumes must appear in top-level volumes\n\t\tfor (const volName of serviceVolumeNames) {\n\t\t\texpect(parsed.volumes).toHaveProperty(volName);\n\t\t}\n\n\t\t// Gateway uses bind-mount volumes (not in top-level volumes section)\n\t\tconst gwVols = parsed.services[\"openclaw-gateway\"].volumes as string[];\n\t\texpect(gwVols.some((v: string) => v.includes(\".openclaw\"))).toBe(true);\n\t});\n\n\tit(\"includes GPU passthrough when gpu=true and service requires it\", () => {\n\t\tconst resolved = resolve({ services: [\"ollama\"], skillPacks: [] });\n\n\t\t// Modify resolved output to simulate a GPU-required service\n\t\tconst gpuResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: { ...s.definition, gpuRequired: true },\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(gpuResolved, { ...defaultOptions, gpu: true });\n\t\tconst parsed = parse(yaml);\n\n\t\t// Ollama service should have GPU device reservation\n\t\tconst ollamaService = parsed.services.ollama;\n\t\texpect(ollamaService.deploy).toBeDefined();\n\t\texpect(ollamaService.deploy.resources.reservations.devices).toEqual([\n\t\t\t{\n\t\t\t\tdriver: \"nvidia\",\n\t\t\t\tcount: \"all\",\n\t\t\t\tcapabilities: [\"gpu\"],\n\t\t\t},\n\t\t]);\n\t});\n\n\tit(\"does not include GPU passthrough when gpu=false\", () => {\n\t\tconst resolved = resolve({ services: [\"ollama\"], skillPacks: [] });\n\n\t\t// Even if service requires GPU, gpu option is false\n\t\tconst gpuResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: { ...s.definition, gpuRequired: true },\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(gpuResolved, { ...defaultOptions, gpu: false });\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should NOT have deploy with GPU devices\n\t\tconst ollamaService = parsed.services.ollama;\n\t\texpect(ollamaService.deploy?.resources?.reservations?.devices).toBeUndefined();\n\t});\n\n\tit(\"does not map internal-only ports to host\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\"], skillPacks: [] });\n\n\t\t// Modify redis to have an internal-only port\n\t\tconst modifiedResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: {\n\t\t\t\t\t...s.definition,\n\t\t\t\t\tports: [\n\t\t\t\t\t\t...s.definition.ports,\n\t\t\t\t\t\t{ host: 16379, container: 16379, description: \"Redis cluster bus\", exposed: false },\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(modifiedResolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should only have the exposed port, not the internal one\n\t\tconst redisPorts = parsed.services.redis.ports;\n\t\texpect(redisPorts).toHaveLength(1);\n\t\texpect(redisPorts[0]).toContain(\"6379\");\n\t\texpect(redisPorts[0]).not.toContain(\"16379\");\n\t});\n\n\tit(\"uses service_started condition for services without healthcheck\", () => {\n\t\tconst resolved = resolve({ services: [\"minio\"], skillPacks: [] });\n\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Minio may or may not have a healthcheck – the gateway depends_on\n\t\t// condition should reflect the definition accurately\n\t\tconst gatewayDeps = parsed.services[\"openclaw-gateway\"].depends_on;\n\t\tfor (const [id, dep] of Object.entries(gatewayDeps)) {\n\t\t\tconst svc = resolved.services.find((s) => s.definition.id === id);\n\t\t\tif (svc?.definition.healthcheck) {\n\t\t\t\texpect((dep as { condition: string }).condition).toBe(\"service_healthy\");\n\t\t\t} else {\n\t\t\t\texpect((dep as { condition: string }).condition).toBe(\"service_started\");\n\t\t\t}\n\t\t}\n\t});\n\n\tit(\"gateway is always the first service in the output\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"ollama\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// First key in services should be openclaw-gateway\n\t\tconst serviceKeys = Object.keys(parsed.services);\n\t\texpect(serviceKeys[0]).toBe(\"openclaw-gateway\");\n\t});\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"composer.test.mjs","names":[],"sources":["../src/composer.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { parse } from \"yaml\";\nimport { compose } from \"./composer.js\";\nimport { resolve } from \"./resolver.js\";\nimport type { ComposeOptions, ResolverOutput } from \"./types.js\";\n\nconst defaultOptions: ComposeOptions = {\n\tprojectName: \"test-project\",\n\tproxy: \"none\",\n\tgpu: false,\n\tplatform: \"linux/amd64\",\n\tdeployment: \"local\",\n\topenclawVersion: \"latest\",\n};\n\ndescribe(\"compose\", () => {\n\tit(\"generates minimal stack with just OpenClaw gateway when no companions\", () => {\n\t\tconst resolved = resolve({ services: [], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should have the gateway service\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].image).toContain(\"ghcr.io/openclaw/openclaw\");\n\n\t\t// Gateway should have core environment\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.HOME).toBe(\"/home/node\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.TERM).toBe(\"xterm-256color\");\n\n\t\t// Gateway uses bind-mount volumes (not named volumes in top-level volumes section)\n\t\tconst gwVolumes = parsed.services[\"openclaw-gateway\"].volumes as string[];\n\t\texpect(gwVolumes.some((v: string) => v.includes(\".openclaw\"))).toBe(true);\n\n\t\t// Should have network\n\t\texpect(parsed.networks).toHaveProperty(\"openclaw-network\");\n\t\texpect(parsed.networks[\"openclaw-network\"].driver).toBe(\"bridge\");\n\n\t\t// Should have CLI companion service\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-cli\");\n\n\t\t// Gateway should have depends_on for mandatory services (convex, mission-control, tailscale)\n\t\t// Even with no user-selected services, mandatory services are always present\n\t\texpect(parsed.services).toHaveProperty(\"convex\");\n\t\texpect(parsed.services).toHaveProperty(\"mission-control\");\n\n\t\t// Gateway should have restart policy\n\t\texpect(parsed.services[\"openclaw-gateway\"].restart).toBe(\"unless-stopped\");\n\t});\n\n\tit(\"generates Redis companion with proper gateway depends_on\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should have gateway and redis services\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services).toHaveProperty(\"redis\");\n\n\t\t// Redis service should have correct image\n\t\texpect(parsed.services.redis.image).toBe(\"redis:8-alpine\");\n\n\t\t// Gateway depends_on should include redis with service_healthy (redis has healthcheck)\n\t\texpect(parsed.services[\"openclaw-gateway\"].depends_on).toHaveProperty(\"redis\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].depends_on.redis.condition).toBe(\"service_healthy\");\n\n\t\t// Redis should have a healthcheck\n\t\texpect(parsed.services.redis.healthcheck).toBeDefined();\n\t\texpect(parsed.services.redis.healthcheck.test).toContain(\"redis-cli ping\");\n\n\t\t// Redis should have restart policy\n\t\texpect(parsed.services.redis.restart).toBe(\"unless-stopped\");\n\n\t\t// Redis should be on openclaw-network\n\t\texpect(parsed.services.redis.networks).toContain(\"openclaw-network\");\n\n\t\t// Gateway environment should include redis-related openclawEnvVars\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment).toHaveProperty(\"REDIS_HOST\");\n\t\texpect(parsed.services[\"openclaw-gateway\"].environment.REDIS_HOST).toBe(\"redis\");\n\t});\n\n\tit(\"generates parseable YAML for a multi-service stack\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"n8n\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\n\t\t// Should not throw when parsing\n\t\texpect(() => parse(yaml)).not.toThrow();\n\n\t\tconst parsed = parse(yaml);\n\t\texpect(parsed).toHaveProperty(\"services\");\n\t\texpect(parsed).toHaveProperty(\"volumes\");\n\t\texpect(parsed).toHaveProperty(\"networks\");\n\n\t\t// Should have gateway + redis + n8n + postgresql (n8n requires postgresql)\n\t\texpect(parsed.services).toHaveProperty(\"openclaw-gateway\");\n\t\texpect(parsed.services).toHaveProperty(\"redis\");\n\t\texpect(parsed.services).toHaveProperty(\"n8n\");\n\t\texpect(parsed.services).toHaveProperty(\"postgresql\");\n\t});\n\n\tit(\"includes all service volumes in top-level volumes section\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"n8n\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Collect all volume names from all resolved services\n\t\tconst serviceVolumeNames = new Set<string>();\n\t\tfor (const rs of resolved.services) {\n\t\t\tfor (const vol of rs.definition.volumes) {\n\t\t\t\tserviceVolumeNames.add(vol.name);\n\t\t\t}\n\t\t}\n\n\t\t// All service volumes must appear in top-level volumes\n\t\tfor (const volName of serviceVolumeNames) {\n\t\t\texpect(parsed.volumes).toHaveProperty(volName);\n\t\t}\n\n\t\t// Gateway uses bind-mount volumes (not in top-level volumes section)\n\t\tconst gwVols = parsed.services[\"openclaw-gateway\"].volumes as string[];\n\t\texpect(gwVols.some((v: string) => v.includes(\".openclaw\"))).toBe(true);\n\t});\n\n\tit(\"includes GPU passthrough when gpu=true and service requires it\", () => {\n\t\tconst resolved = resolve({ services: [\"ollama\"], skillPacks: [] });\n\n\t\t// Modify resolved output to simulate a GPU-required service\n\t\tconst gpuResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: { ...s.definition, gpuRequired: true },\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(gpuResolved, { ...defaultOptions, gpu: true });\n\t\tconst parsed = parse(yaml);\n\n\t\t// Ollama service should have GPU device reservation\n\t\tconst ollamaService = parsed.services.ollama;\n\t\texpect(ollamaService.deploy).toBeDefined();\n\t\texpect(ollamaService.deploy.resources.reservations.devices).toEqual([\n\t\t\t{\n\t\t\t\tdriver: \"nvidia\",\n\t\t\t\tcount: \"all\",\n\t\t\t\tcapabilities: [\"gpu\"],\n\t\t\t},\n\t\t]);\n\t});\n\n\tit(\"does not include GPU passthrough when gpu=false\", () => {\n\t\tconst resolved = resolve({ services: [\"ollama\"], skillPacks: [] });\n\n\t\t// Even if service requires GPU, gpu option is false\n\t\tconst gpuResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: { ...s.definition, gpuRequired: true },\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(gpuResolved, { ...defaultOptions, gpu: false });\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should NOT have deploy with GPU devices\n\t\tconst ollamaService = parsed.services.ollama;\n\t\texpect(ollamaService.deploy?.resources?.reservations?.devices).toBeUndefined();\n\t});\n\n\tit(\"does not map internal-only ports to host\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\"], skillPacks: [] });\n\n\t\t// Modify redis to have an internal-only port\n\t\tconst modifiedResolved: ResolverOutput = {\n\t\t\t...resolved,\n\t\t\tservices: resolved.services.map((s) => ({\n\t\t\t\t...s,\n\t\t\t\tdefinition: {\n\t\t\t\t\t...s.definition,\n\t\t\t\t\tports: [\n\t\t\t\t\t\t...s.definition.ports,\n\t\t\t\t\t\t{ host: 16379, container: 16379, description: \"Redis cluster bus\", exposed: false },\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t})),\n\t\t};\n\n\t\tconst yaml = compose(modifiedResolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Should only have the exposed port, not the internal one\n\t\tconst redisPorts = parsed.services.redis.ports;\n\t\texpect(redisPorts).toHaveLength(1);\n\t\texpect(redisPorts[0]).toContain(\"6379\");\n\t\texpect(redisPorts[0]).not.toContain(\"16379\");\n\t});\n\n\tit(\"uses service_started condition for services without healthcheck\", () => {\n\t\tconst resolved = resolve({ services: [\"minio\"], skillPacks: [] });\n\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// Minio may or may not have a healthcheck – the gateway depends_on\n\t\t// condition should reflect the definition accurately\n\t\tconst gatewayDeps = parsed.services[\"openclaw-gateway\"].depends_on;\n\t\tfor (const [id, dep] of Object.entries(gatewayDeps)) {\n\t\t\tconst svc = resolved.services.find((s) => s.definition.id === id);\n\t\t\tif (svc?.definition.healthcheck) {\n\t\t\t\texpect((dep as { condition: string }).condition).toBe(\"service_healthy\");\n\t\t\t} else {\n\t\t\t\texpect((dep as { condition: string }).condition).toBe(\"service_started\");\n\t\t\t}\n\t\t}\n\t});\n\n\tit(\"gateway is always the first service in the output\", () => {\n\t\tconst resolved = resolve({ services: [\"redis\", \"ollama\"], skillPacks: [] });\n\t\tconst yaml = compose(resolved, defaultOptions);\n\t\tconst parsed = parse(yaml);\n\n\t\t// First key in services should be openclaw-gateway\n\t\tconst serviceKeys = Object.keys(parsed.services);\n\t\texpect(serviceKeys[0]).toBe(\"openclaw-gateway\");\n\t});\n});\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- const require_vi_2VT5v0um = require("../vi.2VT5v0um-CRqXre87.cjs");
1
+ const require_vi_2VT5v0um = require("../vi.2VT5v0um-iVBt6Fyq.cjs");
2
2
  const require_deployers_strip_host_ports = require("./strip-host-ports.cjs");
3
3
  //#region src/deployers/strip-host-ports.test.ts
4
4
  require_vi_2VT5v0um.describe("stripHostPorts", () => {
@@ -1,4 +1,4 @@
1
- import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-DvC3SVNc.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "../vi.2VT5v0um-C_jmO7m2.mjs";
2
2
  import { stripHostPorts } from "./strip-host-ports.mjs";
3
3
  //#region src/deployers/strip-host-ports.test.ts
4
4
  describe("stripHostPorts", () => {
package/dist/generate.cjs CHANGED
@@ -7,6 +7,7 @@ const require_composer = require("./composer.cjs");
7
7
  const require_errors = require("./errors.cjs");
8
8
  const require_generators_bare_metal_install = require("./generators/bare-metal-install.cjs");
9
9
  const require_generators_caddy = require("./generators/caddy.cjs");
10
+ const require_generators_clone_repos = require("./generators/clone-repos.cjs");
10
11
  const require_generators_cloud_init = require("./generators/cloud-init.cjs");
11
12
  const require_generators_env = require("./generators/env.cjs");
12
13
  const require_generators_get_shit_done = require("./generators/get-shit-done.cjs");
@@ -101,7 +102,8 @@ function generate(rawInput) {
101
102
  ".env.local",
102
103
  ".env.*.local",
103
104
  "*.log",
104
- "docker-compose.override.yml"
105
+ "docker-compose.override.yml",
106
+ "repos/"
105
107
  ].join("\n");
106
108
  const manifestFiles = require_generators_stack_manifest.generateStackManifest(resolved, input);
107
109
  for (const [path, content] of Object.entries(manifestFiles)) files[path] = content;
@@ -120,8 +122,10 @@ function generate(rawInput) {
120
122
  hasNativeServices: isBareMetal && nativeServices.length > 0,
121
123
  openclawInstallMethod: input.openclawInstallMethod
122
124
  });
123
- const scripts = require_generators_scripts.generateScripts();
125
+ const scripts = require_generators_scripts.generateScripts({ hasGitServices: resolvedForCompose.services.some((s) => s.definition.gitSource) });
124
126
  for (const [path, content] of Object.entries(scripts)) files[path] = content;
127
+ const cloneScripts = require_generators_clone_repos.generateCloneScripts(resolvedForCompose);
128
+ for (const [path, content] of Object.entries(cloneScripts)) files[path] = content;
125
129
  const healthCheckFiles = require_generators_health_check.generateHealthCheck(resolved, {
126
130
  projectName: input.projectName,
127
131
  deploymentType: input.deploymentType
@@ -1 +1 @@
1
- {"version":3,"file":"generate.cjs","names":["migrateConfig","resolve","StackConfigError","partitionBareMetal","resolvedWithOnlyServices","generateTraefikConfig","composeMultiFile","validate","ValidationError","generateEnvFiles","generateStackManifest","generateSkillFiles","generateOpenClawConfig","generateReadme","generateScripts","generateHealthCheck","generateN8nWorkflows","generatePostgresInit","generateCaddyfile","generatePrometheusConfig","generateGrafanaConfig","generateGrafanaDashboard","platformToNativePlatform","generateNativeInstallScripts","generateBareMetalInstall","generateGsdScripts","generateOpenclawInstallScript","generateCloudInit"],"sources":["../src/generate.ts"],"sourcesContent":["import {\n\tpartitionBareMetal,\n\tplatformToNativePlatform,\n\tresolvedWithOnlyServices,\n} from \"./bare-metal-partition.js\";\nimport { composeMultiFile } from \"./composer.js\";\nimport { StackConfigError, ValidationError } from \"./errors.js\";\nimport { generateBareMetalInstall } from \"./generators/bare-metal-install.js\";\nimport { generateCaddyfile } from \"./generators/caddy.js\";\nimport { generateCloudInit } from \"./generators/cloud-init.js\";\nimport { generateEnvFiles } from \"./generators/env.js\";\nimport { generateGsdScripts } from \"./generators/get-shit-done.js\";\nimport { generateGrafanaConfig, generateGrafanaDashboard } from \"./generators/grafana.js\";\nimport { generateHealthCheck } from \"./generators/health-check.js\";\nimport { generateN8nWorkflows } from \"./generators/n8n-workflows.js\";\nimport { generateNativeInstallScripts } from \"./generators/native-services.js\";\nimport { generateOpenclawInstallScript } from \"./generators/openclaw-install-script.js\";\nimport { generateOpenClawConfig } from \"./generators/openclaw-json.js\";\nimport { generatePostgresInit } from \"./generators/postgres-init.js\";\nimport { generatePrometheusConfig } from \"./generators/prometheus.js\";\nimport { generateReadme } from \"./generators/readme.js\";\nimport { generateScripts } from \"./generators/scripts.js\";\nimport { generateSkillFiles } from \"./generators/skills.js\";\nimport { generateStackManifest } from \"./generators/stack-manifest.js\";\nimport { generateTraefikConfig } from \"./generators/traefik.js\";\nimport { migrateConfig } from \"./migrations.js\";\nimport { resolve } from \"./resolver.js\";\nimport type {\n\tGeneratedFiles,\n\tGenerationInput,\n\tGenerationResult,\n\tPlatform,\n\tResolverInput,\n} from \"./types.js\";\nimport { validate } from \"./validator.js\";\n\n/** Resolver/compose only support linux image platforms; normalize for bare-metal (windows/macos). */\nfunction getComposePlatform(platform: Platform): \"linux/amd64\" | \"linux/arm64\" {\n\tif (platform === \"linux/amd64\" || platform === \"linux/arm64\") return platform;\n\treturn \"linux/amd64\";\n}\n\n/**\n * Main orchestration function: takes generation input, resolves dependencies,\n * generates all files, validates, and returns the complete file tree.\n */\nexport function generate(rawInput: GenerationInput): GenerationResult {\n\t// Apply config migrations if needed\n\tconst input = migrateConfig(rawInput as Record<string, unknown>) as GenerationInput;\n\n\tconst composePlatform = getComposePlatform(input.platform);\n\n\t// 1. Resolve dependencies\n\tconst resolverInput: ResolverInput = {\n\t\tservices: input.services,\n\t\tskillPacks: input.skillPacks,\n\t\taiProviders: input.aiProviders,\n\t\tproxy: input.proxy,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tmonitoring: input.monitoring,\n\t};\n\tconst resolved = resolve(resolverInput);\n\n\tif (!resolved.isValid) {\n\t\tthrow new StackConfigError(\n\t\t\t`Invalid stack configuration: ${resolved.errors.map((e) => e.message).join(\"; \")}`,\n\t\t);\n\t}\n\n\tconst isBareMetal = input.deploymentType === \"bare-metal\";\n\tlet resolvedForCompose = resolved;\n\tlet nativeIds = new Set<string>();\n\tlet nativeServices: typeof resolved.services = [];\n\tlet dockerOnlyServices: typeof resolved.services = [];\n\n\tif (isBareMetal) {\n\t\tconst partition = partitionBareMetal(resolved, input.platform);\n\t\tnativeServices = partition.nativeServices;\n\t\tdockerOnlyServices = partition.dockerOnlyServices;\n\t\tnativeIds = partition.nativeIds;\n\t\tif (nativeServices.length > 0) {\n\t\t\tresolvedForCompose = resolvedWithOnlyServices(resolved, dockerOnlyServices);\n\t\t}\n\t}\n\n\t// 2. Generate Docker Compose YAML (multi-file)\n\t// Compute Traefik labels before composing (labels get injected into docker-compose services)\n\tlet traefikOutput: ReturnType<typeof generateTraefikConfig> | undefined;\n\tif (input.proxy === \"traefik\" && input.domain) {\n\t\ttraefikOutput = generateTraefikConfig(resolvedForCompose, input.domain);\n\t}\n\n\tconst composeOptions = {\n\t\tprojectName: input.projectName,\n\t\tproxy: input.proxy,\n\t\tproxyHttpPort: input.proxyHttpPort,\n\t\tproxyHttpsPort: input.proxyHttpsPort,\n\t\tportOverrides: input.portOverrides,\n\t\tdomain: input.domain,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tdeployment: input.deployment,\n\t\topenclawVersion: input.openclawVersion,\n\t\tbareMetalNativeHost: isBareMetal && nativeIds.size > 0,\n\t\ttraefikLabels: traefikOutput?.serviceLabels,\n\t\topenclawImage: input.openclawImage ?? \"official\",\n\t\thardened: input.hardened ?? true,\n\t\topenclawInstallMethod: input.openclawInstallMethod ?? \"docker\",\n\t};\n\tconst composeResult = composeMultiFile(resolvedForCompose, composeOptions);\n\n\t// 3. Validate (using the base docker-compose.yml)\n\tconst validation = validate(resolvedForCompose, composeResult.files[\"docker-compose.yml\"] ?? \"\", {\n\t\tdomain: input.domain,\n\t\tgenerateSecrets: input.generateSecrets,\n\t});\n\tif (!validation.valid) {\n\t\tthrow new ValidationError(\n\t\t\t`Validation failed: ${validation.errors.map((e) => e.message).join(\"; \")}`,\n\t\t);\n\t}\n\n\t// 4. Generate all files\n\tconst files: GeneratedFiles = {};\n\n\t// Docker Compose (multi-file output)\n\tfor (const [filename, content] of Object.entries(composeResult.files)) {\n\t\tfiles[filename] = content;\n\t}\n\n\t// Environment files (when bare-metal with native services, host vars use host.docker.internal)\n\tconst envFiles = generateEnvFiles(resolved, {\n\t\tgenerateSecrets: input.generateSecrets,\n\t\tdomain: input.domain,\n\t\topenclawVersion: input.openclawVersion,\n\t\tnativeServiceIds: isBareMetal ? nativeIds : undefined,\n\t\tcomposeFiles: Object.keys(composeResult.files),\n\t\tcomposeProfiles: composeResult.profiles,\n\t\topenclawImage: input.openclawImage,\n\t});\n\tfiles[\".env.example\"] = envFiles.envExample;\n\tfiles[\".env\"] = envFiles.env;\n\n\t// .gitignore\n\tfiles[\".gitignore\"] = [\n\t\t\".env\",\n\t\t\".env.local\",\n\t\t\".env.*.local\",\n\t\t\"*.log\",\n\t\t\"docker-compose.override.yml\",\n\t].join(\"\\n\");\n\n\t// Stack manifest (consumed by Mission Control)\n\tconst manifestFiles = generateStackManifest(resolved, input);\n\tfor (const [path, content] of Object.entries(manifestFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// Skills\n\tconst skillFiles = generateSkillFiles(resolved);\n\tfor (const [path, content] of Object.entries(skillFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// OpenClaw Core Configuration\n\tfiles[\"openclaw/config/openclaw.json\"] = generateOpenClawConfig(resolved, {\n\t\tdeploymentType: input.deploymentType,\n\t\tgatewayPort: 18789,\n\t\topenclawVersion: input.openclawVersion,\n\t});\n\n\t// README\n\tfiles[\"README.md\"] = generateReadme(resolved, {\n\t\tprojectName: input.projectName,\n\t\tdomain: input.domain,\n\t\tproxy: input.proxy,\n\t\tdeploymentType: input.deploymentType,\n\t\thasNativeServices: isBareMetal && nativeServices.length > 0,\n\t\topenclawInstallMethod: input.openclawInstallMethod,\n\t});\n\n\t// Scripts\n\tconst scripts = generateScripts();\n\tfor (const [path, content] of Object.entries(scripts)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// Health check scripts (dynamic, stack-specific)\n\tconst healthCheckFiles = generateHealthCheck(resolved, {\n\t\tprojectName: input.projectName,\n\t\tdeploymentType: input.deploymentType,\n\t});\n\tfor (const [path, content] of Object.entries(healthCheckFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// n8n workflows\n\tconst n8nWorkflows = generateN8nWorkflows(resolved);\n\tfor (const [path, content] of Object.entries(n8nWorkflows)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// PostgreSQL init script (creates per-service databases and users)\n\tconst postgresInit = generatePostgresInit(resolved);\n\tif (postgresInit) {\n\t\tfiles[\"postgres/init-databases.sh\"] = postgresInit;\n\t}\n\n\t// Caddy config\n\tif (input.proxy === \"caddy\" && input.domain) {\n\t\tfiles[\"caddy/Caddyfile\"] = generateCaddyfile(resolved, input.domain);\n\t}\n\n\t// Traefik config (labels are already injected via composeOptions.traefikLabels)\n\tif (traefikOutput) {\n\t\tfiles[\"traefik/traefik.yml\"] = traefikOutput.staticConfig;\n\t}\n\n\t// Prometheus config\n\tconst hasPrometheus = resolved.services.some((s) => s.definition.id === \"prometheus\");\n\tif (hasPrometheus) {\n\t\tfiles[\"prometheus/prometheus.yml\"] = generatePrometheusConfig(resolved);\n\t}\n\n\t// Grafana config\n\tconst hasGrafana = resolved.services.some((s) => s.definition.id === \"grafana\");\n\tif (hasGrafana) {\n\t\tconst grafanaFiles = generateGrafanaConfig();\n\t\tfor (const [path, content] of Object.entries(grafanaFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t\t// Grafana dashboard\n\t\tfiles[\"config/grafana/dashboards/openclaw-stack-overview.json\"] = generateGrafanaDashboard();\n\t}\n\n\t// Docker Compose override (empty template)\n\tfiles[\"docker-compose.override.yml\"] = [\n\t\t\"# Local overrides for docker-compose.yml\",\n\t\t\"# This file is gitignored — use it for personal port changes, extra volumes, etc.\",\n\t\t\"services: {}\",\n\t\t\"\",\n\t].join(\"\\n\");\n\n\t// SERVICES.md documentation\n\tfiles[\"docs/SERVICES.md\"] = generateServicesDoc(resolved);\n\n\t// Bare-metal: native install scripts + top-level installer\n\tif (isBareMetal) {\n\t\tif (nativeServices.length > 0) {\n\t\t\tconst nativePlatform = platformToNativePlatform(input.platform);\n\t\t\tconst nativeScripts = generateNativeInstallScripts({\n\t\t\t\tnativeServices,\n\t\t\t\tplatform: nativePlatform,\n\t\t\t\tprojectName: input.projectName,\n\t\t\t});\n\t\t\tfor (const [path, content] of Object.entries(nativeScripts)) {\n\t\t\t\tfiles[path] = content;\n\t\t\t}\n\t\t}\n\t\tconst bareMetalFiles = generateBareMetalInstall({\n\t\t\tplatform: input.platform,\n\t\t\tprojectName: input.projectName,\n\t\t\thasNativeServices: nativeServices.length > 0,\n\t\t});\n\t\tfor (const [path, content] of Object.entries(bareMetalFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t}\n\n\t// Get-Shit-Done setup scripts\n\tif (input.gsdRuntimes && input.gsdRuntimes.length > 0) {\n\t\tconst gsdScripts = generateGsdScripts(input.gsdRuntimes);\n\t\tif (gsdScripts) {\n\t\t\tfiles[\"openclaw/scripts/setup-gsd.sh\"] = gsdScripts.sh;\n\t\t\tfiles[\"openclaw/scripts/setup-gsd.ps1\"] = gsdScripts.ps1;\n\t\t}\n\t}\n\n\t// OpenClaw direct install scripts (host-based, no Docker gateway)\n\tif (input.openclawInstallMethod === \"direct\") {\n\t\tconst installScripts = generateOpenclawInstallScript({\n\t\t\tprojectName: input.projectName,\n\t\t});\n\t\tfor (const [path, content] of Object.entries(installScripts)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t}\n\n\t// Cloud-init deploy target\n\tif (input.deployTarget === \"cloud-init\") {\n\t\tconst mainCompose = composeResult.files[\"docker-compose.yml\"] ?? \"\";\n\t\tfiles[\"cloud-init.yml\"] = generateCloudInit({\n\t\t\tcomposeYaml: mainCompose,\n\t\t\tenvContent: envFiles.env,\n\t\t\tprojectName: input.projectName,\n\t\t\tgatewayPort: 18789,\n\t\t});\n\t}\n\n\t// 5. Calculate metadata\n\tconst skillCount = resolved.services.reduce((sum, s) => sum + s.definition.skills.length, 0);\n\n\treturn {\n\t\tfiles,\n\t\tmetadata: {\n\t\t\tserviceCount: resolved.services.length,\n\t\t\tskillCount,\n\t\t\testimatedMemoryMB: resolved.estimatedMemoryMB,\n\t\t\tresolvedServices: resolved.services.map((s) => s.definition.id),\n\t\t\tgeneratedAt: new Date().toISOString(),\n\t\t},\n\t};\n}\n\nexport function generateServicesDoc(resolved: import(\"./types.js\").ResolverOutput): string {\n\tconst lines: string[] = [\n\t\t\"# Service Reference\",\n\t\t\"\",\n\t\t\"This document describes all companion services in your OpenClaw stack.\",\n\t\t\"\",\n\t];\n\n\tfor (const svc of resolved.services) {\n\t\tconst def = svc.definition;\n\t\tlines.push(`## ${def.icon} ${def.name}`);\n\t\tlines.push(\"\");\n\t\tlines.push(def.description);\n\t\tlines.push(\"\");\n\t\tlines.push(`- **Image**: \\`${def.image}:${def.imageTag}\\``);\n\t\tlines.push(`- **Category**: ${def.category}`);\n\t\tlines.push(`- **Maturity**: ${def.maturity}`);\n\t\tif (def.minMemoryMB) {\n\t\t\tlines.push(`- **Min Memory**: ${def.minMemoryMB}MB`);\n\t\t}\n\t\tif (def.ports.length > 0) {\n\t\t\tlines.push(\"- **Ports**:\");\n\t\t\tfor (const p of def.ports) {\n\t\t\t\tlines.push(\n\t\t\t\t\t` - \\`${p.container}\\` — ${p.description}${p.exposed ? \"\" : \" (internal only)\"}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tlines.push(`- **Docs**: ${def.docsUrl}`);\n\t\tlines.push(\"\");\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SAAS,mBAAmB,UAAmD;AAC9E,KAAI,aAAa,iBAAiB,aAAa,cAAe,QAAO;AACrE,QAAO;;;;;;AAOR,SAAgB,SAAS,UAA6C;CAErE,MAAM,QAAQA,mBAAAA,cAAc,SAAoC;CAEhE,MAAM,kBAAkB,mBAAmB,MAAM,SAAS;CAY1D,MAAM,WAAWC,iBAAAA,QAToB;EACpC,UAAU,MAAM;EAChB,YAAY,MAAM;EAClB,aAAa,MAAM;EACnB,OAAO,MAAM;EACb,KAAK,MAAM;EACX,UAAU;EACV,YAAY,MAAM;EAClB,CACsC;AAEvC,KAAI,CAAC,SAAS,QACb,OAAM,IAAIC,eAAAA,iBACT,gCAAgC,SAAS,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK,GAChF;CAGF,MAAM,cAAc,MAAM,mBAAmB;CAC7C,IAAI,qBAAqB;CACzB,IAAI,4BAAY,IAAI,KAAa;CACjC,IAAI,iBAA2C,EAAE;CACjD,IAAI,qBAA+C,EAAE;AAErD,KAAI,aAAa;EAChB,MAAM,YAAYC,6BAAAA,mBAAmB,UAAU,MAAM,SAAS;AAC9D,mBAAiB,UAAU;AAC3B,uBAAqB,UAAU;AAC/B,cAAY,UAAU;AACtB,MAAI,eAAe,SAAS,EAC3B,sBAAqBC,6BAAAA,yBAAyB,UAAU,mBAAmB;;CAM7E,IAAI;AACJ,KAAI,MAAM,UAAU,aAAa,MAAM,OACtC,iBAAgBC,2BAAAA,sBAAsB,oBAAoB,MAAM,OAAO;CAGxE,MAAM,iBAAiB;EACtB,aAAa,MAAM;EACnB,OAAO,MAAM;EACb,eAAe,MAAM;EACrB,gBAAgB,MAAM;EACtB,eAAe,MAAM;EACrB,QAAQ,MAAM;EACd,KAAK,MAAM;EACX,UAAU;EACV,YAAY,MAAM;EAClB,iBAAiB,MAAM;EACvB,qBAAqB,eAAe,UAAU,OAAO;EACrD,eAAe,eAAe;EAC9B,eAAe,MAAM,iBAAiB;EACtC,UAAU,MAAM,YAAY;EAC5B,uBAAuB,MAAM,yBAAyB;EACtD;CACD,MAAM,gBAAgBC,iBAAAA,iBAAiB,oBAAoB,eAAe;CAG1E,MAAM,aAAaC,kBAAAA,SAAS,oBAAoB,cAAc,MAAM,yBAAyB,IAAI;EAChG,QAAQ,MAAM;EACd,iBAAiB,MAAM;EACvB,CAAC;AACF,KAAI,CAAC,WAAW,MACf,OAAM,IAAIC,eAAAA,gBACT,sBAAsB,WAAW,OAAO,KAAK,MAAM,EAAE,QAAQ,CAAC,KAAK,KAAK,GACxE;CAIF,MAAM,QAAwB,EAAE;AAGhC,MAAK,MAAM,CAAC,UAAU,YAAY,OAAO,QAAQ,cAAc,MAAM,CACpE,OAAM,YAAY;CAInB,MAAM,WAAWC,uBAAAA,iBAAiB,UAAU;EAC3C,iBAAiB,MAAM;EACvB,QAAQ,MAAM;EACd,iBAAiB,MAAM;EACvB,kBAAkB,cAAc,YAAY,KAAA;EAC5C,cAAc,OAAO,KAAK,cAAc,MAAM;EAC9C,iBAAiB,cAAc;EAC/B,eAAe,MAAM;EACrB,CAAC;AACF,OAAM,kBAAkB,SAAS;AACjC,OAAM,UAAU,SAAS;AAGzB,OAAM,gBAAgB;EACrB;EACA;EACA;EACA;EACA;EACA,CAAC,KAAK,KAAK;CAGZ,MAAM,gBAAgBC,kCAAAA,sBAAsB,UAAU,MAAM;AAC5D,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,cAAc,CAC1D,OAAM,QAAQ;CAIf,MAAM,aAAaC,eAAAA,mBAAmB,SAAS;AAC/C,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,WAAW,CACvD,OAAM,QAAQ;AAIf,OAAM,mCAAmCC,iCAAAA,uBAAuB,UAAU;EACzE,gBAAgB,MAAM;EACtB,aAAa;EACb,iBAAiB,MAAM;EACvB,CAAC;AAGF,OAAM,eAAeC,0BAAAA,eAAe,UAAU;EAC7C,aAAa,MAAM;EACnB,QAAQ,MAAM;EACd,OAAO,MAAM;EACb,gBAAgB,MAAM;EACtB,mBAAmB,eAAe,eAAe,SAAS;EAC1D,uBAAuB,MAAM;EAC7B,CAAC;CAGF,MAAM,UAAUC,2BAAAA,iBAAiB;AACjC,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,QAAQ,CACpD,OAAM,QAAQ;CAIf,MAAM,mBAAmBC,gCAAAA,oBAAoB,UAAU;EACtD,aAAa,MAAM;EACnB,gBAAgB,MAAM;EACtB,CAAC;AACF,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,iBAAiB,CAC7D,OAAM,QAAQ;CAIf,MAAM,eAAeC,iCAAAA,qBAAqB,SAAS;AACnD,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,aAAa,CACzD,OAAM,QAAQ;CAIf,MAAM,eAAeC,iCAAAA,qBAAqB,SAAS;AACnD,KAAI,aACH,OAAM,gCAAgC;AAIvC,KAAI,MAAM,UAAU,WAAW,MAAM,OACpC,OAAM,qBAAqBC,yBAAAA,kBAAkB,UAAU,MAAM,OAAO;AAIrE,KAAI,cACH,OAAM,yBAAyB,cAAc;AAK9C,KADsB,SAAS,SAAS,MAAM,MAAM,EAAE,WAAW,OAAO,aAAa,CAEpF,OAAM,+BAA+BC,8BAAAA,yBAAyB,SAAS;AAKxE,KADmB,SAAS,SAAS,MAAM,MAAM,EAAE,WAAW,OAAO,UAAU,EAC/D;EACf,MAAM,eAAeC,2BAAAA,uBAAuB;AAC5C,OAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,aAAa,CACzD,OAAM,QAAQ;AAGf,QAAM,4DAA4DC,2BAAAA,0BAA0B;;AAI7F,OAAM,iCAAiC;EACtC;EACA;EACA;EACA;EACA,CAAC,KAAK,KAAK;AAGZ,OAAM,sBAAsB,oBAAoB,SAAS;AAGzD,KAAI,aAAa;AAChB,MAAI,eAAe,SAAS,GAAG;GAC9B,MAAM,iBAAiBC,6BAAAA,yBAAyB,MAAM,SAAS;GAC/D,MAAM,gBAAgBC,mCAAAA,6BAA6B;IAClD;IACA,UAAU;IACV,aAAa,MAAM;IACnB,CAAC;AACF,QAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,cAAc,CAC1D,OAAM,QAAQ;;EAGhB,MAAM,iBAAiBC,sCAAAA,yBAAyB;GAC/C,UAAU,MAAM;GAChB,aAAa,MAAM;GACnB,mBAAmB,eAAe,SAAS;GAC3C,CAAC;AACF,OAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,eAAe,CAC3D,OAAM,QAAQ;;AAKhB,KAAI,MAAM,eAAe,MAAM,YAAY,SAAS,GAAG;EACtD,MAAM,aAAaC,iCAAAA,mBAAmB,MAAM,YAAY;AACxD,MAAI,YAAY;AACf,SAAM,mCAAmC,WAAW;AACpD,SAAM,oCAAoC,WAAW;;;AAKvD,KAAI,MAAM,0BAA0B,UAAU;EAC7C,MAAM,iBAAiBC,2CAAAA,8BAA8B,EACpD,aAAa,MAAM,aACnB,CAAC;AACF,OAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,eAAe,CAC3D,OAAM,QAAQ;;AAKhB,KAAI,MAAM,iBAAiB,aAE1B,OAAM,oBAAoBC,8BAAAA,kBAAkB;EAC3C,aAFmB,cAAc,MAAM,yBAAyB;EAGhE,YAAY,SAAS;EACrB,aAAa,MAAM;EACnB,aAAa;EACb,CAAC;CAIH,MAAM,aAAa,SAAS,SAAS,QAAQ,KAAK,MAAM,MAAM,EAAE,WAAW,OAAO,QAAQ,EAAE;AAE5F,QAAO;EACN;EACA,UAAU;GACT,cAAc,SAAS,SAAS;GAChC;GACA,mBAAmB,SAAS;GAC5B,kBAAkB,SAAS,SAAS,KAAK,MAAM,EAAE,WAAW,GAAG;GAC/D,8BAAa,IAAI,MAAM,EAAC,aAAa;GACrC;EACD;;AAGF,SAAgB,oBAAoB,UAAuD;CAC1F,MAAM,QAAkB;EACvB;EACA;EACA;EACA;EACA;AAED,MAAK,MAAM,OAAO,SAAS,UAAU;EACpC,MAAM,MAAM,IAAI;AAChB,QAAM,KAAK,MAAM,IAAI,KAAK,GAAG,IAAI,OAAO;AACxC,QAAM,KAAK,GAAG;AACd,QAAM,KAAK,IAAI,YAAY;AAC3B,QAAM,KAAK,GAAG;AACd,QAAM,KAAK,kBAAkB,IAAI,MAAM,GAAG,IAAI,SAAS,IAAI;AAC3D,QAAM,KAAK,mBAAmB,IAAI,WAAW;AAC7C,QAAM,KAAK,mBAAmB,IAAI,WAAW;AAC7C,MAAI,IAAI,YACP,OAAM,KAAK,qBAAqB,IAAI,YAAY,IAAI;AAErD,MAAI,IAAI,MAAM,SAAS,GAAG;AACzB,SAAM,KAAK,eAAe;AAC1B,QAAK,MAAM,KAAK,IAAI,MACnB,OAAM,KACL,SAAS,EAAE,UAAU,OAAO,EAAE,cAAc,EAAE,UAAU,KAAK,qBAC7D;;AAGH,QAAM,KAAK,eAAe,IAAI,UAAU;AACxC,QAAM,KAAK,GAAG;;AAGf,QAAO,MAAM,KAAK,KAAK"}
1
+ {"version":3,"file":"generate.cjs","names":["migrateConfig","resolve","StackConfigError","partitionBareMetal","resolvedWithOnlyServices","generateTraefikConfig","composeMultiFile","validate","ValidationError","generateEnvFiles","generateStackManifest","generateSkillFiles","generateOpenClawConfig","generateReadme","generateScripts","generateCloneScripts","generateHealthCheck","generateN8nWorkflows","generatePostgresInit","generateCaddyfile","generatePrometheusConfig","generateGrafanaConfig","generateGrafanaDashboard","platformToNativePlatform","generateNativeInstallScripts","generateBareMetalInstall","generateGsdScripts","generateOpenclawInstallScript","generateCloudInit"],"sources":["../src/generate.ts"],"sourcesContent":["import {\n\tpartitionBareMetal,\n\tplatformToNativePlatform,\n\tresolvedWithOnlyServices,\n} from \"./bare-metal-partition.js\";\nimport { composeMultiFile } from \"./composer.js\";\nimport { StackConfigError, ValidationError } from \"./errors.js\";\nimport { generateBareMetalInstall } from \"./generators/bare-metal-install.js\";\nimport { generateCaddyfile } from \"./generators/caddy.js\";\nimport { generateCloneScripts } from \"./generators/clone-repos.js\";\nimport { generateCloudInit } from \"./generators/cloud-init.js\";\nimport { generateEnvFiles } from \"./generators/env.js\";\nimport { generateGsdScripts } from \"./generators/get-shit-done.js\";\nimport { generateGrafanaConfig, generateGrafanaDashboard } from \"./generators/grafana.js\";\nimport { generateHealthCheck } from \"./generators/health-check.js\";\nimport { generateN8nWorkflows } from \"./generators/n8n-workflows.js\";\nimport { generateNativeInstallScripts } from \"./generators/native-services.js\";\nimport { generateOpenclawInstallScript } from \"./generators/openclaw-install-script.js\";\nimport { generateOpenClawConfig } from \"./generators/openclaw-json.js\";\nimport { generatePostgresInit } from \"./generators/postgres-init.js\";\nimport { generatePrometheusConfig } from \"./generators/prometheus.js\";\nimport { generateReadme } from \"./generators/readme.js\";\nimport { generateScripts } from \"./generators/scripts.js\";\nimport { generateSkillFiles } from \"./generators/skills.js\";\nimport { generateStackManifest } from \"./generators/stack-manifest.js\";\nimport { generateTraefikConfig } from \"./generators/traefik.js\";\nimport { migrateConfig } from \"./migrations.js\";\nimport { resolve } from \"./resolver.js\";\nimport type {\n\tGeneratedFiles,\n\tGenerationInput,\n\tGenerationResult,\n\tPlatform,\n\tResolverInput,\n} from \"./types.js\";\nimport { validate } from \"./validator.js\";\n\n/** Resolver/compose only support linux image platforms; normalize for bare-metal (windows/macos). */\nfunction getComposePlatform(platform: Platform): \"linux/amd64\" | \"linux/arm64\" {\n\tif (platform === \"linux/amd64\" || platform === \"linux/arm64\") return platform;\n\treturn \"linux/amd64\";\n}\n\n/**\n * Main orchestration function: takes generation input, resolves dependencies,\n * generates all files, validates, and returns the complete file tree.\n */\nexport function generate(rawInput: GenerationInput): GenerationResult {\n\t// Apply config migrations if needed\n\tconst input = migrateConfig(rawInput as Record<string, unknown>) as GenerationInput;\n\n\tconst composePlatform = getComposePlatform(input.platform);\n\n\t// 1. Resolve dependencies\n\tconst resolverInput: ResolverInput = {\n\t\tservices: input.services,\n\t\tskillPacks: input.skillPacks,\n\t\taiProviders: input.aiProviders,\n\t\tproxy: input.proxy,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tmonitoring: input.monitoring,\n\t};\n\tconst resolved = resolve(resolverInput);\n\n\tif (!resolved.isValid) {\n\t\tthrow new StackConfigError(\n\t\t\t`Invalid stack configuration: ${resolved.errors.map((e) => e.message).join(\"; \")}`,\n\t\t);\n\t}\n\n\tconst isBareMetal = input.deploymentType === \"bare-metal\";\n\tlet resolvedForCompose = resolved;\n\tlet nativeIds = new Set<string>();\n\tlet nativeServices: typeof resolved.services = [];\n\tlet dockerOnlyServices: typeof resolved.services = [];\n\n\tif (isBareMetal) {\n\t\tconst partition = partitionBareMetal(resolved, input.platform);\n\t\tnativeServices = partition.nativeServices;\n\t\tdockerOnlyServices = partition.dockerOnlyServices;\n\t\tnativeIds = partition.nativeIds;\n\t\tif (nativeServices.length > 0) {\n\t\t\tresolvedForCompose = resolvedWithOnlyServices(resolved, dockerOnlyServices);\n\t\t}\n\t}\n\n\t// 2. Generate Docker Compose YAML (multi-file)\n\t// Compute Traefik labels before composing (labels get injected into docker-compose services)\n\tlet traefikOutput: ReturnType<typeof generateTraefikConfig> | undefined;\n\tif (input.proxy === \"traefik\" && input.domain) {\n\t\ttraefikOutput = generateTraefikConfig(resolvedForCompose, input.domain);\n\t}\n\n\tconst composeOptions = {\n\t\tprojectName: input.projectName,\n\t\tproxy: input.proxy,\n\t\tproxyHttpPort: input.proxyHttpPort,\n\t\tproxyHttpsPort: input.proxyHttpsPort,\n\t\tportOverrides: input.portOverrides,\n\t\tdomain: input.domain,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tdeployment: input.deployment,\n\t\topenclawVersion: input.openclawVersion,\n\t\tbareMetalNativeHost: isBareMetal && nativeIds.size > 0,\n\t\ttraefikLabels: traefikOutput?.serviceLabels,\n\t\topenclawImage: input.openclawImage ?? \"official\",\n\t\thardened: input.hardened ?? true,\n\t\topenclawInstallMethod: input.openclawInstallMethod ?? \"docker\",\n\t};\n\tconst composeResult = composeMultiFile(resolvedForCompose, composeOptions);\n\n\t// 3. Validate (using the base docker-compose.yml)\n\tconst validation = validate(resolvedForCompose, composeResult.files[\"docker-compose.yml\"] ?? \"\", {\n\t\tdomain: input.domain,\n\t\tgenerateSecrets: input.generateSecrets,\n\t});\n\tif (!validation.valid) {\n\t\tthrow new ValidationError(\n\t\t\t`Validation failed: ${validation.errors.map((e) => e.message).join(\"; \")}`,\n\t\t);\n\t}\n\n\t// 4. Generate all files\n\tconst files: GeneratedFiles = {};\n\n\t// Docker Compose (multi-file output)\n\tfor (const [filename, content] of Object.entries(composeResult.files)) {\n\t\tfiles[filename] = content;\n\t}\n\n\t// Environment files (when bare-metal with native services, host vars use host.docker.internal)\n\tconst envFiles = generateEnvFiles(resolved, {\n\t\tgenerateSecrets: input.generateSecrets,\n\t\tdomain: input.domain,\n\t\topenclawVersion: input.openclawVersion,\n\t\tnativeServiceIds: isBareMetal ? nativeIds : undefined,\n\t\tcomposeFiles: Object.keys(composeResult.files),\n\t\tcomposeProfiles: composeResult.profiles,\n\t\topenclawImage: input.openclawImage,\n\t});\n\tfiles[\".env.example\"] = envFiles.envExample;\n\tfiles[\".env\"] = envFiles.env;\n\n\t// .gitignore\n\tfiles[\".gitignore\"] = [\n\t\t\".env\",\n\t\t\".env.local\",\n\t\t\".env.*.local\",\n\t\t\"*.log\",\n\t\t\"docker-compose.override.yml\",\n\t\t\"repos/\",\n\t].join(\"\\n\");\n\n\t// Stack manifest (consumed by Mission Control)\n\tconst manifestFiles = generateStackManifest(resolved, input);\n\tfor (const [path, content] of Object.entries(manifestFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// Skills\n\tconst skillFiles = generateSkillFiles(resolved);\n\tfor (const [path, content] of Object.entries(skillFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// OpenClaw Core Configuration\n\tfiles[\"openclaw/config/openclaw.json\"] = generateOpenClawConfig(resolved, {\n\t\tdeploymentType: input.deploymentType,\n\t\tgatewayPort: 18789,\n\t\topenclawVersion: input.openclawVersion,\n\t});\n\n\t// README\n\tfiles[\"README.md\"] = generateReadme(resolved, {\n\t\tprojectName: input.projectName,\n\t\tdomain: input.domain,\n\t\tproxy: input.proxy,\n\t\tdeploymentType: input.deploymentType,\n\t\thasNativeServices: isBareMetal && nativeServices.length > 0,\n\t\topenclawInstallMethod: input.openclawInstallMethod,\n\t});\n\n\t// Scripts\n\tconst hasGitServices = resolvedForCompose.services.some((s) => s.definition.gitSource);\n\tconst scripts = generateScripts({ hasGitServices });\n\tfor (const [path, content] of Object.entries(scripts)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// Clone scripts for git-based services (SaaS boilerplates)\n\tconst cloneScripts = generateCloneScripts(resolvedForCompose);\n\tfor (const [path, content] of Object.entries(cloneScripts)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// Health check scripts (dynamic, stack-specific)\n\tconst healthCheckFiles = generateHealthCheck(resolved, {\n\t\tprojectName: input.projectName,\n\t\tdeploymentType: input.deploymentType,\n\t});\n\tfor (const [path, content] of Object.entries(healthCheckFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// n8n workflows\n\tconst n8nWorkflows = generateN8nWorkflows(resolved);\n\tfor (const [path, content] of Object.entries(n8nWorkflows)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// PostgreSQL init script (creates per-service databases and users)\n\tconst postgresInit = generatePostgresInit(resolved);\n\tif (postgresInit) {\n\t\tfiles[\"postgres/init-databases.sh\"] = postgresInit;\n\t}\n\n\t// Caddy config\n\tif (input.proxy === \"caddy\" && input.domain) {\n\t\tfiles[\"caddy/Caddyfile\"] = generateCaddyfile(resolved, input.domain);\n\t}\n\n\t// Traefik config (labels are already injected via composeOptions.traefikLabels)\n\tif (traefikOutput) {\n\t\tfiles[\"traefik/traefik.yml\"] = traefikOutput.staticConfig;\n\t}\n\n\t// Prometheus config\n\tconst hasPrometheus = resolved.services.some((s) => s.definition.id === \"prometheus\");\n\tif (hasPrometheus) {\n\t\tfiles[\"prometheus/prometheus.yml\"] = generatePrometheusConfig(resolved);\n\t}\n\n\t// Grafana config\n\tconst hasGrafana = resolved.services.some((s) => s.definition.id === \"grafana\");\n\tif (hasGrafana) {\n\t\tconst grafanaFiles = generateGrafanaConfig();\n\t\tfor (const [path, content] of Object.entries(grafanaFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t\t// Grafana dashboard\n\t\tfiles[\"config/grafana/dashboards/openclaw-stack-overview.json\"] = generateGrafanaDashboard();\n\t}\n\n\t// Docker Compose override (empty template)\n\tfiles[\"docker-compose.override.yml\"] = [\n\t\t\"# Local overrides for docker-compose.yml\",\n\t\t\"# This file is gitignored — use it for personal port changes, extra volumes, etc.\",\n\t\t\"services: {}\",\n\t\t\"\",\n\t].join(\"\\n\");\n\n\t// SERVICES.md documentation\n\tfiles[\"docs/SERVICES.md\"] = generateServicesDoc(resolved);\n\n\t// Bare-metal: native install scripts + top-level installer\n\tif (isBareMetal) {\n\t\tif (nativeServices.length > 0) {\n\t\t\tconst nativePlatform = platformToNativePlatform(input.platform);\n\t\t\tconst nativeScripts = generateNativeInstallScripts({\n\t\t\t\tnativeServices,\n\t\t\t\tplatform: nativePlatform,\n\t\t\t\tprojectName: input.projectName,\n\t\t\t});\n\t\t\tfor (const [path, content] of Object.entries(nativeScripts)) {\n\t\t\t\tfiles[path] = content;\n\t\t\t}\n\t\t}\n\t\tconst bareMetalFiles = generateBareMetalInstall({\n\t\t\tplatform: input.platform,\n\t\t\tprojectName: input.projectName,\n\t\t\thasNativeServices: nativeServices.length > 0,\n\t\t});\n\t\tfor (const [path, content] of Object.entries(bareMetalFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t}\n\n\t// Get-Shit-Done setup scripts\n\tif (input.gsdRuntimes && input.gsdRuntimes.length > 0) {\n\t\tconst gsdScripts = generateGsdScripts(input.gsdRuntimes);\n\t\tif (gsdScripts) {\n\t\t\tfiles[\"openclaw/scripts/setup-gsd.sh\"] = gsdScripts.sh;\n\t\t\tfiles[\"openclaw/scripts/setup-gsd.ps1\"] = gsdScripts.ps1;\n\t\t}\n\t}\n\n\t// OpenClaw direct install scripts (host-based, no Docker gateway)\n\tif (input.openclawInstallMethod === \"direct\") {\n\t\tconst installScripts = generateOpenclawInstallScript({\n\t\t\tprojectName: input.projectName,\n\t\t});\n\t\tfor (const [path, content] of Object.entries(installScripts)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t}\n\n\t// Cloud-init deploy target\n\tif (input.deployTarget === \"cloud-init\") {\n\t\tconst mainCompose = composeResult.files[\"docker-compose.yml\"] ?? \"\";\n\t\tfiles[\"cloud-init.yml\"] = generateCloudInit({\n\t\t\tcomposeYaml: mainCompose,\n\t\t\tenvContent: envFiles.env,\n\t\t\tprojectName: input.projectName,\n\t\t\tgatewayPort: 18789,\n\t\t});\n\t}\n\n\t// 5. Calculate metadata\n\tconst skillCount = resolved.services.reduce((sum, s) => sum + s.definition.skills.length, 0);\n\n\treturn {\n\t\tfiles,\n\t\tmetadata: {\n\t\t\tserviceCount: resolved.services.length,\n\t\t\tskillCount,\n\t\t\testimatedMemoryMB: resolved.estimatedMemoryMB,\n\t\t\tresolvedServices: resolved.services.map((s) => s.definition.id),\n\t\t\tgeneratedAt: new Date().toISOString(),\n\t\t},\n\t};\n}\n\nexport function generateServicesDoc(resolved: import(\"./types.js\").ResolverOutput): string {\n\tconst lines: string[] = [\n\t\t\"# Service Reference\",\n\t\t\"\",\n\t\t\"This document describes all companion services in your OpenClaw stack.\",\n\t\t\"\",\n\t];\n\n\tfor (const svc of resolved.services) {\n\t\tconst def = svc.definition;\n\t\tlines.push(`## ${def.icon} ${def.name}`);\n\t\tlines.push(\"\");\n\t\tlines.push(def.description);\n\t\tlines.push(\"\");\n\t\tlines.push(`- **Image**: \\`${def.image}:${def.imageTag}\\``);\n\t\tlines.push(`- **Category**: ${def.category}`);\n\t\tlines.push(`- **Maturity**: ${def.maturity}`);\n\t\tif (def.minMemoryMB) {\n\t\t\tlines.push(`- **Min Memory**: ${def.minMemoryMB}MB`);\n\t\t}\n\t\tif (def.ports.length > 0) {\n\t\t\tlines.push(\"- **Ports**:\");\n\t\t\tfor (const p of def.ports) {\n\t\t\t\tlines.push(\n\t\t\t\t\t` - \\`${p.container}\\` — ${p.description}${p.exposed ? \"\" : \" (internal only)\"}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tlines.push(`- **Docs**: ${def.docsUrl}`);\n\t\tlines.push(\"\");\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.cts","names":[],"sources":["../src/generate.ts"],"mappings":";;;;;AA8CA;;iBAAgB,QAAA,CAAS,QAAA,EAAU,eAAA,GAAkB,gBAAA;AAAA,iBA6QrC,mBAAA,CAAoB,QAAA,EAAD,cAAA"}
1
+ {"version":3,"file":"generate.d.cts","names":[],"sources":["../src/generate.ts"],"mappings":";;;;;AA+CA;;iBAAgB,QAAA,CAAS,QAAA,EAAU,eAAA,GAAkB,gBAAA;AAAA,iBAqRrC,mBAAA,CAAoB,QAAA,EAAD,cAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.mts","names":[],"sources":["../src/generate.ts"],"mappings":";;;;;;AA8CA;iBAAgB,QAAA,CAAS,QAAA,EAAU,eAAA,GAAkB,gBAAA;AAAA,iBA6QrC,mBAAA,CAAoB,QAAA,EAAD,cAAA"}
1
+ {"version":3,"file":"generate.d.mts","names":[],"sources":["../src/generate.ts"],"mappings":";;;;;;AA+CA;iBAAgB,QAAA,CAAS,QAAA,EAAU,eAAA,GAAkB,gBAAA;AAAA,iBAqRrC,mBAAA,CAAoB,QAAA,EAAD,cAAA"}
package/dist/generate.mjs CHANGED
@@ -5,6 +5,7 @@ import { composeMultiFile } from "./composer.mjs";
5
5
  import { StackConfigError, ValidationError } from "./errors.mjs";
6
6
  import { generateBareMetalInstall } from "./generators/bare-metal-install.mjs";
7
7
  import { generateCaddyfile } from "./generators/caddy.mjs";
8
+ import { generateCloneScripts } from "./generators/clone-repos.mjs";
8
9
  import { generateCloudInit } from "./generators/cloud-init.mjs";
9
10
  import { generateEnvFiles } from "./generators/env.mjs";
10
11
  import { generateGsdScripts } from "./generators/get-shit-done.mjs";
@@ -100,7 +101,8 @@ function generate(rawInput) {
100
101
  ".env.local",
101
102
  ".env.*.local",
102
103
  "*.log",
103
- "docker-compose.override.yml"
104
+ "docker-compose.override.yml",
105
+ "repos/"
104
106
  ].join("\n");
105
107
  const manifestFiles = generateStackManifest(resolved, input);
106
108
  for (const [path, content] of Object.entries(manifestFiles)) files[path] = content;
@@ -119,8 +121,10 @@ function generate(rawInput) {
119
121
  hasNativeServices: isBareMetal && nativeServices.length > 0,
120
122
  openclawInstallMethod: input.openclawInstallMethod
121
123
  });
122
- const scripts = generateScripts();
124
+ const scripts = generateScripts({ hasGitServices: resolvedForCompose.services.some((s) => s.definition.gitSource) });
123
125
  for (const [path, content] of Object.entries(scripts)) files[path] = content;
126
+ const cloneScripts = generateCloneScripts(resolvedForCompose);
127
+ for (const [path, content] of Object.entries(cloneScripts)) files[path] = content;
124
128
  const healthCheckFiles = generateHealthCheck(resolved, {
125
129
  projectName: input.projectName,
126
130
  deploymentType: input.deploymentType
@@ -1 +1 @@
1
- {"version":3,"file":"generate.mjs","names":[],"sources":["../src/generate.ts"],"sourcesContent":["import {\n\tpartitionBareMetal,\n\tplatformToNativePlatform,\n\tresolvedWithOnlyServices,\n} from \"./bare-metal-partition.js\";\nimport { composeMultiFile } from \"./composer.js\";\nimport { StackConfigError, ValidationError } from \"./errors.js\";\nimport { generateBareMetalInstall } from \"./generators/bare-metal-install.js\";\nimport { generateCaddyfile } from \"./generators/caddy.js\";\nimport { generateCloudInit } from \"./generators/cloud-init.js\";\nimport { generateEnvFiles } from \"./generators/env.js\";\nimport { generateGsdScripts } from \"./generators/get-shit-done.js\";\nimport { generateGrafanaConfig, generateGrafanaDashboard } from \"./generators/grafana.js\";\nimport { generateHealthCheck } from \"./generators/health-check.js\";\nimport { generateN8nWorkflows } from \"./generators/n8n-workflows.js\";\nimport { generateNativeInstallScripts } from \"./generators/native-services.js\";\nimport { generateOpenclawInstallScript } from \"./generators/openclaw-install-script.js\";\nimport { generateOpenClawConfig } from \"./generators/openclaw-json.js\";\nimport { generatePostgresInit } from \"./generators/postgres-init.js\";\nimport { generatePrometheusConfig } from \"./generators/prometheus.js\";\nimport { generateReadme } from \"./generators/readme.js\";\nimport { generateScripts } from \"./generators/scripts.js\";\nimport { generateSkillFiles } from \"./generators/skills.js\";\nimport { generateStackManifest } from \"./generators/stack-manifest.js\";\nimport { generateTraefikConfig } from \"./generators/traefik.js\";\nimport { migrateConfig } from \"./migrations.js\";\nimport { resolve } from \"./resolver.js\";\nimport type {\n\tGeneratedFiles,\n\tGenerationInput,\n\tGenerationResult,\n\tPlatform,\n\tResolverInput,\n} from \"./types.js\";\nimport { validate } from \"./validator.js\";\n\n/** Resolver/compose only support linux image platforms; normalize for bare-metal (windows/macos). */\nfunction getComposePlatform(platform: Platform): \"linux/amd64\" | \"linux/arm64\" {\n\tif (platform === \"linux/amd64\" || platform === \"linux/arm64\") return platform;\n\treturn \"linux/amd64\";\n}\n\n/**\n * Main orchestration function: takes generation input, resolves dependencies,\n * generates all files, validates, and returns the complete file tree.\n */\nexport function generate(rawInput: GenerationInput): GenerationResult {\n\t// Apply config migrations if needed\n\tconst input = migrateConfig(rawInput as Record<string, unknown>) as GenerationInput;\n\n\tconst composePlatform = getComposePlatform(input.platform);\n\n\t// 1. Resolve dependencies\n\tconst resolverInput: ResolverInput = {\n\t\tservices: input.services,\n\t\tskillPacks: input.skillPacks,\n\t\taiProviders: input.aiProviders,\n\t\tproxy: input.proxy,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tmonitoring: input.monitoring,\n\t};\n\tconst resolved = resolve(resolverInput);\n\n\tif (!resolved.isValid) {\n\t\tthrow new StackConfigError(\n\t\t\t`Invalid stack configuration: ${resolved.errors.map((e) => e.message).join(\"; \")}`,\n\t\t);\n\t}\n\n\tconst isBareMetal = input.deploymentType === \"bare-metal\";\n\tlet resolvedForCompose = resolved;\n\tlet nativeIds = new Set<string>();\n\tlet nativeServices: typeof resolved.services = [];\n\tlet dockerOnlyServices: typeof resolved.services = [];\n\n\tif (isBareMetal) {\n\t\tconst partition = partitionBareMetal(resolved, input.platform);\n\t\tnativeServices = partition.nativeServices;\n\t\tdockerOnlyServices = partition.dockerOnlyServices;\n\t\tnativeIds = partition.nativeIds;\n\t\tif (nativeServices.length > 0) {\n\t\t\tresolvedForCompose = resolvedWithOnlyServices(resolved, dockerOnlyServices);\n\t\t}\n\t}\n\n\t// 2. Generate Docker Compose YAML (multi-file)\n\t// Compute Traefik labels before composing (labels get injected into docker-compose services)\n\tlet traefikOutput: ReturnType<typeof generateTraefikConfig> | undefined;\n\tif (input.proxy === \"traefik\" && input.domain) {\n\t\ttraefikOutput = generateTraefikConfig(resolvedForCompose, input.domain);\n\t}\n\n\tconst composeOptions = {\n\t\tprojectName: input.projectName,\n\t\tproxy: input.proxy,\n\t\tproxyHttpPort: input.proxyHttpPort,\n\t\tproxyHttpsPort: input.proxyHttpsPort,\n\t\tportOverrides: input.portOverrides,\n\t\tdomain: input.domain,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tdeployment: input.deployment,\n\t\topenclawVersion: input.openclawVersion,\n\t\tbareMetalNativeHost: isBareMetal && nativeIds.size > 0,\n\t\ttraefikLabels: traefikOutput?.serviceLabels,\n\t\topenclawImage: input.openclawImage ?? \"official\",\n\t\thardened: input.hardened ?? true,\n\t\topenclawInstallMethod: input.openclawInstallMethod ?? \"docker\",\n\t};\n\tconst composeResult = composeMultiFile(resolvedForCompose, composeOptions);\n\n\t// 3. Validate (using the base docker-compose.yml)\n\tconst validation = validate(resolvedForCompose, composeResult.files[\"docker-compose.yml\"] ?? \"\", {\n\t\tdomain: input.domain,\n\t\tgenerateSecrets: input.generateSecrets,\n\t});\n\tif (!validation.valid) {\n\t\tthrow new ValidationError(\n\t\t\t`Validation failed: ${validation.errors.map((e) => e.message).join(\"; \")}`,\n\t\t);\n\t}\n\n\t// 4. Generate all files\n\tconst files: GeneratedFiles = {};\n\n\t// Docker Compose (multi-file output)\n\tfor (const [filename, content] of Object.entries(composeResult.files)) {\n\t\tfiles[filename] = content;\n\t}\n\n\t// Environment files (when bare-metal with native services, host vars use host.docker.internal)\n\tconst envFiles = generateEnvFiles(resolved, {\n\t\tgenerateSecrets: input.generateSecrets,\n\t\tdomain: input.domain,\n\t\topenclawVersion: input.openclawVersion,\n\t\tnativeServiceIds: isBareMetal ? nativeIds : undefined,\n\t\tcomposeFiles: Object.keys(composeResult.files),\n\t\tcomposeProfiles: composeResult.profiles,\n\t\topenclawImage: input.openclawImage,\n\t});\n\tfiles[\".env.example\"] = envFiles.envExample;\n\tfiles[\".env\"] = envFiles.env;\n\n\t// .gitignore\n\tfiles[\".gitignore\"] = [\n\t\t\".env\",\n\t\t\".env.local\",\n\t\t\".env.*.local\",\n\t\t\"*.log\",\n\t\t\"docker-compose.override.yml\",\n\t].join(\"\\n\");\n\n\t// Stack manifest (consumed by Mission Control)\n\tconst manifestFiles = generateStackManifest(resolved, input);\n\tfor (const [path, content] of Object.entries(manifestFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// Skills\n\tconst skillFiles = generateSkillFiles(resolved);\n\tfor (const [path, content] of Object.entries(skillFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// OpenClaw Core Configuration\n\tfiles[\"openclaw/config/openclaw.json\"] = generateOpenClawConfig(resolved, {\n\t\tdeploymentType: input.deploymentType,\n\t\tgatewayPort: 18789,\n\t\topenclawVersion: input.openclawVersion,\n\t});\n\n\t// README\n\tfiles[\"README.md\"] = generateReadme(resolved, {\n\t\tprojectName: input.projectName,\n\t\tdomain: input.domain,\n\t\tproxy: input.proxy,\n\t\tdeploymentType: input.deploymentType,\n\t\thasNativeServices: isBareMetal && nativeServices.length > 0,\n\t\topenclawInstallMethod: input.openclawInstallMethod,\n\t});\n\n\t// Scripts\n\tconst scripts = generateScripts();\n\tfor (const [path, content] of Object.entries(scripts)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// Health check scripts (dynamic, stack-specific)\n\tconst healthCheckFiles = generateHealthCheck(resolved, {\n\t\tprojectName: input.projectName,\n\t\tdeploymentType: input.deploymentType,\n\t});\n\tfor (const [path, content] of Object.entries(healthCheckFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// n8n workflows\n\tconst n8nWorkflows = generateN8nWorkflows(resolved);\n\tfor (const [path, content] of Object.entries(n8nWorkflows)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// PostgreSQL init script (creates per-service databases and users)\n\tconst postgresInit = generatePostgresInit(resolved);\n\tif (postgresInit) {\n\t\tfiles[\"postgres/init-databases.sh\"] = postgresInit;\n\t}\n\n\t// Caddy config\n\tif (input.proxy === \"caddy\" && input.domain) {\n\t\tfiles[\"caddy/Caddyfile\"] = generateCaddyfile(resolved, input.domain);\n\t}\n\n\t// Traefik config (labels are already injected via composeOptions.traefikLabels)\n\tif (traefikOutput) {\n\t\tfiles[\"traefik/traefik.yml\"] = traefikOutput.staticConfig;\n\t}\n\n\t// Prometheus config\n\tconst hasPrometheus = resolved.services.some((s) => s.definition.id === \"prometheus\");\n\tif (hasPrometheus) {\n\t\tfiles[\"prometheus/prometheus.yml\"] = generatePrometheusConfig(resolved);\n\t}\n\n\t// Grafana config\n\tconst hasGrafana = resolved.services.some((s) => s.definition.id === \"grafana\");\n\tif (hasGrafana) {\n\t\tconst grafanaFiles = generateGrafanaConfig();\n\t\tfor (const [path, content] of Object.entries(grafanaFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t\t// Grafana dashboard\n\t\tfiles[\"config/grafana/dashboards/openclaw-stack-overview.json\"] = generateGrafanaDashboard();\n\t}\n\n\t// Docker Compose override (empty template)\n\tfiles[\"docker-compose.override.yml\"] = [\n\t\t\"# Local overrides for docker-compose.yml\",\n\t\t\"# This file is gitignored — use it for personal port changes, extra volumes, etc.\",\n\t\t\"services: {}\",\n\t\t\"\",\n\t].join(\"\\n\");\n\n\t// SERVICES.md documentation\n\tfiles[\"docs/SERVICES.md\"] = generateServicesDoc(resolved);\n\n\t// Bare-metal: native install scripts + top-level installer\n\tif (isBareMetal) {\n\t\tif (nativeServices.length > 0) {\n\t\t\tconst nativePlatform = platformToNativePlatform(input.platform);\n\t\t\tconst nativeScripts = generateNativeInstallScripts({\n\t\t\t\tnativeServices,\n\t\t\t\tplatform: nativePlatform,\n\t\t\t\tprojectName: input.projectName,\n\t\t\t});\n\t\t\tfor (const [path, content] of Object.entries(nativeScripts)) {\n\t\t\t\tfiles[path] = content;\n\t\t\t}\n\t\t}\n\t\tconst bareMetalFiles = generateBareMetalInstall({\n\t\t\tplatform: input.platform,\n\t\t\tprojectName: input.projectName,\n\t\t\thasNativeServices: nativeServices.length > 0,\n\t\t});\n\t\tfor (const [path, content] of Object.entries(bareMetalFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t}\n\n\t// Get-Shit-Done setup scripts\n\tif (input.gsdRuntimes && input.gsdRuntimes.length > 0) {\n\t\tconst gsdScripts = generateGsdScripts(input.gsdRuntimes);\n\t\tif (gsdScripts) {\n\t\t\tfiles[\"openclaw/scripts/setup-gsd.sh\"] = gsdScripts.sh;\n\t\t\tfiles[\"openclaw/scripts/setup-gsd.ps1\"] = gsdScripts.ps1;\n\t\t}\n\t}\n\n\t// OpenClaw direct install scripts (host-based, no Docker gateway)\n\tif (input.openclawInstallMethod === \"direct\") {\n\t\tconst installScripts = generateOpenclawInstallScript({\n\t\t\tprojectName: input.projectName,\n\t\t});\n\t\tfor (const [path, content] of Object.entries(installScripts)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t}\n\n\t// Cloud-init deploy target\n\tif (input.deployTarget === \"cloud-init\") {\n\t\tconst mainCompose = composeResult.files[\"docker-compose.yml\"] ?? \"\";\n\t\tfiles[\"cloud-init.yml\"] = generateCloudInit({\n\t\t\tcomposeYaml: mainCompose,\n\t\t\tenvContent: envFiles.env,\n\t\t\tprojectName: input.projectName,\n\t\t\tgatewayPort: 18789,\n\t\t});\n\t}\n\n\t// 5. Calculate metadata\n\tconst skillCount = resolved.services.reduce((sum, s) => sum + s.definition.skills.length, 0);\n\n\treturn {\n\t\tfiles,\n\t\tmetadata: {\n\t\t\tserviceCount: resolved.services.length,\n\t\t\tskillCount,\n\t\t\testimatedMemoryMB: resolved.estimatedMemoryMB,\n\t\t\tresolvedServices: resolved.services.map((s) => s.definition.id),\n\t\t\tgeneratedAt: new Date().toISOString(),\n\t\t},\n\t};\n}\n\nexport function generateServicesDoc(resolved: import(\"./types.js\").ResolverOutput): string {\n\tconst lines: string[] = [\n\t\t\"# Service Reference\",\n\t\t\"\",\n\t\t\"This document describes all companion services in your OpenClaw stack.\",\n\t\t\"\",\n\t];\n\n\tfor (const svc of resolved.services) {\n\t\tconst def = svc.definition;\n\t\tlines.push(`## ${def.icon} ${def.name}`);\n\t\tlines.push(\"\");\n\t\tlines.push(def.description);\n\t\tlines.push(\"\");\n\t\tlines.push(`- **Image**: \\`${def.image}:${def.imageTag}\\``);\n\t\tlines.push(`- **Category**: ${def.category}`);\n\t\tlines.push(`- **Maturity**: ${def.maturity}`);\n\t\tif (def.minMemoryMB) {\n\t\t\tlines.push(`- **Min Memory**: ${def.minMemoryMB}MB`);\n\t\t}\n\t\tif (def.ports.length > 0) {\n\t\t\tlines.push(\"- **Ports**:\");\n\t\t\tfor (const p of def.ports) {\n\t\t\t\tlines.push(\n\t\t\t\t\t` - \\`${p.container}\\` — ${p.description}${p.exposed ? \"\" : \" (internal only)\"}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tlines.push(`- **Docs**: ${def.docsUrl}`);\n\t\tlines.push(\"\");\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate.mjs","names":[],"sources":["../src/generate.ts"],"sourcesContent":["import {\n\tpartitionBareMetal,\n\tplatformToNativePlatform,\n\tresolvedWithOnlyServices,\n} from \"./bare-metal-partition.js\";\nimport { composeMultiFile } from \"./composer.js\";\nimport { StackConfigError, ValidationError } from \"./errors.js\";\nimport { generateBareMetalInstall } from \"./generators/bare-metal-install.js\";\nimport { generateCaddyfile } from \"./generators/caddy.js\";\nimport { generateCloneScripts } from \"./generators/clone-repos.js\";\nimport { generateCloudInit } from \"./generators/cloud-init.js\";\nimport { generateEnvFiles } from \"./generators/env.js\";\nimport { generateGsdScripts } from \"./generators/get-shit-done.js\";\nimport { generateGrafanaConfig, generateGrafanaDashboard } from \"./generators/grafana.js\";\nimport { generateHealthCheck } from \"./generators/health-check.js\";\nimport { generateN8nWorkflows } from \"./generators/n8n-workflows.js\";\nimport { generateNativeInstallScripts } from \"./generators/native-services.js\";\nimport { generateOpenclawInstallScript } from \"./generators/openclaw-install-script.js\";\nimport { generateOpenClawConfig } from \"./generators/openclaw-json.js\";\nimport { generatePostgresInit } from \"./generators/postgres-init.js\";\nimport { generatePrometheusConfig } from \"./generators/prometheus.js\";\nimport { generateReadme } from \"./generators/readme.js\";\nimport { generateScripts } from \"./generators/scripts.js\";\nimport { generateSkillFiles } from \"./generators/skills.js\";\nimport { generateStackManifest } from \"./generators/stack-manifest.js\";\nimport { generateTraefikConfig } from \"./generators/traefik.js\";\nimport { migrateConfig } from \"./migrations.js\";\nimport { resolve } from \"./resolver.js\";\nimport type {\n\tGeneratedFiles,\n\tGenerationInput,\n\tGenerationResult,\n\tPlatform,\n\tResolverInput,\n} from \"./types.js\";\nimport { validate } from \"./validator.js\";\n\n/** Resolver/compose only support linux image platforms; normalize for bare-metal (windows/macos). */\nfunction getComposePlatform(platform: Platform): \"linux/amd64\" | \"linux/arm64\" {\n\tif (platform === \"linux/amd64\" || platform === \"linux/arm64\") return platform;\n\treturn \"linux/amd64\";\n}\n\n/**\n * Main orchestration function: takes generation input, resolves dependencies,\n * generates all files, validates, and returns the complete file tree.\n */\nexport function generate(rawInput: GenerationInput): GenerationResult {\n\t// Apply config migrations if needed\n\tconst input = migrateConfig(rawInput as Record<string, unknown>) as GenerationInput;\n\n\tconst composePlatform = getComposePlatform(input.platform);\n\n\t// 1. Resolve dependencies\n\tconst resolverInput: ResolverInput = {\n\t\tservices: input.services,\n\t\tskillPacks: input.skillPacks,\n\t\taiProviders: input.aiProviders,\n\t\tproxy: input.proxy,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tmonitoring: input.monitoring,\n\t};\n\tconst resolved = resolve(resolverInput);\n\n\tif (!resolved.isValid) {\n\t\tthrow new StackConfigError(\n\t\t\t`Invalid stack configuration: ${resolved.errors.map((e) => e.message).join(\"; \")}`,\n\t\t);\n\t}\n\n\tconst isBareMetal = input.deploymentType === \"bare-metal\";\n\tlet resolvedForCompose = resolved;\n\tlet nativeIds = new Set<string>();\n\tlet nativeServices: typeof resolved.services = [];\n\tlet dockerOnlyServices: typeof resolved.services = [];\n\n\tif (isBareMetal) {\n\t\tconst partition = partitionBareMetal(resolved, input.platform);\n\t\tnativeServices = partition.nativeServices;\n\t\tdockerOnlyServices = partition.dockerOnlyServices;\n\t\tnativeIds = partition.nativeIds;\n\t\tif (nativeServices.length > 0) {\n\t\t\tresolvedForCompose = resolvedWithOnlyServices(resolved, dockerOnlyServices);\n\t\t}\n\t}\n\n\t// 2. Generate Docker Compose YAML (multi-file)\n\t// Compute Traefik labels before composing (labels get injected into docker-compose services)\n\tlet traefikOutput: ReturnType<typeof generateTraefikConfig> | undefined;\n\tif (input.proxy === \"traefik\" && input.domain) {\n\t\ttraefikOutput = generateTraefikConfig(resolvedForCompose, input.domain);\n\t}\n\n\tconst composeOptions = {\n\t\tprojectName: input.projectName,\n\t\tproxy: input.proxy,\n\t\tproxyHttpPort: input.proxyHttpPort,\n\t\tproxyHttpsPort: input.proxyHttpsPort,\n\t\tportOverrides: input.portOverrides,\n\t\tdomain: input.domain,\n\t\tgpu: input.gpu,\n\t\tplatform: composePlatform,\n\t\tdeployment: input.deployment,\n\t\topenclawVersion: input.openclawVersion,\n\t\tbareMetalNativeHost: isBareMetal && nativeIds.size > 0,\n\t\ttraefikLabels: traefikOutput?.serviceLabels,\n\t\topenclawImage: input.openclawImage ?? \"official\",\n\t\thardened: input.hardened ?? true,\n\t\topenclawInstallMethod: input.openclawInstallMethod ?? \"docker\",\n\t};\n\tconst composeResult = composeMultiFile(resolvedForCompose, composeOptions);\n\n\t// 3. Validate (using the base docker-compose.yml)\n\tconst validation = validate(resolvedForCompose, composeResult.files[\"docker-compose.yml\"] ?? \"\", {\n\t\tdomain: input.domain,\n\t\tgenerateSecrets: input.generateSecrets,\n\t});\n\tif (!validation.valid) {\n\t\tthrow new ValidationError(\n\t\t\t`Validation failed: ${validation.errors.map((e) => e.message).join(\"; \")}`,\n\t\t);\n\t}\n\n\t// 4. Generate all files\n\tconst files: GeneratedFiles = {};\n\n\t// Docker Compose (multi-file output)\n\tfor (const [filename, content] of Object.entries(composeResult.files)) {\n\t\tfiles[filename] = content;\n\t}\n\n\t// Environment files (when bare-metal with native services, host vars use host.docker.internal)\n\tconst envFiles = generateEnvFiles(resolved, {\n\t\tgenerateSecrets: input.generateSecrets,\n\t\tdomain: input.domain,\n\t\topenclawVersion: input.openclawVersion,\n\t\tnativeServiceIds: isBareMetal ? nativeIds : undefined,\n\t\tcomposeFiles: Object.keys(composeResult.files),\n\t\tcomposeProfiles: composeResult.profiles,\n\t\topenclawImage: input.openclawImage,\n\t});\n\tfiles[\".env.example\"] = envFiles.envExample;\n\tfiles[\".env\"] = envFiles.env;\n\n\t// .gitignore\n\tfiles[\".gitignore\"] = [\n\t\t\".env\",\n\t\t\".env.local\",\n\t\t\".env.*.local\",\n\t\t\"*.log\",\n\t\t\"docker-compose.override.yml\",\n\t\t\"repos/\",\n\t].join(\"\\n\");\n\n\t// Stack manifest (consumed by Mission Control)\n\tconst manifestFiles = generateStackManifest(resolved, input);\n\tfor (const [path, content] of Object.entries(manifestFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// Skills\n\tconst skillFiles = generateSkillFiles(resolved);\n\tfor (const [path, content] of Object.entries(skillFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// OpenClaw Core Configuration\n\tfiles[\"openclaw/config/openclaw.json\"] = generateOpenClawConfig(resolved, {\n\t\tdeploymentType: input.deploymentType,\n\t\tgatewayPort: 18789,\n\t\topenclawVersion: input.openclawVersion,\n\t});\n\n\t// README\n\tfiles[\"README.md\"] = generateReadme(resolved, {\n\t\tprojectName: input.projectName,\n\t\tdomain: input.domain,\n\t\tproxy: input.proxy,\n\t\tdeploymentType: input.deploymentType,\n\t\thasNativeServices: isBareMetal && nativeServices.length > 0,\n\t\topenclawInstallMethod: input.openclawInstallMethod,\n\t});\n\n\t// Scripts\n\tconst hasGitServices = resolvedForCompose.services.some((s) => s.definition.gitSource);\n\tconst scripts = generateScripts({ hasGitServices });\n\tfor (const [path, content] of Object.entries(scripts)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// Clone scripts for git-based services (SaaS boilerplates)\n\tconst cloneScripts = generateCloneScripts(resolvedForCompose);\n\tfor (const [path, content] of Object.entries(cloneScripts)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// Health check scripts (dynamic, stack-specific)\n\tconst healthCheckFiles = generateHealthCheck(resolved, {\n\t\tprojectName: input.projectName,\n\t\tdeploymentType: input.deploymentType,\n\t});\n\tfor (const [path, content] of Object.entries(healthCheckFiles)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// n8n workflows\n\tconst n8nWorkflows = generateN8nWorkflows(resolved);\n\tfor (const [path, content] of Object.entries(n8nWorkflows)) {\n\t\tfiles[path] = content;\n\t}\n\n\t// PostgreSQL init script (creates per-service databases and users)\n\tconst postgresInit = generatePostgresInit(resolved);\n\tif (postgresInit) {\n\t\tfiles[\"postgres/init-databases.sh\"] = postgresInit;\n\t}\n\n\t// Caddy config\n\tif (input.proxy === \"caddy\" && input.domain) {\n\t\tfiles[\"caddy/Caddyfile\"] = generateCaddyfile(resolved, input.domain);\n\t}\n\n\t// Traefik config (labels are already injected via composeOptions.traefikLabels)\n\tif (traefikOutput) {\n\t\tfiles[\"traefik/traefik.yml\"] = traefikOutput.staticConfig;\n\t}\n\n\t// Prometheus config\n\tconst hasPrometheus = resolved.services.some((s) => s.definition.id === \"prometheus\");\n\tif (hasPrometheus) {\n\t\tfiles[\"prometheus/prometheus.yml\"] = generatePrometheusConfig(resolved);\n\t}\n\n\t// Grafana config\n\tconst hasGrafana = resolved.services.some((s) => s.definition.id === \"grafana\");\n\tif (hasGrafana) {\n\t\tconst grafanaFiles = generateGrafanaConfig();\n\t\tfor (const [path, content] of Object.entries(grafanaFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t\t// Grafana dashboard\n\t\tfiles[\"config/grafana/dashboards/openclaw-stack-overview.json\"] = generateGrafanaDashboard();\n\t}\n\n\t// Docker Compose override (empty template)\n\tfiles[\"docker-compose.override.yml\"] = [\n\t\t\"# Local overrides for docker-compose.yml\",\n\t\t\"# This file is gitignored — use it for personal port changes, extra volumes, etc.\",\n\t\t\"services: {}\",\n\t\t\"\",\n\t].join(\"\\n\");\n\n\t// SERVICES.md documentation\n\tfiles[\"docs/SERVICES.md\"] = generateServicesDoc(resolved);\n\n\t// Bare-metal: native install scripts + top-level installer\n\tif (isBareMetal) {\n\t\tif (nativeServices.length > 0) {\n\t\t\tconst nativePlatform = platformToNativePlatform(input.platform);\n\t\t\tconst nativeScripts = generateNativeInstallScripts({\n\t\t\t\tnativeServices,\n\t\t\t\tplatform: nativePlatform,\n\t\t\t\tprojectName: input.projectName,\n\t\t\t});\n\t\t\tfor (const [path, content] of Object.entries(nativeScripts)) {\n\t\t\t\tfiles[path] = content;\n\t\t\t}\n\t\t}\n\t\tconst bareMetalFiles = generateBareMetalInstall({\n\t\t\tplatform: input.platform,\n\t\t\tprojectName: input.projectName,\n\t\t\thasNativeServices: nativeServices.length > 0,\n\t\t});\n\t\tfor (const [path, content] of Object.entries(bareMetalFiles)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t}\n\n\t// Get-Shit-Done setup scripts\n\tif (input.gsdRuntimes && input.gsdRuntimes.length > 0) {\n\t\tconst gsdScripts = generateGsdScripts(input.gsdRuntimes);\n\t\tif (gsdScripts) {\n\t\t\tfiles[\"openclaw/scripts/setup-gsd.sh\"] = gsdScripts.sh;\n\t\t\tfiles[\"openclaw/scripts/setup-gsd.ps1\"] = gsdScripts.ps1;\n\t\t}\n\t}\n\n\t// OpenClaw direct install scripts (host-based, no Docker gateway)\n\tif (input.openclawInstallMethod === \"direct\") {\n\t\tconst installScripts = generateOpenclawInstallScript({\n\t\t\tprojectName: input.projectName,\n\t\t});\n\t\tfor (const [path, content] of Object.entries(installScripts)) {\n\t\t\tfiles[path] = content;\n\t\t}\n\t}\n\n\t// Cloud-init deploy target\n\tif (input.deployTarget === \"cloud-init\") {\n\t\tconst mainCompose = composeResult.files[\"docker-compose.yml\"] ?? \"\";\n\t\tfiles[\"cloud-init.yml\"] = generateCloudInit({\n\t\t\tcomposeYaml: mainCompose,\n\t\t\tenvContent: envFiles.env,\n\t\t\tprojectName: input.projectName,\n\t\t\tgatewayPort: 18789,\n\t\t});\n\t}\n\n\t// 5. Calculate metadata\n\tconst skillCount = resolved.services.reduce((sum, s) => sum + s.definition.skills.length, 0);\n\n\treturn {\n\t\tfiles,\n\t\tmetadata: {\n\t\t\tserviceCount: resolved.services.length,\n\t\t\tskillCount,\n\t\t\testimatedMemoryMB: resolved.estimatedMemoryMB,\n\t\t\tresolvedServices: resolved.services.map((s) => s.definition.id),\n\t\t\tgeneratedAt: new Date().toISOString(),\n\t\t},\n\t};\n}\n\nexport function generateServicesDoc(resolved: import(\"./types.js\").ResolverOutput): string {\n\tconst lines: string[] = [\n\t\t\"# Service Reference\",\n\t\t\"\",\n\t\t\"This document describes all companion services in your OpenClaw stack.\",\n\t\t\"\",\n\t];\n\n\tfor (const svc of resolved.services) {\n\t\tconst def = svc.definition;\n\t\tlines.push(`## ${def.icon} ${def.name}`);\n\t\tlines.push(\"\");\n\t\tlines.push(def.description);\n\t\tlines.push(\"\");\n\t\tlines.push(`- **Image**: \\`${def.image}:${def.imageTag}\\``);\n\t\tlines.push(`- **Category**: ${def.category}`);\n\t\tlines.push(`- **Maturity**: ${def.maturity}`);\n\t\tif (def.minMemoryMB) {\n\t\t\tlines.push(`- **Min Memory**: ${def.minMemoryMB}MB`);\n\t\t}\n\t\tif (def.ports.length > 0) {\n\t\t\tlines.push(\"- **Ports**:\");\n\t\t\tfor (const p of def.ports) {\n\t\t\t\tlines.push(\n\t\t\t\t\t` - \\`${p.container}\\` — ${p.description}${p.exposed ? \"\" : \" (internal only)\"}`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tlines.push(`- **Docs**: ${def.docsUrl}`);\n\t\tlines.push(\"\");\n\t}\n\n\treturn lines.join(\"\\n\");\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  require("./skills-BlzpHmpH.cjs");
2
- const require_vi_2VT5v0um = require("./vi.2VT5v0um-CRqXre87.cjs");
2
+ const require_vi_2VT5v0um = require("./vi.2VT5v0um-iVBt6Fyq.cjs");
3
3
  const require_generate = require("./generate.cjs");
4
4
  let yaml = require("yaml");
5
5
  //#region src/generate.test.ts
@@ -146,7 +146,7 @@ require_vi_2VT5v0um.describe("generate (end-to-end)", () => {
146
146
  if (doc?.services && typeof doc.services === "object") for (const id of Object.keys(doc.services)) allServiceIds.add(id);
147
147
  }
148
148
  for (const id of lasuiteMeetServices) require_vi_2VT5v0um.globalExpect(allServiceIds.has(id), `missing service ${id}`).toBe(true);
149
- require_vi_2VT5v0um.globalExpect(result.metadata.serviceCount).toBe(lasuiteMeetServices.length);
149
+ require_vi_2VT5v0um.globalExpect(result.metadata.serviceCount).toBeGreaterThanOrEqual(lasuiteMeetServices.length);
150
150
  });
151
151
  require_vi_2VT5v0um.it("generates bare-metal installer for Windows (install.ps1)", () => {
152
152
  const result = require_generate.generate({
@@ -1 +1 @@
1
- {"version":3,"file":"generate.test.cjs","names":["describe","generate"],"sources":["../src/generate.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { parse } from \"yaml\";\nimport { generate } from \"./generate.js\";\n\ndescribe(\"generate (end-to-end)\", () => {\n\tit(\"generates a minimal stack (redis only)\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"test-stack\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\t// Core files must be present\n\t\texpect(result.files).toHaveProperty(\"docker-compose.yml\");\n\t\texpect(result.files).toHaveProperty(\".env.example\");\n\t\texpect(result.files).toHaveProperty(\".env\");\n\t\texpect(result.files).toHaveProperty(\"README.md\");\n\n\t\t// docker-compose.yml must be valid YAML\n\t\tconst composed = parse(result.files[\"docker-compose.yml\"]!);\n\t\texpect(composed).toHaveProperty(\"services\");\n\n\t\t// .env.example should reference REDIS_PASSWORD\n\t\texpect(result.files[\".env.example\"]).toContain(\"REDIS_PASSWORD\");\n\n\t\t// README should mention the project name\n\t\texpect(result.files[\"README.md\"]).toContain(\"test-stack\");\n\n\t\t// At least one service resolved\n\t\texpect(result.metadata.serviceCount).toBeGreaterThanOrEqual(1);\n\t});\n\n\tit(\"generates research-agent stack from skill pack\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"research-stack\",\n\t\t\tservices: [],\n\t\t\tskillPacks: [\"research-agent\"],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\t// Skill SKILL.md files for each skill in the research-agent pack\n\t\texpect(result.files).toHaveProperty(\"openclaw/workspace/skills/qdrant-memory/SKILL.md\");\n\t\texpect(result.files).toHaveProperty(\"openclaw/workspace/skills/searxng-search/SKILL.md\");\n\t\texpect(result.files).toHaveProperty(\"openclaw/workspace/skills/browserless-browse/SKILL.md\");\n\n\t\t// docker-compose.yml should contain the expected services\n\t\tconst composed = parse(result.files[\"docker-compose.yml\"]!);\n\t\texpect(composed.services).toHaveProperty(\"qdrant\");\n\t\texpect(composed.services).toHaveProperty(\"searxng\");\n\t\texpect(composed.services).toHaveProperty(\"browserless\");\n\t});\n\n\tit(\"generates full preset stack\", () => {\n\t\tconst fullServices = [\n\t\t\t\"redis\",\n\t\t\t\"postgresql\",\n\t\t\t\"qdrant\",\n\t\t\t\"n8n\",\n\t\t\t\"ffmpeg\",\n\t\t\t\"remotion\",\n\t\t\t\"minio\",\n\t\t\t\"caddy\",\n\t\t\t\"browserless\",\n\t\t\t\"searxng\",\n\t\t\t\"meilisearch\",\n\t\t\t\"uptime-kuma\",\n\t\t\t\"grafana\",\n\t\t\t\"prometheus\",\n\t\t\t\"ollama\",\n\t\t\t\"whisper\",\n\t\t\t\"gotify\",\n\t\t];\n\n\t\tconst result = generate({\n\t\t\tprojectName: \"full-stack\",\n\t\t\tservices: fullServices,\n\t\t\tskillPacks: [\n\t\t\t\t\"video-creator\",\n\t\t\t\t\"research-agent\",\n\t\t\t\t\"social-media\",\n\t\t\t\t\"dev-ops\",\n\t\t\t\t\"knowledge-base\",\n\t\t\t\t\"local-ai\",\n\t\t\t],\n\t\t\tproxy: \"caddy\",\n\t\t\tdomain: \"example.com\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\t// Should have many files\n\t\tconst fileCount = Object.keys(result.files).length;\n\t\texpect(fileCount).toBeGreaterThan(10);\n\n\t\t// Should include the start script\n\t\texpect(result.files).toHaveProperty(\"scripts/start.sh\");\n\n\t\t// Should not throw (it already didn't if we got here)\n\t\texpect(result.metadata.serviceCount).toBeGreaterThan(5);\n\t});\n\n\tit(\"generates caddy config when proxy is caddy\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"caddy-stack\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"caddy\",\n\t\t\tdomain: \"example.com\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\texpect(result.files).toHaveProperty(\"caddy/Caddyfile\");\n\t\texpect(result.files[\"caddy/Caddyfile\"]!.length).toBeGreaterThan(0);\n\t});\n\n\tit(\"generates prometheus config when monitoring enabled\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"monitored-stack\",\n\t\t\tservices: [],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t\tmonitoring: true,\n\t\t});\n\n\t\texpect(result.files).toHaveProperty(\"prometheus/prometheus.yml\");\n\t\t// Verify it's valid YAML\n\t\tconst promConfig = parse(result.files[\"prometheus/prometheus.yml\"]!);\n\t\texpect(promConfig).toBeDefined();\n\t});\n\n\tit(\"generates La Suite Meet stack with all expected services\", () => {\n\t\tconst lasuiteMeetServices = [\n\t\t\t\"postgresql\",\n\t\t\t\"redis\",\n\t\t\t\"livekit\",\n\t\t\t\"lasuite-meet-backend\",\n\t\t\t\"lasuite-meet-frontend\",\n\t\t\t\"lasuite-meet-agents\",\n\t\t];\n\t\tconst result = generate({\n\t\t\tprojectName: \"lasuite-meet-stack\",\n\t\t\tservices: lasuiteMeetServices,\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\t// Services may be split across main and profile compose files\n\t\tconst allServiceIds = new Set<string>();\n\t\tfor (const [filename, content] of Object.entries(result.files)) {\n\t\t\tif (filename.endsWith(\".yml\") && content) {\n\t\t\t\tconst doc = parse(content);\n\t\t\t\tif (doc?.services && typeof doc.services === \"object\") {\n\t\t\t\t\tfor (const id of Object.keys(doc.services)) {\n\t\t\t\t\t\tallServiceIds.add(id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const id of lasuiteMeetServices) {\n\t\t\texpect(allServiceIds.has(id), `missing service ${id}`).toBe(true);\n\t\t}\n\t\texpect(result.metadata.serviceCount).toBe(lasuiteMeetServices.length);\n\t});\n\n\tit(\"generates bare-metal installer for Windows (install.ps1)\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"win-stack\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"windows/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tdeploymentType: \"bare-metal\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\t\texpect(result.files).toHaveProperty(\"install.ps1\");\n\t\texpect(result.files[\"install.ps1\"]).toContain(\"docker compose\");\n\t\texpect(result.files[\"install.ps1\"]).toContain(\"PowerShell\");\n\t});\n\n\tit(\"generates bare-metal installer for Linux/macOS (install.sh)\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"linux-stack\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tdeploymentType: \"bare-metal\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\t\texpect(result.files).toHaveProperty(\"install.sh\");\n\t\texpect(result.files[\"install.sh\"]).toContain(\"docker\");\n\t\texpect(result.files[\"install.sh\"]).toContain(\"compose\");\n\t});\n\n\tit(\"bare-metal with redis on Linux: native script, compose excludes redis, gateway has extra_hosts\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"bare-metal-redis\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tdeploymentType: \"bare-metal\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\t// Native install script for Linux\n\t\texpect(result.files).toHaveProperty(\"native/install-linux.sh\");\n\t\texpect(result.files[\"native/install-linux.sh\"]).toContain(\"redis\");\n\n\t\t// Docker compose must NOT include redis (native); only gateway/openclaw\n\t\tconst composed = parse(result.files[\"docker-compose.yml\"]!);\n\t\texpect(composed.services).not.toHaveProperty(\"redis\");\n\t\texpect(composed.services).toHaveProperty(\"openclaw-gateway\");\n\n\t\t// Gateway must have extra_hosts for host.docker.internal\n\t\tconst gateway = composed.services[\"openclaw-gateway\"];\n\t\texpect(gateway).toBeDefined();\n\t\texpect(gateway.extra_hosts).toBeDefined();\n\t\texpect(\n\t\t\t(gateway.extra_hosts as string[]).some(\n\t\t\t\t(h: string) => h.includes(\"host.docker.internal\") && h.includes(\"host-gateway\"),\n\t\t\t),\n\t\t).toBe(true);\n\n\t\t// .env should set REDIS_HOST to host.docker.internal for gateway to reach native Redis\n\t\texpect(result.files[\".env\"]).toContain(\"REDIS_HOST=host.docker.internal\");\n\t});\n\n\tit(\"throws on conflicting services\", () => {\n\t\texpect(() =>\n\t\t\tgenerate({\n\t\t\t\tprojectName: \"conflict-stack\",\n\t\t\t\tservices: [\"redis\", \"valkey\"],\n\t\t\t\tskillPacks: [],\n\t\t\t\tproxy: \"none\",\n\t\t\t\tgpu: false,\n\t\t\t\tplatform: \"linux/amd64\",\n\t\t\t\tdeployment: \"local\",\n\t\t\t\tgenerateSecrets: true,\n\t\t\t\topenclawVersion: \"latest\",\n\t\t\t}),\n\t\t).toThrow();\n\t});\n\n\tit(\"generates scripts directory\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"scripts-stack\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\tconst expectedScripts = [\n\t\t\t\"scripts/start.sh\",\n\t\t\t\"scripts/stop.sh\",\n\t\t\t\"scripts/update.sh\",\n\t\t\t\"scripts/backup.sh\",\n\t\t\t\"scripts/status.sh\",\n\t\t];\n\n\t\tfor (const script of expectedScripts) {\n\t\t\texpect(result.files).toHaveProperty(script);\n\t\t\texpect(result.files[script]!.length).toBeGreaterThan(0);\n\t\t}\n\t});\n\n\tit(\"all generated .env.example vars have comments\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"env-comments-stack\",\n\t\t\tservices: [\"redis\", \"qdrant\", \"n8n\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\tconst envExample = result.files[\".env.example\"]!;\n\t\tconst lines = envExample.split(\"\\n\");\n\n\t\t// Walk through lines: every non-empty, non-comment KEY=VALUE line should\n\t\t// have a preceding comment line (starting with #).\n\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\tconst line = lines[i]!.trim();\n\t\t\tif (line === \"\" || line.startsWith(\"#\")) continue;\n\n\t\t\t// This line looks like KEY=VALUE\n\t\t\tif (line.includes(\"=\")) {\n\t\t\t\t// There must be a comment somewhere before it (look backwards for a # line)\n\t\t\t\tlet foundComment = false;\n\t\t\t\tfor (let j = i - 1; j >= 0; j--) {\n\t\t\t\t\tconst prev = lines[j]!.trim();\n\t\t\t\t\tif (prev === \"\") continue; // skip blank lines\n\t\t\t\t\tif (prev.startsWith(\"#\")) {\n\t\t\t\t\t\tfoundComment = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t// Hit another non-comment, non-empty line — no comment found\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\texpect(foundComment).toBe(true);\n\t\t\t}\n\t\t}\n\t});\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate.test.cjs","names":["describe","generate"],"sources":["../src/generate.test.ts"],"sourcesContent":["import { describe, expect, it } from \"vitest\";\nimport { parse } from \"yaml\";\nimport { generate } from \"./generate.js\";\n\ndescribe(\"generate (end-to-end)\", () => {\n\tit(\"generates a minimal stack (redis only)\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"test-stack\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\t// Core files must be present\n\t\texpect(result.files).toHaveProperty(\"docker-compose.yml\");\n\t\texpect(result.files).toHaveProperty(\".env.example\");\n\t\texpect(result.files).toHaveProperty(\".env\");\n\t\texpect(result.files).toHaveProperty(\"README.md\");\n\n\t\t// docker-compose.yml must be valid YAML\n\t\tconst composed = parse(result.files[\"docker-compose.yml\"]!);\n\t\texpect(composed).toHaveProperty(\"services\");\n\n\t\t// .env.example should reference REDIS_PASSWORD\n\t\texpect(result.files[\".env.example\"]).toContain(\"REDIS_PASSWORD\");\n\n\t\t// README should mention the project name\n\t\texpect(result.files[\"README.md\"]).toContain(\"test-stack\");\n\n\t\t// At least one service resolved\n\t\texpect(result.metadata.serviceCount).toBeGreaterThanOrEqual(1);\n\t});\n\n\tit(\"generates research-agent stack from skill pack\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"research-stack\",\n\t\t\tservices: [],\n\t\t\tskillPacks: [\"research-agent\"],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\t// Skill SKILL.md files for each skill in the research-agent pack\n\t\texpect(result.files).toHaveProperty(\"openclaw/workspace/skills/qdrant-memory/SKILL.md\");\n\t\texpect(result.files).toHaveProperty(\"openclaw/workspace/skills/searxng-search/SKILL.md\");\n\t\texpect(result.files).toHaveProperty(\"openclaw/workspace/skills/browserless-browse/SKILL.md\");\n\n\t\t// docker-compose.yml should contain the expected services\n\t\tconst composed = parse(result.files[\"docker-compose.yml\"]!);\n\t\texpect(composed.services).toHaveProperty(\"qdrant\");\n\t\texpect(composed.services).toHaveProperty(\"searxng\");\n\t\texpect(composed.services).toHaveProperty(\"browserless\");\n\t});\n\n\tit(\"generates full preset stack\", () => {\n\t\tconst fullServices = [\n\t\t\t\"redis\",\n\t\t\t\"postgresql\",\n\t\t\t\"qdrant\",\n\t\t\t\"n8n\",\n\t\t\t\"ffmpeg\",\n\t\t\t\"remotion\",\n\t\t\t\"minio\",\n\t\t\t\"caddy\",\n\t\t\t\"browserless\",\n\t\t\t\"searxng\",\n\t\t\t\"meilisearch\",\n\t\t\t\"uptime-kuma\",\n\t\t\t\"grafana\",\n\t\t\t\"prometheus\",\n\t\t\t\"ollama\",\n\t\t\t\"whisper\",\n\t\t\t\"gotify\",\n\t\t];\n\n\t\tconst result = generate({\n\t\t\tprojectName: \"full-stack\",\n\t\t\tservices: fullServices,\n\t\t\tskillPacks: [\n\t\t\t\t\"video-creator\",\n\t\t\t\t\"research-agent\",\n\t\t\t\t\"social-media\",\n\t\t\t\t\"dev-ops\",\n\t\t\t\t\"knowledge-base\",\n\t\t\t\t\"local-ai\",\n\t\t\t],\n\t\t\tproxy: \"caddy\",\n\t\t\tdomain: \"example.com\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\t// Should have many files\n\t\tconst fileCount = Object.keys(result.files).length;\n\t\texpect(fileCount).toBeGreaterThan(10);\n\n\t\t// Should include the start script\n\t\texpect(result.files).toHaveProperty(\"scripts/start.sh\");\n\n\t\t// Should not throw (it already didn't if we got here)\n\t\texpect(result.metadata.serviceCount).toBeGreaterThan(5);\n\t});\n\n\tit(\"generates caddy config when proxy is caddy\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"caddy-stack\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"caddy\",\n\t\t\tdomain: \"example.com\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\texpect(result.files).toHaveProperty(\"caddy/Caddyfile\");\n\t\texpect(result.files[\"caddy/Caddyfile\"]!.length).toBeGreaterThan(0);\n\t});\n\n\tit(\"generates prometheus config when monitoring enabled\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"monitored-stack\",\n\t\t\tservices: [],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t\tmonitoring: true,\n\t\t});\n\n\t\texpect(result.files).toHaveProperty(\"prometheus/prometheus.yml\");\n\t\t// Verify it's valid YAML\n\t\tconst promConfig = parse(result.files[\"prometheus/prometheus.yml\"]!);\n\t\texpect(promConfig).toBeDefined();\n\t});\n\n\tit(\"generates La Suite Meet stack with all expected services\", () => {\n\t\tconst lasuiteMeetServices = [\n\t\t\t\"postgresql\",\n\t\t\t\"redis\",\n\t\t\t\"livekit\",\n\t\t\t\"lasuite-meet-backend\",\n\t\t\t\"lasuite-meet-frontend\",\n\t\t\t\"lasuite-meet-agents\",\n\t\t];\n\t\tconst result = generate({\n\t\t\tprojectName: \"lasuite-meet-stack\",\n\t\t\tservices: lasuiteMeetServices,\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\t// Services may be split across main and profile compose files\n\t\tconst allServiceIds = new Set<string>();\n\t\tfor (const [filename, content] of Object.entries(result.files)) {\n\t\t\tif (filename.endsWith(\".yml\") && content) {\n\t\t\t\tconst doc = parse(content);\n\t\t\t\tif (doc?.services && typeof doc.services === \"object\") {\n\t\t\t\t\tfor (const id of Object.keys(doc.services)) {\n\t\t\t\t\t\tallServiceIds.add(id);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor (const id of lasuiteMeetServices) {\n\t\t\texpect(allServiceIds.has(id), `missing service ${id}`).toBe(true);\n\t\t}\n\t\t// Service count includes user services + mandatory platform services (convex, mission-control, tailscale)\n\t\texpect(result.metadata.serviceCount).toBeGreaterThanOrEqual(lasuiteMeetServices.length);\n\t});\n\n\tit(\"generates bare-metal installer for Windows (install.ps1)\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"win-stack\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"windows/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tdeploymentType: \"bare-metal\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\t\texpect(result.files).toHaveProperty(\"install.ps1\");\n\t\texpect(result.files[\"install.ps1\"]).toContain(\"docker compose\");\n\t\texpect(result.files[\"install.ps1\"]).toContain(\"PowerShell\");\n\t});\n\n\tit(\"generates bare-metal installer for Linux/macOS (install.sh)\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"linux-stack\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tdeploymentType: \"bare-metal\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\t\texpect(result.files).toHaveProperty(\"install.sh\");\n\t\texpect(result.files[\"install.sh\"]).toContain(\"docker\");\n\t\texpect(result.files[\"install.sh\"]).toContain(\"compose\");\n\t});\n\n\tit(\"bare-metal with redis on Linux: native script, compose excludes redis, gateway has extra_hosts\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"bare-metal-redis\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tdeploymentType: \"bare-metal\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\t// Native install script for Linux\n\t\texpect(result.files).toHaveProperty(\"native/install-linux.sh\");\n\t\texpect(result.files[\"native/install-linux.sh\"]).toContain(\"redis\");\n\n\t\t// Docker compose must NOT include redis (native); only gateway/openclaw\n\t\tconst composed = parse(result.files[\"docker-compose.yml\"]!);\n\t\texpect(composed.services).not.toHaveProperty(\"redis\");\n\t\texpect(composed.services).toHaveProperty(\"openclaw-gateway\");\n\n\t\t// Gateway must have extra_hosts for host.docker.internal\n\t\tconst gateway = composed.services[\"openclaw-gateway\"];\n\t\texpect(gateway).toBeDefined();\n\t\texpect(gateway.extra_hosts).toBeDefined();\n\t\texpect(\n\t\t\t(gateway.extra_hosts as string[]).some(\n\t\t\t\t(h: string) => h.includes(\"host.docker.internal\") && h.includes(\"host-gateway\"),\n\t\t\t),\n\t\t).toBe(true);\n\n\t\t// .env should set REDIS_HOST to host.docker.internal for gateway to reach native Redis\n\t\texpect(result.files[\".env\"]).toContain(\"REDIS_HOST=host.docker.internal\");\n\t});\n\n\tit(\"throws on conflicting services\", () => {\n\t\texpect(() =>\n\t\t\tgenerate({\n\t\t\t\tprojectName: \"conflict-stack\",\n\t\t\t\tservices: [\"redis\", \"valkey\"],\n\t\t\t\tskillPacks: [],\n\t\t\t\tproxy: \"none\",\n\t\t\t\tgpu: false,\n\t\t\t\tplatform: \"linux/amd64\",\n\t\t\t\tdeployment: \"local\",\n\t\t\t\tgenerateSecrets: true,\n\t\t\t\topenclawVersion: \"latest\",\n\t\t\t}),\n\t\t).toThrow();\n\t});\n\n\tit(\"generates scripts directory\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"scripts-stack\",\n\t\t\tservices: [\"redis\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\tconst expectedScripts = [\n\t\t\t\"scripts/start.sh\",\n\t\t\t\"scripts/stop.sh\",\n\t\t\t\"scripts/update.sh\",\n\t\t\t\"scripts/backup.sh\",\n\t\t\t\"scripts/status.sh\",\n\t\t];\n\n\t\tfor (const script of expectedScripts) {\n\t\t\texpect(result.files).toHaveProperty(script);\n\t\t\texpect(result.files[script]!.length).toBeGreaterThan(0);\n\t\t}\n\t});\n\n\tit(\"all generated .env.example vars have comments\", () => {\n\t\tconst result = generate({\n\t\t\tprojectName: \"env-comments-stack\",\n\t\t\tservices: [\"redis\", \"qdrant\", \"n8n\"],\n\t\t\tskillPacks: [],\n\t\t\tproxy: \"none\",\n\t\t\tgpu: false,\n\t\t\tplatform: \"linux/amd64\",\n\t\t\tdeployment: \"local\",\n\t\t\tgenerateSecrets: true,\n\t\t\topenclawVersion: \"latest\",\n\t\t});\n\n\t\tconst envExample = result.files[\".env.example\"]!;\n\t\tconst lines = envExample.split(\"\\n\");\n\n\t\t// Walk through lines: every non-empty, non-comment KEY=VALUE line should\n\t\t// have a preceding comment line (starting with #).\n\t\tfor (let i = 0; i < lines.length; i++) {\n\t\t\tconst line = lines[i]!.trim();\n\t\t\tif (line === \"\" || line.startsWith(\"#\")) continue;\n\n\t\t\t// This line looks like KEY=VALUE\n\t\t\tif (line.includes(\"=\")) {\n\t\t\t\t// There must be a comment somewhere before it (look backwards for a # line)\n\t\t\t\tlet foundComment = false;\n\t\t\t\tfor (let j = i - 1; j >= 0; j--) {\n\t\t\t\t\tconst prev = lines[j]!.trim();\n\t\t\t\t\tif (prev === \"\") continue; // skip blank lines\n\t\t\t\t\tif (prev.startsWith(\"#\")) {\n\t\t\t\t\t\tfoundComment = true;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\t// Hit another non-comment, non-empty line — no comment found\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\texpect(foundComment).toBe(true);\n\t\t\t}\n\t\t}\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-DvC3SVNc.mjs";
1
+ import { n as describe, r as it, t as globalExpect } from "./vi.2VT5v0um-C_jmO7m2.mjs";
2
2
  import { generate } from "./generate.mjs";
3
3
  import { parse } from "yaml";
4
4
  //#region src/generate.test.ts
@@ -145,7 +145,7 @@ describe("generate (end-to-end)", () => {
145
145
  if (doc?.services && typeof doc.services === "object") for (const id of Object.keys(doc.services)) allServiceIds.add(id);
146
146
  }
147
147
  for (const id of lasuiteMeetServices) globalExpect(allServiceIds.has(id), `missing service ${id}`).toBe(true);
148
- globalExpect(result.metadata.serviceCount).toBe(lasuiteMeetServices.length);
148
+ globalExpect(result.metadata.serviceCount).toBeGreaterThanOrEqual(lasuiteMeetServices.length);
149
149
  });
150
150
  it("generates bare-metal installer for Windows (install.ps1)", () => {
151
151
  const result = generate({