smoodly 0.0.50 → 0.0.52

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 (285) hide show
  1. package/README.md +14 -13
  2. package/dist/admin/auth-config.d.ts +6 -5
  3. package/dist/admin/auth.d.ts +3 -11
  4. package/dist/admin/auth.js +1 -30
  5. package/dist/admin/browser/fetch-op.js +3 -2
  6. package/dist/admin/browser/main.js +6 -2
  7. package/dist/admin/client-ops.d.ts +5 -4
  8. package/dist/admin/client-ops.js +14 -11
  9. package/dist/admin/delivery-mode.d.ts +1 -1
  10. package/dist/admin/delivery-mode.js +2 -2
  11. package/dist/admin/drift.d.ts +0 -2
  12. package/dist/admin/drift.js +1 -1
  13. package/dist/admin/editor/EditorView.d.ts +0 -3
  14. package/dist/admin/editor/EditorView.js +11 -16
  15. package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
  16. package/dist/admin/editor/FloatingToolbar.js +1 -1
  17. package/dist/admin/editor/SeoSettings.d.ts +0 -2
  18. package/dist/admin/editor/SeoSettings.js +2 -2
  19. package/dist/admin/editor/canvas-address.d.ts +8 -0
  20. package/dist/admin/editor/canvas-address.js +18 -2
  21. package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
  22. package/dist/admin/editor/canvas-diagnosis.js +3 -0
  23. package/dist/admin/editor/inline-dom.d.ts +0 -4
  24. package/dist/admin/editor/inline-dom.js +1 -1
  25. package/dist/admin/editor/preview-grant.d.ts +6 -0
  26. package/dist/admin/editor/preview-grant.js +25 -0
  27. package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
  28. package/dist/admin/files.generated.js +1 -1
  29. package/dist/admin/fixed-nodes.d.ts +0 -6
  30. package/dist/admin/fixed-nodes.js +1 -81
  31. package/dist/admin/forms/CropEditor.d.ts +4 -2
  32. package/dist/admin/forms/CropEditor.js +58 -5
  33. package/dist/admin/forms/ImagePreview.d.ts +3 -1
  34. package/dist/admin/forms/ImagePreview.js +9 -3
  35. package/dist/admin/forms/MediaGrid.js +2 -1
  36. package/dist/admin/forms/MediaPicker.js +9 -6
  37. package/dist/admin/forms/MediaWidget.js +11 -9
  38. package/dist/admin/forms/assets-context.d.ts +7 -3
  39. package/dist/admin/forms/assets-context.js +5 -3
  40. package/dist/admin/forms/crop-editor.d.ts +24 -2
  41. package/dist/admin/forms/crop-editor.js +36 -2
  42. package/dist/admin/forms/date-picker.d.ts +0 -1
  43. package/dist/admin/forms/date-picker.js +1 -1
  44. package/dist/admin/forms/tabs.d.ts +0 -1
  45. package/dist/admin/forms/tabs.js +1 -1
  46. package/dist/admin/forms/upload.d.ts +18 -11
  47. package/dist/admin/forms/upload.js +62 -24
  48. package/dist/admin/global-items.d.ts +0 -3
  49. package/dist/admin/global-items.js +0 -54
  50. package/dist/admin/handler.d.ts +22 -28
  51. package/dist/admin/handler.js +146 -47
  52. package/dist/admin/health.d.ts +0 -4
  53. package/dist/admin/index.d.ts +5 -8
  54. package/dist/admin/index.js +3 -6
  55. package/dist/admin/ops.d.ts +36 -22
  56. package/dist/admin/page-data.d.ts +14 -6
  57. package/dist/admin/page-data.js +1 -1
  58. package/dist/admin/session-flow.d.ts +43 -0
  59. package/dist/admin/session-flow.js +94 -0
  60. package/dist/admin/shell/AdminApp.d.ts +8 -3
  61. package/dist/admin/shell/AdminApp.js +37 -58
  62. package/dist/admin/shell/ContentHealthPage.js +1 -3
  63. package/dist/admin/shell/GlobalFields.d.ts +0 -3
  64. package/dist/admin/shell/GlobalFields.js +1 -1
  65. package/dist/admin/shell/IconRail.js +5 -4
  66. package/dist/admin/shell/MediaScreen.js +9 -6
  67. package/dist/admin/shell/MembersPage.d.ts +1 -2
  68. package/dist/admin/shell/MembersPage.js +19 -90
  69. package/dist/admin/shell/SettingsView.d.ts +1 -2
  70. package/dist/admin/shell/SettingsView.js +2 -2
  71. package/dist/admin/shell/SignedOutView.d.ts +5 -0
  72. package/dist/admin/shell/SignedOutView.js +9 -0
  73. package/dist/admin/shell/console-links.d.ts +5 -0
  74. package/dist/admin/shell/console-links.js +11 -0
  75. package/dist/admin/shell/members-list.d.ts +1 -14
  76. package/dist/admin/shell/members-list.js +2 -23
  77. package/dist/admin/shell/pages-tree.d.ts +0 -2
  78. package/dist/admin/shell/pages-tree.js +1 -1
  79. package/dist/admin/shell/presence-socket.d.ts +10 -0
  80. package/dist/admin/shell/presence-socket.js +191 -0
  81. package/dist/admin/shell/presence.d.ts +6 -18
  82. package/dist/admin/shell/presence.js +4 -11
  83. package/dist/admin/shell/session.d.ts +20 -12
  84. package/dist/admin/shell/session.js +113 -47
  85. package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
  86. package/dist/admin/ui/LocaleSwitcher.js +2 -2
  87. package/dist/admin/ui/primitives.d.ts +4 -14
  88. package/dist/admin/ui/primitives.js +5 -8
  89. package/dist/admin/ui/theme.js +13 -2
  90. package/dist/admin/validate.js +10 -1
  91. package/dist/api-client.d.ts +26 -0
  92. package/dist/api-client.js +107 -0
  93. package/dist/assemble.d.ts +4 -41
  94. package/dist/assemble.js +4 -302
  95. package/dist/asset-store.d.ts +28 -30
  96. package/dist/asset-store.js +26 -121
  97. package/dist/astro/components/Head.astro +4 -1
  98. package/dist/astro/components/Image.astro +12 -17
  99. package/dist/astro/components/Video.astro +5 -2
  100. package/dist/astro/head.d.ts +3 -0
  101. package/dist/astro/head.js +24 -0
  102. package/dist/astro/index.js +54 -25
  103. package/dist/astro/locale-paths.d.ts +5 -2
  104. package/dist/astro/locale-paths.js +10 -2
  105. package/dist/astro/routes/admin.js +9 -7
  106. package/dist/astro/runtime.d.ts +2 -1
  107. package/dist/astro/runtime.js +4 -3
  108. package/dist/astro/server.d.ts +7 -11
  109. package/dist/astro/server.js +22 -28
  110. package/dist/astro/shell.d.ts +2 -2
  111. package/dist/astro/static-media-node.d.ts +4 -0
  112. package/dist/astro/static-media-node.js +25 -0
  113. package/dist/astro/static-media.d.ts +17 -0
  114. package/dist/astro/static-media.js +55 -0
  115. package/dist/astro/static.d.ts +5 -10
  116. package/dist/astro/static.js +13 -29
  117. package/dist/canvas-marker.d.ts +14 -0
  118. package/dist/canvas-marker.js +36 -0
  119. package/dist/cli/api.d.ts +14 -0
  120. package/dist/cli/api.js +48 -0
  121. package/dist/cli/app-env.d.ts +2 -0
  122. package/dist/cli/app-env.js +20 -0
  123. package/dist/cli/export.d.ts +16 -2
  124. package/dist/cli/export.js +52 -35
  125. package/dist/cli/import.d.ts +7 -11
  126. package/dist/cli/import.js +50 -54
  127. package/dist/cli/load-config.d.ts +0 -1
  128. package/dist/cli/load-config.js +1 -1
  129. package/dist/cli/main.d.ts +4 -12
  130. package/dist/cli/main.js +26 -72
  131. package/dist/cli/push.d.ts +6 -0
  132. package/dist/cli/push.js +31 -0
  133. package/dist/collections.d.ts +0 -1
  134. package/dist/collections.js +1 -1
  135. package/dist/config.d.ts +20 -5
  136. package/dist/config.js +18 -1
  137. package/dist/content/content-api.d.ts +22 -23
  138. package/dist/content/content-api.js +46 -56
  139. package/dist/content/mime.d.ts +1 -0
  140. package/dist/content/mime.js +6 -0
  141. package/dist/deploy-store.d.ts +19 -0
  142. package/dist/deploy-store.js +1 -0
  143. package/dist/entry-store.d.ts +3 -89
  144. package/dist/entry-store.js +2 -550
  145. package/dist/env.d.ts +27 -20
  146. package/dist/env.js +38 -50
  147. package/dist/fields.d.ts +11 -3
  148. package/dist/fields.js +33 -2
  149. package/dist/global-id.d.ts +2 -0
  150. package/dist/global-id.js +4 -2
  151. package/dist/image/SmoodlyImage.d.ts +9 -5
  152. package/dist/image/SmoodlyImage.js +29 -15
  153. package/dist/image/SmoodlyVideo.d.ts +1 -1
  154. package/dist/image/SmoodlyVideo.js +3 -2
  155. package/dist/image/index.d.ts +2 -3
  156. package/dist/image/index.js +0 -2
  157. package/dist/image/markup.d.ts +53 -0
  158. package/dist/image/markup.js +111 -0
  159. package/dist/index.d.ts +12 -22
  160. package/dist/index.js +7 -20
  161. package/dist/media-cut.d.ts +44 -0
  162. package/dist/media-cut.js +70 -0
  163. package/dist/media.d.ts +57 -3
  164. package/dist/media.js +125 -15
  165. package/dist/member-store.d.ts +3 -22
  166. package/dist/member-store.js +1 -53
  167. package/dist/next/admin-route.d.ts +1 -1
  168. package/dist/next/admin-route.js +5 -3
  169. package/dist/next/meta.js +2 -3
  170. package/dist/next/page-renderer.d.ts +2 -2
  171. package/dist/next/page-renderer.js +28 -15
  172. package/dist/next/queries.d.ts +2 -2
  173. package/dist/path-index.d.ts +0 -24
  174. package/dist/path-index.js +0 -98
  175. package/dist/push.d.ts +30 -0
  176. package/dist/push.js +52 -0
  177. package/dist/read-counter.d.ts +2 -0
  178. package/dist/read-counter.js +6 -0
  179. package/dist/reads.d.ts +9 -8
  180. package/dist/reads.js +7 -3
  181. package/dist/registry-json.d.ts +11 -0
  182. package/dist/registry-json.js +81 -0
  183. package/dist/render.d.ts +0 -1
  184. package/dist/render.js +1 -1
  185. package/dist/resolve.d.ts +12 -6
  186. package/dist/resolve.js +11 -8
  187. package/dist/revalidate.d.ts +8 -1
  188. package/dist/revalidate.js +8 -1
  189. package/dist/richtext/index.d.ts +1 -1
  190. package/dist/section-wrapper.d.ts +1 -1
  191. package/dist/section-wrapper.js +0 -9
  192. package/dist/signed-message.d.ts +26 -0
  193. package/dist/signed-message.js +103 -0
  194. package/dist/site.d.ts +59 -18
  195. package/dist/site.js +21 -252
  196. package/dist/space-transfer.d.ts +5 -2
  197. package/dist/space-transfer.js +17 -5
  198. package/dist/store.d.ts +2 -80
  199. package/dist/store.js +3 -489
  200. package/dist/toolset.d.ts +2 -1
  201. package/dist/toolset.js +1 -1
  202. package/dist/version.d.ts +1 -1
  203. package/dist/version.js +1 -1
  204. package/package.json +4 -15
  205. package/dist/admin/ops-impl.d.ts +0 -51
  206. package/dist/admin/ops-impl.js +0 -1400
  207. package/dist/admin/shell/InviteModal.d.ts +0 -6
  208. package/dist/admin/shell/InviteModal.js +0 -28
  209. package/dist/admin/shell/LoginView.d.ts +0 -5
  210. package/dist/admin/shell/LoginView.js +0 -80
  211. package/dist/admin/shell/SchemaNotice.d.ts +0 -4
  212. package/dist/admin/shell/SchemaNotice.js +0 -4
  213. package/dist/admin/shell/SetPasswordView.d.ts +0 -7
  214. package/dist/admin/shell/SetPasswordView.js +0 -41
  215. package/dist/admin/shell/presence-realtime.d.ts +0 -3
  216. package/dist/admin/shell/presence-realtime.js +0 -32
  217. package/dist/admin/supabase-auth.d.ts +0 -7
  218. package/dist/admin/supabase-auth.js +0 -30
  219. package/dist/astro/deploy-deps.d.ts +0 -10
  220. package/dist/astro/deploy-deps.js +0 -8
  221. package/dist/astro/image.d.ts +0 -27
  222. package/dist/astro/image.js +0 -29
  223. package/dist/bind.d.ts +0 -19
  224. package/dist/bind.js +0 -34
  225. package/dist/cli/add-admin.d.ts +0 -11
  226. package/dist/cli/add-admin.js +0 -25
  227. package/dist/cli/load-collections.d.ts +0 -2
  228. package/dist/cli/load-collections.js +0 -27
  229. package/dist/cli/migrate.d.ts +0 -43
  230. package/dist/cli/migrate.js +0 -138
  231. package/dist/cli/rebuild.d.ts +0 -23
  232. package/dist/cli/rebuild.js +0 -22
  233. package/dist/cli/space-io.d.ts +0 -41
  234. package/dist/cli/space-io.js +0 -84
  235. package/dist/client.d.ts +0 -11
  236. package/dist/client.js +0 -54
  237. package/dist/current-space.d.ts +0 -4
  238. package/dist/current-space.js +0 -9
  239. package/dist/document-stamp.d.ts +0 -7
  240. package/dist/document-stamp.js +0 -48
  241. package/dist/image/loaders.d.ts +0 -15
  242. package/dist/image/loaders.js +0 -5
  243. package/dist/image/next-config.d.ts +0 -18
  244. package/dist/image/next-config.js +0 -32
  245. package/dist/image/remote-pattern.d.ts +0 -1
  246. package/dist/image/remote-pattern.js +0 -5
  247. package/dist/migrations.d.ts +0 -37
  248. package/dist/migrations.js +0 -73
  249. package/dist/project.d.ts +0 -88
  250. package/dist/project.js +0 -231
  251. package/dist/published.d.ts +0 -142
  252. package/dist/published.js +0 -164
  253. package/dist/ref-index.d.ts +0 -19
  254. package/dist/ref-index.js +0 -0
  255. package/dist/schema-read.d.ts +0 -23
  256. package/dist/schema-read.js +0 -50
  257. package/dist/schema-version.d.ts +0 -19
  258. package/dist/schema-version.js +0 -34
  259. package/dist/sql-space.d.ts +0 -44
  260. package/dist/sql-space.js +0 -289
  261. package/dist/sql.d.ts +0 -15
  262. package/dist/sql.js +0 -556
  263. package/dist/supabase-asset-store.d.ts +0 -21
  264. package/dist/supabase-asset-store.js +0 -170
  265. package/dist/supabase-entry-store.d.ts +0 -92
  266. package/dist/supabase-entry-store.js +0 -824
  267. package/dist/supabase-member-store.d.ts +0 -21
  268. package/dist/supabase-member-store.js +0 -96
  269. package/dist/supabase-path-index.d.ts +0 -30
  270. package/dist/supabase-path-index.js +0 -138
  271. package/dist/supabase-published-store.d.ts +0 -56
  272. package/dist/supabase-published-store.js +0 -314
  273. package/dist/supabase-store.d.ts +0 -103
  274. package/dist/supabase-store.js +0 -678
  275. package/migrations/20260926120000_smoodly_v1.sql +0 -535
  276. package/migrations/20260926130000_smoodly_v2.sql +0 -26
  277. package/migrations/20260926140000_smoodly_v3.sql +0 -38
  278. package/migrations/20260926150000_smoodly_v4.sql +0 -41
  279. package/migrations/20260929120000_smoodly_v5.sql +0 -56
  280. package/migrations/20260929135523_smoodly_v6.sql +0 -200
  281. package/migrations/20260930040956_smoodly_v7.sql +0 -84
  282. package/migrations/20261007120000_smoodly_v8.sql +0 -154
  283. package/migrations/20261007200000_smoodly_v9.sql +0 -30
  284. package/migrations/20261008120000_smoodly_v10.sql +0 -47
  285. package/migrations/20261009120000_smoodly_v11.sql +0 -49
package/dist/cli/main.js CHANGED
@@ -1,29 +1,25 @@
1
1
  // packages/smoodly/src/cli/main.ts
2
- // `smoodly` — the package's command line. Five subcommands: migrate
3
- // (spec 2026-09-26 §2), export and import (spec 2026-09-28), add-admin
4
- // (spec 2026-09-30 tester findings §1), rebuild (spec 2026-10-07 delivery model §4). Exit codes:
5
- // 0 done or nothing to do, 1 pending (--check), refused or failed, 2 usage.
2
+ // `smoodly` — the package's command line. Three subcommands: push (spec
3
+ // 2026-10-16 the control plane §4), export and import (spec 2026-09-28). A
4
+ // space's schema lives in its object, sent there by the push, so there is
5
+ // nothing to migrate or rebuild;
6
+ // its members are set by smoodly.io (spec 2026-10-16 the control plane §5),
7
+ // so there is no add-admin. Exit codes: 0 done or nothing to do,
8
+ // 1 refused or failed, 2 usage.
6
9
  import { resolve } from "node:path";
7
10
  import { fileURLToPath } from "node:url";
8
- import { DEFAULT_COLLECTIONS, runMigrate } from "./migrate.js";
9
11
  import { runExport } from "./export.js";
10
12
  import { runImport } from "./import.js";
11
- import { runAddAdmin } from "./add-admin.js";
12
- import { runRebuild } from "./rebuild.js";
13
+ import { runPush } from "./push.js";
13
14
  import { SMOODLY_VERSION } from "../version.js";
14
15
  export const USAGE = `usage: smoodly <command> (smoodly --version prints the version)
16
+ Every command reads SMOODLY_API_URL, SMOODLY_SPACE_ID and SMOODLY_SECRET_KEY from the app's .env.local or .env.
15
17
 
16
- migrate [--check] [--collections <file>] [--migrations <dir>]
17
- Copies the core schema files this package ships that supabase/migrations/
18
- lacks, and writes one views file for the collections whose view is new or
19
- changed. Prints the push or reset command; never runs it.
20
-
21
- --check write nothing; exit 1 when anything is pending, or when
22
- SMOODLY_SECRET_KEY (if set) cannot read the database
23
- --collections <file> the module exporting \`collections\` (default
24
- smoodly/collections.ts, or src/smoodly/collections.ts
25
- when only that one exists)
26
- --migrations <dir> the app's migration folder (default supabase/migrations)
18
+ push
19
+ Sends the app's registry (its smoodly config), its address (SITE_URL) and
20
+ the package version to the space. A Next site's prebuild runs it, Astro's
21
+ build and both dev servers push on their own; run it from CI or a script.
22
+ SMOODLY_STATIC=1 marks a static site. As a prebuild it never fails the build.
27
23
 
28
24
  export [--out <dir>]
29
25
  Writes the space SMOODLY_SECRET_KEY names — every content row, history
@@ -32,20 +28,7 @@ export const USAGE = `usage: smoodly <command> (smoodly --version prints the
32
28
 
33
29
  import <dir>
34
30
  Writes an export folder into the key's space, which must be empty or hold
35
- only this export's rows (an earlier run to continue). Never deletes. Ends
36
- by rebuilding the delivery tables with the app's config (smoodly.config.ts,
37
- smoodly/config.ts or src/smoodly/config.ts), so run it in the app's folder.
38
-
39
- rebuild
40
- Rewrites the delivery tables of the key's space: the finished document of
41
- every published page, entry and global, assembled with the app's config.
42
- A publish keeps them current and a document behind the content model is
43
- rebuilt by its first visitor; run this after a deploy for a warm start.
44
-
45
- add-admin <email> [--admin-url <url>]
46
- Makes <email> an admin of the key's space: with SMOODLY_ADMIN_PASSWORD
47
- set, creates the account with that password; without, mails an invite
48
- whose link lands on --admin-url (default: SITE_URL + /admin).`;
31
+ only this export's rows (an earlier run to continue). Never deletes.`;
49
32
  export function parseArgs(argv) {
50
33
  const [command, ...rest] = argv;
51
34
  if (!command || command === "help" || argv.some((a) => a === "--help" || a === "-h"))
@@ -63,42 +46,17 @@ export function parseArgs(argv) {
63
46
  }
64
47
  return { command: "export", args };
65
48
  }
49
+ if (command === "push") {
50
+ if (rest.length > 0)
51
+ return { command: "error", message: `unexpected argument "${rest[0]}"` };
52
+ return { command: "push", args: {} };
53
+ }
66
54
  if (command === "import") {
67
55
  if (rest.length !== 1 || rest[0].startsWith("-"))
68
56
  return { command: "error", message: "import needs one export folder: smoodly import <dir>" };
69
57
  return { command: "import", args: { dir: rest[0] } };
70
58
  }
71
- if (command === "rebuild") {
72
- if (rest.length > 0)
73
- return { command: "error", message: `unexpected argument "${rest[0]}"` };
74
- return { command: "rebuild", args: {} };
75
- }
76
- if (command === "add-admin") {
77
- const usage = { command: "error", message: "add-admin needs one email: smoodly add-admin <email> [--admin-url <url>]" };
78
- const [email, ...flags] = rest;
79
- if (!email || email.startsWith("-"))
80
- return usage;
81
- if (flags.length === 0)
82
- return { command: "add-admin", args: { email } };
83
- if (flags.length !== 2 || flags[0] !== "--admin-url" || flags[1].startsWith("-"))
84
- return usage;
85
- return { command: "add-admin", args: { email, adminUrl: flags[1] } };
86
- }
87
- if (command !== "migrate")
88
- return { command: "error", message: `unknown command "${command}"` };
89
- const args = { check: false, collections: DEFAULT_COLLECTIONS, migrations: "supabase/migrations" };
90
- for (let i = 0; i < rest.length; i++) {
91
- const a = rest[i];
92
- if (a === "--check")
93
- args.check = true;
94
- else if (a === "--collections" && rest[i + 1] !== undefined)
95
- args.collections = rest[++i];
96
- else if (a === "--migrations" && rest[i + 1] !== undefined)
97
- args.migrations = rest[++i];
98
- else
99
- return { command: "error", message: `unexpected argument "${a}"` };
100
- }
101
- return { command: "migrate", args };
59
+ return { command: "error", message: `unknown command "${command}"` };
102
60
  }
103
61
  export async function main(argv, out = console.log) {
104
62
  const parsed = parseArgs(argv);
@@ -115,23 +73,19 @@ export async function main(argv, out = console.log) {
115
73
  return 2;
116
74
  }
117
75
  try {
76
+ if (parsed.command === "push")
77
+ return await runPush(parsed.args, out);
118
78
  if (parsed.command === "import")
119
79
  return await runImport(parsed.args, out);
120
- if (parsed.command === "export")
121
- return await runExport(parsed.args, out);
122
- if (parsed.command === "add-admin")
123
- return await runAddAdmin(parsed.args, out);
124
- if (parsed.command === "rebuild")
125
- return await runRebuild(parsed.args, out);
126
- return await runMigrate(parsed.args, out);
80
+ return await runExport(parsed.args, out);
127
81
  }
128
82
  catch (e) {
129
83
  out(e instanceof Error ? e.message : String(e));
130
84
  return 1;
131
85
  }
132
86
  }
133
- // Run when this file is the entry (`tsx src/cli/main.ts …`, the repo's
134
- // `npm run db:migrate`); the published bin imports `main` and runs it itself.
87
+ // Run when this file is the entry (`tsx src/cli/main.ts …`); the published bin
88
+ // imports `main` and runs it itself.
135
89
  if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(new URL(import.meta.url))) {
136
90
  void main(process.argv.slice(2)).then((code) => {
137
91
  process.exitCode = code;
@@ -0,0 +1,6 @@
1
+ export type PushArgs = Record<string, never>;
2
+ export declare function runPush(_args: PushArgs, out: (line: string) => void, deps?: {
3
+ cwd?: string;
4
+ env?: Record<string, string | undefined>;
5
+ fetch?: typeof fetch;
6
+ }): Promise<number>;
@@ -0,0 +1,31 @@
1
+ // `smoodly push` (spec 2026-10-16 the control plane §4): the app's registry, its address (SITE_URL) and the package
2
+ // version to the space SMOODLY_SECRET_KEY names. The Next templates' prebuild runs it; Astro's build hook and the API
3
+ // client in development push the same body themselves. Static mode when SMOODLY_STATIC=1, else live. A refusal exits 1,
4
+ // except when npm runs the command as a build's `prebuild`: a push never fails a build (the spec's rule), so there it
5
+ // says what failed and exits 0, and the site runs against the schema the space has.
6
+ import { apiEnv } from "../env.js";
7
+ import { pushBody, pushSchema } from "../push.js";
8
+ import { loadAppEnv } from "./app-env.js";
9
+ import { loadConfig } from "./load-config.js";
10
+ export async function runPush(_args, out, deps = {}) {
11
+ const cwd = deps.cwd ?? process.cwd();
12
+ const env = deps.env ?? process.env;
13
+ loadAppEnv(cwd, env);
14
+ try {
15
+ const config = await loadConfig(cwd);
16
+ const { apiUrl, spaceId, key } = apiEnv(env);
17
+ const body = pushBody(config, env, env.SMOODLY_STATIC === "1" ? "static" : "live");
18
+ const { version, changed } = await pushSchema({ apiUrl, spaceId, key, fetch: deps.fetch }, body);
19
+ out(changed ? `Pushed schema version ${version} to ${spaceId}.` : `Schema unchanged (version ${version}).`);
20
+ return 0;
21
+ }
22
+ catch (e) {
23
+ const message = e instanceof Error ? e.message : String(e);
24
+ if (env.npm_lifecycle_event === "prebuild") {
25
+ out(`smoodly: the schema push failed — ${message}; the site runs against the schema the space has.`);
26
+ return 0;
27
+ }
28
+ out(message);
29
+ return 1;
30
+ }
31
+ }
@@ -8,7 +8,6 @@ export type EntryOrder = "manual" | {
8
8
  direction: "asc" | "desc";
9
9
  };
10
10
  export declare const DEFAULT_ORDER: EntryOrder;
11
- export declare const BUILT_IN_ORDER_FIELDS: readonly ["createdAt", "updatedAt"];
12
11
  export type CollectionSchema<F extends BuilderMap = BuilderMap, N extends string = string> = {
13
12
  /** "global" marks a code-declared singleton (spec 2026-09-07 decision
14
13
  * 4): a global item IS a collection with one entry, so both entry
@@ -1,6 +1,6 @@
1
1
  import { SEO_DESCRIPTOR, SEO_KEY, validateCollectionSeo } from "./seo.js";
2
2
  export const DEFAULT_ORDER = { by: "createdAt", direction: "desc" };
3
- export const BUILT_IN_ORDER_FIELDS = ["createdAt", "updatedAt"];
3
+ const BUILT_IN_ORDER_FIELDS = ["createdAt", "updatedAt"];
4
4
  export const collectionOrder = (c) => c.order ?? DEFAULT_ORDER;
5
5
  export const collectionDepth = (c) => c.tree?.depth ?? 1;
6
6
  export function collection(input) {
package/dist/config.d.ts CHANGED
@@ -3,8 +3,10 @@ import type { CollectionSchema } from "./collections.ts";
3
3
  import type { GlobalSchema } from "./global.ts";
4
4
  import { type Addresses } from "./addresses.ts";
5
5
  import type { Anchors } from "./anchors.ts";
6
+ import { type ImageQuality } from "./media-cut.ts";
6
7
  import { type StyleValues, type StyleValuesInput } from "./styles.ts";
7
8
  import { type CustomConfigInput, type ResolvedCustom } from "./custom/index.ts";
9
+ import type { LocaleRouting } from "./astro/locale-paths.ts";
8
10
  /** A section, element or page schema as the config reads it. */
9
11
  export type RegisteredSchema = {
10
12
  kind: string;
@@ -58,16 +60,24 @@ export type SmoodlyConfig = {
58
60
  database?: {
59
61
  provider: string;
60
62
  };
61
- /** Upload limits (spec 2026-09-08 media §10): bytes per file and the accepted mime types. */
63
+ /** Upload limits (spec 2026-09-08 media §10): bytes per file and the accepted mime types. The project's image
64
+ * quality and srcset cap (spec 2026-10-18 image quality and max width §1), under a field's and a view's own. */
62
65
  media?: {
63
66
  maxSize?: number;
64
67
  accept?: string[];
68
+ quality?: ImageQuality;
69
+ maxWidth?: number;
65
70
  };
66
71
  /** Where the admin is mounted (spec 2026-09-17): must match the route
67
72
  * folder under app/(smoodly)/. Default "/admin". */
68
73
  admin?: {
69
74
  path?: string;
70
75
  };
76
+ /** Where the default locale's pages live (spec 2026-10-16 the control plane §4): at the root (default), or under
77
+ * its code like every other locale. Astro's integration holds it equal to astro.config's i18n. */
78
+ routing?: {
79
+ prefixDefaultLocale: boolean;
80
+ };
71
81
  };
72
82
  /** The registry as defineConfig resolves it: schemas, and page types only under `pages`. */
73
83
  export type Registry = {
@@ -77,7 +87,7 @@ export type Registry = {
77
87
  globals: GlobalSchema[];
78
88
  pages: RegisteredSchema[];
79
89
  };
80
- export type ResolvedConfig = Omit<SmoodlyConfig, "styles" | "custom" | "admin" | "registry"> & {
90
+ export type ResolvedConfig = Omit<SmoodlyConfig, "styles" | "custom" | "admin" | "registry" | "routing"> & {
81
91
  registry: Registry;
82
92
  homePageSlug: string;
83
93
  /** How deep editors may nest pages in the tree. Default 1 (flat). */
@@ -93,17 +103,22 @@ export type ResolvedConfig = Omit<SmoodlyConfig, "styles" | "custom" | "admin" |
93
103
  media: {
94
104
  maxSize: number;
95
105
  accept: string[];
106
+ quality?: ImageQuality;
107
+ maxWidth?: number;
96
108
  };
97
109
  /** The admin's mount path, always present. */
98
110
  admin: {
99
111
  path: string;
100
112
  };
113
+ /** Where the default locale's pages live; `prefixDefaultLocale` is false unless the config says otherwise. */
114
+ routing: {
115
+ prefixDefaultLocale: boolean;
116
+ };
101
117
  /** Code-owned addresses by owner key (spec 2026-09-29 nested paths §3). */
102
118
  addresses: Addresses;
103
119
  /** The path index's view of the code-owned addresses (spec 2026-09-29 anchored paths §2): pass it to the index. */
104
120
  anchors: Anchors;
105
121
  };
106
- /** `global`: the placement node type and an admin route; `asset`: the refs
107
- * target of every media edge; `media`: the admin's Media screen route. */
108
- export declare const RESERVED_NAMES: readonly string[];
109
122
  export declare function defineConfig(config: SmoodlyConfig): ResolvedConfig;
123
+ /** The address rule the config's routing asks for: a prefixed default locale, whose bare root goes to its home. */
124
+ export declare const configRouting: (config: Pick<ResolvedConfig, "routing">) => LocaleRouting;
package/dist/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { assertSegment, fixedPageSegments } from "./paths.js";
2
2
  import { addressMap, assertAddress, codeAddresses, lastPart } from "./addresses.js";
3
3
  import { DEFAULT_MEDIA } from "./media.js";
4
+ import { isImageQuality, isMaxWidth, qualityError } from "./media-cut.js";
4
5
  import { DEFAULT_ADMIN_PATH, assertAdminPath, isAdminSlug } from "./admin-path.js";
5
6
  import { resolveStyleValues, styleDescriptors } from "./styles.js";
6
7
  import { resolveCustom } from "./custom/index.js";
@@ -8,7 +9,7 @@ import { isValidZone } from "./time-zone.js";
8
9
  import { schemaOnly } from "./registration.js";
9
10
  /** `global`: the placement node type and an admin route; `asset`: the refs
10
11
  * target of every media edge; `media`: the admin's Media screen route. */
11
- export const RESERVED_NAMES = ["global", "asset", "media"];
12
+ const RESERVED_NAMES = ["global", "asset", "media"];
12
13
  const isReserved = (name) => RESERVED_NAMES.includes(name);
13
14
  function normalizeRegistry(config) {
14
15
  const collections = config.registry.collections ?? [];
@@ -234,13 +235,23 @@ export function defineConfig(config) {
234
235
  const styles = resolveStyleValues(config.styles);
235
236
  const custom = resolveCustom(config.custom);
236
237
  const byName = new Map(registry.sections.map((s) => [s.name, s]));
238
+ const quality = config.media?.quality, maxWidth = config.media?.maxWidth;
237
239
  const media = {
238
240
  maxSize: config.media?.maxSize ?? DEFAULT_MEDIA.maxSize,
239
241
  accept: config.media?.accept ? [...config.media.accept] : [...DEFAULT_MEDIA.accept],
242
+ ...(quality !== undefined ? { quality } : {}),
243
+ ...(maxWidth !== undefined ? { maxWidth } : {}),
240
244
  };
241
245
  if (!Number.isInteger(media.maxSize) || media.maxSize <= 0) {
242
246
  throw new Error("smoodly: media.maxSize must be a positive integer of bytes.");
243
247
  }
248
+ if (quality !== undefined && !isImageQuality(quality))
249
+ throw new Error(qualityError(quality, "media.quality"));
250
+ if (maxWidth !== undefined && !isMaxWidth(maxWidth))
251
+ throw new Error("smoodly: media.maxWidth must be a positive whole number of pixels.");
252
+ const prefixDefaultLocale = config.routing?.prefixDefaultLocale ?? false;
253
+ if (typeof prefixDefaultLocale !== "boolean")
254
+ throw new Error("smoodly: routing.prefixDefaultLocale must be true or false.");
244
255
  const addresses = addressMap(codeAddresses({ locales: config.locales, pages: registry.pages, collections: registry.collections }));
245
256
  return {
246
257
  ...config,
@@ -249,6 +260,7 @@ export function defineConfig(config) {
249
260
  pageDepth: depth,
250
261
  media,
251
262
  admin: { path: adminPath },
263
+ routing: { prefixDefaultLocale },
252
264
  addresses,
253
265
  anchors: { addresses, homePageSlug, defaultLocale: config.locales.default },
254
266
  styles,
@@ -264,3 +276,8 @@ export function defineConfig(config) {
264
276
  },
265
277
  };
266
278
  }
279
+ /** The address rule the config's routing asks for: a prefixed default locale, whose bare root goes to its home. */
280
+ export const configRouting = (config) => ({
281
+ prefixDefault: config.routing.prefixDefaultLocale,
282
+ redirectToDefault: config.routing.prefixDefaultLocale,
283
+ });
@@ -1,11 +1,11 @@
1
- import { createAdminOps } from "../admin/ops-impl.ts";
2
- import type { EntryCreateInput, OpError, PageCreateInput } from "../admin/ops.ts";
1
+ import type { AdminOpCall, EntryCreateInput, OpError, OpResult, PageCreateInput } from "../admin/ops.ts";
2
+ import type { PathHit } from "../path-index.ts";
3
3
  import type { FieldError } from "../admin/validate.ts";
4
4
  import type { EntryRecord } from "../entry-store.ts";
5
5
  import type { PageRecord } from "../store.ts";
6
6
  import type { PageTree } from "../page-context.ts";
7
7
  import type { ImageValue } from "../media.ts";
8
- import type { MemberStore } from "../member-store.ts";
8
+ import type { ResolvedConfig } from "../config.ts";
9
9
  export type UploadMeta = {
10
10
  filename?: string;
11
11
  mime?: string;
@@ -19,17 +19,25 @@ export declare class SmoodlyContentError extends Error {
19
19
  readonly fields: FieldError[];
20
20
  constructor(error: Pick<OpError, "code" | "message" | "fields">);
21
21
  }
22
- export type ContentDeps = Omit<Parameters<typeof createAdminOps>[0], "actor">;
23
- /** With a password: the account is created (or an existing one kept) and made an admin. Without: an invite mail,
24
- * whose link goes to `redirectTo` or the Auth project's Site URL. Running it again changes nothing.
25
- * Its own function because `smoodly add-admin` has a member store and no config. */
26
- export declare function addAdmin(members: MemberStore | undefined, email: string, options?: {
27
- password?: string;
28
- redirectTo?: string;
29
- }): Promise<{
30
- email: string;
31
- invited: boolean;
32
- }>;
22
+ /** What content() writes through: the operations, the paths index's lookup, an upload. */
23
+ export type ContentTransport = {
24
+ op(call: AdminOpCall): Promise<OpResult<unknown>>;
25
+ locate(locale: string, path: string): Promise<PathHit | null>;
26
+ upload(bytes: Uint8Array, filename: string, mime: string | undefined, meta: UploadMeta): Promise<ImageValue>;
27
+ };
28
+ export type ContentDeps = {
29
+ config: ResolvedConfig;
30
+ href?: (path: string, locale: string) => string;
31
+ transport: ContentTransport;
32
+ };
33
+ /** Over the content API with the secret key (a seed, a script, the installer). A refusal throws SmoodlyContentError
34
+ * with the service's code and message; a network failure names SMOODLY_API_URL. */
35
+ export declare function apiTransport(options: {
36
+ apiUrl: string;
37
+ spaceId: string;
38
+ key: string;
39
+ fetch?: typeof fetch;
40
+ }): ContentTransport;
33
41
  export declare function createSmoodlyContent(deps: ContentDeps): {
34
42
  pages: {
35
43
  create: (input: PageCreateInput) => Promise<PageRecord>;
@@ -77,14 +85,5 @@ export declare function createSmoodlyContent(deps: ContentDeps): {
77
85
  /** A file path or bytes in, a ready asset and the value an image field stores out. */
78
86
  upload(file: string | Uint8Array, meta?: UploadMeta): Promise<ImageValue>;
79
87
  };
80
- members: {
81
- addAdmin: (email: string, options?: {
82
- password?: string;
83
- redirectTo?: string;
84
- }) => Promise<{
85
- email: string;
86
- invited: boolean;
87
- }>;
88
- };
89
88
  };
90
89
  export type SmoodlyContent = ReturnType<typeof createSmoodlyContent>;
@@ -2,13 +2,11 @@
2
2
  // operations with no signed-in editor, as a few verbs that throw. Scripts
3
3
  // and route handlers write content through this, never through the stores,
4
4
  // so validation, the code-owned address guards and publish checks apply.
5
- import { createAdminOps } from "../admin/ops-impl.js";
5
+ // One surface over a transport (spec 2026-10-14 §1): the content API with the
6
+ // secret key for a seed or a script, the stores in one process for the service.
7
+ import { makeClientOps } from "../admin/client-ops.js";
6
8
  import { imageDimensions } from "../image-dimensions.js";
7
- const MIME = {
8
- jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif", avif: "image/avif",
9
- svg: "image/svg+xml", mp4: "video/mp4", webm: "video/webm", pdf: "application/pdf",
10
- };
11
- const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9
+ import { mimeOf } from "./mime.js";
12
10
  export class SmoodlyContentError extends Error {
13
11
  constructor(error) {
14
12
  const list = (error.fields ?? []).map((f) => `\n ${f.field}: ${f.message}`).join("");
@@ -24,42 +22,55 @@ async function must(result) {
24
22
  throw new SmoodlyContentError(r);
25
23
  return r.data;
26
24
  }
27
- /** With a password: the account is created (or an existing one kept) and made an admin. Without: an invite mail,
28
- * whose link goes to `redirectTo` or the Auth project's Site URL. Running it again changes nothing.
29
- * Its own function because `smoodly add-admin` has a member store and no config. */
30
- export async function addAdmin(members, email, options = {}) {
31
- if (!members)
32
- throw new SmoodlyContentError({ code: "internal", message: "smoodly: no member store — pass `members` to createSmoodlyContent." });
33
- if (!EMAIL.test(email))
34
- throw new SmoodlyContentError({ code: "validation", message: `smoodly: "${email}" is not an email address.` });
35
- if (options.password !== undefined && options.password.length < 8) {
36
- throw new SmoodlyContentError({ code: "validation", message: "smoodly: the password needs at least 8 characters." });
37
- }
38
- try {
39
- // Where smoodly.io manages the members, nothing here writes them — as the admin's Members page refuses.
40
- if ((await members.consoleUrl()) !== null) {
41
- throw new SmoodlyContentError({ code: "blocked", message: "smoodly: this site's members are managed at smoodly.io." });
25
+ /** Over the content API with the secret key (a seed, a script, the installer). A refusal throws SmoodlyContentError
26
+ * with the service's code and message; a network failure names SMOODLY_API_URL. */
27
+ export function apiTransport(options) {
28
+ const apiUrl = options.apiUrl.replace(/\/+$/, "");
29
+ const base = `${apiUrl}/v1/spaces/${encodeURIComponent(options.spaceId)}`;
30
+ const doFetch = options.fetch ?? fetch;
31
+ async function send(path, init = {}) {
32
+ let res;
33
+ try {
34
+ res = await doFetch(`${base}${path}`, { ...init, headers: { ...init.headers, authorization: `Bearer ${options.key}`, accept: "application/json" } });
42
35
  }
43
- if (options.password !== undefined) {
44
- await members.createAccount({ email, password: options.password, role: "admin" });
45
- return { email, invited: false };
36
+ catch (e) {
37
+ throw new SmoodlyContentError({ code: "internal", message: `smoodly: can't reach SMOODLY_API_URL (${apiUrl}): ${e instanceof Error ? e.message : String(e)}` });
46
38
  }
47
- const { invited } = await members.invite({ email, role: "admin", redirectTo: options.redirectTo ?? "" });
48
- return { email, invited };
49
- }
50
- catch (e) {
51
- if (e instanceof SmoodlyContentError)
52
- throw e;
53
- throw new SmoodlyContentError({ code: "internal", message: e instanceof Error ? e.message : String(e) });
39
+ const body = (await res.json().catch(() => null));
40
+ if (!body || typeof body.ok !== "boolean") {
41
+ return { ok: false, code: "internal", message: `smoodly: SMOODLY_API_URL (${apiUrl}) answered ${res.status} with no message.` };
42
+ }
43
+ return body;
54
44
  }
45
+ const json = (body) => ({ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) });
46
+ return {
47
+ op: (call) => send("/op", json({ path: call.path, args: call.args })),
48
+ locate: (locale, path) => must(send(`/locate?${new URLSearchParams({ locale, path })}`)),
49
+ // The bytes as the body with their length, the rest in the query (spec 2026-10-18 media on R2 and Images §4).
50
+ async upload(bytes, filename, mime, meta) {
51
+ const type = mime ?? mimeOf(filename);
52
+ const params = { name: filename, mime: type };
53
+ const dims = imageDimensions(bytes);
54
+ if (dims) {
55
+ params.width = String(dims.width);
56
+ params.height = String(dims.height);
57
+ }
58
+ for (const key of ["title", "alt", "caption", "credit"])
59
+ if (meta[key] !== undefined)
60
+ params[key] = meta[key];
61
+ const record = await must(send(`/media?${new URLSearchParams(params)}`, { method: "PUT", headers: { "content-type": type }, body: bytes }));
62
+ return { asset: record.id, ...(meta.alt !== undefined ? { alt: meta.alt } : {}) };
63
+ },
64
+ };
55
65
  }
56
66
  export function createSmoodlyContent(deps) {
57
- const ops = createAdminOps(deps);
67
+ const { transport } = deps;
68
+ const ops = makeClientOps((call) => transport.op(call));
58
69
  const def = deps.config.locales.default;
59
70
  async function findPage(locale, path) {
60
- const hit = await deps.pages.resolvePath(locale, path);
71
+ const hit = await transport.locate(locale, path);
61
72
  if (hit?.kind === "page" && !hit.redirect)
62
- return deps.pages.getPage(hit.id);
73
+ return (await must(ops.pages.get(hit.id, locale))).record;
63
74
  return null;
64
75
  }
65
76
  async function updateEntry(collection, id, locale, fields) {
@@ -145,32 +156,11 @@ export function createSmoodlyContent(deps) {
145
156
  media: {
146
157
  /** A file path or bytes in, a ready asset and the value an image field stores out. */
147
158
  async upload(file, meta = {}) {
148
- if (!deps.assets)
149
- throw new SmoodlyContentError({ code: "internal", message: "smoodly: no media store — pass `assets` to createSmoodlyContent." });
150
159
  // Loaded here, not at the top: this module is reachable from `smoodly/admin`, which a bundler may walk.
151
160
  const bytes = typeof file === "string" ? new Uint8Array(await (await import("node:fs/promises")).readFile(file)) : file;
152
161
  const filename = meta.filename ?? (typeof file === "string" ? file.split(/[\\/]/).pop() : "upload");
153
- const mime = meta.mime ?? MIME[filename.split(".").pop().toLowerCase()] ?? "application/octet-stream";
154
- // The media library's own rules, as the admin's upload applies them (config.media).
155
- const { accept, maxSize } = deps.config.media;
156
- if (!accept.includes(mime))
157
- throw new SmoodlyContentError({ code: "validation", message: `smoodly: "${filename}" is ${mime}, not an accepted file type.` });
158
- if (bytes.byteLength > maxSize) {
159
- throw new SmoodlyContentError({ code: "validation", message: `smoodly: "${filename}" is larger than ${Math.round(maxSize / 1024 / 1024)} MB.` });
160
- }
161
- const { alt, title, caption, credit } = meta;
162
- let asset;
163
- try {
164
- asset = await deps.assets.create({ filename, mime, size: bytes.byteLength, bytes, alt, title, caption, credit, ...(imageDimensions(bytes) ?? {}) });
165
- }
166
- catch (e) {
167
- throw new SmoodlyContentError({ code: "internal", message: e instanceof Error ? e.message : String(e) });
168
- }
169
- return { asset: asset.id, ...(alt !== undefined ? { alt } : {}) };
162
+ return transport.upload(bytes, filename, meta.mime, meta);
170
163
  },
171
164
  },
172
- members: {
173
- addAdmin: (email, options = {}) => addAdmin(deps.members, email, options),
174
- },
175
165
  };
176
166
  }
@@ -0,0 +1 @@
1
+ export declare const mimeOf: (filename: string) => string;
@@ -0,0 +1,6 @@
1
+ // A file's type from its extension, for an upload that names none (a script's file path, bytes with a filename).
2
+ const MIME = {
3
+ jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", webp: "image/webp", gif: "image/gif", avif: "image/avif",
4
+ svg: "image/svg+xml", mp4: "video/mp4", webm: "video/webm", pdf: "application/pdf",
5
+ };
6
+ export const mimeOf = (filename) => MIME[filename.split(".").pop().toLowerCase()] ?? "application/octet-stream";
@@ -0,0 +1,19 @@
1
+ export interface DeployStore {
2
+ /** The latest published_at over pages and entries (ms); null when nothing is published. The static build's watermark. */
3
+ newestPublishedAt(): Promise<number | null>;
4
+ /** Locale rows published strictly after `since` (ms), pages plus entries: the tile's "changes waiting". */
5
+ countPublishedAfter(since: number): Promise<number>;
6
+ /** The space's last Deploy press; null until the first. */
7
+ deployRequest(): Promise<{
8
+ at: number;
9
+ by: string | null;
10
+ } | null>;
11
+ requestDeploy(by: string | null, at?: number): Promise<void>;
12
+ /** When a published row last went (an unpublish, a delete): a change the count of newer rows cannot see. */
13
+ lastRemovalAt(): Promise<number | null>;
14
+ /** Written by the operations on an unpublish or a delete of a published row. */
15
+ recordRemoval(at?: number): Promise<void>;
16
+ /** Written by the operations on every publish: the SQLite store ignores it (the locale row carries published_at),
17
+ * the memory one counts it. */
18
+ recordPublish(at?: number): Promise<void>;
19
+ }
@@ -0,0 +1 @@
1
+ export {};