@acms-subir/core 0.1.0 → 0.3.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 (237) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/cli/common.mjs +35 -0
  3. package/cli/legacy.mjs +317 -0
  4. package/cli/main.mjs +66 -0
  5. package/cli/palette.mjs +4 -0
  6. package/cli/preview.mjs +50 -0
  7. package/cli/remote.mjs +55 -0
  8. package/cli/themes.mjs +127 -0
  9. package/cli.mjs +2 -315
  10. package/deploy/Caddyfile +8 -0
  11. package/deploy/Dockerfile +34 -0
  12. package/deploy/compose.https.yml +21 -0
  13. package/deploy/compose.yml +46 -0
  14. package/docs/theme-authoring.md +103 -0
  15. package/email-runner.mjs +32 -0
  16. package/integration.mjs +193 -2
  17. package/ops-check.mjs +27 -0
  18. package/package.json +35 -12
  19. package/src/actions/index.ts +744 -38
  20. package/src/components/AiCleaner.tsx +291 -0
  21. package/src/components/AiSettings.tsx +575 -0
  22. package/src/components/CommandPalette.tsx +200 -0
  23. package/src/components/ContentList.astro +246 -0
  24. package/src/components/DesignAccess.tsx +182 -0
  25. package/src/components/DesignManager.tsx +455 -47
  26. package/src/components/EmailSettings.tsx +521 -0
  27. package/src/components/EventPage.astro +178 -0
  28. package/src/components/LegalPage.astro +27 -0
  29. package/src/components/LinksManager.tsx +216 -97
  30. package/src/components/NavManager.tsx +83 -38
  31. package/src/components/ThemeScript.astro +50 -0
  32. package/src/components/ThemeSections.astro +57 -0
  33. package/src/components/ThemeSectionsEditor.tsx +513 -0
  34. package/src/components/booking/BookingFlow.astro +150 -0
  35. package/src/components/booking/HoursEditor.astro +63 -0
  36. package/src/components/builder/PageBuilder.tsx +10 -2
  37. package/src/components/editor/EventPanel.tsx +401 -0
  38. package/src/components/editor/FormatDock.tsx +81 -0
  39. package/src/components/editor/LivePane.tsx +122 -0
  40. package/src/components/editor/MobileToolbar.tsx +93 -0
  41. package/src/components/editor/PostEditor.tsx +826 -271
  42. package/src/components/editor/PreviewOverlay.tsx +242 -43
  43. package/src/components/editor/extensions/calendar.tsx +51 -7
  44. package/src/components/editor/extensions/cards.tsx +17 -1
  45. package/src/components/editor/extensions/codeBlock.tsx +1 -1
  46. package/src/components/editor/extensions/events.tsx +153 -0
  47. package/src/components/editor/extensions/infoCards.tsx +16 -1
  48. package/src/components/editor/extensions/mediaCards.tsx +36 -2
  49. package/src/components/editor/extensions/slash.tsx +77 -9
  50. package/src/components/editor/extensions/toc.tsx +89 -0
  51. package/src/components/editor/extensions/toggle.tsx +70 -0
  52. package/src/components/site/Pager.astro +24 -0
  53. package/src/components/site/PostCard.astro +64 -0
  54. package/src/components/site/TagLabel.astro +11 -0
  55. package/src/layouts/Admin.astro +289 -49
  56. package/src/layouts/Site.astro +331 -57
  57. package/src/lib/agent-api.ts +13 -0
  58. package/src/lib/agent-discovery.ts +40 -0
  59. package/src/lib/agent.ts +114 -0
  60. package/src/lib/ai-config.ts +58 -0
  61. package/src/lib/ai-schema.ts +22 -0
  62. package/src/lib/ai.ts +90 -0
  63. package/src/lib/analytics.ts +121 -0
  64. package/src/lib/api-keys.ts +65 -0
  65. package/src/lib/api.ts +52 -0
  66. package/src/lib/auth.ts +25 -5
  67. package/src/lib/backup/content.ts +36 -0
  68. package/src/lib/backup/unzip.ts +12 -1
  69. package/src/lib/backup/zip.ts +1 -1
  70. package/src/lib/booking/agent.ts +108 -0
  71. package/src/lib/booking/availability.ts +221 -0
  72. package/src/lib/booking/emails.ts +115 -0
  73. package/src/lib/booking/forms.ts +77 -0
  74. package/src/lib/booking/service.ts +277 -0
  75. package/src/lib/booking/view.ts +54 -0
  76. package/src/lib/calendar/agent-schema.ts +48 -0
  77. package/src/lib/calendar/agent.ts +136 -0
  78. package/src/lib/calendar/api.ts +81 -0
  79. package/src/lib/calendar/cards.ts +102 -0
  80. package/src/lib/calendar/config.ts +52 -0
  81. package/src/lib/calendar/data.ts +130 -0
  82. package/src/lib/calendar/events.ts +261 -0
  83. package/src/lib/calendar/feeds.ts +43 -0
  84. package/src/lib/calendar/hooks.ts +38 -0
  85. package/src/lib/calendar/http.ts +30 -0
  86. package/src/lib/calendar/ics-write.ts +120 -0
  87. package/src/lib/calendar/ics.ts +15 -5
  88. package/src/lib/calendar/import.ts +203 -0
  89. package/src/lib/calendar/render.ts +120 -37
  90. package/src/lib/calendar/rrule.ts +149 -0
  91. package/src/lib/calendar/seo.ts +48 -0
  92. package/src/lib/design-service.ts +58 -0
  93. package/src/lib/designDiff.ts +145 -0
  94. package/src/lib/designPreview.ts +112 -0
  95. package/src/lib/documents/cleanup.ts +31 -0
  96. package/src/lib/documents/extract.ts +154 -0
  97. package/src/lib/documents/vendor.d.ts +4 -0
  98. package/src/lib/drafts.ts +131 -0
  99. package/src/lib/dynamicBlocks.ts +79 -7
  100. package/src/lib/email/accounts.ts +55 -0
  101. package/src/lib/email/config.ts +86 -0
  102. package/src/lib/email/migration.ts +24 -0
  103. package/src/lib/email/providers.ts +89 -0
  104. package/src/lib/email/service.ts +78 -0
  105. package/src/lib/email/store.ts +167 -0
  106. package/src/lib/email/templates.ts +18 -0
  107. package/src/lib/email/types.ts +32 -0
  108. package/src/lib/email/webhooks.ts +87 -0
  109. package/src/lib/email.ts +15 -0
  110. package/src/lib/env.ts +1 -1
  111. package/src/lib/legal/index.ts +25 -0
  112. package/src/lib/mobile/bookings.ts +73 -0
  113. package/src/lib/mobile/calendar.ts +85 -0
  114. package/src/lib/mobile/content.ts +85 -0
  115. package/src/lib/mobile/http.ts +63 -0
  116. package/src/lib/mobile/route.ts +20 -0
  117. package/src/lib/ops.ts +8 -0
  118. package/src/lib/preview.ts +21 -1
  119. package/src/lib/repo/migrations.ts +254 -0
  120. package/src/lib/repo/sql.ts +956 -46
  121. package/src/lib/repo/types.ts +140 -3
  122. package/src/lib/scheduler.ts +25 -0
  123. package/src/lib/schema.ts +362 -0
  124. package/src/lib/search-i18n.ts +3 -0
  125. package/src/lib/socialIcons.ts +32 -0
  126. package/src/lib/tagColor.ts +11 -0
  127. package/src/lib/theme.ts +59 -40
  128. package/src/lib/themeTokens.ts +174 -0
  129. package/src/lib/umami.ts +108 -0
  130. package/src/lib/webhooks.ts +139 -0
  131. package/src/middleware.ts +36 -3
  132. package/src/pages/404.astro +23 -0
  133. package/src/pages/[...slug].astro +10 -5
  134. package/src/pages/account/[action].astro +53 -0
  135. package/src/pages/admin/ai.astro +12 -0
  136. package/src/pages/admin/c/[collection]/[id].astro +14 -4
  137. package/src/pages/admin/c/[collection]/index.astro +127 -91
  138. package/src/pages/admin/calendar/bookings.astro +400 -0
  139. package/src/pages/admin/calendar/calendars.astro +265 -0
  140. package/src/pages/admin/calendar/events/[id].astro +56 -0
  141. package/src/pages/admin/calendar/events/index.astro +408 -0
  142. package/src/pages/admin/calendar/index.ts +4 -0
  143. package/src/pages/admin/calendar/integrations.astro +221 -0
  144. package/src/pages/admin/calendar/resources/[id].astro +180 -0
  145. package/src/pages/admin/calendar/resources/index.astro +106 -0
  146. package/src/pages/admin/calendar/resources/link.astro +196 -0
  147. package/src/pages/admin/design.astro +16 -4
  148. package/src/pages/admin/email.astro +13 -0
  149. package/src/pages/admin/index.astro +237 -87
  150. package/src/pages/admin/links.astro +3 -3
  151. package/src/pages/admin/login.astro +14 -10
  152. package/src/pages/admin/navigation.astro +8 -2
  153. package/src/pages/admin/pages/[id].astro +14 -3
  154. package/src/pages/admin/pages/index.astro +41 -47
  155. package/src/pages/admin/settings.astro +475 -276
  156. package/src/pages/admin/tags.astro +4 -4
  157. package/src/pages/admin/users.astro +38 -28
  158. package/src/pages/api/agent/docs.ts +33 -0
  159. package/src/pages/api/agent/index.ts +21 -0
  160. package/src/pages/api/ai/clean.ts +13 -0
  161. package/src/pages/api/ai/draft.ts +14 -0
  162. package/src/pages/api/ai/settings.ts +53 -0
  163. package/src/pages/api/ai/test.ts +12 -0
  164. package/src/pages/api/backup.ts +8 -21
  165. package/src/pages/api/calendar/availability.ts +35 -0
  166. package/src/pages/api/calendar/bookings/[id].ts +49 -0
  167. package/src/pages/api/calendar/bookings/index.ts +73 -0
  168. package/src/pages/api/calendar/calendars.ts +35 -0
  169. package/src/pages/api/calendar/docs.ts +64 -0
  170. package/src/pages/api/calendar/events/[id].ts +63 -0
  171. package/src/pages/api/calendar/events/index.ts +112 -0
  172. package/src/pages/api/calendar/resources.ts +34 -0
  173. package/src/pages/api/design/capabilities.ts +10 -0
  174. package/src/pages/api/design/index.ts +58 -0
  175. package/src/pages/api/design/keys.ts +24 -0
  176. package/src/pages/api/design/media.ts +32 -0
  177. package/src/pages/api/email/run.ts +11 -0
  178. package/src/pages/api/email/settings.ts +61 -0
  179. package/src/pages/api/email/webhooks/[provider].ts +4 -0
  180. package/src/pages/api/mcp.ts +59 -0
  181. package/src/pages/api/media/upload.ts +2 -2
  182. package/src/pages/api/ops/status.ts +39 -0
  183. package/src/pages/api/restore.ts +27 -37
  184. package/src/pages/api/search.ts +18 -4
  185. package/src/pages/api/v1/availability.ts +7 -0
  186. package/src/pages/api/v1/bookings.ts +17 -0
  187. package/src/pages/api/v1/calendars.ts +7 -0
  188. package/src/pages/api/v1/content/[id].ts +7 -0
  189. package/src/pages/api/v1/content/index.ts +7 -0
  190. package/src/pages/api/v1/events.ts +7 -0
  191. package/src/pages/api/v1/resources.ts +7 -0
  192. package/src/pages/author/[slug].astro +43 -35
  193. package/src/pages/blog/[slug].astro +140 -66
  194. package/src/pages/blog/index.astro +95 -30
  195. package/src/pages/blog/tag/[slug].astro +45 -40
  196. package/src/pages/book/[resource]/[link].astro +70 -0
  197. package/src/pages/book/[resource]/index.astro +63 -0
  198. package/src/pages/book/[resource].ics.ts +38 -0
  199. package/src/pages/book/index.astro +47 -0
  200. package/src/pages/book/manage/[token].astro +108 -0
  201. package/src/pages/calendar/[slug].ics.ts +27 -0
  202. package/src/pages/calendar/feed.ics.ts +15 -0
  203. package/src/pages/calendar/index.astro +191 -0
  204. package/src/pages/embed/book/[resource]/[link].astro +33 -0
  205. package/src/pages/embed/calendar/[slug].astro +43 -0
  206. package/src/pages/events/[slug].astro +74 -0
  207. package/src/pages/events/[slug].ics.ts +25 -0
  208. package/src/pages/index.astro +9 -3
  209. package/src/pages/legal/privacy.astro +4 -0
  210. package/src/pages/legal/terms.astro +4 -0
  211. package/src/pages/links.astro +33 -7
  212. package/src/pages/llms.txt.ts +4 -0
  213. package/src/pages/preview/[type]/[id].astro +34 -8
  214. package/src/pages/search.astro +40 -20
  215. package/src/pages/sitemap.xml.ts +16 -1
  216. package/src/styles/admin.css +928 -105
  217. package/src/styles/cards.css +101 -60
  218. package/src/styles/editor.css +532 -102
  219. package/src/styles/email-settings.css +117 -0
  220. package/src/styles/sections.css +12 -0
  221. package/src/styles/site.css +938 -278
  222. package/src/styles/theme-editor.css +135 -0
  223. package/src/theme-sdk/contract.d.mts +619 -0
  224. package/src/theme-sdk/contract.mjs +80 -0
  225. package/src/theme-sdk/index.ts +15 -0
  226. package/src/theme-sdk/tokens.d.mts +148 -0
  227. package/src/theme-sdk/tokens.mjs +52 -0
  228. package/src/themes/editorial/templates/home.astro +4 -4
  229. package/src/themes/editorial/templates/post-minimal.astro +2 -2
  230. package/src/themes/editorial/theme.css +22 -37
  231. package/src/themes/editorial/theme.json +23 -3
  232. package/src/themes/field/theme.json +26 -9
  233. package/src/themes/ink/theme.json +30 -12
  234. package/src/themes/night/theme.css +4 -7
  235. package/src/themes/night/theme.json +27 -10
  236. package/src/virtual.d.ts +14 -0
  237. package/src/worker.ts +5 -1
@@ -0,0 +1,103 @@
1
+ # ACMS theme authoring contract (API 1)
2
+
3
+ Use the installed version as the authority: `acms inspect --json`, `acms schema theme --json`, and `acms docs theme --format markdown`. `astro sync`, dev or build writes a non-secret configuration inventory used by inspect. Never copy credentials or site content into a theme package.
4
+
5
+ ## Start and validate
6
+
7
+ ```sh
8
+ acms theme create studio --from business
9
+ acms theme dev studio
10
+ acms theme check studio --build --json
11
+ acms theme diff studio
12
+ acms theme pack studio
13
+ ```
14
+
15
+ The business starter includes a typed custom Statement section to adapt. Editorial is another starter: `--from editorial`. An installed project theme can also be used as a starter. Create never overwrites an existing folder. `agent init` writes `.agents/skills/acms-theme/SKILL.md` without modifying your existing instruction files.
16
+
17
+ Themes live in `src/themes/<slug>/` with `theme.json`, optional `theme.css`, `templates/`, `sections/`, and `assets/`. Metadata supports name, slug, semantic version, apiVersion (1), core compatibility range, description, author, license, token defaults, post defaults, templates, section definitions and asset inventory. Legacy manifests without version metadata receive defaults. Do not include package scripts or dependencies in theme archives.
18
+
19
+ The token schema is shared with the runtime, not a separate CLI copy. Color schemes, typography, spacing, width, shape and header/footer defaults are presentation. Site title, page copy, image choices and section order belong to the owner in ACMS. New theme files require build/deployment; database-only edits to installed sections and brand overrides do not.
20
+
21
+ ## Public imports and template props
22
+
23
+ ```astro
24
+ ---
25
+ import type { HomeProps } from '@acms-subir/core/theme';
26
+ import { collectionUrl } from '@acms-subir/core/theme';
27
+ import PostCard from '@acms-subir/core/theme/components/PostCard.astro';
28
+ interface Props extends HomeProps {}
29
+ const { page, posts } = Astro.props;
30
+ ---
31
+ {page?.bodyHtml && <div set:html={page.bodyHtml} />}
32
+ {posts.map(post => <PostCard href={collectionUrl(post.collection, post.slug)} title={post.title} excerpt={post.excerpt} />)}
33
+ ```
34
+
35
+ Public types: HomeProps (settings, page, posts, tags), PageProps (settings, page, html), PostProps (settings, post, html, authors, related), EventProps (settings, event, occurrence, calendars, html, upcoming, origin, preview, rsvp), SectionProps (settings, page, variant, data). Public components: PostCard with card/row/feature variants, Pager with base/page/count, TagLabel with name/href/accent. Helpers include collectionUrl, postUrl, postsIndexUrl, tagUrl, formatDate, slugify, authorSlug, localeFromPath, localeHomeUrl and CONTENT. Use collectionUrl for non-default collections; postUrl is the built-in posts alias.
36
+
37
+ Template filenames: `home.astro`, `event.astro`, `post-<name>.astro`, `page-<name>.astro`. Post/page variants are selected in the content editor. Core owns routes, auth, SEO and published-content queries. Templates only replace their documented content area. Never query private data from a public section or insert untrusted strings with set:html. The core's rendered `html`/`bodyHtml` is the intended rich-content handoff.
38
+
39
+ ## Editable sections
40
+
41
+ Every theme gets Hero, Features, Testimonials, FAQ, Posts, CTA and Existing page content. The last section explicitly adapts the original page renderer: published rich text and widgets can appear among your sections, with their editing lifecycle unchanged. In Design → Pages, select a page, enable Sections, add/reorder/duplicate/hide sections, edit fields and save the section draft. Review and publish through the existing Design workflow. Page content stays intact; switching back to Page content restores the existing renderer without deleting section data. Unknown sections and schema-version mismatches retain their data, show an editor warning and block publication until hidden, removed or migrated.
42
+
43
+ Custom definitions live in `theme.json`:
44
+
45
+ ```json
46
+ {
47
+ "name": "Studio", "slug": "studio", "version": "1.0.0", "apiVersion": 1,
48
+ "core": ">=0.2.0 <1.0.0",
49
+ "sections": {
50
+ "statement": {
51
+ "name": "Statement", "version": 1,
52
+ "component": "sections/Statement.astro", "variants": ["default"],
53
+ "fields": { "heading": { "label": "Heading", "type": "text", "default": "Welcome" } }
54
+ }
55
+ }
56
+ }
57
+ ```
58
+
59
+ The component receives SectionProps. Supported field kinds: text, textarea, image, url, select (options required), number. Instances have stable id/type/version/variant/hidden/data fields. A page layout has mode and sections, persisted by page ID in `settings.sectionPages`. Section values and order have a design draft lifecycle distinct from rich-text/widget page drafts; publishing design does not publish pending page text. Theme switches preserve the map. To migrate a custom schema, transform a pulled draft to the new version, review it, and push it with its revision; incompatible versions never auto-convert silently. Existing widget pages continue using their original renderer.
60
+
61
+ Images in template files should be imported using relative paths so Astro bundles them. Theme CSS can reference `url(assets/name.webp)`; ACMS resolves it through the bundled asset inventory. Brand images and editable section images use the media library. Packaged assets must be local files under assets; declare them in the manifest. Use `theme pack --optimize` to run current img-subir and rewrite references before packing. Original-image backup directories are excluded.
62
+
63
+ ## Styling compatibility
64
+
65
+ The theme API version covers documented template/section contracts and public components. CSS variables emitted from the token schema are the supported customization surface. Arbitrary internal DOM selectors are not a stable API. Public theme v2 changed `.site-header-in`, `.site-tools`, and the position of share buttons in the byline. `header: minimal` is the closest old-header behavior. Legacy token JSON loading does not guarantee old CSS matches new markup. Recheck custom selectors after a core upgrade.
66
+
67
+ ## Local previews and checks
68
+
69
+ `theme dev` starts a loopback Astro server in a new `.acms/previews/` workspace, with an isolated SQLite database, generated local secrets and linked theme sources for live reload. Interactive terminal sessions open `/acms-theme-preview` automatically; otherwise open the printed URL. Initialize synthetic content from its controls. It includes home/page/post/event, archive pagination, long titles, missing covers, calendar, search and 404; empty booking surfaces remain empty. Use the fixture device and scheme controls. No production config or credentials are copied. Theme code is application code and runs in your local process; only run themes you trust. Stop with Ctrl-C. Remove old preview folders when no longer needed.
70
+
71
+ `theme check` validates the manifest, compatibility range, declared files, supported file types, imports and asset references, and reports token contrast estimates. `--build` compiles an isolated consumer with both Node and Cloudflare adapters. Contrast results are not an accessibility certification; review actual custom templates at narrow/wide viewports, keyboard focus and light/dark palettes. A browser-only hover or interaction cannot be proven by a build.
72
+
73
+ `theme diff` compares against the creation snapshot; `--baseline <folder>` selects another version. Runtime settings/navigation differences are available in `design pull`.
74
+
75
+ ## Sharing, versions and rollback
76
+
77
+ `theme pack` produces a deterministic theme-only tgz. `theme install` validates the archive before writing, rejects traversal/links/unexpected files/incompatible core versions, and never runs install scripts. Use `--force` for intentional replacement: the previous directory is retained in `.acms/theme-backups/`. `.acms/themes.lock.json` records the pinned version, digest and backup path. Copy a retained backup to a new theme identity or re-pack/install it to roll code back; rebuild and redeploy. No automatic download of theme dependencies occurs.
78
+
79
+ Deploy new code under a distinct theme identity before selecting it. Editing the active theme's deployed CSS/templates changes live presentation regardless of a settings draft. Design history stores the last 20 outgoing settings/navigation/section snapshots, not asset bytes or compiled code. Restoring a history version stages a draft; publish is separate. Old snapshots without section fields preserve current layouts.
80
+
81
+ ## Remote operations
82
+
83
+ Owners create scoped credentials under Design → Access. A key has design:read, design:write-draft, design:publish, media:read and/or media:write. Publish is off by default. Legacy content-agent keys retain content access only and cannot access the design API. The read-only `/api/design/capabilities` probe accepts any explicit design/media scope, so a media-only profile can connect without gaining design read access.
84
+
85
+ ```sh
86
+ # Set ACMS_TOKEN using your shell/secret manager; do not put it in command arguments.
87
+ acms site connect https://your-site.example --site production
88
+ acms design pull --site production --output draft.json
89
+ # Edit patch values in draft.json; keep its revision.
90
+ acms design push --site production --draft --file draft.json
91
+ acms design pull --site production
92
+ acms design publish --site production --revision 12
93
+ acms design history --site production
94
+ acms design rollback VERSION_ID --site production --revision 13
95
+ ```
96
+
97
+ Use the actual revision returned by your site, not the illustrative numbers above. All remote design writes compare revisions; conflict exits 4 instead of clobbering another editor. Restore and push only stage drafts. Share/revoke require draft-write scope and a revision; reviewer URLs grant no mutation permission and do not reveal unpublished content. Treat a share URL as a credential.
98
+
99
+ Profiles under `~/.config/acms/sites.json` store only URL and token environment-variable name, with restricted permissions. Set ACMS_CONFIG_DIR for an alternate location. Use `--token-env VARIABLE` to choose another token. Tokens are read at invocation and are never written into a project, logs or profile. HTTPS is required except loopback development sites.
100
+
101
+ `content capabilities` discovers the existing content-agent schema; content list/get/create/update/publish use its existing key and API contract with JSON arguments from --file or stdin. Connect a separate profile/token variable when content and design use separate credentials. `media list` and `media upload <image>` use explicit media scopes. Upload runs current img-subir before sending the resulting PNG/JPEG/WebP/GIF; the API caps images at 10 MB. No automatic retry of writes after ambiguous network failure.
102
+
103
+ Every command accepts --json and --help. JSON uses schema acms.cli/1 with ok/data or code/message/details/hints. stdout carries only the result; progress goes to stderr. --dry-run previews mutations. --cwd selects a project. --yes never expands permissions or bypasses compatibility. Exit codes: 0 success, 1 maintenance/check signal, 2 usage/validation, 3 auth, 4 conflict, 5 network/server failure.
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ // Independent Node scheduler: reminders must not depend on page visitors.
3
+ import { setTimeout as sleep } from 'node:timers/promises';
4
+ import { writeFile } from 'node:fs/promises';
5
+ const base = process.env.EMAIL_RUNNER_URL || process.env.SITE_URL;
6
+ const secret = process.env.EMAIL_RUNNER_SECRET;
7
+ if (!base || !secret || secret.length < 32) {
8
+ console.error('Set EMAIL_RUNNER_URL (site origin) and a 32+ character EMAIL_RUNNER_SECRET.');
9
+ process.exit(1);
10
+ }
11
+ const url = new URL('/api/email/run', base);
12
+ if (url.protocol !== 'https:' && !['localhost','127.0.0.1','[::1]'].includes(url.hostname)) {
13
+ console.error('The runner requires HTTPS outside localhost.'); process.exit(1);
14
+ }
15
+ const controller = new AbortController();
16
+ let stopping = false;
17
+ process.on('SIGINT', () => { stopping = true; controller.abort(); });
18
+ process.on('SIGTERM', () => { stopping = true; controller.abort(); });
19
+ do {
20
+ const started = Date.now();
21
+ try {
22
+ const result = await fetch(url, { method: 'POST', headers: { authorization: `Bearer ${secret}`, 'Content-Type': 'application/json', Origin: url.origin }, signal: AbortSignal.any([AbortSignal.timeout(150000), controller.signal]), redirect: 'error' });
23
+ await result.body?.cancel();
24
+ if (!result.ok) throw new Error(`Runner endpoint returned HTTP ${result.status}`);
25
+ if (process.env.SCHEDULER_HEARTBEAT_FILE) await writeFile(process.env.SCHEDULER_HEARTBEAT_FILE, String(Date.now()));
26
+ } catch (error) {
27
+ console.error(new Date().toISOString(), error instanceof Error ? error.message : 'Email runner failed');
28
+ if (process.argv.includes('--once')) process.exitCode = 1;
29
+ }
30
+ if (stopping || process.argv.includes('--once')) break;
31
+ await sleep(Math.max(1000, 60000 - (Date.now() - started)), undefined, { signal: controller.signal }).catch(() => {});
32
+ } while (!stopping);
package/integration.mjs CHANGED
@@ -25,6 +25,7 @@ export const workerEntrypoint = entry('./src/worker.ts');
25
25
  const ROUTES = [
26
26
  // public site (post + tag routes are built from config in acms() below)
27
27
  ['/', 'index.astro'],
28
+ ['/404', '404.astro'],
28
29
  ['/[...slug]', '[...slug].astro'],
29
30
  ['/author/[slug]', 'author/[slug].astro'],
30
31
  ['/links', 'links.astro'],
@@ -53,11 +54,29 @@ const ROUTES = [
53
54
  ['/admin/posts', 'admin/posts/index.astro'],
54
55
  ['/admin/posts/[id]', 'admin/posts/[id].astro'],
55
56
  ['/admin/settings', 'admin/settings.astro'],
57
+ ['/admin/email', 'admin/email.astro'],
58
+ ['/account/[action]', 'account/[action].astro'],
59
+ ['/api/email/settings', 'api/email/settings.ts'],
60
+ ['/api/email/webhooks/[provider]', 'api/email/webhooks/[provider].ts'],
61
+ ['/api/email/run', 'api/email/run.ts'],
62
+ ['/api/ops/status', 'api/ops/status.ts'],
63
+ ['/admin/ai', 'admin/ai.astro'],
56
64
  ['/admin/tags', 'admin/tags.astro'],
57
65
  ['/admin/users', 'admin/users.astro'],
58
66
  // API
59
67
  ['/api/backup', 'api/backup.ts'],
60
68
  ['/api/drafts', 'api/drafts.ts'],
69
+ ['/api/ai/settings', 'api/ai/settings.ts'],
70
+ ['/api/ai/test', 'api/ai/test.ts'],
71
+ ['/api/ai/clean', 'api/ai/clean.ts'],
72
+ ['/api/ai/draft', 'api/ai/draft.ts'],
73
+ ['/api/design', 'api/design/index.ts'],
74
+ ['/api/design/keys', 'api/design/keys.ts'],
75
+ ['/api/design/capabilities', 'api/design/capabilities.ts'],
76
+ ['/api/design/media', 'api/design/media.ts'],
77
+ ['/api/agent', 'api/agent/index.ts'],
78
+ ['/api/agent/docs', 'api/agent/docs.ts'],
79
+ ['/api/mcp', 'api/mcp.ts'],
61
80
  ['/api/media/upload', 'api/media/upload.ts'],
62
81
  ['/api/restore', 'api/restore.ts'],
63
82
  ];
@@ -65,7 +84,7 @@ const ROUTES = [
65
84
  // First segments of the fixed routes above — a configured base landing on one
66
85
  // of these would shadow a real page, so it's a build-time error.
67
86
  const RESERVED_BASES = new Set([
68
- '/admin', '/api', '/author', '/embed', '/go', '/links', '/media', '/preview', '/search',
87
+ '/admin', '/api', '/author', '/embed', '/go', '/links', '/media', '/preview', '/search', '/privacy', '/terms',
69
88
  ]);
70
89
 
71
90
  // Collection keys that would collide with admin sections or read ambiguously.
@@ -199,6 +218,97 @@ function resolveSearch(search) {
199
218
  return { enabled };
200
219
  }
201
220
 
221
+ // Defaults + validation for acms({ calendar }). The calendar module ships on:
222
+ // a main calendar page at `base`, event pages under `eventsBase`, ICS feeds,
223
+ // and the admin Calendar section. `calendar: false` (or enabled: false)
224
+ // removes every calendar route and hides the admin section; events stay in
225
+ // the database. Bases follow the same rules as collection bases.
226
+ const EVENT_FIELD_TYPES = ['text', 'textarea', 'number', 'url', 'select'];
227
+ function resolveCalendar(calendar, contentBases, locales) {
228
+ const defaults = {
229
+ enabled: true, base: '/calendar', eventsBase: '/events', bookings: true, bookBase: '/book',
230
+ resourceKinds: ['Room', 'Person', 'Vehicle', 'Equipment'], defaultView: 'month', eventFields: [],
231
+ };
232
+ if (calendar === false) return { ...defaults, enabled: false };
233
+ if (calendar === undefined || calendar === true) calendar = {};
234
+ if (typeof calendar !== 'object' || calendar === null || Array.isArray(calendar)) {
235
+ throw new Error('acms: calendar must be an object (or false to disable the calendar module)');
236
+ }
237
+ const known = ['enabled', 'base', 'eventsBase', 'bookings', 'bookBase', 'resourceKinds', 'defaultView', 'eventFields'];
238
+ const unknown = Object.keys(calendar).filter((k) => !known.includes(k));
239
+ if (unknown.length > 0) {
240
+ throw new Error(
241
+ `acms: unknown calendar key${unknown.length > 1 ? 's' : ''} ${unknown.map((k) => `"${k}"`).join(', ')} — ` +
242
+ `valid keys are ${known.map((k) => `calendar.${k}`).join(', ')}`
243
+ );
244
+ }
245
+ const enabled = calendar.enabled ?? true;
246
+ if (typeof enabled !== 'boolean') throw new Error('acms: calendar.enabled must be true or false');
247
+ const resolved = {
248
+ enabled,
249
+ base: calendar.base ?? defaults.base,
250
+ eventsBase: calendar.eventsBase ?? defaults.eventsBase,
251
+ bookings: calendar.bookings ?? true,
252
+ bookBase: calendar.bookBase ?? defaults.bookBase,
253
+ resourceKinds: calendar.resourceKinds ?? defaults.resourceKinds,
254
+ defaultView: calendar.defaultView ?? defaults.defaultView,
255
+ eventFields: [],
256
+ };
257
+ if (typeof resolved.bookings !== 'boolean') throw new Error('acms: calendar.bookings must be true or false');
258
+ if (!Array.isArray(resolved.resourceKinds) || !resolved.resourceKinds.length || resolved.resourceKinds.some((k) => typeof k !== 'string' || !k.trim())) {
259
+ throw new Error('acms: calendar.resourceKinds must be a non-empty array of labels, e.g. ["Room", "Person"]');
260
+ }
261
+ if (!['month', 'week', 'list'].includes(resolved.defaultView)) {
262
+ throw new Error(`acms: calendar.defaultView must be "month", "week", or "list" — got "${resolved.defaultView}"`);
263
+ }
264
+ const bases = [['calendar.base', resolved.base], ['calendar.eventsBase', resolved.eventsBase], ['calendar.bookBase', resolved.bookBase]];
265
+ for (const [label, base] of bases) {
266
+ if (typeof base !== 'string' || !/^\/[a-z0-9-]+(\/[a-z0-9-]+)*$/.test(base)) {
267
+ throw new Error(
268
+ `acms: ${label} must start with "/" and use lowercase letters, digits, and hyphens (no trailing slash) — got "${base}"`
269
+ );
270
+ }
271
+ if (RESERVED_BASES.has(base)) throw new Error(`acms: ${label} "${base}" collides with a built-in route — pick another path`);
272
+ if (enabled && contentBases.has(base)) {
273
+ throw new Error(`acms: ${label} "${base}" collides with ${contentBases.get(base)} — pick another path for one of them`);
274
+ }
275
+ if (enabled && locales.some((l) => `/${l}` === base)) {
276
+ throw new Error(`acms: ${label} "${base}" collides with the i18n locale prefix "${base}"`);
277
+ }
278
+ }
279
+ if (new Set(bases.map(([, b]) => b)).size !== bases.length) {
280
+ throw new Error('acms: calendar.base, calendar.eventsBase and calendar.bookBase must be different paths');
281
+ }
282
+ const fields = calendar.eventFields ?? [];
283
+ if (!Array.isArray(fields)) throw new Error('acms: calendar.eventFields must be an array of { key, label, type } objects');
284
+ const keys = new Set();
285
+ for (const [i, f] of fields.entries()) {
286
+ if (!f || typeof f !== 'object') throw new Error(`acms: calendar.eventFields[${i}] must be an object`);
287
+ const key = f.key;
288
+ if (typeof key !== 'string' || !/^[a-z][a-z0-9_]*$/.test(key)) {
289
+ throw new Error(`acms: calendar.eventFields[${i}].key must be lowercase letters, digits, and underscores, starting with a letter — got "${key}"`);
290
+ }
291
+ if (keys.has(key)) throw new Error(`acms: calendar.eventFields has two fields with the key "${key}"`);
292
+ keys.add(key);
293
+ const type = f.type ?? 'text';
294
+ if (!EVENT_FIELD_TYPES.includes(type)) {
295
+ throw new Error(`acms: calendar.eventFields "${key}" has type "${type}" — use one of ${EVENT_FIELD_TYPES.join(', ')}`);
296
+ }
297
+ const options = f.options ?? [];
298
+ if (!Array.isArray(options) || options.some((o) => typeof o !== 'string')) {
299
+ throw new Error(`acms: calendar.eventFields "${key}".options must be an array of strings`);
300
+ }
301
+ if (type === 'select' && options.length === 0) {
302
+ throw new Error(`acms: calendar.eventFields "${key}" is a select field — give it an options array`);
303
+ }
304
+ const label = f.label ?? key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, ' ');
305
+ if (typeof label !== 'string') throw new Error(`acms: calendar.eventFields "${key}".label must be a string`);
306
+ resolved.eventFields.push({ key, label, type, options });
307
+ }
308
+ if (resolved.eventFields.length > 30) throw new Error('acms: calendar.eventFields allows at most 30 fields');
309
+ return resolved;
310
+ }
311
+
202
312
  const semverLt = (a, b) => {
203
313
  const pa = a.split('.').map(Number), pb = b.split('.').map(Number);
204
314
  for (let i = 0; i < 3; i++) { if ((pa[i] ?? 0) < (pb[i] ?? 0)) return true; if ((pa[i] ?? 0) > (pb[i] ?? 0)) return false; }
@@ -236,11 +346,14 @@ const THEMES_ID = 'virtual:acms-user-themes';
236
346
  // they resolve against the consumer's src/ — the core's relative globs can't
237
347
  // see outside the package. Same folder contract as built-in themes.
238
348
  const USER_THEMES_CODE = `
349
+ export const assets = import.meta.glob(['/src/themes/*/assets/**/*', '!**/.img-subir-originals/**'], { eager: true, query: '?url', import: 'default' });
350
+ export const sectionTpls = import.meta.glob('/src/themes/*/sections/*.astro', { eager: true });
239
351
  export const jsons = import.meta.glob('/src/themes/*/theme.json', { eager: true, import: 'default' });
240
352
  export const csses = import.meta.glob('/src/themes/*/theme.css', { eager: true, query: '?raw', import: 'default' });
241
353
  export const homes = import.meta.glob('/src/themes/*/templates/home.astro', { eager: true });
242
354
  export const postTpls = import.meta.glob('/src/themes/*/templates/post-*.astro', { eager: true });
243
355
  export const pageTpls = import.meta.glob('/src/themes/*/templates/page-*.astro', { eager: true });
356
+ export const eventTpls = import.meta.glob('/src/themes/*/templates/event.astro', { eager: true });
244
357
  `;
245
358
 
246
359
  function acmsVitePlugin(userConfig) {
@@ -284,6 +397,15 @@ function acmsVitePlugin(userConfig) {
284
397
  * Public site search: the /search page, the /api/search JSON endpoint, and
285
398
  * the header search modal. On by default — set `enabled: false` to remove
286
399
  * all three.
400
+ * @param {false | {enabled?: boolean, base?: string, eventsBase?: string, defaultView?: 'month' | 'week' | 'list', eventFields?: Array<{key: string, label?: string, type?: 'text' | 'textarea' | 'number' | 'url' | 'select', options?: string[]}>}} [options.calendar]
401
+ * Calendar module: the main calendar page at `base` (default '/calendar')
402
+ * with subscribe feeds (`<base>.ics`, `<base>/<slug>.ics`), event pages at
403
+ * `eventsBase` (default '/events'), a public JSON API under /api/calendar,
404
+ * embeds, and the admin Calendar section. `eventFields` adds custom fields
405
+ * to every event (edited in the event sidebar, exposed to templates and the
406
+ * API). On by default — `calendar: false` removes all of it.
407
+ * @param {false | {enabled?: boolean, operatorName?: string, contactEmail?: string, updatedAt?: string}} [options.legal]
408
+ * Default privacy/terms routes and footer links; published CMS pages at those slugs override the text.
287
409
  * @returns {import('astro').AstroIntegration}
288
410
  */
289
411
  export default function acms(options = {}) {
@@ -294,6 +416,11 @@ export default function acms(options = {}) {
294
416
  ]);
295
417
  const i18n = resolveI18n(options.i18n, contentBases);
296
418
  const search = resolveSearch(options.search);
419
+ const calendar = resolveCalendar(options.calendar, contentBases, i18n.locales);
420
+ const legal = { enabled: true, operatorName: '', contactEmail: '', updatedAt: '', ...(options.legal === false ? { enabled: false } : options.legal ?? {}) };
421
+ if (typeof legal.enabled !== 'boolean' || ['operatorName','contactEmail','updatedAt'].some(k => typeof legal[k] !== 'string')) throw new Error('acms: invalid legal configuration');
422
+ if (legal.contactEmail && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(legal.contactEmail)) throw new Error('acms: legal.contactEmail must be an email address');
423
+ if (legal.updatedAt && (!/^\d{4}-\d{2}-\d{2}$/.test(legal.updatedAt) || !Number.isFinite(Date.parse(legal.updatedAt)) || new Date(legal.updatedAt).toISOString().slice(0,10) !== legal.updatedAt)) throw new Error('acms: legal.updatedAt must be a valid YYYY-MM-DD date');
297
424
  const userConfig = {
298
425
  coreVersion: CORE_VERSION,
299
426
  brand: options.brand ?? {},
@@ -301,8 +428,14 @@ export default function acms(options = {}) {
301
428
  content,
302
429
  i18n,
303
430
  search,
431
+ calendar,
432
+ legal,
304
433
  };
305
- const routes = [...ROUTES];
434
+ const routes = [...ROUTES,
435
+ ['/api/v1/content', 'api/v1/content/index.ts'],
436
+ ['/api/v1/content/[id]', 'api/v1/content/[id].ts'],
437
+ ];
438
+ if (legal.enabled) routes.push(['/privacy', 'legal/privacy.astro'], ['/terms', 'legal/terms.astro']);
306
439
  for (const c of Object.values(content.collections)) {
307
440
  routes.push([c.base, 'blog/index.astro'], [`${c.base}/[slug]`, 'blog/[slug].astro']);
308
441
  }
@@ -320,12 +453,59 @@ export default function acms(options = {}) {
320
453
  if (l !== i18n.defaultLocale) routes.push([`/${l}/search`, 'search.astro']);
321
454
  }
322
455
  }
456
+ if (calendar.enabled) {
457
+ routes.push(
458
+ [calendar.base, 'calendar/index.astro'],
459
+ [`${calendar.base}.ics`, 'calendar/feed.ics.ts'],
460
+ [`${calendar.base}/[slug].ics`, 'calendar/[slug].ics.ts'],
461
+ [`${calendar.eventsBase}/[slug]`, 'events/[slug].astro'],
462
+ [`${calendar.eventsBase}/[slug].ics`, 'events/[slug].ics.ts'],
463
+ ['/embed/calendar/[slug]', 'embed/calendar/[slug].astro'],
464
+ ['/api/v1/events', 'api/v1/events.ts'],
465
+ ['/api/v1/calendars', 'api/v1/calendars.ts'],
466
+ ['/api/calendar/events', 'api/calendar/events/index.ts'],
467
+ ['/api/calendar/calendars', 'api/calendar/calendars.ts'],
468
+ ['/admin/calendar', 'admin/calendar/index.ts'],
469
+ ['/admin/calendar/events', 'admin/calendar/events/index.astro'],
470
+ ['/admin/calendar/events/[id]', 'admin/calendar/events/[id].astro'],
471
+ ['/admin/calendar/calendars', 'admin/calendar/calendars.astro'],
472
+ ['/admin/calendar/integrations', 'admin/calendar/integrations.astro'],
473
+ ['/api/calendar/docs', 'api/calendar/docs.ts'],
474
+ );
475
+ if (calendar.bookings) {
476
+ routes.push(
477
+ [calendar.bookBase, 'book/index.astro'],
478
+ [`${calendar.bookBase}/manage/[token]`, 'book/manage/[token].astro'],
479
+ [`${calendar.bookBase}/[resource].ics`, 'book/[resource].ics.ts'],
480
+ [`${calendar.bookBase}/[resource]`, 'book/[resource]/index.astro'],
481
+ [`${calendar.bookBase}/[resource]/[link]`, 'book/[resource]/[link].astro'],
482
+ ['/embed/book/[resource]/[link]', 'embed/book/[resource]/[link].astro'],
483
+ ['/api/v1/resources', 'api/v1/resources.ts'],
484
+ ['/api/v1/availability', 'api/v1/availability.ts'],
485
+ ['/api/v1/bookings', 'api/v1/bookings.ts'],
486
+ ['/api/calendar/availability', 'api/calendar/availability.ts'],
487
+ ['/api/calendar/bookings', 'api/calendar/bookings/index.ts'],
488
+ ['/api/calendar/bookings/[id]', 'api/calendar/bookings/[id].ts'],
489
+ ['/api/calendar/resources', 'api/calendar/resources.ts'],
490
+ ['/admin/calendar/bookings', 'admin/calendar/bookings.astro'],
491
+ ['/admin/calendar/resources', 'admin/calendar/resources/index.astro'],
492
+ ['/admin/calendar/resources/[id]', 'admin/calendar/resources/[id].astro'],
493
+ ['/admin/calendar/resources/[id]/links/[link]', 'admin/calendar/resources/link.astro'],
494
+ );
495
+ }
496
+ routes.push(['/api/calendar/events/[id]', 'api/calendar/events/[id].ts']);
497
+ }
323
498
  let cacheDir = '';
324
499
  return {
325
500
  name: 'acms',
326
501
  hooks: {
327
502
  'astro:config:setup': ({ config, command, injectRoute, addMiddleware, updateConfig }) => {
328
503
  if (config.cacheDir) cacheDir = fileURLToPath(config.cacheDir);
504
+ if (config.root) {
505
+ const discoveryDir = fileURLToPath(new URL('.astro/', config.root));
506
+ mkdirSync(discoveryDir, {recursive:true});
507
+ writeFileSync(join(discoveryDir,'acms-project.json'), JSON.stringify({ themeApiVersion:1, collections:userConfig.content.collections, calendar:userConfig.calendar }));
508
+ }
329
509
  updateConfig({ vite: { plugins: [acmsVitePlugin(userConfig)] } });
330
510
 
331
511
  // "Project-local routes win" is enforced here: Astro registers
@@ -357,6 +537,17 @@ export default function acms(options = {}) {
357
537
  vite: { resolve: { alias: { 'react-dom/server': 'react-dom/server.edge' } } },
358
538
  });
359
539
  }
540
+ // The Docker runtime image installs only @libsql/client, so the
541
+ // standalone bundle must carry every other dependency. Left to its
542
+ // default, Vite externalizes package imports (react-dom, astro's
543
+ // runtime, zod, marked, …) and the server crashes on its first
544
+ // import in the container. @libsql/client stays external: it is
545
+ // dynamically imported and ships native bindings.
546
+ if (target === 'node' && command === 'build') {
547
+ updateConfig({
548
+ vite: { ssr: { noExternal: true, external: ['@libsql/client'] } },
549
+ });
550
+ }
360
551
  },
361
552
  // fires only for `astro dev` — builds and previews never phone anywhere
362
553
  'astro:server:start': ({ logger }) => {
package/ops-check.mjs ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ // Never accepts a credential in an argument or prints a response body.
3
+ const base = process.env.ACMS_SITE_URL;
4
+ const secret = process.env.EMAIL_RUNNER_SECRET;
5
+ if (!base || !secret || secret.length < 32) {
6
+ console.error('Set ACMS_SITE_URL and EMAIL_RUNNER_SECRET in your environment. Optional: --storage-check'); process.exit(1);
7
+ }
8
+ const origin = new URL(base);
9
+ if (origin.username || origin.password || (origin.protocol !== 'https:' && !(origin.protocol === 'http:' && ['localhost','127.0.0.1','[::1]'].includes(origin.hostname)))) {
10
+ console.error('Use HTTPS, or HTTP on localhost for local testing.'); process.exit(1);
11
+ }
12
+ let failed = false;
13
+ async function check(label, path, options = {}, predicate = r => r.ok) {
14
+ try {
15
+ const response = await fetch(new URL(path, origin), { ...options, redirect: 'error', signal: AbortSignal.timeout(15000) });
16
+ const ok = await predicate(response);
17
+ await response.body?.cancel().catch(() => {});
18
+ console.log(`${ok ? 'PASS' : 'FAIL'} ${label}`); if (!ok) failed = true;
19
+ } catch { console.log(`FAIL ${label}`); failed = true; }
20
+ }
21
+ await check('public content API', '/api/v1/content?limit=1', {}, async r => r.ok && (await r.json()).version === 1);
22
+ await check('privacy page', '/privacy');
23
+ await check('terms page', '/terms');
24
+ const headers = { authorization: `Bearer ${secret}`, 'Content-Type': 'application/json', Origin: origin.origin };
25
+ await check('database, HTTPS settings, secrets and scheduler heartbeat', '/api/ops/status', { headers }, async r => r.ok && (await r.json()).ok === true);
26
+ if (process.argv.includes('--storage-check')) await check('media write/read/delete probe', '/api/ops/status', { method: 'POST', headers }, async r => r.ok && (await r.json()).ok === true);
27
+ process.exitCode = failed ? 1 : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acms-subir/core",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -22,7 +22,9 @@
22
22
  "workers"
23
23
  ],
24
24
  "bin": {
25
- "acms": "./cli.mjs"
25
+ "acms": "cli.mjs",
26
+ "acms-email-runner": "email-runner.mjs",
27
+ "acms-check-deployment": "ops-check.mjs"
26
28
  },
27
29
  "exports": {
28
30
  ".": "./integration.mjs",
@@ -35,32 +37,50 @@
35
37
  "./components/*": "./src/components/*",
36
38
  "./layouts/*": "./src/layouts/*",
37
39
  "./styles/*": "./src/styles/*",
38
- "./themes/*": "./src/themes/*"
40
+ "./themes/*": "./src/themes/*",
41
+ "./theme": "./src/theme-sdk/index.ts",
42
+ "./theme/schema": "./src/theme-sdk/contract.mjs",
43
+ "./theme/components/*": "./src/components/site/*"
39
44
  },
40
45
  "files": [
41
46
  "integration.mjs",
42
47
  "cli.mjs",
43
48
  "src",
44
- "CHANGELOG.md"
49
+ "CHANGELOG.md",
50
+ "email-runner.mjs",
51
+ "deploy",
52
+ "ops-check.mjs",
53
+ "cli",
54
+ "docs"
45
55
  ],
46
56
  "dependencies": {
57
+ "@cloudflare/workers-types": "^5.20260927.1",
47
58
  "@dnd-kit/core": "^6.3.1",
48
59
  "@dnd-kit/sortable": "^10.0.0",
49
60
  "@dnd-kit/utilities": "^3.2.2",
50
61
  "@libsql/client": "^0.15.0",
51
- "@tiptap/extension-code-block-lowlight": "^3.29.2",
52
- "@tiptap/extension-table": "^3.29.2",
53
- "@tiptap/extensions": "^3.29.2",
54
- "@tiptap/pm": "^3.29.2",
55
- "@tiptap/react": "^3.29.2",
56
- "@tiptap/starter-kit": "^3.29.2",
57
- "@tiptap/suggestion": "^3.29.2",
62
+ "@tiptap/extension-code-block-lowlight": "^3.30.1",
63
+ "@tiptap/extension-highlight": "3.30.1",
64
+ "@tiptap/extension-table": "^3.30.1",
65
+ "@tiptap/extensions": "^3.30.1",
66
+ "@tiptap/pm": "^3.30.1",
67
+ "@tiptap/react": "^3.30.1",
68
+ "@tiptap/starter-kit": "^3.30.1",
69
+ "@tiptap/suggestion": "^3.30.1",
70
+ "franc-min": "^6.2.0",
58
71
  "hast-util-to-html": "^9.0.5",
59
72
  "lowlight": "^3.3.0",
73
+ "mammoth": "^1.12.3",
60
74
  "marked": "^15.0.0",
75
+ "pdfjs-dist": "^6.3.289",
61
76
  "prismjs": "^1.30.0",
77
+ "sanitize-html": "^2.17.7",
62
78
  "simple-icons": "^16.27.1",
63
- "zod": "^3.24.0"
79
+ "turndown": "^7.2.4",
80
+ "zod": "^3.24.0",
81
+ "zod-to-json-schema": "^3.25.2",
82
+ "semver": "^7.7.2",
83
+ "tar": "^7.5.1"
64
84
  },
65
85
  "peerDependencies": {
66
86
  "@astrojs/cloudflare": "^12.6.13",
@@ -82,6 +102,9 @@
82
102
  "@types/prismjs": "^1.26.6",
83
103
  "@types/react": "^19.2.18",
84
104
  "@types/react-dom": "^19.2.4",
105
+ "@types/sanitize-html": "^2.16.1",
106
+ "@types/turndown": "^5.0.6",
107
+ "miniflare": "^5.20260801.0-alpha",
85
108
  "vitest": "^3.2.0"
86
109
  },
87
110
  "scripts": {