plum-e2e 1.1.1 → 1.2.1

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 (334) hide show
  1. package/README.md +142 -70
  2. package/backend/Dockerfile +4 -2
  3. package/backend/_scaffold/utils/browser.ts +1 -0
  4. package/backend/app.js +4 -2
  5. package/backend/config/scripts/generate-report.js +38 -30
  6. package/{.prettierignore → backend/entrypoint.sh} +7 -6
  7. package/backend/package-lock.json +453 -10
  8. package/backend/package.json +5 -2
  9. package/backend/prisma/migrations/20260614000000_init/migration.sql +35 -0
  10. package/backend/prisma/migrations/20260614000001_add_project/migration.sql +8 -0
  11. package/backend/prisma/migrations/migration_lock.toml +3 -0
  12. package/backend/prisma/schema.prisma +53 -0
  13. package/backend/routes/backup.routes.js +50 -0
  14. package/backend/routes/cron.routes.js +9 -60
  15. package/backend/routes/reports.routes.js +39 -6
  16. package/backend/routes/settings.routes.js +43 -0
  17. package/backend/server.js +52 -1
  18. package/backend/services/backupService.js +88 -0
  19. package/backend/services/cronService.js +68 -78
  20. package/backend/{routes/schedules.routes.js → services/prisma.js} +3 -13
  21. package/backend/services/reportService.js +48 -20
  22. package/backend/services/settingsService.js +36 -0
  23. package/bin/plum.js +216 -34
  24. package/docker-compose.yml +24 -0
  25. package/frontend/.svelte-kit/adapter-node/.vite/manifest.json +190 -0
  26. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
  27. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/Button.cBruH0aD.css +17 -0
  28. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
  29. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.BVnUajEa.css +17 -0
  30. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
  31. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
  32. package/frontend/.svelte-kit/adapter-node/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
  33. package/frontend/.svelte-kit/adapter-node/chunks/Badge.js +30 -0
  34. package/frontend/.svelte-kit/adapter-node/chunks/Button.js +32 -0
  35. package/frontend/.svelte-kit/adapter-node/chunks/attributes.js +34 -0
  36. package/frontend/.svelte-kit/adapter-node/chunks/client.js +59 -0
  37. package/{backend/services/scheduleService.js → frontend/.svelte-kit/adapter-node/chunks/equality.js} +13 -16
  38. package/frontend/.svelte-kit/adapter-node/chunks/escaping.js +36 -0
  39. package/frontend/.svelte-kit/adapter-node/chunks/exports.js +247 -0
  40. package/frontend/.svelte-kit/adapter-node/chunks/index.js +1135 -0
  41. package/frontend/.svelte-kit/adapter-node/chunks/internal.js +695 -0
  42. package/frontend/.svelte-kit/adapter-node/chunks/reports.js +57 -0
  43. package/frontend/.svelte-kit/adapter-node/entries/fallbacks/error.svelte.js +44 -0
  44. package/frontend/.svelte-kit/adapter-node/entries/pages/_layout.svelte.js +181 -0
  45. package/frontend/.svelte-kit/adapter-node/entries/pages/_page.svelte.js +164 -0
  46. package/frontend/.svelte-kit/adapter-node/entries/pages/reports/_page.svelte.js +141 -0
  47. package/frontend/.svelte-kit/adapter-node/entries/pages/reports/_slug_/_page.svelte.js +42 -0
  48. package/frontend/.svelte-kit/adapter-node/entries/pages/scheduled-tests/_page.svelte.js +179 -0
  49. package/frontend/.svelte-kit/adapter-node/index.js +3144 -0
  50. package/frontend/.svelte-kit/adapter-node/internal.js +30 -0
  51. package/frontend/.svelte-kit/adapter-node/manifest-full.js +77 -0
  52. package/frontend/.svelte-kit/adapter-node/manifest.js +81 -0
  53. package/frontend/.svelte-kit/adapter-node/nodes/0.js +25 -0
  54. package/frontend/.svelte-kit/adapter-node/nodes/1.js +25 -0
  55. package/frontend/.svelte-kit/adapter-node/nodes/2.js +25 -0
  56. package/frontend/.svelte-kit/adapter-node/nodes/3.js +25 -0
  57. package/frontend/.svelte-kit/adapter-node/nodes/4.js +25 -0
  58. package/frontend/.svelte-kit/adapter-node/nodes/5.js +25 -0
  59. package/frontend/.svelte-kit/ambient.d.ts +240 -0
  60. package/frontend/.svelte-kit/generated/client/app.js +51 -0
  61. package/frontend/.svelte-kit/generated/client/matchers.js +18 -0
  62. package/frontend/.svelte-kit/generated/client/nodes/0.js +18 -0
  63. package/frontend/.svelte-kit/generated/client/nodes/1.js +18 -0
  64. package/frontend/.svelte-kit/generated/client/nodes/2.js +18 -0
  65. package/frontend/.svelte-kit/generated/client/nodes/3.js +18 -0
  66. package/frontend/.svelte-kit/generated/client/nodes/4.js +18 -0
  67. package/frontend/.svelte-kit/generated/client/nodes/5.js +18 -0
  68. package/frontend/.svelte-kit/generated/client-optimized/app.js +51 -0
  69. package/frontend/.svelte-kit/generated/client-optimized/matchers.js +18 -0
  70. package/frontend/.svelte-kit/generated/client-optimized/nodes/0.js +18 -0
  71. package/frontend/.svelte-kit/generated/client-optimized/nodes/1.js +18 -0
  72. package/frontend/.svelte-kit/generated/client-optimized/nodes/2.js +18 -0
  73. package/frontend/.svelte-kit/generated/client-optimized/nodes/3.js +18 -0
  74. package/frontend/.svelte-kit/generated/client-optimized/nodes/4.js +18 -0
  75. package/frontend/.svelte-kit/generated/client-optimized/nodes/5.js +18 -0
  76. package/frontend/.svelte-kit/generated/root.js +20 -0
  77. package/frontend/.svelte-kit/generated/root.svelte +83 -0
  78. package/frontend/.svelte-kit/generated/server/internal.js +65 -0
  79. package/frontend/.svelte-kit/non-ambient.d.ts +42 -0
  80. package/frontend/.svelte-kit/output/client/.vite/manifest.json +289 -0
  81. package/frontend/.svelte-kit/output/client/_app/immutable/assets/0.CnXRuPt4.css +17 -0
  82. package/frontend/.svelte-kit/output/client/_app/immutable/assets/2.CGnCsn5q.css +17 -0
  83. package/frontend/.svelte-kit/output/client/_app/immutable/assets/3.BVnUajEa.css +17 -0
  84. package/frontend/.svelte-kit/output/client/_app/immutable/assets/4.DBhBrHFz.css +17 -0
  85. package/frontend/.svelte-kit/output/client/_app/immutable/assets/5.D93VAB-w.css +17 -0
  86. package/frontend/.svelte-kit/output/client/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
  87. package/frontend/.svelte-kit/output/client/_app/immutable/assets/Button.cBruH0aD.css +17 -0
  88. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
  89. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.BVnUajEa.css +17 -0
  90. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
  91. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
  92. package/frontend/.svelte-kit/output/client/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
  93. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/B0bn91RF.js +18 -0
  94. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/B1kE5jmt.js +23 -0
  95. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/BKz6FZdB.js +18 -0
  96. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/Be4iUAB0.js +18 -0
  97. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/BpaMqo9O.js +18 -0
  98. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/Bt3-Z7H1.js +18 -0
  99. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/CHV2KnPr.js +18 -0
  100. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/CJ8q7g9Y.js +18 -0
  101. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/Cit9MuSg.js +20 -0
  102. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/CwBTqcbj.js +18 -0
  103. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/DBk3zeq4.js +18 -0
  104. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/DFJLQFic.js +18 -0
  105. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/DTUE7kkY.js +18 -0
  106. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/IYPwzKs_.js +20 -0
  107. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/oc0we5Us.js +18 -0
  108. package/frontend/.svelte-kit/output/client/_app/immutable/chunks/rVEKg0Ak.js +18 -0
  109. package/frontend/.svelte-kit/output/client/_app/immutable/entry/app.C2MbX06K.js +19 -0
  110. package/frontend/.svelte-kit/output/client/_app/immutable/entry/start.GiBry-lw.js +18 -0
  111. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/0.BsCYK-Tn.js +21 -0
  112. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/1.N4gjrCnX.js +18 -0
  113. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/2.D9yQNzK8.js +18 -0
  114. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/3.CBC5WvNb.js +18 -0
  115. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/4.BU6UZHSE.js +19 -0
  116. package/frontend/.svelte-kit/output/client/_app/immutable/nodes/5.Dzzfrntl.js +18 -0
  117. package/frontend/.svelte-kit/output/client/_app/version.json +1 -0
  118. package/frontend/.svelte-kit/output/client/favicon.png +0 -0
  119. package/frontend/.svelte-kit/output/server/.vite/manifest.json +190 -0
  120. package/frontend/.svelte-kit/output/server/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
  121. package/frontend/.svelte-kit/output/server/_app/immutable/assets/Button.cBruH0aD.css +17 -0
  122. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
  123. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.BVnUajEa.css +17 -0
  124. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
  125. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
  126. package/frontend/.svelte-kit/output/server/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
  127. package/frontend/.svelte-kit/output/server/chunks/Badge.js +30 -0
  128. package/frontend/.svelte-kit/output/server/chunks/Button.js +32 -0
  129. package/frontend/.svelte-kit/output/server/chunks/attributes.js +34 -0
  130. package/frontend/.svelte-kit/output/server/chunks/client.js +59 -0
  131. package/frontend/.svelte-kit/output/server/chunks/equality.js +31 -0
  132. package/frontend/.svelte-kit/output/server/chunks/escaping.js +36 -0
  133. package/frontend/.svelte-kit/output/server/chunks/exports.js +247 -0
  134. package/frontend/.svelte-kit/output/server/chunks/index.js +1135 -0
  135. package/frontend/.svelte-kit/output/server/chunks/internal.js +695 -0
  136. package/frontend/.svelte-kit/output/server/chunks/reports.js +57 -0
  137. package/frontend/.svelte-kit/output/server/entries/fallbacks/error.svelte.js +44 -0
  138. package/frontend/.svelte-kit/output/server/entries/pages/_layout.svelte.js +181 -0
  139. package/frontend/.svelte-kit/output/server/entries/pages/_page.svelte.js +164 -0
  140. package/frontend/.svelte-kit/output/server/entries/pages/reports/_page.svelte.js +141 -0
  141. package/frontend/.svelte-kit/output/server/entries/pages/reports/_slug_/_page.svelte.js +42 -0
  142. package/frontend/.svelte-kit/output/server/entries/pages/scheduled-tests/_page.svelte.js +179 -0
  143. package/frontend/.svelte-kit/output/server/index.js +3144 -0
  144. package/frontend/.svelte-kit/output/server/internal.js +30 -0
  145. package/frontend/.svelte-kit/output/server/manifest-full.js +77 -0
  146. package/frontend/.svelte-kit/output/server/manifest.js +77 -0
  147. package/frontend/.svelte-kit/output/server/nodes/0.js +25 -0
  148. package/frontend/.svelte-kit/output/server/nodes/1.js +25 -0
  149. package/frontend/.svelte-kit/output/server/nodes/2.js +25 -0
  150. package/frontend/.svelte-kit/output/server/nodes/3.js +25 -0
  151. package/frontend/.svelte-kit/output/server/nodes/4.js +25 -0
  152. package/frontend/.svelte-kit/output/server/nodes/5.js +25 -0
  153. package/frontend/.svelte-kit/tsconfig.json +49 -0
  154. package/frontend/build/client/_app/immutable/assets/0.CnXRuPt4.css +17 -0
  155. package/frontend/build/client/_app/immutable/assets/0.CnXRuPt4.css.br +0 -0
  156. package/frontend/build/client/_app/immutable/assets/0.CnXRuPt4.css.gz +0 -0
  157. package/frontend/build/client/_app/immutable/assets/2.CGnCsn5q.css +17 -0
  158. package/frontend/build/client/_app/immutable/assets/2.CGnCsn5q.css.br +0 -0
  159. package/frontend/build/client/_app/immutable/assets/2.CGnCsn5q.css.gz +0 -0
  160. package/frontend/build/client/_app/immutable/assets/3.BVnUajEa.css +17 -0
  161. package/frontend/build/client/_app/immutable/assets/3.BVnUajEa.css.br +0 -0
  162. package/frontend/build/client/_app/immutable/assets/3.BVnUajEa.css.gz +0 -0
  163. package/frontend/build/client/_app/immutable/assets/4.DBhBrHFz.css +17 -0
  164. package/frontend/build/client/_app/immutable/assets/4.DBhBrHFz.css.br +0 -0
  165. package/frontend/build/client/_app/immutable/assets/4.DBhBrHFz.css.gz +0 -0
  166. package/frontend/build/client/_app/immutable/assets/5.D93VAB-w.css +17 -0
  167. package/frontend/build/client/_app/immutable/assets/5.D93VAB-w.css.br +0 -0
  168. package/frontend/build/client/_app/immutable/assets/5.D93VAB-w.css.gz +0 -0
  169. package/frontend/build/client/_app/immutable/assets/Badge.DLLowvEA.css +17 -0
  170. package/frontend/build/client/_app/immutable/assets/Badge.DLLowvEA.css.br +0 -0
  171. package/frontend/build/client/_app/immutable/assets/Badge.DLLowvEA.css.gz +0 -0
  172. package/frontend/build/client/_app/immutable/assets/Button.cBruH0aD.css +17 -0
  173. package/frontend/build/client/_app/immutable/assets/Button.cBruH0aD.css.br +0 -0
  174. package/frontend/build/client/_app/immutable/assets/Button.cBruH0aD.css.gz +0 -0
  175. package/frontend/build/client/_app/immutable/assets/_layout.D7eM-6MV.css +17 -0
  176. package/frontend/build/client/_app/immutable/assets/_layout.D7eM-6MV.css.br +0 -0
  177. package/frontend/build/client/_app/immutable/assets/_layout.D7eM-6MV.css.gz +0 -0
  178. package/frontend/build/client/_app/immutable/assets/_page.BVnUajEa.css +17 -0
  179. package/frontend/build/client/_app/immutable/assets/_page.BVnUajEa.css.br +0 -0
  180. package/frontend/build/client/_app/immutable/assets/_page.BVnUajEa.css.gz +0 -0
  181. package/frontend/build/client/_app/immutable/assets/_page.CGnCsn5q.css +17 -0
  182. package/frontend/build/client/_app/immutable/assets/_page.CGnCsn5q.css.br +0 -0
  183. package/frontend/build/client/_app/immutable/assets/_page.CGnCsn5q.css.gz +0 -0
  184. package/frontend/build/client/_app/immutable/assets/_page.DBhBrHFz.css +17 -0
  185. package/frontend/build/client/_app/immutable/assets/_page.DBhBrHFz.css.br +0 -0
  186. package/frontend/build/client/_app/immutable/assets/_page.DBhBrHFz.css.gz +0 -0
  187. package/frontend/build/client/_app/immutable/assets/_page.DOqo0UR4.css +17 -0
  188. package/frontend/build/client/_app/immutable/assets/_page.DOqo0UR4.css.br +0 -0
  189. package/frontend/build/client/_app/immutable/assets/_page.DOqo0UR4.css.gz +0 -0
  190. package/frontend/build/client/_app/immutable/chunks/B0bn91RF.js +18 -0
  191. package/frontend/build/client/_app/immutable/chunks/B0bn91RF.js.br +0 -0
  192. package/frontend/build/client/_app/immutable/chunks/B0bn91RF.js.gz +0 -0
  193. package/frontend/build/client/_app/immutable/chunks/B1kE5jmt.js +23 -0
  194. package/frontend/build/client/_app/immutable/chunks/B1kE5jmt.js.br +0 -0
  195. package/frontend/build/client/_app/immutable/chunks/B1kE5jmt.js.gz +0 -0
  196. package/frontend/build/client/_app/immutable/chunks/BKz6FZdB.js +18 -0
  197. package/frontend/build/client/_app/immutable/chunks/BKz6FZdB.js.br +0 -0
  198. package/frontend/build/client/_app/immutable/chunks/BKz6FZdB.js.gz +0 -0
  199. package/frontend/build/client/_app/immutable/chunks/Be4iUAB0.js +18 -0
  200. package/frontend/build/client/_app/immutable/chunks/Be4iUAB0.js.br +0 -0
  201. package/frontend/build/client/_app/immutable/chunks/Be4iUAB0.js.gz +0 -0
  202. package/frontend/build/client/_app/immutable/chunks/BpaMqo9O.js +18 -0
  203. package/frontend/build/client/_app/immutable/chunks/BpaMqo9O.js.br +0 -0
  204. package/frontend/build/client/_app/immutable/chunks/BpaMqo9O.js.gz +0 -0
  205. package/frontend/build/client/_app/immutable/chunks/Bt3-Z7H1.js +18 -0
  206. package/frontend/build/client/_app/immutable/chunks/Bt3-Z7H1.js.br +0 -0
  207. package/frontend/build/client/_app/immutable/chunks/Bt3-Z7H1.js.gz +0 -0
  208. package/frontend/build/client/_app/immutable/chunks/CHV2KnPr.js +18 -0
  209. package/frontend/build/client/_app/immutable/chunks/CHV2KnPr.js.br +18 -0
  210. package/frontend/build/client/_app/immutable/chunks/CHV2KnPr.js.gz +0 -0
  211. package/frontend/build/client/_app/immutable/chunks/CJ8q7g9Y.js +18 -0
  212. package/{resources/comments-format.text → frontend/build/client/_app/immutable/chunks/CJ8q7g9Y.js.br} +0 -0
  213. package/frontend/build/client/_app/immutable/chunks/CJ8q7g9Y.js.gz +0 -0
  214. package/frontend/build/client/_app/immutable/chunks/Cit9MuSg.js +20 -0
  215. package/frontend/build/client/_app/immutable/chunks/Cit9MuSg.js.br +0 -0
  216. package/frontend/build/client/_app/immutable/chunks/Cit9MuSg.js.gz +0 -0
  217. package/frontend/build/client/_app/immutable/chunks/CwBTqcbj.js +18 -0
  218. package/frontend/build/client/_app/immutable/chunks/CwBTqcbj.js.br +0 -0
  219. package/frontend/build/client/_app/immutable/chunks/CwBTqcbj.js.gz +0 -0
  220. package/frontend/build/client/_app/immutable/chunks/DBk3zeq4.js +18 -0
  221. package/frontend/build/client/_app/immutable/chunks/DBk3zeq4.js.br +0 -0
  222. package/frontend/build/client/_app/immutable/chunks/DBk3zeq4.js.gz +0 -0
  223. package/frontend/build/client/_app/immutable/chunks/DFJLQFic.js +18 -0
  224. package/frontend/build/client/_app/immutable/chunks/DFJLQFic.js.br +0 -0
  225. package/frontend/build/client/_app/immutable/chunks/DFJLQFic.js.gz +0 -0
  226. package/frontend/build/client/_app/immutable/chunks/DTUE7kkY.js +18 -0
  227. package/frontend/build/client/_app/immutable/chunks/DTUE7kkY.js.br +0 -0
  228. package/frontend/build/client/_app/immutable/chunks/DTUE7kkY.js.gz +0 -0
  229. package/frontend/build/client/_app/immutable/chunks/IYPwzKs_.js +20 -0
  230. package/frontend/build/client/_app/immutable/chunks/IYPwzKs_.js.br +0 -0
  231. package/frontend/build/client/_app/immutable/chunks/IYPwzKs_.js.gz +0 -0
  232. package/frontend/build/client/_app/immutable/chunks/oc0we5Us.js +18 -0
  233. package/frontend/build/client/_app/immutable/chunks/oc0we5Us.js.br +0 -0
  234. package/frontend/build/client/_app/immutable/chunks/oc0we5Us.js.gz +0 -0
  235. package/frontend/build/client/_app/immutable/chunks/rVEKg0Ak.js +18 -0
  236. package/frontend/build/client/_app/immutable/chunks/rVEKg0Ak.js.br +0 -0
  237. package/frontend/build/client/_app/immutable/chunks/rVEKg0Ak.js.gz +0 -0
  238. package/frontend/build/client/_app/immutable/entry/app.C2MbX06K.js +19 -0
  239. package/frontend/build/client/_app/immutable/entry/app.C2MbX06K.js.br +0 -0
  240. package/frontend/build/client/_app/immutable/entry/app.C2MbX06K.js.gz +0 -0
  241. package/frontend/build/client/_app/immutable/entry/start.GiBry-lw.js +18 -0
  242. package/frontend/build/client/_app/immutable/entry/start.GiBry-lw.js.br +18 -0
  243. package/frontend/build/client/_app/immutable/entry/start.GiBry-lw.js.gz +0 -0
  244. package/frontend/build/client/_app/immutable/nodes/0.BsCYK-Tn.js +21 -0
  245. package/frontend/build/client/_app/immutable/nodes/0.BsCYK-Tn.js.br +0 -0
  246. package/frontend/build/client/_app/immutable/nodes/0.BsCYK-Tn.js.gz +0 -0
  247. package/frontend/build/client/_app/immutable/nodes/1.N4gjrCnX.js +18 -0
  248. package/frontend/build/client/_app/immutable/nodes/1.N4gjrCnX.js.br +0 -0
  249. package/frontend/build/client/_app/immutable/nodes/1.N4gjrCnX.js.gz +0 -0
  250. package/frontend/build/client/_app/immutable/nodes/2.D9yQNzK8.js +18 -0
  251. package/frontend/build/client/_app/immutable/nodes/2.D9yQNzK8.js.br +0 -0
  252. package/frontend/build/client/_app/immutable/nodes/2.D9yQNzK8.js.gz +0 -0
  253. package/frontend/build/client/_app/immutable/nodes/3.CBC5WvNb.js +18 -0
  254. package/frontend/build/client/_app/immutable/nodes/3.CBC5WvNb.js.br +0 -0
  255. package/frontend/build/client/_app/immutable/nodes/3.CBC5WvNb.js.gz +0 -0
  256. package/frontend/build/client/_app/immutable/nodes/4.BU6UZHSE.js +19 -0
  257. package/frontend/build/client/_app/immutable/nodes/4.BU6UZHSE.js.br +0 -0
  258. package/frontend/build/client/_app/immutable/nodes/4.BU6UZHSE.js.gz +0 -0
  259. package/frontend/build/client/_app/immutable/nodes/5.Dzzfrntl.js +18 -0
  260. package/frontend/build/client/_app/immutable/nodes/5.Dzzfrntl.js.br +0 -0
  261. package/frontend/build/client/_app/immutable/nodes/5.Dzzfrntl.js.gz +0 -0
  262. package/frontend/build/client/_app/version.json +1 -0
  263. package/frontend/build/client/_app/version.json.br +0 -0
  264. package/frontend/build/client/_app/version.json.gz +0 -0
  265. package/frontend/build/client/favicon.png +0 -0
  266. package/frontend/build/env.js +111 -0
  267. package/frontend/build/handler.js +1511 -0
  268. package/frontend/build/index.js +362 -0
  269. package/frontend/build/server/chunks/0-BFfqmlsS.js +26 -0
  270. package/frontend/build/server/chunks/0-BFfqmlsS.js.map +17 -0
  271. package/frontend/build/server/chunks/1-hso3H4Ax.js +26 -0
  272. package/frontend/build/server/chunks/1-hso3H4Ax.js.map +17 -0
  273. package/frontend/build/server/chunks/2-BSZWXCWp.js +26 -0
  274. package/frontend/build/server/chunks/2-BSZWXCWp.js.map +17 -0
  275. package/frontend/build/server/chunks/3-BYumY9dv.js +26 -0
  276. package/frontend/build/server/chunks/3-BYumY9dv.js.map +17 -0
  277. package/frontend/build/server/chunks/4-2UUrCVtM.js +26 -0
  278. package/frontend/build/server/chunks/4-2UUrCVtM.js.map +17 -0
  279. package/frontend/build/server/chunks/5-C2_y_fwT.js +26 -0
  280. package/frontend/build/server/chunks/5-C2_y_fwT.js.map +17 -0
  281. package/frontend/build/server/chunks/Badge-CC4U7C0c.js +30 -0
  282. package/frontend/build/server/chunks/Badge-CC4U7C0c.js.map +17 -0
  283. package/frontend/build/server/chunks/Button-BLkPicwS.js +33 -0
  284. package/frontend/build/server/chunks/Button-BLkPicwS.js.map +17 -0
  285. package/frontend/build/server/chunks/_layout.svelte-Bfxcj9C6.js +183 -0
  286. package/frontend/build/server/chunks/_layout.svelte-Bfxcj9C6.js.map +17 -0
  287. package/frontend/build/server/chunks/_page.svelte-DEK6UvaI.js +45 -0
  288. package/frontend/build/server/chunks/_page.svelte-DEK6UvaI.js.map +17 -0
  289. package/frontend/build/server/chunks/_page.svelte-DScVdHOq.js +142 -0
  290. package/frontend/build/server/chunks/_page.svelte-DScVdHOq.js.map +17 -0
  291. package/frontend/build/server/chunks/_page.svelte-Ns7w2sZt.js +180 -0
  292. package/frontend/build/server/chunks/_page.svelte-Ns7w2sZt.js.map +17 -0
  293. package/frontend/build/server/chunks/_page.svelte-s6ZIOitT.js +165 -0
  294. package/frontend/build/server/chunks/_page.svelte-s6ZIOitT.js.map +17 -0
  295. package/frontend/build/server/chunks/attributes-BeaNKpgU.js +34 -0
  296. package/frontend/build/server/chunks/attributes-BeaNKpgU.js.map +17 -0
  297. package/frontend/build/server/chunks/client-C9IaEwNo.js +52 -0
  298. package/frontend/build/server/chunks/client-C9IaEwNo.js.map +17 -0
  299. package/frontend/build/server/chunks/error.svelte-HKBv5sQT.js +45 -0
  300. package/frontend/build/server/chunks/error.svelte-HKBv5sQT.js.map +17 -0
  301. package/frontend/build/server/chunks/escaping-CqgfEcN3.js +36 -0
  302. package/frontend/build/server/chunks/escaping-CqgfEcN3.js.map +17 -0
  303. package/frontend/build/server/chunks/exports-OxlCJ8yE.js +192 -0
  304. package/frontend/build/server/chunks/exports-OxlCJ8yE.js.map +17 -0
  305. package/frontend/build/server/chunks/index-CjHlq1kt.js +1076 -0
  306. package/frontend/build/server/chunks/index-CjHlq1kt.js.map +17 -0
  307. package/frontend/build/server/chunks/reports-D3ZeYSmi.js +56 -0
  308. package/frontend/build/server/chunks/reports-D3ZeYSmi.js.map +17 -0
  309. package/frontend/build/server/index.js +5015 -0
  310. package/frontend/build/server/index.js.map +17 -0
  311. package/frontend/build/server/manifest.js +84 -0
  312. package/frontend/build/server/manifest.js.map +17 -0
  313. package/frontend/build/shims.js +49 -0
  314. package/frontend/package-lock.json +2 -2
  315. package/frontend/package.json +1 -1
  316. package/frontend/src/lib/api/reports.js +38 -27
  317. package/frontend/src/lib/api/schedules.js +9 -25
  318. package/frontend/src/lib/api/settings.js +48 -0
  319. package/frontend/src/lib/components/layout/Nav.svelte +2 -1
  320. package/frontend/src/lib/components/layout/RunnerPanel.svelte +160 -21
  321. package/frontend/src/lib/components/ui/Terminal.svelte +2 -2
  322. package/frontend/src/lib/stores/runner.js +9 -0
  323. package/frontend/src/routes/+page.svelte +10 -3
  324. package/frontend/src/routes/reports/+page.svelte +342 -51
  325. package/frontend/src/routes/reports/[slug]/+page.svelte +2 -0
  326. package/frontend/src/routes/scheduled-tests/+page.svelte +247 -11
  327. package/frontend/src/routes/settings/+page.svelte +410 -0
  328. package/package.json +6 -2
  329. package/.claude/settings.local.json +0 -27
  330. package/.husky/pre-commit +0 -2
  331. package/.prettierrc +0 -15
  332. package/.vscode/settings.json +0 -10
  333. package/backend/config/scripts/create-settings.js +0 -53
  334. package/license-config.json +0 -37
@@ -15,113 +15,103 @@
15
15
  * along with Plum. If not, see https://www.gnu.org/licenses/.
16
16
  */
17
17
 
18
- const fs = require('fs');
19
- const path = require('path');
20
18
  const cron = require('node-cron');
21
19
  const { spawn } = require('child_process');
20
+ const prisma = require('./prisma');
22
21
 
23
- const CRON_JOBS_FILE = path.join(__dirname, '../config/cron-jobs.json');
24
- let cronJobs = {};
22
+ const scheduledJobs = {};
23
+ let _io = null;
25
24
 
26
- const loadCronJobs = () => {
27
- if (fs.existsSync(CRON_JOBS_FILE)) {
28
- cronJobs = JSON.parse(fs.readFileSync(CRON_JOBS_FILE, 'utf8'));
25
+ const setSocketIO = (io) => {
26
+ _io = io;
27
+ };
29
28
 
30
- // Schedule all loaded cron jobs and store only necessary data in memory
31
- Object.keys(cronJobs).forEach((taskName) => {
32
- const { cronExpression, tags } = cronJobs[taskName];
33
- const scheduledCronJob = cron.schedule(cronExpression, () => {
34
- console.log(`Running new task: ${taskName}`);
29
+ function scheduleJob(taskName, cronExpression, tags, workers) {
30
+ if (scheduledJobs[taskName]) {
31
+ scheduledJobs[taskName].stop();
32
+ delete scheduledJobs[taskName];
33
+ }
35
34
 
36
- const task = spawn('npm', ['run', 'test'], {
37
- env: { ...process.env, TAG: tags, TRIGGER: taskName }
38
- });
35
+ scheduledJobs[taskName] = cron.schedule(cronExpression, () => {
36
+ console.log(`Running scheduled task: ${taskName}`);
37
+ if (_io) _io.emit('cron-start', { taskName });
39
38
 
40
- task.stdout.on('data', (data) => console.log(data.toString()));
41
- task.stderr.on('data', (data) => console.error(data.toString()));
42
- task.on('close', (code) => console.log(`Task ${taskName} finished with code ${code}`));
43
- });
39
+ const env = { ...process.env, TAG: tags, TRIGGER: taskName };
40
+ if (workers && workers > 1) env.PARALLEL = String(workers);
44
41
 
45
- // Store the reference to the cron job only in memory
46
- cronJobs[taskName].cronJob = scheduledCronJob;
42
+ const task = spawn('npm', ['run', 'test'], { env });
43
+ task.stdout.on('data', (data) => console.log(data.toString()));
44
+ task.stderr.on('data', (data) => console.error(data.toString()));
45
+ task.on('close', (code) => {
46
+ console.log(`Task ${taskName} finished with code ${code}`);
47
+ if (_io) _io.emit('cron-done', { taskName, code });
47
48
  });
49
+ });
50
+ }
51
+
52
+ const init = async () => {
53
+ const jobs = await prisma.cronJob.findMany();
54
+ for (const job of jobs) {
55
+ scheduleJob(job.taskName, job.cronExpression, job.tags, job.workers);
48
56
  }
57
+ console.log(`⏰ Scheduled ${jobs.length} cron job(s) from database`);
49
58
  };
50
59
 
51
- const saveCronJobs = () => {
52
- // Save only cron job data (not the reference to the cron job object) to the file
53
- const cronJobsData = Object.keys(cronJobs).reduce((acc, taskName) => {
54
- const { cronExpression, tags } = cronJobs[taskName];
55
- acc[taskName] = { cronExpression, tags }; // Exclude the cronJob reference
56
- return acc;
57
- }, {});
58
-
59
- fs.writeFileSync(CRON_JOBS_FILE, JSON.stringify(cronJobsData, null, 2), 'utf8');
60
+ const reload = async () => {
61
+ for (const name of Object.keys(scheduledJobs)) {
62
+ scheduledJobs[name].stop();
63
+ delete scheduledJobs[name];
64
+ }
65
+ await init();
60
66
  };
61
67
 
62
- const getAllCronJobs = () =>
63
- Object.keys(cronJobs).map((taskName) => ({
64
- taskName,
65
- cronExpression: cronJobs[taskName].cronExpression,
66
- tags: cronJobs[taskName].tags
67
- }));
68
+ const getAllCronJobs = async () => {
69
+ return prisma.cronJob.findMany({ orderBy: { createdAt: 'asc' } });
70
+ };
68
71
 
69
- const addCronJob = ({ cronExpression, taskName, tags }) => {
72
+ const addCronJob = async ({ taskName, cronExpression, tags, workers }) => {
70
73
  if (!cronExpression || !taskName || !tags) {
71
74
  return { status: 400, message: 'Missing required parameters' };
72
75
  }
73
-
74
- cronJobs[taskName] = { cronExpression, tags };
75
- saveCronJobs();
76
- loadCronJobs(); // Re-load and schedule the new cron job
76
+ const job = await prisma.cronJob.create({
77
+ data: { taskName, cronExpression, tags, workers: workers ?? 1 }
78
+ });
79
+ scheduleJob(job.taskName, job.cronExpression, job.tags, job.workers);
77
80
  return { status: 201, message: `Cron job ${taskName} added` };
78
81
  };
79
82
 
80
- const removeCronJob = (taskName) => {
81
- if (!cronJobs[taskName]) {
82
- return { status: 404, message: `Cron job ${taskName} not found` };
83
- }
83
+ const removeCronJob = async (taskName) => {
84
+ const job = await prisma.cronJob.findUnique({ where: { taskName } });
85
+ if (!job) return { status: 404, message: `Cron job ${taskName} not found` };
84
86
 
85
- // Stop the cron job before removing
86
- cronJobs[taskName].cronJob.stop();
87
+ if (scheduledJobs[taskName]) {
88
+ scheduledJobs[taskName].stop();
89
+ delete scheduledJobs[taskName];
90
+ }
87
91
 
88
- delete cronJobs[taskName];
89
- saveCronJobs();
90
- loadCronJobs(); // Re-load and re-schedule cron jobs without the removed one
92
+ await prisma.cronJob.delete({ where: { taskName } });
91
93
  return { status: 200, message: `Cron job ${taskName} deleted` };
92
94
  };
93
95
 
94
- const updateCronJob = (taskName, { cronExpression, tags }) => {
95
- if (!cronJobs[taskName]) {
96
- return { status: 404, message: `Cron job ${taskName} not found` };
97
- }
98
-
99
- // Stop the old cron job
100
- cronJobs[taskName].cronJob.stop();
101
-
102
- // Update the cron job with new values
103
- cronJobs[taskName] = { cronExpression, tags };
104
-
105
- // Reschedule the updated cron job and store the reference
106
- const scheduledCronJob = cron.schedule(cronExpression, () => {
107
- console.log(`Running updated task: ${taskName}`);
108
-
109
- const task = spawn('npm', ['run', 'test'], {
110
- env: { ...process.env, TAG: tags, TRIGGER: taskName }
111
- });
96
+ const updateCronJob = async (taskName, { cronExpression, tags, workers }) => {
97
+ const job = await prisma.cronJob.findUnique({ where: { taskName } });
98
+ if (!job) return { status: 404, message: `Cron job ${taskName} not found` };
112
99
 
113
- task.stdout.on('data', (data) => console.log(data.toString()));
114
- task.stderr.on('data', (data) => console.error(data.toString()));
115
- task.on('close', (code) => console.log(`Task ${taskName} finished with code ${code}`));
100
+ const updated = await prisma.cronJob.update({
101
+ where: { taskName },
102
+ data: { cronExpression, tags, workers: workers ?? 1 }
116
103
  });
117
104
 
118
- // Store the new cron job reference in memory
119
- cronJobs[taskName].cronJob = scheduledCronJob;
120
-
121
- saveCronJobs(); // Save the updated cron jobs to file (excluding cron job references)
105
+ scheduleJob(updated.taskName, updated.cronExpression, updated.tags, updated.workers);
122
106
  return { status: 200, message: `Cron job ${taskName} updated` };
123
107
  };
124
108
 
125
- loadCronJobs(); // Initial load and scheduling of cron jobs
126
-
127
- module.exports = { getAllCronJobs, addCronJob, removeCronJob, updateCronJob };
109
+ module.exports = {
110
+ init,
111
+ reload,
112
+ getAllCronJobs,
113
+ addCronJob,
114
+ removeCronJob,
115
+ updateCronJob,
116
+ setSocketIO
117
+ };
@@ -15,18 +15,8 @@
15
15
  * along with Plum. If not, see https://www.gnu.org/licenses/.
16
16
  */
17
17
 
18
- const express = require('express');
19
- const router = express.Router();
20
- const scheduleService = require('../services/scheduleService');
18
+ const { PrismaClient } = require('@prisma/client');
21
19
 
22
- /* -----------------------------------------------------
23
- * Get Schedules
24
- * Description:
25
- * Get all schedules from schedules/
26
- * ------------------------------------------------------ */
27
- router.get('/', (req, res) => {
28
- const schedules = scheduleService.getAllSchedules();
29
- res.json({ schedules });
30
- });
20
+ const prisma = new PrismaClient();
31
21
 
32
- module.exports = router;
22
+ module.exports = prisma;
@@ -17,29 +17,36 @@
17
17
 
18
18
  const fs = require('fs');
19
19
  const path = require('path');
20
+ const prisma = require('./prisma');
20
21
 
21
22
  const REPORTS_DIR = path.join(__dirname, '../reports');
22
23
 
23
- const getAllReports = () => {
24
- const files = fs.readdirSync(REPORTS_DIR);
25
- const jsonFiles = files.filter(
26
- (f) => f.endsWith('.json') && (f.startsWith('PASS_') || f.startsWith('FAIL_'))
27
- );
28
- return jsonFiles.sort((a, b) => {
29
- const at = fs.statSync(path.join(REPORTS_DIR, a)).mtime;
30
- const bt = fs.statSync(path.join(REPORTS_DIR, b)).mtime;
31
- return bt - at;
32
- });
24
+ const getAllReports = async () => {
25
+ return prisma.report.findMany({ orderBy: { createdAt: 'desc' } });
26
+ };
27
+
28
+ const getLatestReport = async () => {
29
+ const report = await prisma.report.findFirst({ orderBy: { createdAt: 'desc' } });
30
+ return report ? report.fileName : null;
33
31
  };
34
32
 
35
- const getLatestReport = () => {
36
- const files = getAllReports();
37
- return files.length ? files[0] : null;
33
+ const saveReport = async ({ fileName, status, tags, triggerType, runners }) => {
34
+ let cronJobId = null;
35
+ const builtInTriggers = ['manual-trigger', 'command-line-trigger', 'undefined'];
36
+ if (triggerType && !builtInTriggers.includes(triggerType)) {
37
+ const job = await prisma.cronJob.findUnique({ where: { taskName: triggerType } });
38
+ if (job) cronJobId = job.id;
39
+ }
40
+ return prisma.report.upsert({
41
+ where: { fileName },
42
+ create: { fileName, status, tags, triggerType: triggerType || 'undefined', runners, cronJobId },
43
+ update: {}
44
+ });
38
45
  };
39
46
 
40
47
  const getReportDetail = (fileName) => {
41
48
  const filePath = path.join(REPORTS_DIR, fileName);
42
- if (!filePath.startsWith(REPORTS_DIR)) return null; // path traversal guard
49
+ if (!filePath.startsWith(REPORTS_DIR)) return null;
43
50
  if (!fs.existsSync(filePath)) return null;
44
51
 
45
52
  let raw;
@@ -58,11 +65,11 @@ const getReportDetail = (fileName) => {
58
65
  const failedStepIndex = visibleSteps.findLastIndex((s) => s.result?.status === 'failed');
59
66
 
60
67
  const steps = visibleSteps.map((step, index) => ({
61
- keyword: step.keyword.trim(),
62
- name: step.name ?? '',
63
- status: step.result?.status ?? 'pending',
64
- duration: Math.round((step.result?.duration ?? 0) / 1_000_000), // ns → ms
65
- error: step.result?.error_message ?? null,
68
+ keyword: step.keyword.trim(),
69
+ name: step.name ?? '',
70
+ status: step.result?.status ?? 'pending',
71
+ duration: Math.round((step.result?.duration ?? 0) / 1_000_000),
72
+ error: step.result?.error_message ?? null,
66
73
  screenshot:
67
74
  step.embeddings?.find((e) => e.mime_type === 'image/png')?.data ??
68
75
  (index === failedStepIndex ? hookScreenshots[0]?.data : null) ??
@@ -97,4 +104,25 @@ const getReportDetail = (fileName) => {
97
104
  return { features };
98
105
  };
99
106
 
100
- module.exports = { getAllReports, getLatestReport, getReportDetail };
107
+ const deleteReport = async (fileName) => {
108
+ await prisma.report.delete({ where: { fileName } });
109
+ const filePath = path.join(REPORTS_DIR, fileName);
110
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
111
+ };
112
+
113
+ const deleteReports = async (fileNames) => {
114
+ await prisma.report.deleteMany({ where: { fileName: { in: fileNames } } });
115
+ for (const fileName of fileNames) {
116
+ const filePath = path.join(REPORTS_DIR, fileName);
117
+ if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
118
+ }
119
+ };
120
+
121
+ module.exports = {
122
+ getAllReports,
123
+ getLatestReport,
124
+ saveReport,
125
+ getReportDetail,
126
+ deleteReport,
127
+ deleteReports
128
+ };
@@ -0,0 +1,36 @@
1
+ /*
2
+ * This file is part of Plum.
3
+ *
4
+ * Plum is free software: you can redistribute it and/or modify
5
+ * it under the terms of the GNU General Public License as published by
6
+ * the Free Software Foundation, either version 3 of the License, or
7
+ * (at your option) any later version.
8
+ *
9
+ * Plum is distributed in the hope that it will be useful,
10
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ * GNU General Public License for more details.
13
+ *
14
+ * You should have received a copy of the GNU General Public License
15
+ * along with Plum. If not, see https://www.gnu.org/licenses/.
16
+ */
17
+
18
+ const prisma = require('./prisma');
19
+
20
+ const getProject = async () => {
21
+ let project = await prisma.project.findUnique({ where: { id: 1 } });
22
+ if (!project) {
23
+ project = await prisma.project.create({ data: { id: 1, name: '', logoUrl: '' } });
24
+ }
25
+ return project;
26
+ };
27
+
28
+ const updateProject = async ({ name, logoUrl }) => {
29
+ return prisma.project.upsert({
30
+ where: { id: 1 },
31
+ create: { id: 1, name: name ?? '', logoUrl: logoUrl ?? '' },
32
+ update: { name: name ?? '', logoUrl: logoUrl ?? '' }
33
+ });
34
+ };
35
+
36
+ module.exports = { getProject, updateProject };
package/bin/plum.js CHANGED
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { execSync } from 'child_process';
20
20
  import fs from 'fs';
21
+ import os from 'os';
21
22
  import path from 'path';
22
23
  import { fileURLToPath } from 'url';
23
24
  import fse from 'fs-extra';
@@ -55,6 +56,70 @@ IS_HEADLESS=false
55
56
  console.log('✅ .env file created with default values.\n');
56
57
  }
57
58
 
59
+ // Scaffold plum.plugins.json if it doesn't exist yet
60
+ function scaffoldPluginsFile() {
61
+ const pluginsPath = path.join(process.cwd(), 'plum.plugins.json');
62
+ if (fs.existsSync(pluginsPath)) {
63
+ console.log('⚠️ plum.plugins.json already exists. Skipping.\n');
64
+ return;
65
+ }
66
+ const content = {
67
+ '//': 'Add npm packages your tests depend on. Plum installs them automatically before each run.',
68
+ '// example':
69
+ 'To add a package: put its name and version under "dependencies", e.g. "@faker-js/faker": "^9.0.0"',
70
+ dependencies: {}
71
+ };
72
+ fs.writeFileSync(pluginsPath, JSON.stringify(content, null, 2) + '\n', 'utf8');
73
+ console.log('✅ plum.plugins.json created. Add npm packages here to extend your tests.\n');
74
+ }
75
+
76
+ // Install user plugins listed in plum.plugins.json into the backend
77
+ function installPlugins() {
78
+ const pluginsPath = path.join(process.cwd(), 'plum.plugins.json');
79
+ if (!fs.existsSync(pluginsPath)) return;
80
+
81
+ let plugins;
82
+ try {
83
+ plugins = JSON.parse(fs.readFileSync(pluginsPath, 'utf8'));
84
+ } catch {
85
+ console.log('⚠️ Could not parse plum.plugins.json. Skipping plugin install.\n');
86
+ return;
87
+ }
88
+
89
+ const deps = plugins.dependencies ?? {};
90
+ const packages = Object.entries(deps).map(([name, version]) => `${name}@${version}`);
91
+ if (packages.length === 0) return;
92
+
93
+ console.log(`📦 Installing plugins: ${packages.join(', ')}\n`);
94
+ execSync(`npm install ${packages.join(' ')}`, {
95
+ cwd: path.join(plumRoot, 'backend'),
96
+ stdio: 'inherit'
97
+ });
98
+ }
99
+
100
+ // Ensure user's .gitignore contains Plum-generated entries
101
+ function ensureGitignore() {
102
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
103
+ const plumEntries = ['reports/'];
104
+ const plumBlock = `\n# Plum (auto-generated)\n${plumEntries.join('\n')}\n`;
105
+
106
+ if (!fs.existsSync(gitignorePath)) {
107
+ fs.writeFileSync(gitignorePath, plumBlock.trimStart(), 'utf8');
108
+ console.log('✅ .gitignore created with Plum entries.\n');
109
+ return;
110
+ }
111
+
112
+ const existing = fs.readFileSync(gitignorePath, 'utf8');
113
+ const missing = plumEntries.filter((e) => !existing.includes(e));
114
+ if (missing.length === 0) {
115
+ console.log('⚠️ .gitignore already contains Plum entries. Skipping.\n');
116
+ return;
117
+ }
118
+
119
+ fs.appendFileSync(gitignorePath, `\n# Plum (auto-generated)\n${missing.join('\n')}\n`);
120
+ console.log('✅ .gitignore updated with Plum entries.\n');
121
+ }
122
+
58
123
  // Function to copy .env file from root to backend
59
124
  function copyEnvFile() {
60
125
  try {
@@ -91,32 +156,112 @@ switch (command) {
91
156
  // Create .env file with default values
92
157
  createEnvFile();
93
158
 
94
- // Create .vscode/settings.json for Cucumber extension step linkage
95
- const vscodeSettingsPath = path.join(process.cwd(), '.vscode', 'settings.json');
96
- if (!fs.existsSync(vscodeSettingsPath)) {
97
- fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
98
- const vscodeSettings = JSON.stringify(
99
- {
100
- 'cucumber.glue': ['tests/step_definitions/**/*.ts'],
101
- 'cucumber.features': ['tests/features/**/*.feature']
102
- },
103
- null,
104
- 2
105
- );
106
- fs.writeFileSync(vscodeSettingsPath, vscodeSettings, 'utf8');
107
- console.log('✅ .vscode/settings.json created for Cucumber extension.\n');
108
- } else {
109
- console.log('⚠️ .vscode/settings.json already exists. Skipping.\n');
159
+ // Create or update .gitignore with Plum-generated paths
160
+ ensureGitignore();
161
+
162
+ // Scaffold plum.plugins.json for user-managed dependencies
163
+ scaffoldPluginsFile();
164
+
165
+ // Create .vscode/settings.json and install Cucumber extension — only if VS Code is available
166
+ {
167
+ let vscodeAvailable = false;
168
+ try {
169
+ execSync('code --version', { stdio: 'ignore' });
170
+ vscodeAvailable = true;
171
+ } catch {}
172
+
173
+ if (vscodeAvailable) {
174
+ const vscodeSettingsPath = path.join(process.cwd(), '.vscode', 'settings.json');
175
+ if (!fs.existsSync(vscodeSettingsPath)) {
176
+ fs.mkdirSync(path.dirname(vscodeSettingsPath), { recursive: true });
177
+ fs.writeFileSync(
178
+ vscodeSettingsPath,
179
+ JSON.stringify(
180
+ {
181
+ 'cucumber.glue': ['tests/step_definitions/**/*.ts'],
182
+ 'cucumber.features': ['tests/features/**/*.feature']
183
+ },
184
+ null,
185
+ 2
186
+ ) + '\n',
187
+ 'utf8'
188
+ );
189
+ console.log('✅ .vscode/settings.json created for Cucumber extension.\n');
190
+ } else {
191
+ console.log('⚠️ .vscode/settings.json already exists. Skipping.\n');
192
+ }
193
+
194
+ try {
195
+ execSync('code --install-extension cucumberopen.cucumber-official', { stdio: 'inherit' });
196
+ console.log('✅ Cucumber VS Code extension installed.\n');
197
+ } catch {
198
+ console.log(
199
+ '⚠️ Could not install VS Code extension automatically. Install manually: cucumberopen.cucumber-official\n'
200
+ );
201
+ }
202
+ } else {
203
+ console.log('ℹ️ VS Code not detected — skipping .vscode setup.\n');
204
+ }
110
205
  }
111
206
 
112
- // Install Cucumber VS Code extension
113
- try {
114
- execSync('code --install-extension cucumberopen.cucumber-official', { stdio: 'inherit' });
115
- console.log('✅ Cucumber VS Code extension installed.\n');
116
- } catch {
117
- console.log(
118
- '⚠️ Could not install VS Code extension automatically. Install manually: cucumberopen.cucumber-official\n'
119
- );
207
+ // Create README.md in user's project if one doesn't exist
208
+ {
209
+ const userReadmePath = path.join(process.cwd(), 'README.md');
210
+ if (!fs.existsSync(userReadmePath)) {
211
+ const readmeContent = [
212
+ '# My Tests',
213
+ '',
214
+ 'Powered by [Plum](https://github.com/silverlunah/plum) — Playwright + Cucumber.',
215
+ '',
216
+ '## Commands',
217
+ '',
218
+ '| Command | Description |',
219
+ '|---|---|',
220
+ '| `plum dev` | Run all tests locally |',
221
+ '| `plum dev @tag` | Run tests matching a tag |',
222
+ '| `plum dev --parallel N` | Run tests across N parallel workers |',
223
+ '| `plum start` | Start the full UI via Docker (`http://localhost:5173`) |',
224
+ '| `plum create-step` | Interactively generate a new step definition |',
225
+ '',
226
+ '## Configuration',
227
+ '',
228
+ '| File | Purpose |',
229
+ '|---|---|',
230
+ '| `.env` | Set `BASE_URL` and `IS_HEADLESS` |',
231
+ '| `plum.plugins.json` | Add extra npm packages for your tests |',
232
+ '',
233
+ '## Test Structure',
234
+ '',
235
+ '```',
236
+ 'tests/',
237
+ ' features/ — Gherkin .feature files',
238
+ ' step_definitions/ — TypeScript step implementations',
239
+ ' pages/ — Page Object Models',
240
+ ' utils/ — Browser setup, hooks, helpers',
241
+ '```',
242
+ '',
243
+ 'Tags are used to filter which tests to run:',
244
+ '',
245
+ '```gherkin',
246
+ '@suite-login',
247
+ 'Feature: Login',
248
+ '',
249
+ ' @test-login-1',
250
+ ' Scenario: User can log in',
251
+ ' Given I am on the login page',
252
+ ' ...',
253
+ '```',
254
+ '',
255
+ '```bash',
256
+ 'plum dev @test-login-1 # single scenario',
257
+ 'plum dev @suite-login # whole suite',
258
+ '```'
259
+ ].join('\n');
260
+ fs.writeFileSync(userReadmePath, readmeContent + '\n', 'utf8');
261
+ console.log('✅ README.md created with command reference.\n');
262
+ } else {
263
+ console.log('⚠️ README.md already exists. Skipping.\n');
264
+ }
120
265
  }
121
266
 
122
267
  // Initialize project
@@ -128,7 +273,7 @@ switch (command) {
128
273
  });
129
274
 
130
275
  console.log(
131
- '🟣 Plum is now ready!\n\n Scaffold test cases are included in the `tests/` folder.\n For more information about Cucumber, visit: https://cucumber.io/\n\n - To start the server, run:\n `plum start` \n\n - If you are developing locally, run:\n `plum dev <@tag/blank if you want to run all tests>`'
276
+ '🟣 Plum is now ready!\n\n Scaffold test cases are in `tests/`.\n Add extra npm packages to `plum.plugins.json`.\n\n - Run tests locally:\n `plum dev` or `plum dev @tag`\n\n - Start the full UI (requires Docker):\n `plum start`\n\n - Generate a step:\n `plum create-step`'
132
277
  );
133
278
  console.log('--------------------------------------\n');
134
279
  break;
@@ -141,13 +286,32 @@ switch (command) {
141
286
  // Copy .env file from root to backend
142
287
  copyEnvFile();
143
288
 
144
- // Copy config from package root to user's project dir so Docker can mount it
145
- const userConfigPath = path.join(process.cwd(), '.plum', 'config');
289
+ // Merge user plugins into backend/package.json before Docker build
290
+ {
291
+ const userPluginsPath = path.join(process.cwd(), 'plum.plugins.json');
292
+ if (fs.existsSync(userPluginsPath)) {
293
+ try {
294
+ const userPlugins = JSON.parse(fs.readFileSync(userPluginsPath, 'utf8'));
295
+ const backendPkgPath = path.join(plumRoot, 'backend', 'package.json');
296
+ const backendPkg = JSON.parse(fs.readFileSync(backendPkgPath, 'utf8'));
297
+ const pluginDeps = userPlugins.dependencies ?? {};
298
+ if (Object.keys(pluginDeps).length > 0) {
299
+ backendPkg.dependencies = { ...backendPkg.dependencies, ...pluginDeps };
300
+ fs.writeFileSync(backendPkgPath, JSON.stringify(backendPkg, null, '\t') + '\n', 'utf8');
301
+ console.log(`📦 Merged plugins into backend: ${Object.keys(pluginDeps).join(', ')}\n`);
302
+ }
303
+ } catch {
304
+ console.log('⚠️ Could not read plum.plugins.json. Skipping plugin merge.\n');
305
+ }
306
+ }
307
+ }
308
+
309
+ // Copy config to ~/.plum/config so Docker can mount it without polluting the project
310
+ const userConfigPath = path.join(os.homedir(), '.plum', 'config');
146
311
  fse.copySync(path.join(plumRoot, 'backend', 'config'), userConfigPath);
147
312
 
148
313
  // Convert Windows paths to safe format
149
314
  const userTestsAbs = path.resolve(process.cwd(), 'tests').replace(/\\/g, '/');
150
- const userModulesAbs = path.resolve(process.cwd(), 'node_modules').replace(/\\/g, '/');
151
315
  const userReportsAbs = path.resolve(process.cwd(), 'reports').replace(/\\/g, '/');
152
316
  const userConfigAbs = userConfigPath.replace(/\\/g, '/');
153
317
 
@@ -158,15 +322,14 @@ switch (command) {
158
322
  ' volumes:',
159
323
  ` - "${userReportsAbs}:/app/reports"`,
160
324
  ` - "${userConfigAbs}:/app/config"`,
161
- ` - "${userTestsAbs}:/app/tests"`,
162
- ` - "${userModulesAbs}:/app/tests/node_modules"`
325
+ ` - "${userTestsAbs}:/app/tests"`
163
326
  ].join('\n');
164
327
 
165
328
  fs.writeFileSync(overrideFilePath, overrideYAML + '\n', 'utf8');
166
329
  console.log('✅ docker-compose.override.yml written');
167
330
 
168
- // Run docker compose
169
- execSync('docker compose up', {
331
+ // Run docker compose (--build picks up any plugin or config changes)
332
+ execSync('docker compose up --build', {
170
333
  cwd: plumRoot,
171
334
  stdio: 'inherit'
172
335
  });
@@ -204,6 +367,9 @@ switch (command) {
204
367
  stdio: 'inherit'
205
368
  });
206
369
 
370
+ // Install user-defined plugins from plum.plugins.json
371
+ installPlugins();
372
+
207
373
  console.log('Running `npx playwright install`...');
208
374
 
209
375
  execSync('npx playwright install', {
@@ -232,6 +398,17 @@ switch (command) {
232
398
  break;
233
399
  }
234
400
 
401
+ case 'stop':
402
+ console.log('--------------------------------------\n');
403
+ console.log('🛑 Stopping Plum...');
404
+ execSync('docker compose down', {
405
+ cwd: plumRoot,
406
+ stdio: 'inherit'
407
+ });
408
+ console.log('✅ Plum stopped. Your data is preserved in the database volume.\n');
409
+ console.log('--------------------------------------\n');
410
+ break;
411
+
235
412
  case 'create-step': {
236
413
  const createStepScript = path.join(plumRoot, 'backend', 'config', 'scripts', 'create-step.mjs');
237
414
  execSync(`node ${createStepScript}`, {
@@ -247,6 +424,11 @@ switch (command) {
247
424
 
248
425
  default:
249
426
  console.log('--------------------------------------\n');
250
- console.log('Usage: plum <init|start|dev|create-step>');
251
- console.log('--------------------------------------\n');
427
+ console.log('Usage: plum <command>\n');
428
+ console.log(' init Set up a new Plum project');
429
+ console.log(' start Start the full UI stack via Docker');
430
+ console.log(' stop Stop Docker containers (data is preserved)');
431
+ console.log(' dev Run tests locally without Docker');
432
+ console.log(' create-step Interactively scaffold a new step definition');
433
+ console.log('\n--------------------------------------\n');
252
434
  }