smoodly 0.0.51 → 0.0.53

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 (292) hide show
  1. package/README.md +14 -13
  2. package/dist/admin/auth.d.ts +3 -11
  3. package/dist/admin/auth.js +1 -30
  4. package/dist/admin/client-ops.d.ts +5 -4
  5. package/dist/admin/client-ops.js +15 -12
  6. package/dist/admin/delivery-mode.d.ts +1 -1
  7. package/dist/admin/delivery-mode.js +2 -2
  8. package/dist/admin/drift.d.ts +0 -2
  9. package/dist/admin/drift.js +1 -1
  10. package/dist/admin/editor/EditorView.d.ts +0 -3
  11. package/dist/admin/editor/EditorView.js +11 -16
  12. package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
  13. package/dist/admin/editor/FloatingToolbar.js +1 -1
  14. package/dist/admin/editor/SeoSettings.d.ts +0 -2
  15. package/dist/admin/editor/SeoSettings.js +2 -2
  16. package/dist/admin/editor/canvas-address.d.ts +8 -0
  17. package/dist/admin/editor/canvas-address.js +18 -2
  18. package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
  19. package/dist/admin/editor/canvas-diagnosis.js +3 -0
  20. package/dist/admin/editor/inline-dom.d.ts +0 -4
  21. package/dist/admin/editor/inline-dom.js +1 -1
  22. package/dist/admin/editor/preview-grant.d.ts +6 -0
  23. package/dist/admin/editor/preview-grant.js +25 -0
  24. package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
  25. package/dist/admin/files.generated.js +1 -1
  26. package/dist/admin/fixed-nodes.d.ts +0 -6
  27. package/dist/admin/fixed-nodes.js +1 -81
  28. package/dist/admin/forms/CropEditor.d.ts +4 -2
  29. package/dist/admin/forms/CropEditor.js +58 -5
  30. package/dist/admin/forms/ImagePreview.d.ts +3 -1
  31. package/dist/admin/forms/ImagePreview.js +9 -3
  32. package/dist/admin/forms/MediaGrid.js +2 -1
  33. package/dist/admin/forms/MediaPicker.js +9 -6
  34. package/dist/admin/forms/MediaWidget.js +11 -9
  35. package/dist/admin/forms/assets-context.d.ts +7 -3
  36. package/dist/admin/forms/assets-context.js +5 -3
  37. package/dist/admin/forms/crop-editor.d.ts +24 -2
  38. package/dist/admin/forms/crop-editor.js +36 -2
  39. package/dist/admin/forms/date-picker.d.ts +0 -1
  40. package/dist/admin/forms/date-picker.js +1 -1
  41. package/dist/admin/forms/tabs.d.ts +0 -1
  42. package/dist/admin/forms/tabs.js +1 -1
  43. package/dist/admin/forms/upload.d.ts +18 -11
  44. package/dist/admin/forms/upload.js +62 -24
  45. package/dist/admin/global-items.d.ts +0 -3
  46. package/dist/admin/global-items.js +0 -54
  47. package/dist/admin/handler.d.ts +22 -28
  48. package/dist/admin/handler.js +146 -47
  49. package/dist/admin/health.d.ts +0 -4
  50. package/dist/admin/index.d.ts +6 -9
  51. package/dist/admin/index.js +3 -6
  52. package/dist/admin/ops.d.ts +40 -32
  53. package/dist/admin/page-data.d.ts +14 -6
  54. package/dist/admin/page-data.js +1 -1
  55. package/dist/admin/session-flow.d.ts +65 -0
  56. package/dist/admin/session-flow.js +121 -0
  57. package/dist/admin/shell/AdminApp.d.ts +8 -3
  58. package/dist/admin/shell/AdminApp.js +37 -58
  59. package/dist/admin/shell/ContentHealthPage.js +1 -3
  60. package/dist/admin/shell/GlobalFields.d.ts +0 -3
  61. package/dist/admin/shell/GlobalFields.js +1 -1
  62. package/dist/admin/shell/IconRail.js +10 -8
  63. package/dist/admin/shell/MediaScreen.js +9 -6
  64. package/dist/admin/shell/MembersPage.d.ts +1 -2
  65. package/dist/admin/shell/MembersPage.js +19 -90
  66. package/dist/admin/shell/SettingsView.d.ts +1 -2
  67. package/dist/admin/shell/SettingsView.js +2 -2
  68. package/dist/admin/shell/SignedOutView.d.ts +5 -0
  69. package/dist/admin/shell/SignedOutView.js +9 -0
  70. package/dist/admin/shell/console-links.d.ts +5 -0
  71. package/dist/admin/shell/console-links.js +11 -0
  72. package/dist/admin/shell/members-list.d.ts +1 -14
  73. package/dist/admin/shell/members-list.js +2 -23
  74. package/dist/admin/shell/pages-tree.d.ts +0 -2
  75. package/dist/admin/shell/pages-tree.js +1 -1
  76. package/dist/admin/shell/presence-socket.d.ts +10 -0
  77. package/dist/admin/shell/presence-socket.js +191 -0
  78. package/dist/admin/shell/presence.d.ts +6 -18
  79. package/dist/admin/shell/presence.js +4 -11
  80. package/dist/admin/shell/session.d.ts +19 -12
  81. package/dist/admin/shell/session.js +114 -47
  82. package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
  83. package/dist/admin/ui/LocaleSwitcher.js +2 -2
  84. package/dist/admin/ui/primitives.d.ts +4 -14
  85. package/dist/admin/ui/primitives.js +7 -9
  86. package/dist/admin/ui/theme.js +13 -2
  87. package/dist/admin/validate.js +10 -1
  88. package/dist/api-client.d.ts +26 -0
  89. package/dist/api-client.js +107 -0
  90. package/dist/assemble.d.ts +4 -41
  91. package/dist/assemble.js +5 -303
  92. package/dist/asset-store.d.ts +28 -30
  93. package/dist/asset-store.js +26 -121
  94. package/dist/astro/cache.d.ts +1 -1
  95. package/dist/astro/components/Head.astro +4 -1
  96. package/dist/astro/components/Image.astro +12 -17
  97. package/dist/astro/components/Video.astro +5 -2
  98. package/dist/astro/head.d.ts +3 -0
  99. package/dist/astro/head.js +24 -0
  100. package/dist/astro/index.js +61 -27
  101. package/dist/astro/locale-paths.d.ts +5 -2
  102. package/dist/astro/locale-paths.js +10 -2
  103. package/dist/astro/routes/admin.js +9 -7
  104. package/dist/astro/runtime.d.ts +3 -2
  105. package/dist/astro/runtime.js +5 -4
  106. package/dist/astro/server.d.ts +7 -11
  107. package/dist/astro/server.js +22 -28
  108. package/dist/astro/shell.d.ts +3 -3
  109. package/dist/astro/shell.js +1 -1
  110. package/dist/astro/static-media-node.d.ts +4 -0
  111. package/dist/astro/static-media-node.js +25 -0
  112. package/dist/astro/static-media.d.ts +17 -0
  113. package/dist/astro/static-media.js +55 -0
  114. package/dist/astro/static.d.ts +5 -10
  115. package/dist/astro/static.js +13 -29
  116. package/dist/canvas-marker.d.ts +14 -0
  117. package/dist/canvas-marker.js +36 -0
  118. package/dist/cli/api.d.ts +14 -0
  119. package/dist/cli/api.js +48 -0
  120. package/dist/cli/app-env.d.ts +2 -0
  121. package/dist/cli/app-env.js +20 -0
  122. package/dist/cli/export.d.ts +16 -2
  123. package/dist/cli/export.js +52 -35
  124. package/dist/cli/import.d.ts +7 -11
  125. package/dist/cli/import.js +50 -54
  126. package/dist/cli/load-config.d.ts +0 -1
  127. package/dist/cli/load-config.js +1 -1
  128. package/dist/cli/main.d.ts +4 -12
  129. package/dist/cli/main.js +26 -72
  130. package/dist/cli/push.d.ts +6 -0
  131. package/dist/cli/push.js +31 -0
  132. package/dist/collections.d.ts +0 -1
  133. package/dist/collections.js +1 -1
  134. package/dist/config.d.ts +20 -5
  135. package/dist/config.js +18 -1
  136. package/dist/content/content-api.d.ts +23 -23
  137. package/dist/content/content-api.js +48 -56
  138. package/dist/content/mime.d.ts +1 -0
  139. package/dist/content/mime.js +6 -0
  140. package/dist/css.js +1 -1
  141. package/dist/deploy-store.d.ts +19 -0
  142. package/dist/entry-store.d.ts +8 -94
  143. package/dist/entry-store.js +9 -557
  144. package/dist/env.d.ts +27 -20
  145. package/dist/env.js +38 -50
  146. package/dist/fields.d.ts +11 -3
  147. package/dist/fields.js +33 -2
  148. package/dist/global-id.d.ts +2 -0
  149. package/dist/global-id.js +4 -2
  150. package/dist/image/SmoodlyImage.d.ts +9 -5
  151. package/dist/image/SmoodlyImage.js +29 -15
  152. package/dist/image/SmoodlyVideo.d.ts +1 -1
  153. package/dist/image/SmoodlyVideo.js +3 -2
  154. package/dist/image/index.d.ts +2 -3
  155. package/dist/image/index.js +0 -2
  156. package/dist/image/markup.d.ts +53 -0
  157. package/dist/image/markup.js +111 -0
  158. package/dist/index.d.ts +13 -23
  159. package/dist/index.js +7 -20
  160. package/dist/media-cut.d.ts +44 -0
  161. package/dist/media-cut.js +70 -0
  162. package/dist/media.d.ts +58 -4
  163. package/dist/media.js +126 -16
  164. package/dist/member-store.d.ts +2 -50
  165. package/dist/member-store.js +0 -56
  166. package/dist/next/admin-route.d.ts +1 -1
  167. package/dist/next/admin-route.js +5 -3
  168. package/dist/next/meta.js +2 -3
  169. package/dist/next/page-renderer.d.ts +2 -2
  170. package/dist/next/page-renderer.js +22 -11
  171. package/dist/next/queries.d.ts +2 -2
  172. package/dist/path-index.d.ts +0 -24
  173. package/dist/path-index.js +0 -98
  174. package/dist/paths.d.ts +1 -1
  175. package/dist/paths.js +1 -1
  176. package/dist/push.d.ts +30 -0
  177. package/dist/push.js +52 -0
  178. package/dist/read-counter.d.ts +2 -0
  179. package/dist/read-counter.js +6 -0
  180. package/dist/reads.d.ts +7 -7
  181. package/dist/reads.js +3 -0
  182. package/dist/registry-json.d.ts +11 -0
  183. package/dist/registry-json.js +81 -0
  184. package/dist/render.d.ts +0 -1
  185. package/dist/render.js +1 -1
  186. package/dist/resolve.d.ts +12 -6
  187. package/dist/resolve.js +11 -8
  188. package/dist/revalidate.d.ts +8 -1
  189. package/dist/revalidate.js +8 -1
  190. package/dist/richtext/index.d.ts +1 -1
  191. package/dist/section-wrapper.d.ts +1 -1
  192. package/dist/section-wrapper.js +0 -9
  193. package/dist/signed-message.d.ts +26 -0
  194. package/dist/signed-message.js +103 -0
  195. package/dist/site.d.ts +59 -18
  196. package/dist/site.js +21 -252
  197. package/dist/space-transfer.d.ts +6 -3
  198. package/dist/space-transfer.js +18 -6
  199. package/dist/store.d.ts +2 -80
  200. package/dist/store.js +3 -489
  201. package/dist/toolset.d.ts +2 -1
  202. package/dist/toolset.js +1 -1
  203. package/dist/version.d.ts +1 -1
  204. package/dist/version.js +1 -1
  205. package/package.json +4 -15
  206. package/dist/admin/auth-config.d.ts +0 -6
  207. package/dist/admin/browser/fetch-op.d.ts +0 -10
  208. package/dist/admin/browser/fetch-op.js +0 -44
  209. package/dist/admin/browser/main.d.ts +0 -1
  210. package/dist/admin/browser/main.js +0 -23
  211. package/dist/admin/ops-impl.d.ts +0 -51
  212. package/dist/admin/ops-impl.js +0 -1403
  213. package/dist/admin/shell/InviteModal.d.ts +0 -6
  214. package/dist/admin/shell/InviteModal.js +0 -28
  215. package/dist/admin/shell/LoginView.d.ts +0 -5
  216. package/dist/admin/shell/LoginView.js +0 -80
  217. package/dist/admin/shell/SchemaNotice.d.ts +0 -4
  218. package/dist/admin/shell/SchemaNotice.js +0 -4
  219. package/dist/admin/shell/SetPasswordView.d.ts +0 -7
  220. package/dist/admin/shell/SetPasswordView.js +0 -41
  221. package/dist/admin/shell/presence-realtime.d.ts +0 -3
  222. package/dist/admin/shell/presence-realtime.js +0 -32
  223. package/dist/admin/supabase-auth.d.ts +0 -7
  224. package/dist/admin/supabase-auth.js +0 -30
  225. package/dist/astro/deploy-deps.d.ts +0 -10
  226. package/dist/astro/deploy-deps.js +0 -8
  227. package/dist/astro/image.d.ts +0 -27
  228. package/dist/astro/image.js +0 -29
  229. package/dist/bind.d.ts +0 -19
  230. package/dist/bind.js +0 -34
  231. package/dist/cli/add-admin.d.ts +0 -11
  232. package/dist/cli/add-admin.js +0 -25
  233. package/dist/cli/load-collections.d.ts +0 -2
  234. package/dist/cli/load-collections.js +0 -27
  235. package/dist/cli/migrate.d.ts +0 -43
  236. package/dist/cli/migrate.js +0 -138
  237. package/dist/cli/rebuild.d.ts +0 -23
  238. package/dist/cli/rebuild.js +0 -22
  239. package/dist/cli/space-io.d.ts +0 -41
  240. package/dist/cli/space-io.js +0 -84
  241. package/dist/client.d.ts +0 -11
  242. package/dist/client.js +0 -54
  243. package/dist/current-space.d.ts +0 -4
  244. package/dist/current-space.js +0 -9
  245. package/dist/document-stamp.d.ts +0 -7
  246. package/dist/document-stamp.js +0 -48
  247. package/dist/image/loaders.d.ts +0 -15
  248. package/dist/image/loaders.js +0 -5
  249. package/dist/image/next-config.d.ts +0 -18
  250. package/dist/image/next-config.js +0 -32
  251. package/dist/image/remote-pattern.d.ts +0 -1
  252. package/dist/image/remote-pattern.js +0 -5
  253. package/dist/migrations.d.ts +0 -37
  254. package/dist/migrations.js +0 -73
  255. package/dist/project.d.ts +0 -88
  256. package/dist/project.js +0 -231
  257. package/dist/published.d.ts +0 -142
  258. package/dist/published.js +0 -164
  259. package/dist/ref-index.d.ts +0 -19
  260. package/dist/ref-index.js +0 -0
  261. package/dist/schema-read.d.ts +0 -23
  262. package/dist/schema-read.js +0 -50
  263. package/dist/schema-version.d.ts +0 -19
  264. package/dist/schema-version.js +0 -34
  265. package/dist/sql-space.d.ts +0 -44
  266. package/dist/sql-space.js +0 -289
  267. package/dist/sql.d.ts +0 -15
  268. package/dist/sql.js +0 -556
  269. package/dist/supabase-asset-store.d.ts +0 -21
  270. package/dist/supabase-asset-store.js +0 -170
  271. package/dist/supabase-entry-store.d.ts +0 -92
  272. package/dist/supabase-entry-store.js +0 -824
  273. package/dist/supabase-member-store.d.ts +0 -21
  274. package/dist/supabase-member-store.js +0 -96
  275. package/dist/supabase-path-index.d.ts +0 -30
  276. package/dist/supabase-path-index.js +0 -138
  277. package/dist/supabase-published-store.d.ts +0 -56
  278. package/dist/supabase-published-store.js +0 -314
  279. package/dist/supabase-store.d.ts +0 -103
  280. package/dist/supabase-store.js +0 -678
  281. package/migrations/20260926120000_smoodly_v1.sql +0 -535
  282. package/migrations/20260926130000_smoodly_v2.sql +0 -26
  283. package/migrations/20260926140000_smoodly_v3.sql +0 -38
  284. package/migrations/20260926150000_smoodly_v4.sql +0 -41
  285. package/migrations/20260929120000_smoodly_v5.sql +0 -56
  286. package/migrations/20260929135523_smoodly_v6.sql +0 -200
  287. package/migrations/20260930040956_smoodly_v7.sql +0 -84
  288. package/migrations/20261007120000_smoodly_v8.sql +0 -154
  289. package/migrations/20261007200000_smoodly_v9.sql +0 -30
  290. package/migrations/20261008120000_smoodly_v10.sql +0 -47
  291. package/migrations/20261009120000_smoodly_v11.sql +0 -49
  292. /package/dist/{admin/auth-config.js → deploy-store.js} +0 -0
@@ -0,0 +1,55 @@
1
+ export const STATIC_MEDIA_DIR = "/_smoodly/media";
2
+ const hex = (bytes) => [...new Uint8Array(bytes)].map((b) => b.toString(16).padStart(2, "0")).join("");
3
+ /** `/_smoodly/media/<sha256(sourceUrl + format), first 16 hex>.<ext>`: the same address and format, the same file. */
4
+ export async function staticMediaPath(sourceUrl, format, ext) {
5
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`${sourceUrl}\n${format}`));
6
+ return `${STATIC_MEDIA_DIR}/${hex(digest).slice(0, 16)}.${ext}`;
7
+ }
8
+ /** An original's extension from its address (`…/a1.jpg` → `jpg`), else `bin`. */
9
+ const extOf = (url) => /\.([a-z0-9]{1,8})$/i.exec(new URL(url).pathname)?.[1]?.toLowerCase() ?? "bin";
10
+ const candidates = (srcset) => srcset.split(", ").map((c) => { const [url, width] = c.trim().split(/\s+/); return { url: url, ...(width ? { width } : {}) }; });
11
+ /** The markup with every media address replaced by a file of the output, each file handed to `register`. */
12
+ export async function rewriteForStatic(m, register) {
13
+ const file = async (url, format) => {
14
+ const path = await staticMediaPath(url, format, format === "original" ? extOf(url) : format);
15
+ register({ url, format, path });
16
+ return path;
17
+ };
18
+ const inFormat = async (list, format) => (await Promise.all(list.map(async (c) => `${await file(c.url, format)}${c.width ? ` ${c.width}` : ""}`))).join(", ");
19
+ // A cut lists widths; an original is one address with no descriptor.
20
+ const cutOf = (srcset) => (srcset && candidates(srcset).every((c) => c.width) ? candidates(srcset) : null);
21
+ const sources = [];
22
+ const both = async (base, list) => {
23
+ sources.push({ ...base, type: "image/avif", srcset: await inFormat(list, "avif") });
24
+ sources.push({ ...base, type: "image/webp", srcset: await inFormat(list, "webp") });
25
+ };
26
+ for (const { srcset, type: _type, ...base } of m.sources) {
27
+ const list = cutOf(srcset);
28
+ if (list)
29
+ await both(base, list);
30
+ else
31
+ sources.push({ ...base, srcset: await inFormat(candidates(srcset), "original") });
32
+ }
33
+ const main = cutOf(m.img.srcset);
34
+ const { srcset: _srcset, sizes, ...img } = m.img;
35
+ if (main) {
36
+ await both({ ...(sizes ? { sizes } : {}), width: img.width, height: img.height }, main);
37
+ return { ...m, sources, img: { ...img, src: await file(m.img.src, "webp") } };
38
+ }
39
+ return { ...m, sources, img: { ...img, ...(sizes ? { sizes } : {}), src: await file(m.img.src, "original") } };
40
+ }
41
+ /** Where a prerendered page hands its files: installed by the integration for one static build. */
42
+ const COLLECTOR = Symbol.for("smoodly.staticMedia");
43
+ /** The build's collector (index.ts, astro:build:start): every file the pages register, once per path. */
44
+ export function collectStaticMedia() {
45
+ const entries = new Map();
46
+ globalThis[COLLECTOR] = (entry) => void entries.set(entry.path, entry);
47
+ return entries;
48
+ }
49
+ /** What Image.astro calls in a prerendered page of static output: the rewrite, each file handed to the build. */
50
+ export async function staticImage(m) {
51
+ const register = globalThis[COLLECTOR];
52
+ if (!register)
53
+ throw new Error("smoodly: a static page rendered outside the integration's `astro build` — no build collects its images.");
54
+ return rewriteForStatic(m, register);
55
+ }
@@ -1,15 +1,10 @@
1
1
  import type { ResolvedConfig } from "../config.ts";
2
- import type { PublishedStore } from "../published.ts";
3
- import type { PageStore } from "../store.ts";
2
+ import type { DeliveryReads } from "../site.ts";
4
3
  import { type LocaleRouting } from "./locale-paths.ts";
5
4
  type Href = (path: string, locale: string) => string;
6
- /** Every published page and entry address of every locale, as Astro's getStaticPaths entries. Pages come from
7
- * the delivery tree (a node with no path, hidden or under a code address, is not a page); entries from the paths
8
- * index's live rows, kept to the entries with a published delivery row — the Supabase delivery store lists entries
9
- * with their document only, no path (0.0.48 built no entry page for it), and the index is what visitors reach. */
5
+ /** Every published page and entry address of every locale, as Astro's getStaticPaths entries, each once. */
10
6
  export declare function staticPaths(deps: {
11
- published: PublishedStore;
12
- pages: PageStore;
7
+ delivery: DeliveryReads;
13
8
  config: ResolvedConfig;
14
9
  href: Href;
15
10
  }): Promise<{
@@ -18,9 +13,9 @@ export declare function staticPaths(deps: {
18
13
  };
19
14
  }[]>;
20
15
  /** The redirect rows as Astro's `redirects` config: old address → the target's current address, 308 (spec
21
- * 2026-09-26 redirects §7 at the host). A target with no live path in the locale is left out: a 404 as on demand. */
16
+ * 2026-09-26 redirects §7 at the host). The service leaves out a target with no live path in the locale. */
22
17
  export declare function redirectEntries(deps: {
23
- pages: PageStore;
18
+ delivery: DeliveryReads;
24
19
  config: ResolvedConfig;
25
20
  href: Href;
26
21
  }): Promise<Record<string, {
@@ -4,32 +4,19 @@ const param = (site) => {
4
4
  const trimmed = site.replace(/^\/+/, "").replace(/\/+$/, "");
5
5
  return trimmed === "" ? undefined : trimmed;
6
6
  };
7
- /** Every published page and entry address of every locale, as Astro's getStaticPaths entries. Pages come from
8
- * the delivery tree (a node with no path, hidden or under a code address, is not a page); entries from the paths
9
- * index's live rows, kept to the entries with a published delivery row — the Supabase delivery store lists entries
10
- * with their document only, no path (0.0.48 built no entry page for it), and the index is what visitors reach. */
7
+ /** Every published page and entry address of every locale, as Astro's getStaticPaths entries, each once. */
11
8
  export async function staticPaths(deps) {
12
9
  const seen = new Set();
13
10
  const out = [];
14
- const add = (site) => {
15
- const p = param(site);
16
- if (seen.has(p))
17
- return;
18
- seen.add(p);
19
- out.push({ params: { path: p } });
20
- };
21
- const collections = (deps.config.registry.collections ?? []).map((c) => c.name);
22
11
  for (const locale of deps.config.locales.supported) {
23
- for (const row of await deps.published.tree(locale))
24
- if (row.path !== null)
25
- add(deps.href(row.path, locale));
26
- const published = new Set();
27
- for (const name of collections)
28
- for (const row of await deps.published.entries(name, { locale }))
29
- published.add(row.id);
30
- for (const row of await deps.pages.listLive(locale))
31
- if (row.kind === "entry" && published.has(row.id))
32
- add(deps.href(row.path, locale));
12
+ const { pages, entries } = await deps.delivery.addresses(locale);
13
+ for (const path of [...pages, ...entries]) {
14
+ const p = param(deps.href(path, locale));
15
+ if (seen.has(p))
16
+ continue;
17
+ seen.add(p);
18
+ out.push({ params: { path: p } });
19
+ }
33
20
  }
34
21
  return out;
35
22
  }
@@ -37,16 +24,13 @@ export async function staticPaths(deps) {
37
24
  * some hosts. A stored path with any of them would redirect more than itself, so it is a literal address or nothing. */
38
25
  const literalAddress = (path) => !/[[\]*:]/.test(path);
39
26
  /** The redirect rows as Astro's `redirects` config: old address → the target's current address, 308 (spec
40
- * 2026-09-26 redirects §7 at the host). A target with no live path in the locale is left out: a 404 as on demand. */
27
+ * 2026-09-26 redirects §7 at the host). The service leaves out a target with no live path in the locale. */
41
28
  export async function redirectEntries(deps) {
42
29
  const out = {};
43
30
  for (const locale of deps.config.locales.supported) {
44
- for (const row of await deps.pages.listRedirects(locale)) {
45
- if (!literalAddress(row.path))
46
- continue;
47
- const current = (await deps.pages.targetPaths({ kind: row.kind, id: row.id }))[locale];
48
- if (current && literalAddress(current))
49
- out[deps.href(row.path, locale)] = { status: 308, destination: deps.href(current, locale) };
31
+ for (const row of (await deps.delivery.addresses(locale)).redirects) {
32
+ if (literalAddress(row.from) && literalAddress(row.to))
33
+ out[deps.href(row.from, locale)] = { status: 308, destination: deps.href(row.to, locale) };
50
34
  }
51
35
  }
52
36
  return out;
@@ -0,0 +1,14 @@
1
+ export type CanvasFlags = {
2
+ editor: true;
3
+ } | {
4
+ version: string;
5
+ };
6
+ /** `path` with the marker as its last segment, before any query or hash. */
7
+ export declare function markCanvasPath(path: string, flags: CanvasFlags): string;
8
+ export type CanvasMarker = {
9
+ segments: string[];
10
+ editor: boolean;
11
+ version?: string;
12
+ };
13
+ /** The marker taken off a catch-all's segments, which a host may hand over encoded or not. */
14
+ export declare function readCanvasMarker(segments: string[]): CanvasMarker;
@@ -0,0 +1,36 @@
1
+ // The canvas marker: where the editor's canvas and History's frame say what they want of a draft render when the
2
+ // host strips the query. Vercel keeps only a prerendered route's own params in the query, draft mode included, so
3
+ // on Next the flags ride the path's last segment: `~editor` (mount the editor bridge) or `~version~<id>` (render a
4
+ // stored version). The admin writes it (admin/editor/canvas-address.ts), the Next renderer reads it inside draft
5
+ // mode only; outside draft mode a marked address is an ordinary miss. A slug never starts with `~`.
6
+ const EDITOR = "~editor";
7
+ const VERSION = "~version~";
8
+ /** `path` with the marker as its last segment, before any query or hash. */
9
+ export function markCanvasPath(path, flags) {
10
+ const cut = path.search(/[?#]/);
11
+ const [pathname, rest] = cut === -1 ? [path, ""] : [path.slice(0, cut), path.slice(cut)];
12
+ const segment = "editor" in flags ? EDITOR : VERSION + encodeURIComponent(flags.version);
13
+ return `${pathname.replace(/\/+$/, "")}/${segment}${rest}`;
14
+ }
15
+ /** The marker taken off a catch-all's segments, which a host may hand over encoded or not. */
16
+ export function readCanvasMarker(segments) {
17
+ const last = segments.length > 0 ? safeDecode(segments[segments.length - 1]) : "";
18
+ const rest = segments.slice(0, -1);
19
+ if (last === EDITOR)
20
+ return { segments: rest, editor: true };
21
+ if (last.startsWith(VERSION) && last.length > VERSION.length) {
22
+ const raw = segments[segments.length - 1];
23
+ // The id was encoded once when the marker was written; a host that decoded the segment already handed it over plain.
24
+ const id = raw.startsWith(VERSION) ? safeDecode(raw.slice(VERSION.length)) : last.slice(VERSION.length);
25
+ return { segments: rest, editor: false, version: id };
26
+ }
27
+ return { segments, editor: false };
28
+ }
29
+ function safeDecode(segment) {
30
+ try {
31
+ return decodeURIComponent(segment);
32
+ }
33
+ catch {
34
+ return segment;
35
+ }
36
+ }
@@ -0,0 +1,14 @@
1
+ export type CliApi = {
2
+ get<T>(path: string): Promise<T>;
3
+ post<T>(path: string, body: unknown): Promise<T>;
4
+ /** The bytes, or null when the API answers 404. */
5
+ getBytes(path: string): Promise<Uint8Array | null>;
6
+ postBytes<T>(path: string, bytes: Uint8Array, mime: string): Promise<T>;
7
+ };
8
+ /** The API refused: its status, its code and its message, as the content API words them. */
9
+ export declare class CliApiError extends Error {
10
+ readonly status: number;
11
+ readonly code: string;
12
+ constructor(status: number, code: string, message: string);
13
+ }
14
+ export declare function connectApi(cwd: string, env?: Record<string, string | undefined>, doFetch?: typeof fetch): CliApi;
@@ -0,0 +1,48 @@
1
+ // The CLI's door to Smoodly's API (plan 2026-10-14 the service holds the stores): the site's env — SMOODLY_API_URL,
2
+ // SMOODLY_SPACE_ID, SMOODLY_SECRET_KEY, from the app's env files — and the content API's envelope `{ ok, data }`.
3
+ import { apiEnv } from "../env.js";
4
+ import { loadAppEnv } from "./app-env.js";
5
+ /** The API refused: its status, its code and its message, as the content API words them. */
6
+ export class CliApiError extends Error {
7
+ constructor(status, code, message) {
8
+ super(message);
9
+ this.status = status;
10
+ this.code = code;
11
+ this.name = "CliApiError";
12
+ }
13
+ }
14
+ export function connectApi(cwd, env = process.env, doFetch = fetch) {
15
+ const read = loadAppEnv(cwd, env);
16
+ if (read.length === 0 && !env.SMOODLY_API_URL && !env.SMOODLY_SECRET_KEY) {
17
+ throw new Error(`smoodly: no .env.local or .env in ${cwd} — run this in your app's folder.`);
18
+ }
19
+ const { apiUrl, spaceId, key } = apiEnv(env);
20
+ const base = `${apiUrl}/v1/spaces/${encodeURIComponent(spaceId)}`;
21
+ async function call(path, init = {}) {
22
+ try {
23
+ return await doFetch(`${base}${path}`, { ...init, headers: { ...init.headers, authorization: `Bearer ${key}` } });
24
+ }
25
+ catch (e) {
26
+ throw new Error(`smoodly: can't reach SMOODLY_API_URL (${apiUrl}): ${e instanceof Error ? e.message : String(e)}`);
27
+ }
28
+ }
29
+ async function data(res) {
30
+ const body = (await res.json().catch(() => null));
31
+ if (body?.ok === true)
32
+ return body.data;
33
+ throw new CliApiError(res.status, typeof body?.code === "string" ? body.code : "internal", typeof body?.message === "string" ? body.message : `smoodly: SMOODLY_API_URL (${apiUrl}) answered ${res.status} with no message.`);
34
+ }
35
+ return {
36
+ get: async (path) => data(await call(path)),
37
+ post: async (path, body) => data(await call(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) })),
38
+ async getBytes(path) {
39
+ const res = await call(path);
40
+ if (res.status === 404)
41
+ return null;
42
+ if (!res.ok)
43
+ return data(res);
44
+ return new Uint8Array(await res.arrayBuffer());
45
+ },
46
+ postBytes: async (path, bytes, mime) => data(await call(path, { method: "POST", headers: { "content-type": mime }, body: bytes })),
47
+ };
48
+ }
@@ -0,0 +1,2 @@
1
+ /** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). Returns the files it read. */
2
+ export declare function loadAppEnv(cwd: string, env?: Record<string, string | undefined>): string[];
@@ -0,0 +1,20 @@
1
+ // The app's env files as the CLI reads them (spec 2026-09-28 export/import §5): `.env.local`, then `.env`, as Next
2
+ // reads them; a variable already set wins.
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { parseEnv } from "node:util";
6
+ /** `.env.local`, then `.env`, as Next reads them; a variable already set wins (spec §5). Returns the files it read. */
7
+ export function loadAppEnv(cwd, env = process.env) {
8
+ const read = [];
9
+ for (const name of [".env.local", ".env"]) {
10
+ const file = join(cwd, name);
11
+ if (!existsSync(file))
12
+ continue;
13
+ read.push(name);
14
+ for (const [key, value] of Object.entries(parseEnv(readFileSync(file, "utf8")))) {
15
+ if (env[key] === undefined)
16
+ env[key] = value;
17
+ }
18
+ }
19
+ return read;
20
+ }
@@ -1,14 +1,28 @@
1
- import { type SpaceConnection } from "./space-io.ts";
1
+ import { type ContentTable } from "../space-transfer.ts";
2
+ import { type ExportMember, type Row } from "../space-transfer.ts";
3
+ import { type CliApi } from "./api.ts";
2
4
  export type ExportArgs = {
3
5
  out: string | null;
4
6
  };
5
7
  export type ExportDeps = {
6
8
  cwd?: string;
7
- connection?: SpaceConnection;
9
+ api?: CliApi;
8
10
  now?: () => Date;
11
+ /** Where SMOODLY_SPACE_ID is read, after connectApi loaded the app's env files into it; default process.env. */
12
+ env?: Record<string, string | undefined>;
13
+ };
14
+ /** What a transfer starts from (the service's GET /transfer). */
15
+ export type TransferHead = {
16
+ schemaVersion: number;
17
+ members: ExportMember[];
9
18
  };
10
19
  export declare const defaultExportDir: (now: Date) => string;
11
20
  export declare const folderExists: (dir: string) => string;
12
21
  export declare const CHANGED_DURING_EXPORT = "Content changed during export \u2014 run it again.";
13
22
  export declare const missingFile: (path: string) => string;
23
+ /** The transfer's head, or null after printing the API's refusal of a database without the schema. */
24
+ export declare function transferHead(api: CliApi, out: (line: string) => void): Promise<TransferHead | null>;
25
+ /** Every page of one table, as the service answers it: `keys` reads only the key columns. */
26
+ export declare function readTable(api: CliApi, table: ContentTable, keys?: boolean): Promise<Row[]>;
27
+ export declare const mediaUrl: (path: string) => string;
14
28
  export declare function runExport(args: ExportArgs, out: (line: string) => void, deps?: ExportDeps): Promise<number>;
@@ -1,21 +1,50 @@
1
1
  // packages/smoodly/src/cli/export.ts
2
- // `smoodly export` (spec 2026-09-28 export/import §8): the key's space —
3
- // every content row, history included, and the media files — into a
4
- // folder. Reads only; the folder appears whole or not at all (written as
5
- // <dir>.partial, renamed last). The message strings are what the guide
6
- // quotes.
7
- import { existsSync } from "node:fs";
2
+ // `smoodly export` (spec 2026-09-28 export/import §8; over the API since plan
3
+ // 2026-10-14 the service holds the stores): the key's space — every content
4
+ // row, history included, and the media files — into a folder. The service
5
+ // pipes the rows without their space; the consistency check runs here, over
6
+ // the whole export. Reads only; the folder appears whole or not at all
7
+ // (written as <dir>.partial, renamed last). The message strings are what the
8
+ // guide quotes.
9
+ import { existsSync, readFileSync } from "node:fs";
8
10
  import { mkdir, rename, rm, writeFile } from "node:fs/promises";
9
11
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
10
- import { schemaBehind, schemaBehindMessage } from "../schema-version.js";
11
- import { CONTENT_TABLES } from "../sql-space.js";
12
- import { FORMAT, brokenReferences, countRows, describeExport, emptyTables, fromSpace, sortRows, } from "../space-transfer.js";
13
- import { MEDIA_BUCKET, connectFromEnv, isNotFound, packageVersion, readSchemaVersion, readSpaceRows, schemaRefusal, } from "./space-io.js";
14
- import { readCurrentSpace } from "../current-space.js";
12
+ import { fileURLToPath } from "node:url";
13
+ import { CONTENT_TABLES } from "../space-transfer.js";
14
+ import { FORMAT, brokenReferences, countRows, describeExport, emptyTables, sortRows, } from "../space-transfer.js";
15
+ import { CliApiError, connectApi } from "./api.js";
15
16
  export const defaultExportDir = (now) => `smoodly-export-${now.toISOString().slice(0, 10)}`;
16
17
  export const folderExists = (dir) => `Refusing: ${dir} already exists. Remove it, or pass --out <dir>.`;
17
18
  export const CHANGED_DURING_EXPORT = "Content changed during export — run it again.";
18
19
  export const missingFile = (path) => `Warning: ${path} is missing from the media bucket (already broken on the site); its asset row is exported without a file.`;
20
+ /** The installed package's version for the manifest: `../../package.json` from src/cli/ and dist/cli/ alike. */
21
+ function packageVersion() {
22
+ return JSON.parse(readFileSync(fileURLToPath(new URL("../../package.json", import.meta.url).href), "utf8")).version;
23
+ }
24
+ /** The transfer's head, or null after printing the API's refusal of a database without the schema. */
25
+ export async function transferHead(api, out) {
26
+ try {
27
+ return await api.get("/transfer");
28
+ }
29
+ catch (e) {
30
+ if (e instanceof CliApiError && e.code === "validation") {
31
+ out(e.message);
32
+ return null;
33
+ }
34
+ throw e;
35
+ }
36
+ }
37
+ /** Every page of one table, as the service answers it: `keys` reads only the key columns. */
38
+ export async function readTable(api, table, keys = false) {
39
+ const rows = [];
40
+ for (let offset = 0; offset !== null;) {
41
+ const page = await api.get(`/transfer/rows/${table}?offset=${offset}${keys ? "&keys=1" : ""}`);
42
+ rows.push(...page.rows);
43
+ offset = page.next;
44
+ }
45
+ return rows;
46
+ }
47
+ export const mediaUrl = (path) => `/transfer/media?path=${encodeURIComponent(path)}`;
19
48
  export async function runExport(args, out, deps = {}) {
20
49
  const cwd = deps.cwd ?? process.cwd();
21
50
  const now = (deps.now ?? (() => new Date()))();
@@ -27,22 +56,16 @@ export async function runExport(args, out, deps = {}) {
27
56
  out(folderExists(shown));
28
57
  return 1;
29
58
  }
30
- const { db } = deps.connection ?? connectFromEnv(cwd);
31
- const schema = await readSchemaVersion(db);
32
- if ("missing" in schema) {
33
- out(schemaRefusal(schema.missing, "Export"));
34
- return 1;
35
- }
36
- const space = await readCurrentSpace(db);
37
- if (schemaBehind(schema.version)) {
38
- out(schemaBehindMessage(schema.version));
59
+ const env = deps.env ?? process.env;
60
+ const api = deps.api ?? connectApi(cwd, env);
61
+ // Read after connectApi: it loads the app's .env files. The space's public id; the service knows its database's.
62
+ const space = env.SMOODLY_SPACE_ID ?? "";
63
+ const head = await transferHead(api, out);
64
+ if (!head)
39
65
  return 1;
40
- }
41
66
  const rows = emptyTables();
42
- for (const table of CONTENT_TABLES) {
43
- const read = await readSpaceRows(db, table, space, table === "assets" ? { status: "ready" } : {});
44
- rows[table] = sortRows(table, read.map((row) => fromSpace(table, row, space)));
45
- }
67
+ for (const table of CONTENT_TABLES)
68
+ rows[table] = sortRows(table, await readTable(api, table));
46
69
  const broken = brokenReferences(rows);
47
70
  if (broken.length > 0) {
48
71
  out(CHANGED_DURING_EXPORT);
@@ -50,9 +73,6 @@ export async function runExport(args, out, deps = {}) {
50
73
  out(` ${line}`);
51
74
  return 1;
52
75
  }
53
- const members = await db.from("members").select("email, role").eq("space_id", space).order("email");
54
- if (members.error)
55
- throw new Error(`smoodly: reading members: ${members.error.message}`);
56
76
  const partial = `${dir}.partial`;
57
77
  await rm(partial, { recursive: true, force: true });
58
78
  await mkdir(join(partial, "rows"), { recursive: true });
@@ -62,15 +82,12 @@ export async function runExport(args, out, deps = {}) {
62
82
  let bytes = 0;
63
83
  for (const asset of rows.assets) {
64
84
  const path = String(asset.path);
65
- const got = await db.storage.from(MEDIA_BUCKET).download(`${space}/${path}`);
66
- if (got.error) {
67
- if (!isNotFound(got.error))
68
- throw new Error(`smoodly: downloading ${path}: ${got.error.message}`);
85
+ const data = await api.getBytes(mediaUrl(path));
86
+ if (!data) {
69
87
  missing.push(path);
70
88
  out(missingFile(path));
71
89
  continue;
72
90
  }
73
- const data = new Uint8Array(await got.data.arrayBuffer());
74
91
  const file = join(partial, "media", path);
75
92
  await mkdir(dirname(file), { recursive: true });
76
93
  await writeFile(file, data);
@@ -82,13 +99,13 @@ export async function runExport(args, out, deps = {}) {
82
99
  }
83
100
  const manifest = {
84
101
  format: FORMAT,
85
- schemaVersion: schema.version,
102
+ schemaVersion: head.schemaVersion,
86
103
  smoodlyVersion: packageVersion(),
87
104
  exportedAt: now.toISOString(),
88
105
  sourceSpace: space,
89
106
  counts: countRows(rows),
90
107
  media: { files, bytes, missing },
91
- members: (members.data ?? []),
108
+ members: head.members,
92
109
  };
93
110
  await writeFile(join(partial, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
94
111
  await rename(partial, dir);
@@ -1,23 +1,19 @@
1
- import { type ContentTable } from "../sql-space.ts";
2
- import { type SpaceConnection } from "./space-io.ts";
3
- import type { ResolvedConfig } from "../config.ts";
1
+ import { type ContentTable } from "../space-transfer.ts";
2
+ import { type CliApi } from "./api.ts";
4
3
  export type ImportArgs = {
5
4
  dir: string;
6
5
  };
7
6
  export type ImportDeps = {
8
7
  cwd?: string;
9
- connection?: SpaceConnection;
10
- /** The app's config, which the closing rebuild assembles with; default: loaded from the folder (load-config.ts). */
11
- config?: ResolvedConfig;
8
+ api?: CliApi;
12
9
  batchSize?: number;
13
10
  /** Test seam: runs before each batch; a throw stops the import there. */
14
11
  beforeBatch?: (table: ContentTable, index: number) => void;
12
+ /** Where SMOODLY_SPACE_ID is read, after connectApi loaded the app's env files into it; default process.env. */
13
+ env?: Record<string, string | undefined>;
15
14
  };
16
- export declare const BATCH_ROWS = 500;
17
- export declare const notAnExport: (dir: string) => string;
18
- export declare const formatMismatch: (format: unknown) => string;
19
15
  export declare const RUN_AGAIN = "Run the same command again to continue: rows and files that landed are skipped.";
20
- export declare const MEMBERS_HEADING = "Members in the source \u2014 invite them in Settings \u203A Members:";
21
- export declare const nothingNew: (space: string) => string;
16
+ export declare const MEMBERS_HEADING = "Members in the source \u2014 add them to the space's members in the project at smoodly.io:";
17
+ export declare const nothingNew: () => string;
22
18
  export declare const CACHE_NOTE = "Restart or redeploy the app: it may have cached pages from while the space was empty.";
23
19
  export declare function runImport(args: ImportArgs, out: (line: string) => void, deps?: ImportDeps): Promise<number>;