seams 0.1.0

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 (414) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +335 -0
  3. data/LICENSE +21 -0
  4. data/README.md +104 -0
  5. data/lib/generators/seams/accounts/accounts_generator.rb +272 -0
  6. data/lib/generators/seams/accounts/templates/README.md.tt +219 -0
  7. data/lib/generators/seams/accounts/templates/app/models/account.rb.tt +124 -0
  8. data/lib/generators/seams/accounts/templates/app/models/application_record.rb.tt +7 -0
  9. data/lib/generators/seams/accounts/templates/app/models/current.rb.tt +38 -0
  10. data/lib/generators/seams/accounts/templates/app/models/membership.rb.tt +114 -0
  11. data/lib/generators/seams/accounts/templates/config/routes.rb.tt +8 -0
  12. data/lib/generators/seams/accounts/templates/db/migrate/create_accounts.rb.tt +29 -0
  13. data/lib/generators/seams/accounts/templates/db/migrate/create_accounts_memberships.rb.tt +49 -0
  14. data/lib/generators/seams/accounts/templates/lib/accounts.rb.tt +21 -0
  15. data/lib/generators/seams/accounts/templates/lib/concerns/account_scoped.rb.tt +97 -0
  16. data/lib/generators/seams/accounts/templates/lib/concerns/authorization.rb.tt +105 -0
  17. data/lib/generators/seams/accounts/templates/lib/configuration.rb.tt +26 -0
  18. data/lib/generators/seams/accounts/templates/lib/engine.rb.tt +55 -0
  19. data/lib/generators/seams/accounts/templates/spec/factories/accounts.rb.tt +49 -0
  20. data/lib/generators/seams/accounts/templates/spec/models/accounts/account_spec.rb.tt +64 -0
  21. data/lib/generators/seams/accounts/templates/spec/models/accounts/membership_spec.rb.tt +99 -0
  22. data/lib/generators/seams/accounts/templates/spec/runtime/accounts_boot_spec.rb.tt +181 -0
  23. data/lib/generators/seams/admin/admin_generator.rb +852 -0
  24. data/lib/generators/seams/admin/templates/README.md.tt +266 -0
  25. data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_controller.rb.tt +16 -0
  26. data/lib/generators/seams/admin/templates/app/controllers/admin/accounts_memberships_controller.rb.tt +16 -0
  27. data/lib/generators/seams/admin/templates/app/controllers/admin/application_controller.rb.tt +282 -0
  28. data/lib/generators/seams/admin/templates/app/controllers/admin/identities_controller.rb.tt +26 -0
  29. data/lib/generators/seams/admin/templates/app/controllers/admin/invitations_controller.rb.tt +14 -0
  30. data/lib/generators/seams/admin/templates/app/controllers/admin/invoices_controller.rb.tt +14 -0
  31. data/lib/generators/seams/admin/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +14 -0
  32. data/lib/generators/seams/admin/templates/app/controllers/admin/notification_preferences_controller.rb.tt +15 -0
  33. data/lib/generators/seams/admin/templates/app/controllers/admin/notifications_controller.rb.tt +18 -0
  34. data/lib/generators/seams/admin/templates/app/controllers/admin/plans_controller.rb.tt +14 -0
  35. data/lib/generators/seams/admin/templates/app/controllers/admin/subscriptions_controller.rb.tt +14 -0
  36. data/lib/generators/seams/admin/templates/app/controllers/admin/teams_controller.rb.tt +14 -0
  37. data/lib/generators/seams/admin/templates/app/controllers/admin/teams_memberships_controller.rb.tt +16 -0
  38. data/lib/generators/seams/admin/templates/app/dashboards/admin/account_dashboard.rb.tt +50 -0
  39. data/lib/generators/seams/admin/templates/app/dashboards/admin/accounts_membership_dashboard.rb.tt +58 -0
  40. data/lib/generators/seams/admin/templates/app/dashboards/admin/identity_dashboard.rb.tt +48 -0
  41. data/lib/generators/seams/admin/templates/app/dashboards/admin/invitation_dashboard.rb.tt +51 -0
  42. data/lib/generators/seams/admin/templates/app/dashboards/admin/invoice_dashboard.rb.tt +67 -0
  43. data/lib/generators/seams/admin/templates/app/dashboards/admin/lifetime_pass_dashboard.rb.tt +65 -0
  44. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_dashboard.rb.tt +58 -0
  45. data/lib/generators/seams/admin/templates/app/dashboards/admin/notification_preference_dashboard.rb.tt +43 -0
  46. data/lib/generators/seams/admin/templates/app/dashboards/admin/plan_dashboard.rb.tt +72 -0
  47. data/lib/generators/seams/admin/templates/app/dashboards/admin/subscription_dashboard.rb.tt +59 -0
  48. data/lib/generators/seams/admin/templates/app/dashboards/admin/team_dashboard.rb.tt +39 -0
  49. data/lib/generators/seams/admin/templates/app/dashboards/admin/teams_membership_dashboard.rb.tt +43 -0
  50. data/lib/generators/seams/admin/templates/app/policies/admin/platform/account_policy.rb.tt +10 -0
  51. data/lib/generators/seams/admin/templates/app/policies/admin/platform/accounts_membership_policy.rb.tt +10 -0
  52. data/lib/generators/seams/admin/templates/app/policies/admin/platform/application_policy.rb.tt +85 -0
  53. data/lib/generators/seams/admin/templates/app/policies/admin/platform/identity_policy.rb.tt +18 -0
  54. data/lib/generators/seams/admin/templates/app/policies/admin/platform/invitation_policy.rb.tt +9 -0
  55. data/lib/generators/seams/admin/templates/app/policies/admin/platform/invoice_policy.rb.tt +9 -0
  56. data/lib/generators/seams/admin/templates/app/policies/admin/platform/lifetime_pass_policy.rb.tt +9 -0
  57. data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_policy.rb.tt +9 -0
  58. data/lib/generators/seams/admin/templates/app/policies/admin/platform/notification_preference_policy.rb.tt +9 -0
  59. data/lib/generators/seams/admin/templates/app/policies/admin/platform/plan_policy.rb.tt +11 -0
  60. data/lib/generators/seams/admin/templates/app/policies/admin/platform/subscription_policy.rb.tt +9 -0
  61. data/lib/generators/seams/admin/templates/app/policies/admin/platform/team_policy.rb.tt +9 -0
  62. data/lib/generators/seams/admin/templates/app/policies/admin/platform/teams_membership_policy.rb.tt +9 -0
  63. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/account_policy.rb.tt +33 -0
  64. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/accounts_membership_policy.rb.tt +24 -0
  65. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/application_policy.rb.tt +169 -0
  66. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/identity_policy.rb.tt +67 -0
  67. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invitation_policy.rb.tt +24 -0
  68. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/invoice_policy.rb.tt +21 -0
  69. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/lifetime_pass_policy.rb.tt +21 -0
  70. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_policy.rb.tt +25 -0
  71. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/notification_preference_policy.rb.tt +23 -0
  72. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/plan_policy.rb.tt +47 -0
  73. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/subscription_policy.rb.tt +22 -0
  74. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/team_policy.rb.tt +28 -0
  75. data/lib/generators/seams/admin/templates/app/policies/admin/tenant/teams_membership_policy.rb.tt +24 -0
  76. data/lib/generators/seams/admin/templates/config/routes.rb.tt +38 -0
  77. data/lib/generators/seams/admin/templates/lib/admin.rb.tt +36 -0
  78. data/lib/generators/seams/admin/templates/lib/concerns/authenticator.rb.tt +66 -0
  79. data/lib/generators/seams/admin/templates/lib/configuration.rb.tt +90 -0
  80. data/lib/generators/seams/admin/templates/lib/context.rb.tt +44 -0
  81. data/lib/generators/seams/admin/templates/lib/engine.rb.tt +68 -0
  82. data/lib/generators/seams/admin/templates/spec/factories/admin.rb.tt +10 -0
  83. data/lib/generators/seams/admin/templates/spec/runtime/admin_boot_spec.rb.tt +604 -0
  84. data/lib/generators/seams/auth/add_oauth_provider/add_oauth_provider_generator.rb +157 -0
  85. data/lib/generators/seams/auth/add_oauth_provider/templates/adapter.rb.tt +95 -0
  86. data/lib/generators/seams/auth/add_oauth_provider/templates/adapter_spec.rb.tt +58 -0
  87. data/lib/generators/seams/auth/auth_generator.rb +311 -0
  88. data/lib/generators/seams/auth/templates/README.md.tt +289 -0
  89. data/lib/generators/seams/auth/templates/app/controllers/oauth/callbacks_controller.rb.tt +80 -0
  90. data/lib/generators/seams/auth/templates/app/controllers/password_resets_controller.rb.tt +44 -0
  91. data/lib/generators/seams/auth/templates/app/controllers/registrations_controller.rb.tt +34 -0
  92. data/lib/generators/seams/auth/templates/app/controllers/sessions_controller.rb.tt +49 -0
  93. data/lib/generators/seams/auth/templates/app/jobs/application_job.rb.tt +7 -0
  94. data/lib/generators/seams/auth/templates/app/jobs/cleanup_expired_sessions_job.rb.tt +30 -0
  95. data/lib/generators/seams/auth/templates/app/mailers/passwords_mailer.rb.tt +15 -0
  96. data/lib/generators/seams/auth/templates/app/models/api_token.rb.tt +62 -0
  97. data/lib/generators/seams/auth/templates/app/models/application_record.rb.tt +7 -0
  98. data/lib/generators/seams/auth/templates/app/models/current.rb.tt +15 -0
  99. data/lib/generators/seams/auth/templates/app/models/identity.rb.tt +74 -0
  100. data/lib/generators/seams/auth/templates/app/models/oauth/provider.rb.tt +48 -0
  101. data/lib/generators/seams/auth/templates/app/models/session.rb.tt +28 -0
  102. data/lib/generators/seams/auth/templates/app/services/authenticate_identity.rb.tt +31 -0
  103. data/lib/generators/seams/auth/templates/app/services/generate_api_token.rb.tt +35 -0
  104. data/lib/generators/seams/auth/templates/app/services/oauth/authenticator.rb.tt +94 -0
  105. data/lib/generators/seams/auth/templates/app/services/register_identity.rb.tt +57 -0
  106. data/lib/generators/seams/auth/templates/app/services/reset_password.rb.tt +41 -0
  107. data/lib/generators/seams/auth/templates/app/services/revoke_api_token.rb.tt +38 -0
  108. data/lib/generators/seams/auth/templates/app/views/password_resets/edit.html.erb.tt +12 -0
  109. data/lib/generators/seams/auth/templates/app/views/password_resets/new.html.erb.tt +11 -0
  110. data/lib/generators/seams/auth/templates/app/views/passwords_mailer/reset_email.html.erb.tt +7 -0
  111. data/lib/generators/seams/auth/templates/app/views/registrations/new.html.erb.tt +26 -0
  112. data/lib/generators/seams/auth/templates/app/views/sessions/_oauth_buttons.html.erb.tt +18 -0
  113. data/lib/generators/seams/auth/templates/app/views/sessions/new.html.erb.tt +17 -0
  114. data/lib/generators/seams/auth/templates/config/routes.rb.tt +21 -0
  115. data/lib/generators/seams/auth/templates/db/migrate/create_auth_api_tokens.rb.tt +26 -0
  116. data/lib/generators/seams/auth/templates/db/migrate/create_auth_identities.rb.tt +29 -0
  117. data/lib/generators/seams/auth/templates/db/migrate/create_auth_oauth_providers.rb.tt +35 -0
  118. data/lib/generators/seams/auth/templates/db/migrate/create_auth_sessions.rb.tt +19 -0
  119. data/lib/generators/seams/auth/templates/lib/auth.rb.tt +39 -0
  120. data/lib/generators/seams/auth/templates/lib/concerns/api_authenticatable.rb.tt +58 -0
  121. data/lib/generators/seams/auth/templates/lib/concerns/authenticatable.rb.tt +32 -0
  122. data/lib/generators/seams/auth/templates/lib/concerns/authentication.rb.tt +60 -0
  123. data/lib/generators/seams/auth/templates/lib/configuration.rb.tt +45 -0
  124. data/lib/generators/seams/auth/templates/lib/engine.rb.tt +46 -0
  125. data/lib/generators/seams/auth/templates/lib/oauth/abstract.rb.tt +87 -0
  126. data/lib/generators/seams/auth/templates/lib/oauth/github.rb.tt +112 -0
  127. data/lib/generators/seams/auth/templates/lib/oauth/google.rb.tt +78 -0
  128. data/lib/generators/seams/auth/templates/lib/tasks/auth_pii.rake.tt +68 -0
  129. data/lib/generators/seams/auth/templates/spec/factories/auth.rb.tt +38 -0
  130. data/lib/generators/seams/auth/templates/spec/mailers/passwords_mailer_spec.rb.tt +37 -0
  131. data/lib/generators/seams/auth/templates/spec/models/api_token_spec.rb.tt +84 -0
  132. data/lib/generators/seams/auth/templates/spec/models/identity_spec.rb.tt +56 -0
  133. data/lib/generators/seams/auth/templates/spec/models/oauth/provider_spec.rb.tt +64 -0
  134. data/lib/generators/seams/auth/templates/spec/models/session_spec.rb.tt +34 -0
  135. data/lib/generators/seams/auth/templates/spec/runtime/boot_spec.rb.tt +30 -0
  136. data/lib/generators/seams/auth/templates/spec/runtime/event_payload_spec.rb.tt +29 -0
  137. data/lib/generators/seams/auth/templates/spec/runtime/login_flow_spec.rb.tt +45 -0
  138. data/lib/generators/seams/billing/billing_generator.rb +476 -0
  139. data/lib/generators/seams/billing/templates/README.md.tt +355 -0
  140. data/lib/generators/seams/billing/templates/app/controllers/admin/lifetime_passes_controller.rb.tt +84 -0
  141. data/lib/generators/seams/billing/templates/app/controllers/checkout_controller.rb.tt +92 -0
  142. data/lib/generators/seams/billing/templates/app/controllers/invoices_controller.rb.tt +63 -0
  143. data/lib/generators/seams/billing/templates/app/controllers/plans_controller.rb.tt +14 -0
  144. data/lib/generators/seams/billing/templates/app/controllers/portal_controller.rb.tt +45 -0
  145. data/lib/generators/seams/billing/templates/app/controllers/subscriptions_controller.rb.tt +119 -0
  146. data/lib/generators/seams/billing/templates/app/controllers/webhooks_controller.rb.tt +98 -0
  147. data/lib/generators/seams/billing/templates/app/helpers/currency_helper.rb.tt +44 -0
  148. data/lib/generators/seams/billing/templates/app/jobs/application_job.rb.tt +6 -0
  149. data/lib/generators/seams/billing/templates/app/jobs/cancel_subscription_job.rb.tt +39 -0
  150. data/lib/generators/seams/billing/templates/app/jobs/start_subscription_job.rb.tt +32 -0
  151. data/lib/generators/seams/billing/templates/app/jobs/webhooks/process_event_job.rb.tt +37 -0
  152. data/lib/generators/seams/billing/templates/app/models/application_record.rb.tt +7 -0
  153. data/lib/generators/seams/billing/templates/app/models/invoice.rb.tt +35 -0
  154. data/lib/generators/seams/billing/templates/app/models/lifetime_pass.rb.tt +60 -0
  155. data/lib/generators/seams/billing/templates/app/models/plan.rb.tt +95 -0
  156. data/lib/generators/seams/billing/templates/app/models/subscription.rb.tt +31 -0
  157. data/lib/generators/seams/billing/templates/app/models/webhook_event.rb.tt +13 -0
  158. data/lib/generators/seams/billing/templates/app/services/checkout_session_service.rb.tt +25 -0
  159. data/lib/generators/seams/billing/templates/app/services/customers/find_or_create_service.rb.tt +73 -0
  160. data/lib/generators/seams/billing/templates/app/services/invoices/sync_service.rb.tt +50 -0
  161. data/lib/generators/seams/billing/templates/app/services/lifetime/create_lifetime_session_service.rb.tt +82 -0
  162. data/lib/generators/seams/billing/templates/app/services/lifetime/create_pass_from_checkout_service.rb.tt +88 -0
  163. data/lib/generators/seams/billing/templates/app/services/lifetime/grant_pass_service.rb.tt +80 -0
  164. data/lib/generators/seams/billing/templates/app/services/lifetime/revoke_pass_service.rb.tt +59 -0
  165. data/lib/generators/seams/billing/templates/app/services/portal_session_service.rb.tt +23 -0
  166. data/lib/generators/seams/billing/templates/app/services/service_result.rb.tt +38 -0
  167. data/lib/generators/seams/billing/templates/app/services/stripe_service.rb.tt +67 -0
  168. data/lib/generators/seams/billing/templates/app/services/subscriptions/cancel_service.rb.tt +42 -0
  169. data/lib/generators/seams/billing/templates/app/services/subscriptions/change_plan_service.rb.tt +48 -0
  170. data/lib/generators/seams/billing/templates/app/services/subscriptions/reactivate_service.rb.tt +28 -0
  171. data/lib/generators/seams/billing/templates/app/services/webhooks/event_router.rb.tt +54 -0
  172. data/lib/generators/seams/billing/templates/app/services/webhooks/handler.rb.tt +93 -0
  173. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/charge_refunded_handler.rb.tt +18 -0
  174. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/checkout_session_completed_handler.rb.tt +58 -0
  175. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_created_handler.rb.tt +16 -0
  176. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_finalized_handler.rb.tt +14 -0
  177. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_handler_base.rb.tt +80 -0
  178. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_paid_handler.rb.tt +12 -0
  179. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_payment_failed_handler.rb.tt +12 -0
  180. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/invoice_voided_handler.rb.tt +12 -0
  181. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_failed_handler.rb.tt +15 -0
  182. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/payment_succeeded_handler.rb.tt +19 -0
  183. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_created_handler.rb.tt +11 -0
  184. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_deleted_handler.rb.tt +15 -0
  185. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_handler_base.rb.tt +92 -0
  186. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_trial_will_end_handler.rb.tt +15 -0
  187. data/lib/generators/seams/billing/templates/app/services/webhooks/handlers/subscription_updated_handler.rb.tt +11 -0
  188. data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/index.html.erb.tt +36 -0
  189. data/lib/generators/seams/billing/templates/app/views/admin/lifetime_passes/new.html.erb.tt +37 -0
  190. data/lib/generators/seams/billing/templates/app/views/checkout/success.html.erb.tt +5 -0
  191. data/lib/generators/seams/billing/templates/app/views/invoices/index.html.erb.tt +22 -0
  192. data/lib/generators/seams/billing/templates/app/views/invoices/show.html.erb.tt +14 -0
  193. data/lib/generators/seams/billing/templates/app/views/plans/index.html.erb.tt +51 -0
  194. data/lib/generators/seams/billing/templates/app/views/subscriptions/index.html.erb.tt +16 -0
  195. data/lib/generators/seams/billing/templates/app/views/subscriptions/show.html.erb.tt +25 -0
  196. data/lib/generators/seams/billing/templates/config/routes.rb.tt +39 -0
  197. data/lib/generators/seams/billing/templates/db/migrate/create_billing_invoices.rb.tt +32 -0
  198. data/lib/generators/seams/billing/templates/db/migrate/create_billing_lifetime_passes.rb.tt +43 -0
  199. data/lib/generators/seams/billing/templates/db/migrate/create_billing_plans.rb.tt +31 -0
  200. data/lib/generators/seams/billing/templates/db/migrate/create_billing_subscriptions.rb.tt +33 -0
  201. data/lib/generators/seams/billing/templates/db/migrate/create_billing_webhook_events.rb.tt +24 -0
  202. data/lib/generators/seams/billing/templates/lib/billing.rb.tt +34 -0
  203. data/lib/generators/seams/billing/templates/lib/concerns/billable.rb.tt +100 -0
  204. data/lib/generators/seams/billing/templates/lib/configuration.rb.tt +52 -0
  205. data/lib/generators/seams/billing/templates/lib/engine.rb.tt +72 -0
  206. data/lib/generators/seams/billing/templates/lib/gateways/abstract.rb.tt +65 -0
  207. data/lib/generators/seams/billing/templates/lib/gateways/adyen.rb.tt +16 -0
  208. data/lib/generators/seams/billing/templates/lib/gateways/paddle.rb.tt +22 -0
  209. data/lib/generators/seams/billing/templates/lib/gateways/stripe.rb.tt +155 -0
  210. data/lib/generators/seams/billing/templates/lib/stripe/client.rb.tt +101 -0
  211. data/lib/generators/seams/billing/templates/lib/stripe/webhook_signature.rb.tt +43 -0
  212. data/lib/generators/seams/billing/templates/lib/tasks/billing_check.rake.tt +34 -0
  213. data/lib/generators/seams/billing/templates/spec/factories/billing.rb.tt +65 -0
  214. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/charge_refunded.json.tt +19 -0
  215. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/checkout_session_completed.json.tt +17 -0
  216. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_created.json.tt +25 -0
  217. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_deleted.json.tt +17 -0
  218. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_trial_will_end.json.tt +17 -0
  219. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/customer_subscription_updated.json.tt +28 -0
  220. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_created.json.tt +18 -0
  221. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_finalized.json.tt +18 -0
  222. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_paid.json.tt +19 -0
  223. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_payment_failed.json.tt +20 -0
  224. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/invoice_voided.json.tt +18 -0
  225. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_payment_failed.json.tt +21 -0
  226. data/lib/generators/seams/billing/templates/spec/fixtures/stripe/payment_intent_succeeded.json.tt +17 -0
  227. data/lib/generators/seams/billing/templates/spec/gateways/contract_spec.rb.tt +11 -0
  228. data/lib/generators/seams/billing/templates/spec/gateways/stripe_spec.rb.tt +53 -0
  229. data/lib/generators/seams/billing/templates/spec/models/plan_spec.rb.tt +81 -0
  230. data/lib/generators/seams/billing/templates/spec/models/subscription_spec.rb.tt +43 -0
  231. data/lib/generators/seams/billing/templates/spec/runtime/boot_spec.rb.tt +38 -0
  232. data/lib/generators/seams/billing/templates/spec/runtime/webhook_handlers_spec.rb.tt +382 -0
  233. data/lib/generators/seams/billing/templates/spec/support/shared_examples/a_billing_gateway.rb.tt +100 -0
  234. data/lib/generators/seams/billing/templates/spec/support/stripe_helpers.rb.tt +59 -0
  235. data/lib/generators/seams/core/core_generator.rb +191 -0
  236. data/lib/generators/seams/core/templates/README.md.tt +45 -0
  237. data/lib/generators/seams/core/templates/app/controllers/concerns/has_current_attributes.rb.tt +71 -0
  238. data/lib/generators/seams/core/templates/app/models/application_record.rb.tt +7 -0
  239. data/lib/generators/seams/core/templates/app/models/audit_log.rb.tt +19 -0
  240. data/lib/generators/seams/core/templates/app/models/concerns/auditable.rb.tt +64 -0
  241. data/lib/generators/seams/core/templates/app/models/concerns/sluggable.rb.tt +53 -0
  242. data/lib/generators/seams/core/templates/app/models/concerns/soft_deletable.rb.tt +37 -0
  243. data/lib/generators/seams/core/templates/app/models/concerns/tenant_scoped.rb.tt +39 -0
  244. data/lib/generators/seams/core/templates/app/models/current.rb.tt +16 -0
  245. data/lib/generators/seams/core/templates/app/services/event_publisher.rb.tt +23 -0
  246. data/lib/generators/seams/core/templates/app/validators/email_format_validator.rb.tt +21 -0
  247. data/lib/generators/seams/core/templates/db/migrate/create_core_audit_logs.rb.tt +29 -0
  248. data/lib/generators/seams/core/templates/lib/core.rb.tt +8 -0
  249. data/lib/generators/seams/core/templates/lib/engine.rb.tt +28 -0
  250. data/lib/generators/seams/core/templates/spec/concerns/auditable_spec.rb.tt +39 -0
  251. data/lib/generators/seams/core/templates/spec/concerns/sluggable_spec.rb.tt +29 -0
  252. data/lib/generators/seams/core/templates/spec/models/audit_log_spec.rb.tt +22 -0
  253. data/lib/generators/seams/core/templates/spec/runtime/boot_spec.rb.tt +25 -0
  254. data/lib/generators/seams/core/templates/spec/validators/email_format_validator_spec.rb.tt +29 -0
  255. data/lib/generators/seams/engine/engine_generator.rb +165 -0
  256. data/lib/generators/seams/engine/templates/Gemfile.tt +19 -0
  257. data/lib/generators/seams/engine/templates/LICENSE.tt +21 -0
  258. data/lib/generators/seams/engine/templates/README.md.tt +40 -0
  259. data/lib/generators/seams/engine/templates/Rakefile.tt +14 -0
  260. data/lib/generators/seams/engine/templates/app/application_controller.rb.tt +6 -0
  261. data/lib/generators/seams/engine/templates/app/application_record.rb.tt +16 -0
  262. data/lib/generators/seams/engine/templates/config/locales/en.yml.tt +14 -0
  263. data/lib/generators/seams/engine/templates/config/routes.rb.tt +4 -0
  264. data/lib/generators/seams/engine/templates/gemspec.tt +20 -0
  265. data/lib/generators/seams/engine/templates/host_initializer.rb.tt +13 -0
  266. data/lib/generators/seams/engine/templates/lib/engine.rb.tt +27 -0
  267. data/lib/generators/seams/engine/templates/lib/root.rb.tt +7 -0
  268. data/lib/generators/seams/engine/templates/lib/version.rb.tt +5 -0
  269. data/lib/generators/seams/engine/templates/rubocop.yml.tt +55 -0
  270. data/lib/generators/seams/engine/templates/spec/example_spec.rb.tt +16 -0
  271. data/lib/generators/seams/engine/templates/spec/spec_helper.rb.tt +23 -0
  272. data/lib/generators/seams/install/install_generator.rb +211 -0
  273. data/lib/generators/seams/install/templates/Dockerfile.tt +52 -0
  274. data/lib/generators/seams/install/templates/Procfile.tt +14 -0
  275. data/lib/generators/seams/install/templates/bin_seams.tt +107 -0
  276. data/lib/generators/seams/install/templates/ci.yml.tt +123 -0
  277. data/lib/generators/seams/install/templates/deploy.yml.tt +63 -0
  278. data/lib/generators/seams/install/templates/doc/ARCHITECTURE.md.tt +86 -0
  279. data/lib/generators/seams/install/templates/docker-entrypoint.tt +27 -0
  280. data/lib/generators/seams/install/templates/rubocop.yml.tt +33 -0
  281. data/lib/generators/seams/install/templates/ruby-version.tt +1 -0
  282. data/lib/generators/seams/install/templates/script/collate_coverage.rb.tt +33 -0
  283. data/lib/generators/seams/install/templates/script/run_affected_tests.sh.tt +64 -0
  284. data/lib/generators/seams/install/templates/seams.rake.tt +65 -0
  285. data/lib/generators/seams/install/templates/seams.rb.tt +9 -0
  286. data/lib/generators/seams/install/templates/seams_engines.rb.tt +15 -0
  287. data/lib/generators/seams/notifications/notifications_generator.rb +395 -0
  288. data/lib/generators/seams/notifications/templates/README.md.tt +269 -0
  289. data/lib/generators/seams/notifications/templates/app/channels/notification_channel.rb.tt +36 -0
  290. data/lib/generators/seams/notifications/templates/app/controllers/notifications_controller.rb.tt +58 -0
  291. data/lib/generators/seams/notifications/templates/app/controllers/preferences_controller.rb.tt +54 -0
  292. data/lib/generators/seams/notifications/templates/app/javascript/controllers/notification_bell_controller.js.tt +34 -0
  293. data/lib/generators/seams/notifications/templates/app/jobs/application_job.rb.tt +6 -0
  294. data/lib/generators/seams/notifications/templates/app/jobs/create_notification_job.rb.tt +31 -0
  295. data/lib/generators/seams/notifications/templates/app/jobs/send_due_notifications_job.rb.tt +22 -0
  296. data/lib/generators/seams/notifications/templates/app/jobs/send_notification_job.rb.tt +13 -0
  297. data/lib/generators/seams/notifications/templates/app/mailers/application_mailer.rb.tt +12 -0
  298. data/lib/generators/seams/notifications/templates/app/mailers/notification_mailer.rb.tt +23 -0
  299. data/lib/generators/seams/notifications/templates/app/models/application_record.rb.tt +7 -0
  300. data/lib/generators/seams/notifications/templates/app/models/delivery.rb.tt +13 -0
  301. data/lib/generators/seams/notifications/templates/app/models/notification.rb.tt +218 -0
  302. data/lib/generators/seams/notifications/templates/app/models/notification_preference.rb.tt +29 -0
  303. data/lib/generators/seams/notifications/templates/app/models/strategies/email.rb.tt +38 -0
  304. data/lib/generators/seams/notifications/templates/app/models/strategies/in_app.rb.tt +26 -0
  305. data/lib/generators/seams/notifications/templates/app/models/strategies/sms.rb.tt +33 -0
  306. data/lib/generators/seams/notifications/templates/app/subscribers/auth_subscriber.rb.tt +71 -0
  307. data/lib/generators/seams/notifications/templates/app/subscribers/billing_subscriber.rb.tt +127 -0
  308. data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.html.erb.tt +22 -0
  309. data/lib/generators/seams/notifications/templates/app/views/layouts/notifications/mailer.text.erb.tt +4 -0
  310. data/lib/generators/seams/notifications/templates/app/views/notifications/_bell.html.erb.tt +15 -0
  311. data/lib/generators/seams/notifications/templates/app/views/notifications/index.html.erb.tt +15 -0
  312. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.html.erb.tt +4 -0
  313. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_failed.text.erb.tt +4 -0
  314. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.html.erb.tt +3 -0
  315. data/lib/generators/seams/notifications/templates/app/views/templates/billing/invoice_paid.text.erb.tt +3 -0
  316. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.html.erb.tt +5 -0
  317. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_granted.text.erb.tt +5 -0
  318. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.html.erb.tt +5 -0
  319. data/lib/generators/seams/notifications/templates/app/views/templates/billing/lifetime_purchased.text.erb.tt +5 -0
  320. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.html.erb.tt +4 -0
  321. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_canceled.text.erb.tt +4 -0
  322. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.html.erb.tt +4 -0
  323. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_started.text.erb.tt +5 -0
  324. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.html.erb.tt +3 -0
  325. data/lib/generators/seams/notifications/templates/app/views/templates/billing/subscription_updated.text.erb.tt +3 -0
  326. data/lib/generators/seams/notifications/templates/app/views/templates/default.html.erb.tt +10 -0
  327. data/lib/generators/seams/notifications/templates/app/views/templates/default.text.erb.tt +11 -0
  328. data/lib/generators/seams/notifications/templates/app/views/templates/welcome.html.erb.tt +6 -0
  329. data/lib/generators/seams/notifications/templates/app/views/templates/welcome.text.erb.tt +6 -0
  330. data/lib/generators/seams/notifications/templates/config/initializers/notifications.rb.tt +58 -0
  331. data/lib/generators/seams/notifications/templates/config/routes.rb.tt +17 -0
  332. data/lib/generators/seams/notifications/templates/db/migrate/create_notification_deliveries.rb.tt +16 -0
  333. data/lib/generators/seams/notifications/templates/db/migrate/create_notification_preferences.rb.tt +25 -0
  334. data/lib/generators/seams/notifications/templates/db/migrate/create_notifications.rb.tt +35 -0
  335. data/lib/generators/seams/notifications/templates/lib/adapters/abstract.rb.tt +20 -0
  336. data/lib/generators/seams/notifications/templates/lib/adapters/action_mailer.rb.tt +17 -0
  337. data/lib/generators/seams/notifications/templates/lib/adapters/null_sms.rb.tt +23 -0
  338. data/lib/generators/seams/notifications/templates/lib/concerns/notifiable.rb.tt +135 -0
  339. data/lib/generators/seams/notifications/templates/lib/configuration.rb.tt +24 -0
  340. data/lib/generators/seams/notifications/templates/lib/engine.rb.tt +35 -0
  341. data/lib/generators/seams/notifications/templates/lib/notifications.rb.tt +75 -0
  342. data/lib/generators/seams/notifications/templates/lib/type_registry.rb.tt +74 -0
  343. data/lib/generators/seams/notifications/templates/spec/factories/notifications.rb.tt +53 -0
  344. data/lib/generators/seams/notifications/templates/spec/models/delivery_spec.rb.tt +28 -0
  345. data/lib/generators/seams/notifications/templates/spec/models/notification_preference_spec.rb.tt +46 -0
  346. data/lib/generators/seams/notifications/templates/spec/models/notification_spec.rb.tt +60 -0
  347. data/lib/generators/seams/notifications/templates/spec/runtime/bell_broadcast_spec.rb.tt +59 -0
  348. data/lib/generators/seams/notifications/templates/spec/runtime/billing_subscriber_skip_spec.rb.tt +87 -0
  349. data/lib/generators/seams/notifications/templates/spec/runtime/boot_spec.rb.tt +39 -0
  350. data/lib/generators/seams/notifications/templates/spec/runtime/schedule_round_trip_spec.rb.tt +55 -0
  351. data/lib/generators/seams/remove/remove_generator.rb +259 -0
  352. data/lib/generators/seams/teams/teams_generator.rb +298 -0
  353. data/lib/generators/seams/teams/templates/README.md.tt +88 -0
  354. data/lib/generators/seams/teams/templates/app/controllers/invitations_controller.rb.tt +102 -0
  355. data/lib/generators/seams/teams/templates/app/controllers/memberships_controller.rb.tt +54 -0
  356. data/lib/generators/seams/teams/templates/app/controllers/teams_controller.rb.tt +68 -0
  357. data/lib/generators/seams/teams/templates/app/jobs/application_job.rb.tt +6 -0
  358. data/lib/generators/seams/teams/templates/app/mailers/invitation_mailer.rb.tt +34 -0
  359. data/lib/generators/seams/teams/templates/app/models/application_record.rb.tt +7 -0
  360. data/lib/generators/seams/teams/templates/app/models/current.rb.tt +30 -0
  361. data/lib/generators/seams/teams/templates/app/models/invitation.rb.tt +36 -0
  362. data/lib/generators/seams/teams/templates/app/models/membership.rb.tt +36 -0
  363. data/lib/generators/seams/teams/templates/app/models/team.rb.tt +32 -0
  364. data/lib/generators/seams/teams/templates/app/subscribers/invitation_subscriber.rb.tt +36 -0
  365. data/lib/generators/seams/teams/templates/app/views/invitation_mailer/invite.text.erb.tt +8 -0
  366. data/lib/generators/seams/teams/templates/app/views/invitations/index.html.erb.tt +44 -0
  367. data/lib/generators/seams/teams/templates/app/views/memberships/index.html.erb.tt +32 -0
  368. data/lib/generators/seams/teams/templates/app/views/teams/edit.html.erb.tt +28 -0
  369. data/lib/generators/seams/teams/templates/app/views/teams/index.html.erb.tt +15 -0
  370. data/lib/generators/seams/teams/templates/app/views/teams/new.html.erb.tt +24 -0
  371. data/lib/generators/seams/teams/templates/app/views/teams/show.html.erb.tt +17 -0
  372. data/lib/generators/seams/teams/templates/config/routes.rb.tt +19 -0
  373. data/lib/generators/seams/teams/templates/db/migrate/create_team_invitations.rb.tt +24 -0
  374. data/lib/generators/seams/teams/templates/db/migrate/create_team_memberships.rb.tt +25 -0
  375. data/lib/generators/seams/teams/templates/db/migrate/create_teams.rb.tt +18 -0
  376. data/lib/generators/seams/teams/templates/lib/concerns/account_scoped.rb.tt +79 -0
  377. data/lib/generators/seams/teams/templates/lib/concerns/authorization.rb.tt +55 -0
  378. data/lib/generators/seams/teams/templates/lib/configuration.rb.tt +45 -0
  379. data/lib/generators/seams/teams/templates/lib/engine.rb.tt +51 -0
  380. data/lib/generators/seams/teams/templates/lib/teams.rb.tt +22 -0
  381. data/lib/generators/seams/teams/templates/spec/factories/teams.rb.tt +47 -0
  382. data/lib/generators/seams/teams/templates/spec/models/invitation_spec.rb.tt +25 -0
  383. data/lib/generators/seams/teams/templates/spec/models/membership_spec.rb.tt +29 -0
  384. data/lib/generators/seams/teams/templates/spec/models/team_spec.rb.tt +23 -0
  385. data/lib/generators/seams/teams/templates/spec/runtime/boot_spec.rb.tt +32 -0
  386. data/lib/seams/cli/list.rb +111 -0
  387. data/lib/seams/cli/quality.rb +99 -0
  388. data/lib/seams/cli/resolve.rb +276 -0
  389. data/lib/seams/cli/test_changed.rb +116 -0
  390. data/lib/seams/cli.rb +48 -0
  391. data/lib/seams/configuration.rb +19 -0
  392. data/lib/seams/cops/known_queue_names.rb +42 -0
  393. data/lib/seams/cops/migration_comments.rb +68 -0
  394. data/lib/seams/cops/no_cross_engine_dependency.rb +58 -0
  395. data/lib/seams/cops/no_cross_engine_model_access.rb +153 -0
  396. data/lib/seams/cops.rb +18 -0
  397. data/lib/seams/event_registry.rb +49 -0
  398. data/lib/seams/events/adapter.rb +24 -0
  399. data/lib/seams/events/adapters/active_support.rb +31 -0
  400. data/lib/seams/events/publisher.rb +178 -0
  401. data/lib/seams/events.rb +39 -0
  402. data/lib/seams/generators/dummy_app_writer.rb +424 -0
  403. data/lib/seams/generators/eject_aware.rb +102 -0
  404. data/lib/seams/generators/follow_up_generator.rb +148 -0
  405. data/lib/seams/generators/host_injector.rb +124 -0
  406. data/lib/seams/generators/sibling_rubocop_writer.rb +77 -0
  407. data/lib/seams/generators/splicer.rb +217 -0
  408. data/lib/seams/observability/adapter.rb +33 -0
  409. data/lib/seams/observability/adapters/rails_logger.rb +59 -0
  410. data/lib/seams/observability.rb +34 -0
  411. data/lib/seams/runtime.rb +23 -0
  412. data/lib/seams/version.rb +5 -0
  413. data/lib/seams.rb +23 -0
  414. metadata +493 -0
@@ -0,0 +1,276 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "seams"
4
+ require "seams/generators/splicer"
5
+
6
+ module Seams
7
+ module CLI
8
+ # Implementation behind `bin/seams resolve` — gap-report 1.2 from
9
+ # the 2026-05 framework feature-gap survey: the documented escape
10
+ # hatch from seams' generators.
11
+ #
12
+ # Three modes:
13
+ #
14
+ # bin/seams resolve --eject <engine>/<file>
15
+ #
16
+ # Marks a single host file as host-owned. The next regeneration
17
+ # of the engine skips this file. The file already lives in the
18
+ # host's working tree (seams generates "the code is in your
19
+ # repo") — eject just prepends an explicit ownership header
20
+ # and tells the engine generator to leave it alone.
21
+ #
22
+ # bin/seams resolve --list-markers <engine>
23
+ #
24
+ # Lists every `# seams:insertion-point ...` marker the engine
25
+ # ships across all of its templated files. Helps the host
26
+ # operator see which extension points are public contract
27
+ # before writing a follow-up generator.
28
+ #
29
+ # bin/seams resolve --list-ejected
30
+ #
31
+ # Surveys engines/ for files marked with the eject header and
32
+ # lists them. Useful for "what's diverged from the gem".
33
+ #
34
+ # Returns true on success / false on failure. The caller (the
35
+ # `bin/seams` shim) translates that into a non-zero exit code.
36
+ #
37
+ # Several methods here legitimately return true/false to signal
38
+ # success/failure but are command verbs (`run_eject`,
39
+ # `engine_present?` is fine, `fail_with` etc). Rubocop's
40
+ # PredicateMethod cop wants every bool-returning method renamed
41
+ # with a trailing `?`, but that's wrong for the run_* dispatchers
42
+ # (they're imperative, not predicates). AbcSize / CyclomaticComplexity
43
+ # likewise trigger on the run_* methods because CLI command
44
+ # branches are inherently branchy. The cops are disabled at file
45
+ # scope and the methods are kept linear and well-commented.
46
+ # rubocop:disable Naming/PredicateMethod, Metrics/AbcSize, Metrics/CyclomaticComplexity
47
+ class Resolve
48
+ DEFAULT_ENGINES_ROOT = "engines"
49
+
50
+ # Header injected at the top of every ejected file. Position
51
+ # matters: future regenerations check the FIRST line of an
52
+ # existing destination file for this exact prefix.
53
+ EJECT_HEADER_PREFIX = "# seams:ejected from"
54
+ EJECT_HEADER_LINES = lambda do |from|
55
+ <<~HEADER
56
+ #{EJECT_HEADER_PREFIX} #{from}
57
+ # Re-running `bin/rails generate seams:#{from.split(".").first}` will NOT overwrite this file.
58
+ # To return to the gem version: delete this file and re-run the generator.
59
+ HEADER
60
+ end
61
+
62
+ # Files at this list of relative paths under engines/<engine>/
63
+ # are NOT eject-eligible. See doc note in EjectAware module.
64
+ INELIGIBLE_RELATIVE_PATTERNS = [
65
+ %r{\Adb/migrate/}, # one-shot, host runs them
66
+ %r{\Alib/[^/]+/engine\.rb\z}, # framework-managed boot file
67
+ %r{\Alib/[^/]+/version\.rb\z}, # framework-managed version constant
68
+ /\AGemfile\z/, # engine's own Gemfile
69
+ %r{\A[^/]+\.gemspec\z}, # engine's gemspec
70
+ /\ARakefile\z/ # engine's Rakefile (loads engine tasks)
71
+ ].freeze
72
+
73
+ def initialize(mode:, argument: nil, engines_root: DEFAULT_ENGINES_ROOT, output: $stdout, error: $stderr)
74
+ @mode = mode
75
+ @argument = argument
76
+ @engines_root = engines_root
77
+ @output = output
78
+ @error = error
79
+ end
80
+
81
+ def call
82
+ case @mode
83
+ when :eject then run_eject
84
+ when :list_markers then run_list_markers
85
+ when :list_ejected then run_list_ejected
86
+ else
87
+ fail_with("unknown mode: #{@mode.inspect}")
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ # ---- Mode 1: --eject <engine>/<file_relative> ----
94
+
95
+ def run_eject
96
+ return false unless argument_present?(usage: "bin/seams resolve --eject <engine>/<file>")
97
+
98
+ engine, relative = split_eject_argument(@argument)
99
+ return false unless engine && relative
100
+
101
+ return false unless engine_present?(engine)
102
+ return false unless eject_eligible?(engine, relative)
103
+
104
+ full_path = File.join(@engines_root, engine, relative)
105
+ return fail_with("file not found: #{full_path}") unless File.exist?(full_path)
106
+
107
+ contents = File.read(full_path)
108
+ if contents.start_with?(EJECT_HEADER_PREFIX)
109
+ @output.puts("already ejected: #{full_path}")
110
+ return true
111
+ end
112
+
113
+ from = "#{engine}.#{relative}"
114
+ File.write(full_path, EJECT_HEADER_LINES.call(from) + contents)
115
+ line_count = File.read(full_path).each_line.count
116
+ @output.puts("ejected: #{full_path} (lines: #{line_count}; from: #{from})")
117
+ true
118
+ end
119
+
120
+ def split_eject_argument(argument)
121
+ # Argument shape: "<engine>/<file/path>". The first segment is
122
+ # the engine; everything after the first slash is the relative
123
+ # path within the engine root. We deliberately match on the
124
+ # FIRST slash so paths like "auth/app/mailers/auth/foo.rb"
125
+ # round-trip correctly.
126
+ if argument.include?("/")
127
+ engine, relative = argument.split("/", 2)
128
+ return [engine, relative] if engine && !engine.empty? && relative && !relative.empty?
129
+ end
130
+
131
+ fail_with("expected '<engine>/<file>', got #{argument.inspect}")
132
+ [nil, nil]
133
+ end
134
+
135
+ def eject_eligible?(engine, relative)
136
+ return true unless INELIGIBLE_RELATIVE_PATTERNS.any? { |pattern| pattern.match?(relative) }
137
+
138
+ fail_with(
139
+ "refusing to eject #{engine}/#{relative}: this file is framework-managed " \
140
+ "(migrations, engine.rb, version.rb, Gemfile, .gemspec) and is not eject-eligible. " \
141
+ "See doc/INSERTION_POINTS.md and Seams::Generators::EjectAware for the rule."
142
+ )
143
+ false
144
+ end
145
+
146
+ # ---- Mode 2: --list-markers <engine> ----
147
+
148
+ def run_list_markers
149
+ return false unless argument_present?(usage: "bin/seams resolve --list-markers <engine>")
150
+ return false unless engine_present?(@argument)
151
+
152
+ engine_root = File.join(@engines_root, @argument)
153
+ markers = collect_markers(engine_root)
154
+
155
+ if markers.empty?
156
+ @output.puts("#{@argument}: no insertion-point markers found in #{engine_root}/")
157
+ @output.puts(" This engine may not have been retrofitted to Wave 10. " \
158
+ "Re-run `bin/rails generate seams:#{@argument}` to pick up the marker set.")
159
+ return true
160
+ end
161
+
162
+ print_marker_table(markers)
163
+ true
164
+ end
165
+
166
+ def collect_markers(engine_root)
167
+ rb_files = Dir.glob(File.join(engine_root, "**", "*.rb"))
168
+ rb_files.flat_map do |path|
169
+ Seams::Generators::Splicer.list_markers(file_path: path).map do |info|
170
+ relative = path.sub(%r{\A#{Regexp.escape(engine_root)}/}, "")
171
+ description = description_for(path, info[:line_number])
172
+ info.merge(file: relative, description: description)
173
+ end
174
+ end
175
+ end
176
+
177
+ # Best-effort one-line description: read the comment immediately
178
+ # PRECEDING the marker (one or two lines back) — the catalogue
179
+ # convention is to document the marker's purpose in a sibling
180
+ # comment line. Falls back to an empty string if no such comment
181
+ # exists; the table prints "(no description)" in that case.
182
+ def description_for(file_path, marker_line_number)
183
+ return "" unless File.exist?(file_path)
184
+
185
+ lines = File.readlines(file_path)
186
+ # marker_line_number is 1-indexed; the description line, if any,
187
+ # sits immediately above. Guard against marker_line_number == 1
188
+ # explicitly — Ruby's negative array index would otherwise wrap
189
+ # to the LAST line of the file, which is nonsensical here.
190
+ return "" if marker_line_number <= 1
191
+
192
+ candidate = lines[marker_line_number - 2]
193
+ return "" unless candidate
194
+
195
+ stripped = candidate.strip
196
+ return "" unless stripped.start_with?("#")
197
+ return "" if stripped.start_with?("# seams:insertion-point")
198
+
199
+ stripped.sub(/\A#\s?/, "").strip
200
+ end
201
+
202
+ def print_marker_table(markers)
203
+ marker_width = markers.map { |m| m[:marker].length }.max
204
+ location_width = markers.map { |m| "#{m[:file]}:#{m[:line_number]}".length }.max
205
+
206
+ markers.each do |info|
207
+ location = "#{info[:file]}:#{info[:line_number]}"
208
+ description = info[:description].empty? ? "(no description)" : %("#{info[:description]}")
209
+ @output.puts("#{info[:marker].ljust(marker_width)} #{location.ljust(location_width)} #{description}")
210
+ end
211
+ end
212
+
213
+ # ---- Mode 3: --list-ejected ----
214
+
215
+ def run_list_ejected
216
+ unless Dir.exist?(@engines_root)
217
+ @output.puts("no engines directory at #{@engines_root}/")
218
+ return true
219
+ end
220
+
221
+ ejected = collect_ejected_files
222
+ if ejected.empty?
223
+ @output.puts("no ejected files in #{@engines_root}/")
224
+ return true
225
+ end
226
+
227
+ @output.puts("seams: #{ejected.size} ejected file(s)")
228
+ ejected.each { |path, source| @output.puts(" #{path} (from: #{source})") }
229
+ true
230
+ end
231
+
232
+ def collect_ejected_files
233
+ # Cheap two-pass scan: read first 200 bytes of every text file
234
+ # under engines/, look for the prefix. We deliberately skip
235
+ # binaries (anything that isn't .rb / .erb / .yml / .yaml / .rake / .css / .js)
236
+ # because the eject header is always a `#`-comment and only
237
+ # text-ish files carry it.
238
+ text_extensions = %w[.rb .erb .yml .yaml .rake .css .js .txt .md].freeze
239
+ Dir.glob(File.join(@engines_root, "**", "*"))
240
+ .select { |path| File.file?(path) && text_extensions.include?(File.extname(path)) }
241
+ .sort
242
+ .filter_map do |path|
243
+ head = File.read(path, 200)
244
+ next nil unless head.start_with?(EJECT_HEADER_PREFIX)
245
+
246
+ source = head.lines.first.to_s.sub(EJECT_HEADER_PREFIX, "").strip
247
+ [path, source]
248
+ end
249
+ end
250
+
251
+ # ---- Shared helpers ----
252
+
253
+ def argument_present?(usage:)
254
+ return true if @argument && !@argument.empty?
255
+
256
+ fail_with("missing argument. Usage: #{usage}")
257
+ false
258
+ end
259
+
260
+ def engine_present?(engine)
261
+ engine_root = File.join(@engines_root, engine)
262
+ return true if File.directory?(engine_root)
263
+
264
+ fail_with("engine #{engine.inspect} not found at #{engine_root}/. " \
265
+ "Run `bin/rails generate seams:#{engine}` first.")
266
+ false
267
+ end
268
+
269
+ def fail_with(message)
270
+ @error.puts("seams resolve: #{message}")
271
+ false
272
+ end
273
+ end
274
+ # rubocop:enable Naming/PredicateMethod, Metrics/AbcSize, Metrics/CyclomaticComplexity
275
+ end
276
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "seams"
4
+
5
+ module Seams
6
+ module CLI
7
+ # Runs RSpec for every engine that has changed against the merge
8
+ # base with `main` (or another base via the `base:` keyword).
9
+ # Falls back to "run every engine's specs" when the merge base
10
+ # cannot be resolved (CI on a shallow clone, no git, etc) — better
11
+ # to over-run than to silently skip.
12
+ #
13
+ # bin/rails seams:test:changed # base = main
14
+ # bin/rails seams:test:changed BASE=develop
15
+ #
16
+ # Returns true when every engine spec run passed; false otherwise.
17
+ # The caller (rake task or bin/seams) translates that to an exit
18
+ # code.
19
+ class TestChanged
20
+ DEFAULT_BASE = "main"
21
+ DEFAULT_ENGINES_ROOT = "engines"
22
+
23
+ def initialize(base: DEFAULT_BASE, engines_root: DEFAULT_ENGINES_ROOT, output: $stdout)
24
+ @base = base
25
+ @engines_root = engines_root
26
+ @output = output
27
+ end
28
+
29
+ def call
30
+ engines = changed_engines
31
+ if engines.empty?
32
+ @output.puts("seams:test:changed — no engines changed since #{@base}; skipping.")
33
+ return true
34
+ end
35
+
36
+ @output.puts("seams:test:changed — running specs for #{engines.size} engine(s):")
37
+ engines.each { |name| @output.puts(" - #{name}") }
38
+ @output.puts("")
39
+
40
+ failed = engines.reject { |name| run_engine_specs(name) }
41
+ if failed.empty?
42
+ @output.puts("All affected engine specs passed.")
43
+ true
44
+ else
45
+ @output.puts("Failed engines: #{failed.join(", ")}")
46
+ false
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def changed_engines
53
+ return all_engines unless git_available?
54
+
55
+ merge_base = resolve_merge_base
56
+ return all_engines if merge_base.empty?
57
+
58
+ engine_names_from_diff(merge_base)
59
+ end
60
+
61
+ # Array-form shell-out: no interpolation into a shell, so a
62
+ # @base value like `; rm -rf /` passes as one argv element to
63
+ # git rather than getting evaluated. shell_escape is defence in
64
+ # depth on top of that.
65
+ def resolve_merge_base
66
+ capture_command(["git", "merge-base", shell_escape(@base), "HEAD"]).strip
67
+ end
68
+
69
+ def engine_names_from_diff(merge_base)
70
+ diff = capture_command(["git", "diff", "--name-only", merge_base, "HEAD", "--",
71
+ "#{@engines_root}/*"])
72
+ diff.lines
73
+ .filter_map { |line| line.split("/")[1] }
74
+ .uniq
75
+ .sort
76
+ .select { |name| File.directory?(File.join(@engines_root, name)) }
77
+ end
78
+
79
+ def capture_command(argv)
80
+ IO.popen(argv, err: File::NULL, &:read)
81
+ rescue StandardError
82
+ ""
83
+ end
84
+
85
+ def all_engines
86
+ return [] unless Dir.exist?(@engines_root)
87
+
88
+ Dir.children(@engines_root)
89
+ .select { |child| File.directory?(File.join(@engines_root, child)) }
90
+ .reject { |child| child.start_with?(".") }
91
+ .sort
92
+ end
93
+
94
+ def run_engine_specs(name)
95
+ spec_dir = File.join(@engines_root, name, "spec")
96
+ return true if Dir.glob(File.join(spec_dir, "**", "*_spec.rb")).empty?
97
+
98
+ @output.puts("=== bundle exec rspec #{spec_dir} ===")
99
+ system("bundle", "exec", "rspec", spec_dir)
100
+ end
101
+
102
+ def git_available?
103
+ system("which git > /dev/null 2>&1")
104
+ end
105
+
106
+ # The base branch comes from a keyword arg or env var — both
107
+ # untrusted. Allow only branch-safe characters before
108
+ # interpolating into a shell-out (we never reach the shell
109
+ # because we shell-escape, but defence in depth keeps the
110
+ # intent explicit).
111
+ def shell_escape(value)
112
+ value.to_s.gsub(%r{[^A-Za-z0-9_\-/.]}, "")
113
+ end
114
+ end
115
+ end
116
+ end
data/lib/seams/cli.rb ADDED
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "seams"
4
+ require "seams/cli/list"
5
+ require "seams/cli/test_changed"
6
+ require "seams/cli/quality"
7
+ require "seams/cli/resolve"
8
+
9
+ module Seams
10
+ # Top-level CLI aggregator. Each public method delegates to a
11
+ # single-purpose CLI class so the rake tasks (and bin/seams) have
12
+ # one entry point. Returns true on success, false on failure —
13
+ # callers translate that into a process exit code.
14
+ #
15
+ # Seams::CLI.list # bin/rails seams:list
16
+ # Seams::CLI.test_changed(base: "main") # seams:test:changed
17
+ # Seams::CLI.quality # seams:quality:all
18
+ # Seams::CLI.resolve(mode: :eject, ...) # bin/seams resolve --eject ...
19
+ module CLI
20
+ module_function
21
+
22
+ def list(engines_root: "engines", output: $stdout)
23
+ Seams::CLI::List.new(engines_root: engines_root, output: output).call
24
+ end
25
+
26
+ def test_changed(base: "main", engines_root: "engines", output: $stdout)
27
+ Seams::CLI::TestChanged.new(
28
+ base: base,
29
+ engines_root: engines_root,
30
+ output: output
31
+ ).call
32
+ end
33
+
34
+ def quality(engines_root: "engines", output: $stdout)
35
+ Seams::CLI::Quality.new(engines_root: engines_root, output: output).call
36
+ end
37
+
38
+ def resolve(mode:, argument: nil, engines_root: "engines", output: $stdout, error: $stderr)
39
+ Seams::CLI::Resolve.new(
40
+ mode: mode,
41
+ argument: argument,
42
+ engines_root: engines_root,
43
+ output: output,
44
+ error: error
45
+ ).call
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Seams
4
+ # Global Seams configuration. Set via Seams.configure { |c| ... } in
5
+ # config/initializers/seams.rb of the host application.
6
+ class Configuration
7
+ attr_accessor :event_bus_adapter,
8
+ :observability_adapter,
9
+ :event_namespace_separator,
10
+ :host_app_name
11
+
12
+ def initialize
13
+ @event_bus_adapter = "Seams::Events::Adapters::ActiveSupport"
14
+ @observability_adapter = "Seams::Observability::Adapters::RailsLogger"
15
+ @event_namespace_separator = "."
16
+ @host_app_name = nil
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Seams
8
+ # Ensures every `queue_as` call uses a queue name that has been
9
+ # registered in the host application's `.rubocop.yml`. Catches typos
10
+ # and prevents jobs from being silently routed to a queue that no
11
+ # worker is listening on.
12
+ class KnownQueueNames < Base
13
+ MSG = "Queue `%<name>s` is not registered. Add it to .rubocop.yml " \
14
+ "under Seams/KnownQueueNames#KnownQueues, or pick one of: %<known>s."
15
+
16
+ # @!method queue_as_literal?(node)
17
+ def_node_matcher :queue_as_literal?, <<~PATTERN
18
+ (send nil? :queue_as ${sym str})
19
+ PATTERN
20
+
21
+ def on_send(node)
22
+ literal = queue_as_literal?(node)
23
+ return unless literal
24
+
25
+ name = literal.value.to_s
26
+ return if known_queues.include?(name)
27
+
28
+ add_offense(
29
+ node,
30
+ message: format(MSG, name: name, known: known_queues.join(", "))
31
+ )
32
+ end
33
+
34
+ private
35
+
36
+ def known_queues
37
+ Array(cop_config["KnownQueues"]).map(&:to_s)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Seams
8
+ # Requires every migration to start with a leading comment block
9
+ # so that future readers know what the migration does, why it was
10
+ # needed, and what its data/downtime implications are.
11
+ class MigrationComments < Base
12
+ MSG = "Migration `%<name>s` must be preceded by a comment block explaining " \
13
+ "what changes and why (data implications, downtime risk, rollback notes)."
14
+
15
+ # Comments that look like directives, not documentation. We strip these
16
+ # out before deciding whether the migration carries a real doc block.
17
+ #
18
+ # The Parser associator already drops the encoding/shebang/frozen_string_literal
19
+ # family of magic comments (skip_directives), so this regex only has to
20
+ # cover the directives Parser leaves attached: Sorbet sigils, RuboCop
21
+ # disable/enable, shareable_constant_value, warn_indent.
22
+ MAGIC_COMMENT = /
23
+ \A\s*\#\s*
24
+ (?:
25
+ frozen_string_literal
26
+ | encoding
27
+ | warn_indent
28
+ | shareable_constant_value
29
+ | typed
30
+ | rubocop:(?:disable|enable|todo)
31
+ )
32
+ /x
33
+
34
+ # @!method migration_class?(node)
35
+ def_node_matcher :migration_class?, <<~PATTERN
36
+ (class (const nil? $_) (send (const (const _ :ActiveRecord) :Migration) :[] _) ...)
37
+ PATTERN
38
+
39
+ def on_class(node)
40
+ name = migration_class?(node)
41
+ return unless name
42
+ return if leading_comment?(node)
43
+
44
+ add_offense(node, message: format(MSG, name: name))
45
+ end
46
+
47
+ private
48
+
49
+ # True if the migration class is preceded by at least one comment
50
+ # that the parser associates with this specific class node and that
51
+ # isn't a magic comment / directive.
52
+ #
53
+ # We rely on `ast_with_comments` (Parser::Source::Comment.associate_by_identity)
54
+ # rather than line-based scanning so that comments which actually
55
+ # belong to a sibling class or method above the migration are not
56
+ # misread as documentation for the migration.
57
+ def leading_comment?(node)
58
+ documenting_comments_for(node).any?
59
+ end
60
+
61
+ def documenting_comments_for(node)
62
+ comments = processed_source.ast_with_comments&.fetch(node, nil) || []
63
+ comments.reject { |comment| MAGIC_COMMENT.match?(comment.text) }
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubocop"
4
+
5
+ module RuboCop
6
+ module Cop
7
+ module Seams
8
+ # Flags `require`/`require_relative` calls that pull source files
9
+ # out of another engine. Engines should depend on each other only
10
+ # through public events (Seams::Events::Publisher) and
11
+ # explicitly-exposed concerns — not by requiring private files.
12
+ class NoCrossEngineDependency < Base
13
+ MSG = "Engine `%<own>s` must not require `%<path>s` from another engine. " \
14
+ "Communicate via events or via `%<other>s`'s exposed concerns."
15
+
16
+ # @!method require_call?(node)
17
+ def_node_matcher :require_call?, <<~PATTERN
18
+ (send nil? {:require :require_relative} (str $_))
19
+ PATTERN
20
+
21
+ def on_send(node)
22
+ path = require_call?(node)
23
+ return unless path
24
+
25
+ offending_engine = other_engine_for(path)
26
+ return unless offending_engine
27
+
28
+ add_offense(
29
+ node,
30
+ message: format(MSG, own: own_engine, path: path,
31
+ other: capitalize(offending_engine))
32
+ )
33
+ end
34
+
35
+ private
36
+
37
+ def own_engine
38
+ cop_config["OwnEngine"].to_s
39
+ end
40
+
41
+ def other_engines
42
+ Array(cop_config["OtherEngines"]).map(&:to_s)
43
+ end
44
+
45
+ def other_engine_for(path)
46
+ # Match either "billing/foo" or "../../billing/foo" — the engine
47
+ # name is whichever directory segment matches another engine.
48
+ segments = path.split("/")
49
+ other_engines.find { |engine| segments.include?(engine) }
50
+ end
51
+
52
+ def capitalize(name)
53
+ name.split(/[_-]/).map(&:capitalize).join
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end