@better-openclaw/core 1.0.23 → 1.0.25

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 (335) hide show
  1. package/dist/addon-stack.cjs +673 -0
  2. package/dist/addon-stack.cjs.map +1 -0
  3. package/dist/addon-stack.d.cts +23 -0
  4. package/dist/addon-stack.d.cts.map +1 -0
  5. package/dist/addon-stack.d.mts +23 -0
  6. package/dist/addon-stack.d.mts.map +1 -0
  7. package/dist/addon-stack.mjs +671 -0
  8. package/dist/addon-stack.mjs.map +1 -0
  9. package/dist/addon-stack.test.cjs +349 -0
  10. package/dist/addon-stack.test.cjs.map +1 -0
  11. package/dist/addon-stack.test.d.cts +1 -0
  12. package/dist/addon-stack.test.d.mts +1 -0
  13. package/dist/addon-stack.test.mjs +349 -0
  14. package/dist/addon-stack.test.mjs.map +1 -0
  15. package/dist/bare-metal-partition.test.cjs +20 -21
  16. package/dist/bare-metal-partition.test.cjs.map +1 -1
  17. package/dist/bare-metal-partition.test.mjs +4 -5
  18. package/dist/bare-metal-partition.test.mjs.map +1 -1
  19. package/dist/composer.cjs +17 -1
  20. package/dist/composer.cjs.map +1 -1
  21. package/dist/composer.d.cts +24 -1
  22. package/dist/composer.d.cts.map +1 -1
  23. package/dist/composer.d.mts +24 -1
  24. package/dist/composer.d.mts.map +1 -1
  25. package/dist/composer.mjs +14 -2
  26. package/dist/composer.mjs.map +1 -1
  27. package/dist/composer.snapshot.test.cjs +20 -20
  28. package/dist/composer.snapshot.test.cjs.map +1 -1
  29. package/dist/composer.snapshot.test.mjs +2 -2
  30. package/dist/composer.test.cjs +53 -52
  31. package/dist/composer.test.cjs.map +1 -1
  32. package/dist/composer.test.mjs +4 -3
  33. package/dist/composer.test.mjs.map +1 -1
  34. package/dist/deployers/strip-host-ports.test.cjs +26 -26
  35. package/dist/deployers/strip-host-ports.test.cjs.map +1 -1
  36. package/dist/deployers/strip-host-ports.test.mjs +1 -1
  37. package/dist/generate.cjs +8 -4
  38. package/dist/generate.cjs.map +1 -1
  39. package/dist/generate.d.cts.map +1 -1
  40. package/dist/generate.d.mts.map +1 -1
  41. package/dist/generate.mjs +9 -5
  42. package/dist/generate.mjs.map +1 -1
  43. package/dist/generate.test.cjs +55 -55
  44. package/dist/generate.test.cjs.map +1 -1
  45. package/dist/generate.test.mjs +2 -2
  46. package/dist/generate.test.mjs.map +1 -1
  47. package/dist/generators/bare-metal-install.test.cjs +18 -18
  48. package/dist/generators/bare-metal-install.test.cjs.map +1 -1
  49. package/dist/generators/bare-metal-install.test.mjs +1 -1
  50. package/dist/generators/caddy.test.cjs +13 -13
  51. package/dist/generators/caddy.test.cjs.map +1 -1
  52. package/dist/generators/caddy.test.mjs +1 -1
  53. package/dist/generators/clone-repos.cjs +140 -0
  54. package/dist/generators/clone-repos.cjs.map +1 -0
  55. package/dist/generators/clone-repos.d.cts +11 -0
  56. package/dist/generators/clone-repos.d.cts.map +1 -0
  57. package/dist/generators/clone-repos.d.mts +11 -0
  58. package/dist/generators/clone-repos.d.mts.map +1 -0
  59. package/dist/generators/clone-repos.mjs +139 -0
  60. package/dist/generators/clone-repos.mjs.map +1 -0
  61. package/dist/generators/clone-repos.test.cjs +140 -0
  62. package/dist/generators/clone-repos.test.cjs.map +1 -0
  63. package/dist/generators/clone-repos.test.d.cts +1 -0
  64. package/dist/generators/clone-repos.test.d.mts +1 -0
  65. package/dist/generators/clone-repos.test.mjs +141 -0
  66. package/dist/generators/clone-repos.test.mjs.map +1 -0
  67. package/dist/generators/env.test.cjs +17 -17
  68. package/dist/generators/env.test.cjs.map +1 -1
  69. package/dist/generators/env.test.mjs +1 -1
  70. package/dist/generators/health-check.test.cjs +39 -39
  71. package/dist/generators/health-check.test.cjs.map +1 -1
  72. package/dist/generators/health-check.test.mjs +1 -1
  73. package/dist/generators/postgres-init.cjs +20 -0
  74. package/dist/generators/postgres-init.cjs.map +1 -1
  75. package/dist/generators/postgres-init.d.cts.map +1 -1
  76. package/dist/generators/postgres-init.d.mts.map +1 -1
  77. package/dist/generators/postgres-init.mjs +20 -0
  78. package/dist/generators/postgres-init.mjs.map +1 -1
  79. package/dist/generators/scripts.cjs +332 -3
  80. package/dist/generators/scripts.cjs.map +1 -1
  81. package/dist/generators/scripts.d.cts +3 -1
  82. package/dist/generators/scripts.d.cts.map +1 -1
  83. package/dist/generators/scripts.d.mts +3 -1
  84. package/dist/generators/scripts.d.mts.map +1 -1
  85. package/dist/generators/scripts.mjs +332 -3
  86. package/dist/generators/scripts.mjs.map +1 -1
  87. package/dist/generators/scripts.test.cjs +57 -23
  88. package/dist/generators/scripts.test.cjs.map +1 -1
  89. package/dist/generators/scripts.test.mjs +39 -5
  90. package/dist/generators/scripts.test.mjs.map +1 -1
  91. package/dist/generators/stack-manifest.cjs +1 -0
  92. package/dist/generators/stack-manifest.cjs.map +1 -1
  93. package/dist/generators/stack-manifest.d.cts +3 -2
  94. package/dist/generators/stack-manifest.d.cts.map +1 -1
  95. package/dist/generators/stack-manifest.d.mts +3 -2
  96. package/dist/generators/stack-manifest.d.mts.map +1 -1
  97. package/dist/generators/stack-manifest.mjs +1 -0
  98. package/dist/generators/stack-manifest.mjs.map +1 -1
  99. package/dist/generators/traefik.test.cjs +32 -32
  100. package/dist/generators/traefik.test.cjs.map +1 -1
  101. package/dist/generators/traefik.test.mjs +1 -1
  102. package/dist/index.cjs +28 -5
  103. package/dist/index.d.cts +7 -4
  104. package/dist/index.d.mts +7 -4
  105. package/dist/index.mjs +10 -7
  106. package/dist/migrations.test.cjs +16 -16
  107. package/dist/migrations.test.cjs.map +1 -1
  108. package/dist/migrations.test.mjs +1 -1
  109. package/dist/presets/registry.cjs.map +1 -1
  110. package/dist/presets/registry.d.cts.map +1 -1
  111. package/dist/presets/registry.d.mts.map +1 -1
  112. package/dist/presets/registry.mjs.map +1 -1
  113. package/dist/presets/registry.test.cjs +14 -14
  114. package/dist/presets/registry.test.cjs.map +1 -1
  115. package/dist/presets/registry.test.mjs +1 -1
  116. package/dist/resolver.cjs +8 -0
  117. package/dist/resolver.cjs.map +1 -1
  118. package/dist/resolver.mjs +9 -1
  119. package/dist/resolver.mjs.map +1 -1
  120. package/dist/resolver.test.cjs +125 -90
  121. package/dist/resolver.test.cjs.map +1 -1
  122. package/dist/resolver.test.mjs +47 -12
  123. package/dist/resolver.test.mjs.map +1 -1
  124. package/dist/{schema-B4c64P8N.d.cts → schema-CKBRu-Rt.d.cts} +355 -8
  125. package/dist/schema-CKBRu-Rt.d.cts.map +1 -0
  126. package/dist/{schema-CXNhYci1.d.mts → schema-Dn-_Jpb6.d.mts} +355 -8
  127. package/dist/schema-Dn-_Jpb6.d.mts.map +1 -0
  128. package/dist/schema.cjs +160 -5
  129. package/dist/schema.cjs.map +1 -1
  130. package/dist/schema.d.cts +2 -2
  131. package/dist/schema.d.mts +2 -2
  132. package/dist/schema.mjs +150 -6
  133. package/dist/schema.mjs.map +1 -1
  134. package/dist/schema.test.cjs +86 -86
  135. package/dist/schema.test.cjs.map +1 -1
  136. package/dist/schema.test.mjs +1 -1
  137. package/dist/services/definitions/apptension-saas.cjs +87 -0
  138. package/dist/services/definitions/apptension-saas.cjs.map +1 -0
  139. package/dist/services/definitions/apptension-saas.d.cts +7 -0
  140. package/dist/services/definitions/apptension-saas.d.cts.map +1 -0
  141. package/dist/services/definitions/apptension-saas.d.mts +7 -0
  142. package/dist/services/definitions/apptension-saas.d.mts.map +1 -0
  143. package/dist/services/definitions/apptension-saas.mjs +86 -0
  144. package/dist/services/definitions/apptension-saas.mjs.map +1 -0
  145. package/dist/services/definitions/boxyhq-saas.cjs +88 -0
  146. package/dist/services/definitions/boxyhq-saas.cjs.map +1 -0
  147. package/dist/services/definitions/boxyhq-saas.d.cts +7 -0
  148. package/dist/services/definitions/boxyhq-saas.d.cts.map +1 -0
  149. package/dist/services/definitions/boxyhq-saas.d.mts +7 -0
  150. package/dist/services/definitions/boxyhq-saas.d.mts.map +1 -0
  151. package/dist/services/definitions/boxyhq-saas.mjs +87 -0
  152. package/dist/services/definitions/boxyhq-saas.mjs.map +1 -0
  153. package/dist/services/definitions/browserless.cjs +4 -1
  154. package/dist/services/definitions/browserless.cjs.map +1 -1
  155. package/dist/services/definitions/browserless.mjs +4 -1
  156. package/dist/services/definitions/browserless.mjs.map +1 -1
  157. package/dist/services/definitions/cmsaas-starter.cjs +86 -0
  158. package/dist/services/definitions/cmsaas-starter.cjs.map +1 -0
  159. package/dist/services/definitions/cmsaas-starter.d.cts +7 -0
  160. package/dist/services/definitions/cmsaas-starter.d.cts.map +1 -0
  161. package/dist/services/definitions/cmsaas-starter.d.mts +7 -0
  162. package/dist/services/definitions/cmsaas-starter.d.mts.map +1 -0
  163. package/dist/services/definitions/cmsaas-starter.mjs +85 -0
  164. package/dist/services/definitions/cmsaas-starter.mjs.map +1 -0
  165. package/dist/services/definitions/convex.cjs +43 -1
  166. package/dist/services/definitions/convex.cjs.map +1 -1
  167. package/dist/services/definitions/convex.mjs +43 -1
  168. package/dist/services/definitions/convex.mjs.map +1 -1
  169. package/dist/services/definitions/grafana.cjs +11 -1
  170. package/dist/services/definitions/grafana.cjs.map +1 -1
  171. package/dist/services/definitions/grafana.mjs +11 -1
  172. package/dist/services/definitions/grafana.mjs.map +1 -1
  173. package/dist/services/definitions/index.cjs +51 -36
  174. package/dist/services/definitions/index.cjs.map +1 -1
  175. package/dist/services/definitions/index.d.cts +30 -25
  176. package/dist/services/definitions/index.d.cts.map +1 -1
  177. package/dist/services/definitions/index.d.mts +30 -25
  178. package/dist/services/definitions/index.d.mts.map +1 -1
  179. package/dist/services/definitions/index.mjs +47 -37
  180. package/dist/services/definitions/index.mjs.map +1 -1
  181. package/dist/services/definitions/ixartz-saas.cjs +88 -0
  182. package/dist/services/definitions/ixartz-saas.cjs.map +1 -0
  183. package/dist/services/definitions/ixartz-saas.d.cts +7 -0
  184. package/dist/services/definitions/ixartz-saas.d.cts.map +1 -0
  185. package/dist/services/definitions/ixartz-saas.d.mts +7 -0
  186. package/dist/services/definitions/ixartz-saas.d.mts.map +1 -0
  187. package/dist/services/definitions/ixartz-saas.mjs +87 -0
  188. package/dist/services/definitions/ixartz-saas.mjs.map +1 -0
  189. package/dist/services/definitions/meilisearch.cjs +11 -1
  190. package/dist/services/definitions/meilisearch.cjs.map +1 -1
  191. package/dist/services/definitions/meilisearch.mjs +11 -1
  192. package/dist/services/definitions/meilisearch.mjs.map +1 -1
  193. package/dist/services/definitions/minio.cjs +3 -1
  194. package/dist/services/definitions/minio.cjs.map +1 -1
  195. package/dist/services/definitions/minio.mjs +3 -1
  196. package/dist/services/definitions/minio.mjs.map +1 -1
  197. package/dist/services/definitions/mission-control.cjs +16 -2
  198. package/dist/services/definitions/mission-control.cjs.map +1 -1
  199. package/dist/services/definitions/mission-control.mjs +16 -2
  200. package/dist/services/definitions/mission-control.mjs.map +1 -1
  201. package/dist/services/definitions/n8n.cjs +11 -1
  202. package/dist/services/definitions/n8n.cjs.map +1 -1
  203. package/dist/services/definitions/n8n.mjs +11 -1
  204. package/dist/services/definitions/n8n.mjs.map +1 -1
  205. package/dist/services/definitions/ollama.cjs +3 -1
  206. package/dist/services/definitions/ollama.cjs.map +1 -1
  207. package/dist/services/definitions/ollama.mjs +3 -1
  208. package/dist/services/definitions/ollama.mjs.map +1 -1
  209. package/dist/services/definitions/open-saas.cjs +81 -0
  210. package/dist/services/definitions/open-saas.cjs.map +1 -0
  211. package/dist/services/definitions/open-saas.d.cts +7 -0
  212. package/dist/services/definitions/open-saas.d.cts.map +1 -0
  213. package/dist/services/definitions/open-saas.d.mts +7 -0
  214. package/dist/services/definitions/open-saas.d.mts.map +1 -0
  215. package/dist/services/definitions/open-saas.mjs +80 -0
  216. package/dist/services/definitions/open-saas.mjs.map +1 -0
  217. package/dist/services/definitions/qdrant.cjs +3 -1
  218. package/dist/services/definitions/qdrant.cjs.map +1 -1
  219. package/dist/services/definitions/qdrant.mjs +3 -1
  220. package/dist/services/definitions/qdrant.mjs.map +1 -1
  221. package/dist/services/definitions/searxng.cjs +8 -1
  222. package/dist/services/definitions/searxng.cjs.map +1 -1
  223. package/dist/services/definitions/searxng.mjs +8 -1
  224. package/dist/services/definitions/searxng.mjs.map +1 -1
  225. package/dist/services/definitions/uptime-kuma.cjs +8 -1
  226. package/dist/services/definitions/uptime-kuma.cjs.map +1 -1
  227. package/dist/services/definitions/uptime-kuma.mjs +8 -1
  228. package/dist/services/definitions/uptime-kuma.mjs.map +1 -1
  229. package/dist/services/registry.cjs +3 -0
  230. package/dist/services/registry.cjs.map +1 -1
  231. package/dist/services/registry.d.cts.map +1 -1
  232. package/dist/services/registry.d.mts.map +1 -1
  233. package/dist/services/registry.mjs +3 -0
  234. package/dist/services/registry.mjs.map +1 -1
  235. package/dist/services/registry.test.cjs +40 -33
  236. package/dist/services/registry.test.cjs.map +1 -1
  237. package/dist/services/registry.test.mjs +8 -1
  238. package/dist/services/registry.test.mjs.map +1 -1
  239. package/dist/{skill-manifest-BVUXU0__.mjs → skill-manifest-6XhrhWsG.mjs} +49 -1
  240. package/dist/{skill-manifest--IgY9REK.cjs.map → skill-manifest-6XhrhWsG.mjs.map} +1 -1
  241. package/dist/{skill-manifest--IgY9REK.cjs → skill-manifest-B8znSsym.cjs} +49 -1
  242. package/dist/{skill-manifest-BVUXU0__.mjs.map → skill-manifest-B8znSsym.cjs.map} +1 -1
  243. package/dist/skills/registry.cjs +3 -3
  244. package/dist/skills/registry.cjs.map +1 -1
  245. package/dist/skills/registry.mjs +3 -3
  246. package/dist/skills/registry.mjs.map +1 -1
  247. package/dist/skills/skill-manifest.cjs +1 -1
  248. package/dist/skills/skill-manifest.mjs +1 -1
  249. package/dist/{vi.2VT5v0um-DvC3SVNc.mjs → test.CTcmp4Su-ClCHJ3FA.mjs} +6793 -6403
  250. package/dist/test.CTcmp4Su-ClCHJ3FA.mjs.map +1 -0
  251. package/dist/{vi.2VT5v0um-CRqXre87.cjs → test.CTcmp4Su-DlzTarwH.cjs} +6793 -6403
  252. package/dist/test.CTcmp4Su-DlzTarwH.cjs.map +1 -0
  253. package/dist/track-analytics.cjs +50 -0
  254. package/dist/track-analytics.cjs.map +1 -0
  255. package/dist/track-analytics.d.cts +34 -0
  256. package/dist/track-analytics.d.cts.map +1 -0
  257. package/dist/track-analytics.d.mts +34 -0
  258. package/dist/track-analytics.d.mts.map +1 -0
  259. package/dist/track-analytics.mjs +48 -0
  260. package/dist/track-analytics.mjs.map +1 -0
  261. package/dist/track-analytics.test.cjs +91 -0
  262. package/dist/track-analytics.test.cjs.map +1 -0
  263. package/dist/track-analytics.test.d.cts +1 -0
  264. package/dist/track-analytics.test.d.mts +1 -0
  265. package/dist/track-analytics.test.mjs +92 -0
  266. package/dist/track-analytics.test.mjs.map +1 -0
  267. package/dist/types.cjs +7 -0
  268. package/dist/types.cjs.map +1 -1
  269. package/dist/types.d.cts +12 -2
  270. package/dist/types.d.cts.map +1 -1
  271. package/dist/types.d.mts +12 -2
  272. package/dist/types.d.mts.map +1 -1
  273. package/dist/types.mjs +7 -0
  274. package/dist/types.mjs.map +1 -1
  275. package/dist/validator.test.cjs +15 -15
  276. package/dist/validator.test.cjs.map +1 -1
  277. package/dist/validator.test.mjs +2 -2
  278. package/dist/version-manager.cjs +1 -1
  279. package/dist/version-manager.cjs.map +1 -1
  280. package/dist/version-manager.mjs +1 -1
  281. package/dist/version-manager.mjs.map +1 -1
  282. package/dist/version-manager.test.cjs +40 -38
  283. package/dist/version-manager.test.cjs.map +1 -1
  284. package/dist/version-manager.test.mjs +7 -5
  285. package/dist/version-manager.test.mjs.map +1 -1
  286. package/package.json +4 -4
  287. package/src/__snapshots__/composer.snapshot.test.ts.snap +160 -0
  288. package/src/addon-stack.test.ts +490 -0
  289. package/src/addon-stack.ts +998 -0
  290. package/src/bare-metal-partition.test.ts +4 -3
  291. package/src/composer.test.ts +4 -2
  292. package/src/composer.ts +24 -5
  293. package/src/generate.test.ts +2 -1
  294. package/src/generate.ts +10 -1
  295. package/src/generators/clone-repos.test.ts +154 -0
  296. package/src/generators/clone-repos.ts +159 -0
  297. package/src/generators/postgres-init.ts +17 -0
  298. package/src/generators/scripts.test.ts +52 -4
  299. package/src/generators/scripts.ts +351 -3
  300. package/src/generators/stack-manifest.ts +4 -2
  301. package/src/index.ts +28 -2
  302. package/src/presets/registry.ts +241 -329
  303. package/src/resolver.test.ts +53 -15
  304. package/src/resolver.ts +13 -1
  305. package/src/schema.ts +216 -4
  306. package/src/services/definitions/apptension-saas.ts +84 -0
  307. package/src/services/definitions/boxyhq-saas.ts +84 -0
  308. package/src/services/definitions/browserless.ts +3 -0
  309. package/src/services/definitions/cmsaas-starter.ts +84 -0
  310. package/src/services/definitions/convex.ts +31 -0
  311. package/src/services/definitions/grafana.ts +9 -0
  312. package/src/services/definitions/index.ts +90 -70
  313. package/src/services/definitions/ixartz-saas.ts +84 -0
  314. package/src/services/definitions/meilisearch.ts +9 -0
  315. package/src/services/definitions/minio.ts +2 -0
  316. package/src/services/definitions/mission-control.ts +19 -2
  317. package/src/services/definitions/n8n.ts +9 -0
  318. package/src/services/definitions/ollama.ts +2 -0
  319. package/src/services/definitions/open-saas.ts +79 -0
  320. package/src/services/definitions/qdrant.ts +2 -0
  321. package/src/services/definitions/searxng.ts +3 -0
  322. package/src/services/definitions/uptime-kuma.ts +3 -0
  323. package/src/services/registry.test.ts +8 -0
  324. package/src/services/registry.ts +7 -0
  325. package/src/skills/manifest.json +64 -0
  326. package/src/skills/registry.ts +3 -3
  327. package/src/track-analytics.test.ts +82 -0
  328. package/src/track-analytics.ts +76 -0
  329. package/src/types.ts +29 -0
  330. package/src/version-manager.test.ts +10 -5
  331. package/src/version-manager.ts +1 -1
  332. package/dist/schema-B4c64P8N.d.cts.map +0 -1
  333. package/dist/schema-CXNhYci1.d.mts.map +0 -1
  334. package/dist/vi.2VT5v0um-CRqXre87.cjs.map +0 -1
  335. package/dist/vi.2VT5v0um-DvC3SVNc.mjs.map +0 -1
@@ -25,9 +25,10 @@ describe("bare-metal partition", () => {
25
25
  });
26
26
  const result = partitionBareMetal(resolved, "linux/amd64");
27
27
  expect(result.nativeIds.has("redis")).toBe(true);
28
- expect(result.nativeServices.length).toBe(1);
29
- expect(result.nativeServices[0].definition.id).toBe("redis");
30
- expect(result.dockerOnlyServices.length).toBe(0);
28
+ const redisNative = result.nativeServices.find((s) => s.definition.id === "redis");
29
+ expect(redisNative).toBeDefined();
30
+ // Mandatory services (convex, mission-control, tailscale) are Docker-only
31
+ expect(result.dockerOnlyServices.length).toBeGreaterThanOrEqual(0);
31
32
  });
32
33
 
33
34
  it("partitions n8n as Docker-only (no native recipe)", () => {
@@ -38,8 +38,10 @@ describe("compose", () => {
38
38
  // Should have CLI companion service
39
39
  expect(parsed.services).toHaveProperty("openclaw-cli");
40
40
 
41
- // Gateway should have no depends_on (no companions)
42
- expect(parsed.services["openclaw-gateway"]).not.toHaveProperty("depends_on");
41
+ // Gateway should have depends_on for mandatory services (convex, mission-control, tailscale)
42
+ // Even with no user-selected services, mandatory services are always present
43
+ expect(parsed.services).toHaveProperty("convex");
44
+ expect(parsed.services).toHaveProperty("mission-control");
43
45
 
44
46
  // Gateway should have restart policy
45
47
  expect(parsed.services["openclaw-gateway"].restart).toBe("unless-stopped");
package/src/composer.ts CHANGED
@@ -23,7 +23,7 @@ function getOpenclawImage(variant: OpenclawImageVariant, version: string): strin
23
23
  }
24
24
 
25
25
  /** Creates a YAML scalar that is always quoted — avoids YAML 1.1 bare `no` → false. */
26
- function quotedStr(value: string): Scalar {
26
+ export function quotedStr(value: string): Scalar {
27
27
  const s = new Scalar(value);
28
28
  s.type = Scalar.QUOTE_DOUBLE;
29
29
  return s;
@@ -50,9 +50,10 @@ const CATEGORY_PROFILE_MAP: Partial<Record<ServiceCategory, { file: string; prof
50
50
  "social-media": { file: "docker-compose.social.yml", profile: "social" },
51
51
  knowledge: { file: "docker-compose.knowledge.yml", profile: "knowledge" },
52
52
  communication: { file: "docker-compose.communication.yml", profile: "communication" },
53
+ "saas-boilerplate": { file: "docker-compose.saas.yml", profile: "saas" },
53
54
  };
54
55
 
55
- const YAML_OPTIONS = { lineWidth: 120, nullStr: "" };
56
+ export const YAML_OPTIONS = { lineWidth: 120, nullStr: "" };
56
57
 
57
58
  // ── Shared Gateway Builder ──────────────────────────────────────────────────
58
59
 
@@ -221,7 +222,7 @@ function buildGatewayServices(
221
222
 
222
223
  // ── Shared Companion Service Builder ────────────────────────────────────────
223
224
 
224
- function buildCompanionService(
225
+ export function buildCompanionService(
225
226
  def: ResolverOutput["services"][number]["definition"],
226
227
  resolved: ResolverOutput,
227
228
  options: ComposeOptions,
@@ -230,7 +231,25 @@ function buildCompanionService(
230
231
  const svc: Record<string, unknown> = {};
231
232
  const volumeNames: string[] = [];
232
233
 
233
- svc.image = `${def.image}:${def.imageTag}`;
234
+ // Git-based services use build: context; image-based services use image:
235
+ if (def.gitSource && def.buildContext) {
236
+ const subdir = def.gitSource.subdirectory || ".";
237
+ const ctxPath = def.buildContext.context || ".";
238
+ const contextFull = subdir === "." ? `./repos/${def.id}/${ctxPath}` : `./repos/${def.id}/${subdir}/${ctxPath}`;
239
+ const buildBlock: Record<string, unknown> = { context: contextFull };
240
+ if (def.buildContext.dockerfile) {
241
+ buildBlock.dockerfile = def.buildContext.dockerfile;
242
+ }
243
+ if (def.buildContext.args && Object.keys(def.buildContext.args).length > 0) {
244
+ buildBlock.args = def.buildContext.args;
245
+ }
246
+ if (def.buildContext.target) {
247
+ buildBlock.target = def.buildContext.target;
248
+ }
249
+ svc.build = buildBlock;
250
+ } else {
251
+ svc.image = `${def.image}:${def.imageTag}`;
252
+ }
234
253
 
235
254
  if (def.environment.length > 0) {
236
255
  const env: Record<string, string> = {};
@@ -426,7 +445,7 @@ function buildCompanionService(
426
445
  *
427
446
  * Returns null when no setup is needed (no PostgreSQL or no DB requirements).
428
447
  */
429
- function buildPostgresSetup(resolved: ResolverOutput): Record<string, unknown> | null {
448
+ export function buildPostgresSetup(resolved: ResolverOutput): Record<string, unknown> | null {
430
449
  const hasPostgres = resolved.services.some((s) => s.definition.id === "postgresql");
431
450
  if (!hasPostgres) return null;
432
451
 
@@ -187,7 +187,8 @@ describe("generate (end-to-end)", () => {
187
187
  for (const id of lasuiteMeetServices) {
188
188
  expect(allServiceIds.has(id), `missing service ${id}`).toBe(true);
189
189
  }
190
- expect(result.metadata.serviceCount).toBe(lasuiteMeetServices.length);
190
+ // Service count includes user services + mandatory platform services (convex, mission-control, tailscale)
191
+ expect(result.metadata.serviceCount).toBeGreaterThanOrEqual(lasuiteMeetServices.length);
191
192
  });
192
193
 
193
194
  it("generates bare-metal installer for Windows (install.ps1)", () => {
package/src/generate.ts CHANGED
@@ -7,6 +7,7 @@ import { composeMultiFile } from "./composer.js";
7
7
  import { StackConfigError, ValidationError } from "./errors.js";
8
8
  import { generateBareMetalInstall } from "./generators/bare-metal-install.js";
9
9
  import { generateCaddyfile } from "./generators/caddy.js";
10
+ import { generateCloneScripts } from "./generators/clone-repos.js";
10
11
  import { generateCloudInit } from "./generators/cloud-init.js";
11
12
  import { generateEnvFiles } from "./generators/env.js";
12
13
  import { generateGsdScripts } from "./generators/get-shit-done.js";
@@ -149,6 +150,7 @@ export function generate(rawInput: GenerationInput): GenerationResult {
149
150
  ".env.*.local",
150
151
  "*.log",
151
152
  "docker-compose.override.yml",
153
+ "repos/",
152
154
  ].join("\n");
153
155
 
154
156
  // Stack manifest (consumed by Mission Control)
@@ -181,11 +183,18 @@ export function generate(rawInput: GenerationInput): GenerationResult {
181
183
  });
182
184
 
183
185
  // Scripts
184
- const scripts = generateScripts();
186
+ const hasGitServices = resolvedForCompose.services.some((s) => s.definition.gitSource);
187
+ const scripts = generateScripts({ hasGitServices });
185
188
  for (const [path, content] of Object.entries(scripts)) {
186
189
  files[path] = content;
187
190
  }
188
191
 
192
+ // Clone scripts for git-based services (SaaS boilerplates)
193
+ const cloneScripts = generateCloneScripts(resolvedForCompose);
194
+ for (const [path, content] of Object.entries(cloneScripts)) {
195
+ files[path] = content;
196
+ }
197
+
189
198
  // Health check scripts (dynamic, stack-specific)
190
199
  const healthCheckFiles = generateHealthCheck(resolved, {
191
200
  projectName: input.projectName,
@@ -0,0 +1,154 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateCloneScripts } from "./clone-repos.js";
3
+ import type { ResolverOutput } from "../types.js";
4
+
5
+ /** Minimal resolved output with no services */
6
+ function emptyResolved(): ResolverOutput {
7
+ return {
8
+ services: [],
9
+ addedDependencies: [],
10
+ removedConflicts: [],
11
+ warnings: [],
12
+ errors: [],
13
+ isValid: true,
14
+ estimatedMemoryMB: 0,
15
+ aiProviders: [],
16
+ gsdRuntimes: [],
17
+ };
18
+ }
19
+
20
+ /** Create a resolved output with image-based services only */
21
+ function imageOnlyResolved(): ResolverOutput {
22
+ return {
23
+ ...emptyResolved(),
24
+ services: [
25
+ {
26
+ definition: {
27
+ id: "redis",
28
+ name: "Redis",
29
+ description: "In-memory cache",
30
+ category: "database",
31
+ icon: "🔴",
32
+ image: "redis",
33
+ imageTag: "7-alpine",
34
+ ports: [],
35
+ volumes: [],
36
+ environment: [],
37
+ dependsOn: [],
38
+ restartPolicy: "unless-stopped",
39
+ networks: ["openclaw-network"],
40
+ skills: [],
41
+ openclawEnvVars: [],
42
+ docsUrl: "https://redis.io",
43
+ tags: [],
44
+ maturity: "stable",
45
+ requires: [],
46
+ recommends: [],
47
+ conflictsWith: [],
48
+ gpuRequired: false,
49
+ },
50
+ addedBy: "user",
51
+ },
52
+ ],
53
+ };
54
+ }
55
+
56
+ /** Create a resolved output with a git-based service */
57
+ function gitServiceResolved(): ResolverOutput {
58
+ return {
59
+ ...emptyResolved(),
60
+ services: [
61
+ {
62
+ definition: {
63
+ id: "open-saas",
64
+ name: "Open SaaS",
65
+ description: "SaaS boilerplate",
66
+ category: "saas-boilerplate",
67
+ icon: "🚀",
68
+ gitSource: {
69
+ repoUrl: "https://github.com/wasp-lang/open-saas.git",
70
+ branch: "main",
71
+ subdirectory: "template",
72
+ postCloneCommands: ["cp .env.example .env"],
73
+ },
74
+ buildContext: {
75
+ dockerfile: "Dockerfile",
76
+ context: ".",
77
+ },
78
+ ports: [{ host: 3100, container: 3000, description: "Web app", exposed: true }],
79
+ volumes: [],
80
+ environment: [],
81
+ dependsOn: [],
82
+ restartPolicy: "unless-stopped",
83
+ networks: ["openclaw-network"],
84
+ skills: [],
85
+ openclawEnvVars: [],
86
+ docsUrl: "https://opensaas.sh/docs",
87
+ tags: [],
88
+ maturity: "beta",
89
+ requires: [],
90
+ recommends: [],
91
+ conflictsWith: [],
92
+ gpuRequired: false,
93
+ },
94
+ addedBy: "user",
95
+ },
96
+ ],
97
+ };
98
+ }
99
+
100
+ describe("generateCloneScripts", () => {
101
+ it("returns empty object when no git-based services exist", () => {
102
+ const result = generateCloneScripts(emptyResolved());
103
+ expect(result).toEqual({});
104
+ });
105
+
106
+ it("returns empty object when only image-based services exist", () => {
107
+ const result = generateCloneScripts(imageOnlyResolved());
108
+ expect(result).toEqual({});
109
+ });
110
+
111
+ it("generates bash and PowerShell scripts for git-based services", () => {
112
+ const result = generateCloneScripts(gitServiceResolved());
113
+ expect(Object.keys(result)).toHaveLength(2);
114
+ expect(result).toHaveProperty("scripts/clone-repos.sh");
115
+ expect(result).toHaveProperty("scripts/clone-repos.ps1");
116
+ });
117
+
118
+ it("bash script contains clone command with repo URL", () => {
119
+ const result = generateCloneScripts(gitServiceResolved());
120
+ const bash = result["scripts/clone-repos.sh"];
121
+ expect(bash).toContain("https://github.com/wasp-lang/open-saas.git");
122
+ expect(bash).toContain('"open-saas"');
123
+ expect(bash).toContain('"main"');
124
+ });
125
+
126
+ it("bash script includes postCloneCommands", () => {
127
+ const result = generateCloneScripts(gitServiceResolved());
128
+ const bash = result["scripts/clone-repos.sh"];
129
+ expect(bash).toContain("cp .env.example .env");
130
+ });
131
+
132
+ it("bash script includes git check and idempotency logic", () => {
133
+ const result = generateCloneScripts(gitServiceResolved());
134
+ const bash = result["scripts/clone-repos.sh"];
135
+ expect(bash).toContain("command -v git");
136
+ expect(bash).toContain("clone_or_update");
137
+ expect(bash).toContain("pull --ff-only");
138
+ expect(bash).toContain("git clone --depth 1");
139
+ });
140
+
141
+ it("PowerShell script contains clone command with repo URL", () => {
142
+ const result = generateCloneScripts(gitServiceResolved());
143
+ const ps = result["scripts/clone-repos.ps1"];
144
+ expect(ps).toContain("https://github.com/wasp-lang/open-saas.git");
145
+ expect(ps).toContain('"open-saas"');
146
+ expect(ps).toContain("Clone-OrUpdate");
147
+ });
148
+
149
+ it("bash script is executable (starts with shebang)", () => {
150
+ const result = generateCloneScripts(gitServiceResolved());
151
+ const bash = result["scripts/clone-repos.sh"];
152
+ expect(bash).toMatch(/^#!/);
153
+ });
154
+ });
@@ -0,0 +1,159 @@
1
+ import type { ResolverOutput } from "../types.js";
2
+
3
+ /**
4
+ * Generates clone scripts for git-based services (SaaS boilerplates).
5
+ * Returns empty object if no git-based services exist in the resolved stack.
6
+ */
7
+ export function generateCloneScripts(resolved: ResolverOutput): Record<string, string> {
8
+ const gitServices = resolved.services.filter(
9
+ (s) => s.definition.gitSource && s.definition.buildContext,
10
+ );
11
+
12
+ if (gitServices.length === 0) return {};
13
+
14
+ const files: Record<string, string> = {};
15
+
16
+ // ── scripts/clone-repos.sh ─────────────────────────────────────────────
17
+
18
+ const bashEntries = gitServices
19
+ .map((s) => {
20
+ const gs = s.definition.gitSource!;
21
+ const branchArg = gs.branch ? `"${gs.branch}"` : '""';
22
+ let block = `clone_or_update "${s.definition.id}" "${gs.repoUrl}" ${branchArg}`;
23
+ if (gs.postCloneCommands && gs.postCloneCommands.length > 0) {
24
+ const cmds = gs.postCloneCommands
25
+ .map((cmd) => ` (cd "$REPOS_DIR/${s.definition.id}${gs.subdirectory ? `/${gs.subdirectory}` : ""}" && ${cmd})`)
26
+ .join("\n");
27
+ block += `\n${cmds}`;
28
+ }
29
+ return block;
30
+ })
31
+ .join("\n\n");
32
+
33
+ files["scripts/clone-repos.sh"] = `#!/usr/bin/env bash
34
+ set -euo pipefail
35
+
36
+ # ─── Clone/Update Git-Based Service Repositories ────────────────────────────
37
+ # Idempotent: clones if missing, pulls if already present.
38
+
39
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
40
+ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
41
+ REPOS_DIR="$PROJECT_DIR/repos"
42
+
43
+ # ── Colour helpers ──────────────────────────────────────────────────────────
44
+ if [ -t 1 ]; then
45
+ GREEN='\\033[0;32m'; YELLOW='\\033[1;33m'; CYAN='\\033[0;36m'; RED='\\033[0;31m'; NC='\\033[0m'
46
+ else
47
+ GREEN=''; YELLOW=''; CYAN=''; RED=''; NC=''
48
+ fi
49
+ info() { echo -e "\${CYAN}i $*\${NC}"; }
50
+ ok() { echo -e "\${GREEN}✓ $*\${NC}"; }
51
+ warn() { echo -e "\${YELLOW}⚠ $*\${NC}"; }
52
+ err() { echo -e "\${RED}✗ $*\${NC}" >&2; }
53
+
54
+ # ── Check git ───────────────────────────────────────────────────────────────
55
+ if ! command -v git &> /dev/null; then
56
+ err "git is not installed. Please install git first."
57
+ exit 1
58
+ fi
59
+
60
+ mkdir -p "$REPOS_DIR"
61
+
62
+ clone_or_update() {
63
+ local name="$1" url="$2" branch="\${3:-}"
64
+ local dir="$REPOS_DIR/$name"
65
+
66
+ if [ -d "$dir/.git" ]; then
67
+ info "Updating $name..."
68
+ git -C "$dir" pull --ff-only 2>/dev/null || warn "Could not fast-forward $name (you may have local changes)"
69
+ else
70
+ info "Cloning $name..."
71
+ if [ -n "$branch" ]; then
72
+ git clone --depth 1 --branch "$branch" "$url" "$dir"
73
+ else
74
+ git clone --depth 1 "$url" "$dir"
75
+ fi
76
+ fi
77
+ }
78
+
79
+ echo ""
80
+ info "Cloning/updating SaaS boilerplate repositories..."
81
+ echo ""
82
+
83
+ ${bashEntries}
84
+
85
+ echo ""
86
+ ok "All repositories ready."
87
+ `;
88
+
89
+ // ── scripts/clone-repos.ps1 ────────────────────────────────────────────
90
+
91
+ const psEntries = gitServices
92
+ .map((s) => {
93
+ const gs = s.definition.gitSource!;
94
+ const branchArg = gs.branch ? ` -Branch "${gs.branch}"` : "";
95
+ let block = `Clone-OrUpdate -Name "${s.definition.id}" -Url "${gs.repoUrl}"${branchArg}`;
96
+ if (gs.postCloneCommands && gs.postCloneCommands.length > 0) {
97
+ const subdir = gs.subdirectory ? `/${gs.subdirectory}` : "";
98
+ const cmds = gs.postCloneCommands
99
+ .map((cmd) => `Push-Location "$ReposDir/${s.definition.id}${subdir}"; ${cmd}; Pop-Location`)
100
+ .join("\n");
101
+ block += `\n${cmds}`;
102
+ }
103
+ return block;
104
+ })
105
+ .join("\n\n");
106
+
107
+ files["scripts/clone-repos.ps1"] = `#Requires -Version 5.1
108
+ <#
109
+ .SYNOPSIS
110
+ Clone/update git-based service repositories.
111
+ Idempotent: clones if missing, pulls if already present.
112
+ #>
113
+ $ErrorActionPreference = "Stop"
114
+
115
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
116
+ $ProjectDir = Split-Path -Parent $ScriptDir
117
+ $ReposDir = Join-Path $ProjectDir "repos"
118
+
119
+ if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
120
+ Write-Error "git is not installed. Please install git first."
121
+ exit 1
122
+ }
123
+
124
+ if (-not (Test-Path $ReposDir)) { New-Item -ItemType Directory -Path $ReposDir -Force | Out-Null }
125
+
126
+ function Clone-OrUpdate {
127
+ param(
128
+ [string]$Name,
129
+ [string]$Url,
130
+ [string]$Branch = ""
131
+ )
132
+ $dir = Join-Path $ReposDir $Name
133
+
134
+ if (Test-Path (Join-Path $dir ".git")) {
135
+ Write-Host " Updating $Name..." -ForegroundColor Cyan
136
+ git -C $dir pull --ff-only 2>$null
137
+ if ($LASTEXITCODE -ne 0) { Write-Warning "Could not fast-forward $Name" }
138
+ } else {
139
+ Write-Host " Cloning $Name..." -ForegroundColor Cyan
140
+ if ($Branch) {
141
+ git clone --depth 1 --branch $Branch $Url $dir
142
+ } else {
143
+ git clone --depth 1 $Url $dir
144
+ }
145
+ }
146
+ }
147
+
148
+ Write-Host ""
149
+ Write-Host "Cloning/updating SaaS boilerplate repositories..." -ForegroundColor Cyan
150
+ Write-Host ""
151
+
152
+ ${psEntries}
153
+
154
+ Write-Host ""
155
+ Write-Host "All repositories ready." -ForegroundColor Green
156
+ `;
157
+
158
+ return files;
159
+ }
@@ -52,6 +52,23 @@ const DB_REQUIREMENTS: Record<string, Omit<DbRequirement, "serviceId" | "service
52
52
  openpanel: { dbName: "openpanel", dbUser: "openpanel", passwordEnvVar: "OPENPANEL_DB_PASSWORD" },
53
53
  usesend: { dbName: "usesend", dbUser: "usesend", passwordEnvVar: "USESEND_DB_PASSWORD" },
54
54
  nextcloud: { dbName: "nextcloud", dbUser: "nextcloud", passwordEnvVar: "NEXTCLOUD_DB_PASSWORD" },
55
+ // ── SaaS Boilerplates ────────────────────────────────────────────────────
56
+ "open-saas": { dbName: "opensaas", dbUser: "opensaas", passwordEnvVar: "OPENSAAS_DB_PASSWORD" },
57
+ "apptension-saas": {
58
+ dbName: "apptensionsaas",
59
+ dbUser: "apptensionsaas",
60
+ passwordEnvVar: "APPTENSION_SAAS_DB_PASSWORD",
61
+ },
62
+ "boxyhq-saas": {
63
+ dbName: "boxyhqsaas",
64
+ dbUser: "boxyhqsaas",
65
+ passwordEnvVar: "BOXYHQ_SAAS_DB_PASSWORD",
66
+ },
67
+ "ixartz-saas": {
68
+ dbName: "ixartzsaas",
69
+ dbUser: "ixartzsaas",
70
+ passwordEnvVar: "IXARTZ_SAAS_DB_PASSWORD",
71
+ },
55
72
  };
56
73
 
57
74
  /**
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import { generateScripts } from "./scripts.js";
3
3
 
4
4
  describe("generateScripts", () => {
5
- it("generates all 5 expected scripts", () => {
5
+ it("generates all 10 expected scripts (5 bash + 5 PowerShell)", () => {
6
6
  const result = generateScripts();
7
7
 
8
8
  const expectedScripts = [
@@ -11,6 +11,11 @@ describe("generateScripts", () => {
11
11
  "scripts/update.sh",
12
12
  "scripts/backup.sh",
13
13
  "scripts/status.sh",
14
+ "scripts/start.ps1",
15
+ "scripts/stop.ps1",
16
+ "scripts/update.ps1",
17
+ "scripts/backup.ps1",
18
+ "scripts/status.ps1",
14
19
  ];
15
20
 
16
21
  for (const script of expectedScripts) {
@@ -50,11 +55,54 @@ describe("generateScripts", () => {
50
55
  expect(result["scripts/status.sh"]).toContain("ps");
51
56
  });
52
57
 
53
- it("all scripts start with bash shebang", () => {
58
+ it("all bash scripts start with shebang", () => {
54
59
  const result = generateScripts();
55
60
 
56
- for (const [, content] of Object.entries(result)) {
57
- expect(content.startsWith("#!/")).toBe(true);
61
+ for (const [path, content] of Object.entries(result)) {
62
+ if (path.endsWith(".sh")) {
63
+ expect(content.startsWith("#!/")).toBe(true);
64
+ }
58
65
  }
59
66
  });
67
+
68
+ it("all PowerShell scripts start with #Requires", () => {
69
+ const result = generateScripts();
70
+
71
+ for (const [path, content] of Object.entries(result)) {
72
+ if (path.endsWith(".ps1")) {
73
+ expect(content.startsWith("#Requires")).toBe(true);
74
+ }
75
+ }
76
+ });
77
+
78
+ it("start.ps1 calls docker compose up", () => {
79
+ const result = generateScripts();
80
+ expect(result["scripts/start.ps1"]).toContain("docker compose");
81
+ expect(result["scripts/start.ps1"]).toContain("up");
82
+ });
83
+
84
+ it("stop.ps1 calls docker compose down", () => {
85
+ const result = generateScripts();
86
+ expect(result["scripts/stop.ps1"]).toContain("docker compose");
87
+ expect(result["scripts/stop.ps1"]).toContain("down");
88
+ });
89
+
90
+ it("update.ps1 calls docker compose pull", () => {
91
+ const result = generateScripts();
92
+ expect(result["scripts/update.ps1"]).toContain("docker compose");
93
+ expect(result["scripts/update.ps1"]).toContain("pull");
94
+ });
95
+
96
+ it("backup.ps1 references volumes or backup", () => {
97
+ const result = generateScripts();
98
+ const backup = result["scripts/backup.ps1"]!;
99
+ expect(backup).toBeDefined();
100
+ expect(backup.length).toBeGreaterThan(50);
101
+ });
102
+
103
+ it("status.ps1 calls docker compose ps", () => {
104
+ const result = generateScripts();
105
+ expect(result["scripts/status.ps1"]).toContain("docker compose");
106
+ expect(result["scripts/status.ps1"]).toContain("ps");
107
+ });
60
108
  });