domma-cms 0.55.1 → 0.69.3

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 (305) hide show
  1. package/CLAUDE.md +194 -3
  2. package/README.md +18 -14
  3. package/admin/css/admin.css +1 -1
  4. package/admin/css/contacts.css +1 -0
  5. package/admin/dist/domma/domma-tools.css +3 -3
  6. package/admin/dist/domma/domma-tools.min.js +3 -3
  7. package/admin/index.html +1 -0
  8. package/admin/js/api.js +1 -1
  9. package/admin/js/app.js +4 -4
  10. package/admin/js/lib/analytics-shape.js +1 -0
  11. package/admin/js/lib/contacts-arrange.js +1 -0
  12. package/admin/js/lib/help-popover.js +1 -0
  13. package/admin/js/lib/notes-arrange.js +1 -0
  14. package/admin/js/lib/page-picker.js +1 -0
  15. package/admin/js/lib/plugin-accent.js +1 -0
  16. package/admin/js/lib/plugin-chrome.js +1 -0
  17. package/admin/js/lib/shortcode-context-menu.js +2 -2
  18. package/admin/js/lib/sidebar-grouping.js +1 -1
  19. package/admin/js/lib/sidebar-grouping.test.js +1 -1
  20. package/admin/js/lib/sidebar-renderer.js +4 -4
  21. package/admin/js/lib/slideover-resizable.js +1 -0
  22. package/admin/js/lib/todo-arrange.js +1 -0
  23. package/admin/js/lib/tool-kit.js +1 -0
  24. package/admin/js/templates/analytics.html +138 -0
  25. package/admin/js/templates/contacts.html +156 -0
  26. package/admin/js/templates/context-menu-editor.html +212 -0
  27. package/admin/js/templates/context-menus.html +16 -0
  28. package/admin/js/templates/notes.html +137 -0
  29. package/admin/js/templates/plugin-code.html +1 -1
  30. package/admin/js/templates/plugin-marketplace.html +17 -4
  31. package/admin/js/templates/plugins.html +13 -7
  32. package/{plugins/site-search/admin/templates/site-search.html → admin/js/templates/search.html} +50 -4
  33. package/admin/js/templates/settings.html +16 -97
  34. package/admin/js/templates/theme.html +211 -0
  35. package/admin/js/templates/todo.html +113 -0
  36. package/admin/js/views/analytics.js +62 -0
  37. package/admin/js/views/contacts.js +174 -0
  38. package/admin/js/views/context-menu-editor.js +55 -0
  39. package/admin/js/views/context-menus.js +5 -0
  40. package/admin/js/views/form-editor.js +7 -7
  41. package/admin/js/views/index.js +1 -1
  42. package/admin/js/views/notes.js +104 -0
  43. package/admin/js/views/plugin-marketplace.js +1 -1
  44. package/admin/js/views/plugins.js +28 -24
  45. package/admin/js/views/search.js +1 -0
  46. package/admin/js/views/settings.js +3 -3
  47. package/admin/js/views/theme.js +1 -0
  48. package/admin/js/views/todo.js +96 -0
  49. package/bin/cli.js +6 -10
  50. package/bin/lib/plugin-version.js +28 -0
  51. package/bin/update.js +27 -2
  52. package/config/menus/admin-sidebar.json +129 -23
  53. package/config/plugins.json +5 -5
  54. package/config/search.json +13 -0
  55. package/config/server.json +3 -1
  56. package/package.json +14 -10
  57. package/plugins/_lib/admin/mail/compose-window.js +914 -0
  58. package/plugins/_lib/admin/mail/contacts.js +301 -0
  59. package/plugins/_lib/admin/mail/diagnostics-section.js +133 -0
  60. package/plugins/_lib/admin/mail/folder-tree.js +254 -0
  61. package/plugins/_lib/admin/mail/identity.js +480 -0
  62. package/plugins/_lib/admin/mail/image-senders-section.js +131 -0
  63. package/plugins/_lib/admin/mail/keyboard.js +136 -0
  64. package/plugins/_lib/admin/mail/mail.css +1 -0
  65. package/plugins/_lib/admin/mail/mail.html +71 -0
  66. package/plugins/_lib/admin/mail/panes.js +253 -0
  67. package/plugins/_lib/admin/mail/reader-view.js +4473 -0
  68. package/plugins/_lib/admin/mail/resizable.js +26 -0
  69. package/plugins/_lib/admin/mail/rules.js +343 -0
  70. package/plugins/_lib/admin/mail/scheduling.js +203 -0
  71. package/plugins/_lib/admin/mail/section-kit.js +277 -0
  72. package/plugins/_lib/admin/mail/templates.js +238 -0
  73. package/plugins/_lib/admin/mail/threads.js +200 -0
  74. package/plugins/_lib/admin/mail/vacation.js +269 -0
  75. package/plugins/_lib/admin/ui/help.css +1 -0
  76. package/plugins/_lib/admin/ui/help.js +174 -0
  77. package/plugins/_lib/admin/ui/resizable.js +151 -0
  78. package/plugins/_lib/dataStore.js +117 -0
  79. package/plugins/_lib/mail/accounts.js +919 -0
  80. package/plugins/_lib/mail/bodyTokens.js +101 -0
  81. package/plugins/_lib/mail/compose.js +256 -0
  82. package/plugins/_lib/mail/defaults.js +57 -0
  83. package/plugins/_lib/mail/diagnostics.js +154 -0
  84. package/plugins/_lib/mail/envelope.js +161 -0
  85. package/plugins/_lib/mail/folders.js +192 -0
  86. package/plugins/_lib/mail/handoff.js +274 -0
  87. package/plugins/_lib/mail/imapPool.js +293 -0
  88. package/plugins/_lib/mail/mbox.js +74 -0
  89. package/plugins/_lib/mail/pollSchedule.js +79 -0
  90. package/plugins/_lib/mail/poller.js +135 -0
  91. package/plugins/_lib/mail/priority.js +138 -0
  92. package/plugins/_lib/mail/readRoutes.js +680 -0
  93. package/plugins/_lib/mail/render.js +291 -0
  94. package/plugins/_lib/mail/ruleRunner.js +152 -0
  95. package/plugins/_lib/mail/scheduler.js +254 -0
  96. package/plugins/_lib/mail/secretbox.js +229 -0
  97. package/plugins/_lib/mail/send.js +396 -0
  98. package/plugins/_lib/mail/store.js +1022 -0
  99. package/plugins/_lib/mail/sync.js +292 -0
  100. package/plugins/_lib/mail/syncPlan.js +126 -0
  101. package/plugins/_lib/mail/syncSelection.js +82 -0
  102. package/plugins/_lib/mail/unsubscribe.js +183 -0
  103. package/plugins/_lib/mail/vacationRunner.js +114 -0
  104. package/plugins/_lib/mail/write.js +473 -0
  105. package/plugins/_lib/schemaSync.js +83 -0
  106. package/plugins/_template/admin/css/index.css +0 -0
  107. package/plugins/_template/admin/templates/index.html +4 -4
  108. package/plugins/_template/admin/views/index.js +7 -0
  109. package/plugins/blog/CLAUDE.md +237 -0
  110. package/plugins/blog/admin/css/index.css +178 -0
  111. package/plugins/blog/admin/templates/blog.html +35 -41
  112. package/plugins/blog/admin/templates/categories.html +19 -6
  113. package/plugins/blog/admin/templates/comments.html +26 -9
  114. package/plugins/blog/admin/templates/post-editor.html +63 -40
  115. package/plugins/blog/admin/templates/settings.html +3 -9
  116. package/plugins/blog/admin/views/blog.js +288 -159
  117. package/plugins/blog/admin/views/categories.js +212 -206
  118. package/plugins/blog/admin/views/comments.js +207 -158
  119. package/plugins/blog/admin/views/kit.js +372 -0
  120. package/plugins/blog/admin/views/post-editor.js +306 -39
  121. package/plugins/blog/admin/views/settings-cog.js +56 -0
  122. package/plugins/blog/admin/views/settings.js +16 -84
  123. package/plugins/blog/blocks/blog-card-compact.css +17 -0
  124. package/plugins/blog/blocks/blog-card-compact.html +8 -0
  125. package/plugins/blog/blocks/blog-card-row.css +22 -0
  126. package/plugins/blog/blocks/blog-card-row.html +11 -0
  127. package/plugins/blog/blocks/blog-card.css +35 -0
  128. package/plugins/blog/blocks/blog-card.html +16 -0
  129. package/plugins/blog/blocks/blog-post-classic.css +15 -0
  130. package/plugins/blog/blocks/blog-post-classic.html +28 -0
  131. package/plugins/blog/blocks/blog-post-feature.css +20 -0
  132. package/plugins/blog/blocks/blog-post-feature.html +30 -0
  133. package/plugins/blog/blocks/blog-post-minimal.css +10 -0
  134. package/plugins/blog/blocks/blog-post-minimal.html +23 -0
  135. package/plugins/blog/blocks/blog-post-sidebar.css +22 -0
  136. package/plugins/blog/blocks/blog-post-sidebar.html +36 -0
  137. package/plugins/blog/collections/categories/schema.json +7 -6
  138. package/plugins/blog/collections/comments/schema.json +11 -10
  139. package/plugins/blog/collections/posts/schema.json +16 -13
  140. package/plugins/blog/config.js +6 -0
  141. package/plugins/blog/lib/layouts.js +304 -0
  142. package/plugins/blog/lib/page.js +158 -0
  143. package/plugins/blog/lib/render.js +119 -0
  144. package/plugins/blog/lib/samples.js +234 -0
  145. package/plugins/blog/plugin.js +259 -26
  146. package/plugins/blog/plugin.json +21 -73
  147. package/plugins/blog/plugin.public.js +217 -0
  148. package/plugins/blog/public/blog.css +177 -0
  149. package/plugins/blog/public/blog.js +586 -0
  150. package/plugins/blog/public/samples/aurora.svg +7 -0
  151. package/plugins/blog/public/samples/dusk.svg +7 -0
  152. package/plugins/blog/public/samples/ember.svg +7 -0
  153. package/plugins/blog/public/samples/harbour.svg +7 -0
  154. package/plugins/blog/public/samples/meadow.svg +7 -0
  155. package/plugins/blog/public/samples/rosewood.svg +7 -0
  156. package/plugins/blog/templates/index.html +9 -8
  157. package/plugins/blog/templates/post.html +4 -17
  158. package/plugins/blog/tests/layouts.test.js +97 -0
  159. package/plugins/blog/tests/public.test.js +71 -0
  160. package/plugins/free-tier.lock.json +74 -0
  161. package/plugins/mail-reader/CLAUDE.md +78 -0
  162. package/plugins/mail-reader/admin/views/mail.js +19 -0
  163. package/plugins/mail-reader/config.js +7 -0
  164. package/plugins/mail-reader/plugin.js +48 -0
  165. package/plugins/mail-reader/plugin.json +34 -0
  166. package/public/css/forms.css +1 -1
  167. package/public/css/search.css +1 -0
  168. package/public/css/site.css +1 -1
  169. package/public/css/theme-switcher.css +1 -0
  170. package/public/js/analytics.js +1 -0
  171. package/public/js/collection-context.js +2 -2
  172. package/public/js/context-menus.js +1 -0
  173. package/public/js/form-logic-engine.js +1 -1
  174. package/public/js/forms.js +2 -2
  175. package/public/js/search.js +1 -0
  176. package/public/js/site.js +1 -1
  177. package/public/js/theme-switcher.js +1 -0
  178. package/scripts/build.js +77 -4
  179. package/scripts/copy-domma.js +48 -0
  180. package/scripts/run-tests.mjs +92 -0
  181. package/scripts/seed.js +1996 -0
  182. package/{plugins/analytics/plugin.js → server/routes/api/analytics.js} +89 -41
  183. package/server/routes/api/collections.js +34 -0
  184. package/server/routes/api/contacts.js +506 -0
  185. package/server/routes/api/context-menus.js +104 -0
  186. package/server/routes/api/dashboard.js +8 -2
  187. package/server/routes/api/forms.js +42 -3
  188. package/{plugins/notes/plugin.js → server/routes/api/notes.js} +61 -17
  189. package/server/routes/api/notifications.js +69 -19
  190. package/server/routes/api/plugin-marketplace.js +78 -23
  191. package/server/routes/api/plugins.js +198 -9
  192. package/server/routes/api/search.js +43 -0
  193. package/server/routes/api/theme.js +69 -0
  194. package/server/routes/api/todo.js +178 -0
  195. package/server/routes/public.js +42 -7
  196. package/server/server.js +135 -1
  197. package/server/services/adapters/FileAdapter.js +6 -1
  198. package/server/services/collections.js +13 -3
  199. package/server/services/content.js +26 -0
  200. package/server/services/contextMenus.js +477 -0
  201. package/server/services/managerClient.js +72 -136
  202. package/server/services/markdown.js +124 -10
  203. package/server/services/permissionRegistry.js +74 -0
  204. package/server/services/pluginEntitlement.js +171 -0
  205. package/server/services/pluginEnvelope.js +242 -0
  206. package/server/services/pluginFiles.js +52 -11
  207. package/server/services/pluginInstaller.js +246 -26
  208. package/server/services/pluginScaffold.js +5 -0
  209. package/server/services/plugins.js +340 -7
  210. package/server/services/presetCollections.js +344 -0
  211. package/server/services/promoted-plugins-migration.js +254 -0
  212. package/server/services/publicCors.js +46 -0
  213. package/server/services/renderer.js +224 -22
  214. package/server/services/roles.js +1 -1
  215. package/server/services/search-migration.js +82 -0
  216. package/server/services/search.js +413 -0
  217. package/server/services/sidebar-migration.js +1 -0
  218. package/server/services/sidebarBadges.js +37 -0
  219. package/server/services/siteGitignore.js +307 -0
  220. package/server/services/themeSettings.js +580 -0
  221. package/server/services/users.js +8 -0
  222. package/server/templates/page.html +4 -2
  223. package/plugins/analytics/admin/templates/analytics.html +0 -61
  224. package/plugins/analytics/admin/views/analytics.js +0 -176
  225. package/plugins/analytics/config.js +0 -14
  226. package/plugins/analytics/plugin.json +0 -57
  227. package/plugins/analytics/public/inject-body.html +0 -60
  228. package/plugins/analytics/public/inject-head.html +0 -1
  229. package/plugins/blog/templates/author.html +0 -9
  230. package/plugins/blog/templates/category.html +0 -9
  231. package/plugins/blog/templates/tag.html +0 -9
  232. package/plugins/contacts/admin/templates/contacts.html +0 -126
  233. package/plugins/contacts/admin/views/contacts.js +0 -710
  234. package/plugins/contacts/collections/user-contact-groups/schema.json +0 -35
  235. package/plugins/contacts/collections/user-contacts/schema.json +0 -71
  236. package/plugins/contacts/config.js +0 -6
  237. package/plugins/contacts/data/contacts.json +0 -20
  238. package/plugins/contacts/plugin.js +0 -291
  239. package/plugins/contacts/plugin.json +0 -33
  240. package/plugins/demo-viewer/config.js +0 -4
  241. package/plugins/demo-viewer/plugin.js +0 -36
  242. package/plugins/demo-viewer/plugin.json +0 -9
  243. package/plugins/invoice/admin/templates/editor.html +0 -129
  244. package/plugins/invoice/admin/templates/index.html +0 -43
  245. package/plugins/invoice/admin/templates/issuers.html +0 -5
  246. package/plugins/invoice/admin/templates/receivers.html +0 -5
  247. package/plugins/invoice/admin/views/editor.js +0 -267
  248. package/plugins/invoice/admin/views/index.js +0 -155
  249. package/plugins/invoice/admin/views/issuers.js +0 -23
  250. package/plugins/invoice/admin/views/party-view.js +0 -148
  251. package/plugins/invoice/admin/views/receivers.js +0 -22
  252. package/plugins/invoice/collections/invoice-issuers/schema.json +0 -16
  253. package/plugins/invoice/collections/invoice-receivers/schema.json +0 -15
  254. package/plugins/invoice/collections/invoices/schema.json +0 -27
  255. package/plugins/invoice/config.js +0 -16
  256. package/plugins/invoice/plugin.js +0 -283
  257. package/plugins/invoice/plugin.json +0 -85
  258. package/plugins/invoice/templates/invoice-print.html +0 -213
  259. package/plugins/notes/admin/templates/notes.html +0 -83
  260. package/plugins/notes/admin/views/notes.js +0 -281
  261. package/plugins/notes/collections/user-notes/schema.json +0 -54
  262. package/plugins/notes/config.js +0 -6
  263. package/plugins/notes/data/notes.json +0 -1
  264. package/plugins/notes/plugin.json +0 -33
  265. package/plugins/site-search/admin/views/site-search.js +0 -116
  266. package/plugins/site-search/config.js +0 -15
  267. package/plugins/site-search/plugin.js +0 -188
  268. package/plugins/site-search/plugin.json +0 -40
  269. package/plugins/site-search/public/inject-body.html +0 -17
  270. package/plugins/site-search/public/inject-head.html +0 -1
  271. package/plugins/site-search/public/search.css +0 -1
  272. package/plugins/site-search/public/search.js +0 -1
  273. package/plugins/surveys/admin/templates/audience.html +0 -47
  274. package/plugins/surveys/admin/templates/results.html +0 -58
  275. package/plugins/surveys/admin/templates/survey-editor.html +0 -141
  276. package/plugins/surveys/admin/templates/surveys.html +0 -25
  277. package/plugins/surveys/admin/views/audience.js +0 -301
  278. package/plugins/surveys/admin/views/results.js +0 -172
  279. package/plugins/surveys/admin/views/survey-editor.js +0 -211
  280. package/plugins/surveys/admin/views/surveys.js +0 -161
  281. package/plugins/surveys/collections/survey-contacts/schema.json +0 -13
  282. package/plugins/surveys/collections/survey-groups/schema.json +0 -11
  283. package/plugins/surveys/collections/survey-invites/schema.json +0 -16
  284. package/plugins/surveys/collections/surveys/schema.json +0 -23
  285. package/plugins/surveys/config.js +0 -8
  286. package/plugins/surveys/plugin.js +0 -174
  287. package/plugins/surveys/plugin.json +0 -65
  288. package/plugins/surveys/public/dist-shared.mjs +0 -1
  289. package/plugins/surveys/public/survey.css +0 -1
  290. package/plugins/surveys/public/survey.mjs +0 -1
  291. package/plugins/surveys/templates/survey-page.html +0 -63
  292. package/plugins/theme-switcher/admin/templates/theme-switcher.html +0 -86
  293. package/plugins/theme-switcher/admin/views/theme-switcher.js +0 -65
  294. package/plugins/theme-switcher/config.js +0 -10
  295. package/plugins/theme-switcher/plugin.js +0 -26
  296. package/plugins/theme-switcher/plugin.json +0 -74
  297. package/plugins/theme-switcher/public/inject-body.html +0 -153
  298. package/plugins/theme-switcher/public/inject-head.html +0 -260
  299. package/plugins/todo/admin/templates/todo.html +0 -158
  300. package/plugins/todo/admin/views/todo.js +0 -343
  301. package/plugins/todo/collections/todos/schema.json +0 -60
  302. package/plugins/todo/config.js +0 -7
  303. package/plugins/todo/data/todos.json +0 -1
  304. package/plugins/todo/plugin.js +0 -102
  305. package/plugins/todo/plugin.json +0 -33
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Site .gitignore - the content-only allowlist
3
+ *
4
+ * A site repo holds the site's CONTENT and nothing else: pages, media, blocks,
5
+ * views, forms, components, collections the site built, API endpoints, menus,
6
+ * theme and every other config/ file, plus plugins the site wrote itself. The
7
+ * engine - server/, admin/, public/, scripts/, node_modules/ (which carries
8
+ * domma-js and domma-reactive) - and every distributed plugin arrive through
9
+ * the updater, so they are never committed.
10
+ *
11
+ * The file is an ALLOWLIST: ignore everything at the root, then re-include what
12
+ * the site owns. A denylist would have to know every engine path in advance and
13
+ * would start tracking whatever a new release adds; an allowlist can only fail
14
+ * by leaving something out, and the two places a site grows new owned code
15
+ * append their own rule through `allowInGitignore()`:
16
+ * - pluginScaffold.js: a plugin created in the admin is the site's code
17
+ * - pluginInstaller.js: a distributed plugin that stores what users author
18
+ * inside its own folder declares it in plugin.json `contentPaths`
19
+ *
20
+ * ONE COPY, TWO REPOS. domma-cms-manager keeps a byte-identical copy at
21
+ * plugins/site-manager/services/siteGitignore.js (its production install runs
22
+ * an npm domma-cms that may predate this file), and a test there fails when the
23
+ * two drift. Change this one, then copy it across.
24
+ *
25
+ * No imports beyond node built-ins: bin/cli.js loads this before a site exists.
26
+ */
27
+ import fs from 'node:fs';
28
+ import path from 'node:path';
29
+
30
+ export const MANAGED_HEADER = '# ── Managed by Domma: content-only allowlist (server/services/siteGitignore.js) ──';
31
+ export const SITE_RULES_MARKER = '# ── Site-specific rules (kept when this file is regenerated) ──';
32
+
33
+ /** Top-level entries that belong to the engine or the host, never to the site. */
34
+ export const ENGINE_TOP_LEVEL = new Set([
35
+ 'server', 'admin', 'public', 'scripts', 'node_modules', 'bin',
36
+ 'package.json', 'package-lock.json', '.cmsVersion', '.domma', 'CLAUDE.md',
37
+ 'logs', 'backups', '_backups', '.domma-backups', '.env', '.env.example', '.claude', '.git'
38
+ ]);
39
+
40
+ /** Always-kept roots. public/ is handled separately: only site-added entries in it are kept. */
41
+ const SITE_ROOTS = ['content', 'config', 'plugins'];
42
+
43
+ /**
44
+ * Plugins that were promoted into core (0.67.0) or retired. A site that has not
45
+ * yet booted a release that removes them still has the folder; it is engine,
46
+ * not the site's.
47
+ */
48
+ export const RETIRED_PLUGINS = [
49
+ 'analytics', 'contacts', 'notes', 'todo', 'demo-viewer', 'theme-switcher', 'site-search'
50
+ ];
51
+
52
+ /**
53
+ * Distributed plugins that keep what users AUTHOR inside their own folder, for
54
+ * installs whose plugin.json predates `contentPaths`. Both ship the folder
55
+ * empty, so everything in it was written by the site.
56
+ */
57
+ export const KNOWN_PLUGIN_CONTENT = {
58
+ 'docs': ['data'], // documents, folders, templates, versions
59
+ 'theme-roller': ['data/themes'] // themes built in the roller
60
+ };
61
+
62
+ /**
63
+ * Collections whose schema.json the CMS rewrites on every boot
64
+ * (presetCollections.seedAll, roles.seed, userProfiles). Tracking them records
65
+ * the engine's schema, not the site's, and shows a diff after every update.
66
+ * Their data.json is a separate question - see PRIVATE_DATA.
67
+ */
68
+ export const ENGINE_SCHEMAS = [
69
+ 'notifications', 'projects', 'api-tokens', 'api-endpoints', 'notes-notes', 'todo-items',
70
+ 'contacts-contacts', 'contacts-groups', 'roles', 'user-profiles'
71
+ ];
72
+
73
+ /**
74
+ * Collection entries that are secrets, visitor-submitted or per-user private
75
+ * data, or runtime state - not something the site's authors built. Everything
76
+ * else in content/collections is tracked, including every collection a site
77
+ * creates, projects and api-endpoints.
78
+ */
79
+ export const PRIVATE_DATA = [
80
+ 'notifications', 'api-tokens', 'notes-notes', 'todo-items',
81
+ 'contacts-contacts', 'contacts-groups', 'roles', 'user-profiles',
82
+ 'user-messages', 'user-message-threads', 'analytics-events', 'enquiries',
83
+ 'blog-comments', 'invoices', 'invoice-receivers', 'invoice-issuers',
84
+ 'surveys-contacts', 'surveys-invites'
85
+ ];
86
+
87
+ /**
88
+ * Lines bin/update.js appends when it cannot find them VERBATIM. They are
89
+ * already ignored by `/*` and `/plugins/*`, but writing them out keeps the
90
+ * updater from editing the file on every site after every release.
91
+ */
92
+ const UPDATER_REQUIRED = ['.domma-backups/', 'plugins/analytics/data/'];
93
+
94
+ /**
95
+ * Split a plugin folder listing into distributed and site-owned.
96
+ *
97
+ * @param {string[]} sitePlugins - Directory names under the site's plugins/
98
+ * @param {Iterable<string>} distributed - Every plugin name an upstream ships
99
+ * @returns {{distributed: string[], siteOwned: string[]}}
100
+ */
101
+ export function classifyPlugins(sitePlugins, distributed) {
102
+ const known = new Set([...distributed, ...RETIRED_PLUGINS]);
103
+ const out = {distributed: [], siteOwned: []};
104
+ for (const name of [...sitePlugins].sort()) {
105
+ if (name.startsWith('.')) continue;
106
+ (known.has(name) ? out.distributed : out.siteOwned).push(name);
107
+ }
108
+ return out;
109
+ }
110
+
111
+ /**
112
+ * Rules that re-include a site-owned plugin.
113
+ *
114
+ * @param {string} name
115
+ * @returns {string[]}
116
+ */
117
+ export function sitePluginRules(name) {
118
+ return [`!/plugins/${name}/`];
119
+ }
120
+
121
+ /**
122
+ * Rules that re-include ONE content folder inside an otherwise ignored
123
+ * distributed plugin. Git cannot re-include a file whose parent is excluded,
124
+ * so every level is opened and its siblings closed again:
125
+ * data/themes -> !plugins/x/, plugins/x/*, !plugins/x/data/, plugins/x/data/*, !plugins/x/data/themes/
126
+ *
127
+ * @param {string} name - Plugin folder name
128
+ * @param {string} rel - Folder inside the plugin, e.g. 'data' or 'data/themes'
129
+ * @returns {string[]}
130
+ */
131
+ export function pluginContentRules(name, rel) {
132
+ const parts = rel.split('/').filter(p => p && p !== '.' && p !== '..');
133
+ if (!parts.length) return [];
134
+ const out = [`!/plugins/${name}/`, `/plugins/${name}/*`];
135
+ for (let i = 1; i <= parts.length; i++) {
136
+ const sub = parts.slice(0, i).join('/');
137
+ out.push(`!/plugins/${name}/${sub}/`);
138
+ if (i < parts.length) out.push(`/plugins/${name}/${sub}/*`);
139
+ }
140
+ return out;
141
+ }
142
+
143
+ /**
144
+ * Build the .gitignore text.
145
+ *
146
+ * @param {object} [opts]
147
+ * @param {string[]} [opts.sitePlugins=[]] - Site-owned plugin folder names
148
+ * @param {Object<string,string[]>} [opts.pluginContent={}] - Extra content folders in distributed plugins
149
+ * @param {string[]} [opts.sitePublic=[]] - Site-added entries directly under public/
150
+ * @param {string[]} [opts.extras=[]] - Other site-owned top-level entries (docs, tools, README.md)
151
+ * @param {string[]} [opts.siteRules=[]] - The site-specific section
152
+ * @returns {string}
153
+ */
154
+ export function buildGitignore({sitePlugins = [], pluginContent = {}, sitePublic = [], extras = [], siteRules = []} = {}) {
155
+ const L = [
156
+ MANAGED_HEADER,
157
+ '# This repo holds the site\'s CONTENT only. Everything is ignored, then what',
158
+ '# the site owns is re-included. The engine (server/, admin/, public/, scripts/,',
159
+ '# node_modules/ incl. domma-js + domma-reactive) and distributed plugins come',
160
+ '# from the updater and are never committed. A plugin created in the admin adds',
161
+ '# its own rule to the site section below.',
162
+ '/*',
163
+ '!/.gitignore'
164
+ ];
165
+ for (const root of SITE_ROOTS) L.push(`!/${root}/`);
166
+ for (const x of [...extras].sort()) L.push(`!/${x}${x.includes('.') ? '' : '/'}`);
167
+
168
+ L.push('', '# Plugins: only the ones this site wrote itself', '/plugins/*');
169
+ for (const p of [...sitePlugins].sort()) L.push(...sitePluginRules(p));
170
+
171
+ const content = {};
172
+ for (const [name, rels] of [...Object.entries(KNOWN_PLUGIN_CONTENT), ...Object.entries(pluginContent)]) {
173
+ if (sitePlugins.includes(name)) continue; // already tracked whole
174
+ content[name] = [...new Set([...(content[name] || []), ...rels])];
175
+ }
176
+ L.push('', '# ...and what users author inside a distributed plugin (its code stays out)');
177
+ for (const name of Object.keys(content).sort()) {
178
+ const seen = new Set();
179
+ for (const rel of content[name].sort()) {
180
+ for (const r of pluginContentRules(name, rel)) if (!seen.has(r)) { seen.add(r); L.push(r); }
181
+ }
182
+ }
183
+
184
+ if (sitePublic.length) {
185
+ L.push('', '# public/ is engine except what the site added to it', '!/public/', '/public/*');
186
+ for (const p of [...sitePublic].sort()) L.push(`!/public/${p}`);
187
+ }
188
+
189
+ L.push('', '# Schemas the CMS rewrites on every boot - engine-owned, not authored');
190
+ for (const c of ENGINE_SCHEMAS) L.push(`/content/collections/${c}/schema.json`);
191
+ L.push('', '# Secrets, visitor-submitted and per-user data, runtime state');
192
+ for (const c of PRIVATE_DATA) L.push(`/content/collections/${c}/data.json`);
193
+ L.push(
194
+ '/content/users/',
195
+ '/content/versions/',
196
+ '/content/analytics/',
197
+ '/content/instances_log.json',
198
+ '/content/preview-links.json',
199
+ '/config/connections.json*',
200
+ '/config/keys/',
201
+ '/plugins/*/data/*.jsonl',
202
+ '/plugins/*/backups/',
203
+ ...UPDATER_REQUIRED,
204
+ '',
205
+ '# Cruft',
206
+ '*.log', '*.pid', '*.bak', '*.bak-*', '*.orig', '*.swp', '*.prev-*/', '.DS_Store',
207
+ '',
208
+ SITE_RULES_MARKER,
209
+ ...siteRules
210
+ );
211
+ return L.join('\n') + '\n';
212
+ }
213
+
214
+ /** Every non-comment rule above the site section, for de-duplicating carried rules. */
215
+ export function generatedRules(text) {
216
+ const end = text.indexOf(SITE_RULES_MARKER);
217
+ return (end === -1 ? text : text.slice(0, end)).split('\n').map(l => l.trim())
218
+ .filter(l => l && !l.startsWith('#'));
219
+ }
220
+
221
+ /**
222
+ * Old rules that would hide a whole content root. The first scaffolded
223
+ * .gitignore ignored content/media/, and media is content - carrying that rule
224
+ * would keep every uploaded image out of the repo that exists to hold them.
225
+ */
226
+ const NEVER_CARRY = /^(content|config|content\/(media|pages|blocks|components|forms|views|collections))\/?\*?$/;
227
+
228
+ function norm(rule) {
229
+ return rule.replace(/^\//, '').replace(/\/$/, '');
230
+ }
231
+
232
+ /**
233
+ * Pull the site-specific section out of an existing .gitignore.
234
+ *
235
+ * A file this module wrote keeps everything after the marker verbatim. A
236
+ * hand-written file has no marker, so only the rules that still narrow
237
+ * something kept (content/, config/, a kept plugin or public entry) are
238
+ * carried; engine paths are covered by the allowlist and dropped.
239
+ *
240
+ * @param {string} existing - Current .gitignore text ('' if none)
241
+ * @param {{keptPlugins: string[], keptPublic: string[]}} kept
242
+ * @param {string[]} generated - Rules the new file already has
243
+ * @returns {string[]}
244
+ */
245
+ export function carrySiteRules(existing, {keptPlugins, keptPublic}, generated) {
246
+ if (!existing) return [];
247
+ const idx = existing.indexOf(SITE_RULES_MARKER);
248
+ if (idx !== -1) {
249
+ return existing.slice(idx + SITE_RULES_MARKER.length).replace(/^\n+/, '').replace(/\n+$/, '').split('\n')
250
+ .filter(l => l.length);
251
+ }
252
+ const have = new Set(generated.map(norm));
253
+ // A generated `foo*` already covers every carried rule that starts with foo.
254
+ const globs = generated.filter(r => r.endsWith('*') && !r.startsWith('!')).map(r => norm(r).slice(0, -1))
255
+ .filter(g => g.includes('/') && !g.endsWith('/') && !g.includes('*'));
256
+ const keptPrefixes = [
257
+ 'content/', 'config/',
258
+ ...keptPlugins.map(p => `plugins/${p}/`),
259
+ ...keptPublic.map(p => `public/${p}`)
260
+ ];
261
+ const carried = [];
262
+ for (const raw of existing.split('\n')) {
263
+ const line = raw.trim();
264
+ if (!line || line.startsWith('#')) continue;
265
+ const bare = line.replace(/^!?\//, '').replace(/^!/, '');
266
+ if (!keptPrefixes.some(p => bare.startsWith(p))) continue;
267
+ if (NEVER_CARRY.test(bare)) continue;
268
+ if (have.has(norm(line)) || globs.some(g => norm(line).startsWith(g))) continue;
269
+ carried.push(line);
270
+ }
271
+ return carried;
272
+ }
273
+
274
+ /**
275
+ * Add rules to the site section of a managed .gitignore, skipping any already
276
+ * present. A missing or hand-written file is left alone: the site chose its own
277
+ * rules and this must not rewrite them.
278
+ *
279
+ * @param {string} siteRoot - Directory holding .gitignore
280
+ * @param {string[]} rules
281
+ * @returns {string[]} The rules actually added
282
+ */
283
+ export function allowInGitignore(siteRoot, rules) {
284
+ const file = path.join(siteRoot, '.gitignore');
285
+ let text;
286
+ try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
287
+ if (!text.startsWith(MANAGED_HEADER) || !text.includes(SITE_RULES_MARKER)) return [];
288
+ const present = new Set(text.split('\n').map(l => l.trim()));
289
+ const added = rules.filter(r => !present.has(r));
290
+ if (added.length) fs.writeFileSync(file, text.replace(/\n*$/, '\n') + added.join('\n') + '\n');
291
+ return added;
292
+ }
293
+
294
+ /**
295
+ * The content folders a plugin manifest declares, sanitised. `contentPaths`
296
+ * is a list of folders relative to the plugin root; anything that could climb
297
+ * out of it is dropped.
298
+ *
299
+ * @param {object} manifest - Parsed plugin.json
300
+ * @returns {string[]}
301
+ */
302
+ export function manifestContentPaths(manifest) {
303
+ const raw = Array.isArray(manifest?.contentPaths) ? manifest.contentPaths : [];
304
+ return raw.filter(p => typeof p === 'string')
305
+ .map(p => p.replace(/^\.?\/+/, '').replace(/\/+$/, ''))
306
+ .filter(p => p && !p.split('/').some(s => s === '..' || s === '.' || s === ''));
307
+ }