smoodly 0.0.51 → 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.
- package/README.md +14 -13
- package/dist/admin/auth-config.d.ts +6 -5
- package/dist/admin/auth.d.ts +3 -11
- package/dist/admin/auth.js +1 -30
- package/dist/admin/browser/fetch-op.js +3 -2
- package/dist/admin/browser/main.js +6 -2
- package/dist/admin/client-ops.d.ts +5 -4
- package/dist/admin/client-ops.js +14 -11
- package/dist/admin/delivery-mode.d.ts +1 -1
- package/dist/admin/delivery-mode.js +2 -2
- package/dist/admin/drift.d.ts +0 -2
- package/dist/admin/drift.js +1 -1
- package/dist/admin/editor/EditorView.d.ts +0 -3
- package/dist/admin/editor/EditorView.js +11 -16
- package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
- package/dist/admin/editor/FloatingToolbar.js +1 -1
- package/dist/admin/editor/SeoSettings.d.ts +0 -2
- package/dist/admin/editor/SeoSettings.js +2 -2
- package/dist/admin/editor/canvas-address.d.ts +8 -0
- package/dist/admin/editor/canvas-address.js +18 -2
- package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
- package/dist/admin/editor/canvas-diagnosis.js +3 -0
- package/dist/admin/editor/inline-dom.d.ts +0 -4
- package/dist/admin/editor/inline-dom.js +1 -1
- package/dist/admin/editor/preview-grant.d.ts +6 -0
- package/dist/admin/editor/preview-grant.js +25 -0
- package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/fixed-nodes.d.ts +0 -6
- package/dist/admin/fixed-nodes.js +1 -81
- package/dist/admin/forms/CropEditor.d.ts +4 -2
- package/dist/admin/forms/CropEditor.js +58 -5
- package/dist/admin/forms/ImagePreview.d.ts +3 -1
- package/dist/admin/forms/ImagePreview.js +9 -3
- package/dist/admin/forms/MediaGrid.js +2 -1
- package/dist/admin/forms/MediaPicker.js +9 -6
- package/dist/admin/forms/MediaWidget.js +11 -9
- package/dist/admin/forms/assets-context.d.ts +7 -3
- package/dist/admin/forms/assets-context.js +5 -3
- package/dist/admin/forms/crop-editor.d.ts +24 -2
- package/dist/admin/forms/crop-editor.js +36 -2
- package/dist/admin/forms/date-picker.d.ts +0 -1
- package/dist/admin/forms/date-picker.js +1 -1
- package/dist/admin/forms/tabs.d.ts +0 -1
- package/dist/admin/forms/tabs.js +1 -1
- package/dist/admin/forms/upload.d.ts +18 -11
- package/dist/admin/forms/upload.js +62 -24
- package/dist/admin/global-items.d.ts +0 -3
- package/dist/admin/global-items.js +0 -54
- package/dist/admin/handler.d.ts +22 -28
- package/dist/admin/handler.js +146 -47
- package/dist/admin/health.d.ts +0 -4
- package/dist/admin/index.d.ts +5 -8
- package/dist/admin/index.js +3 -6
- package/dist/admin/ops.d.ts +36 -22
- package/dist/admin/page-data.d.ts +14 -6
- package/dist/admin/page-data.js +1 -1
- package/dist/admin/session-flow.d.ts +43 -0
- package/dist/admin/session-flow.js +94 -0
- package/dist/admin/shell/AdminApp.d.ts +8 -3
- package/dist/admin/shell/AdminApp.js +37 -58
- package/dist/admin/shell/ContentHealthPage.js +1 -3
- package/dist/admin/shell/GlobalFields.d.ts +0 -3
- package/dist/admin/shell/GlobalFields.js +1 -1
- package/dist/admin/shell/IconRail.js +5 -4
- package/dist/admin/shell/MediaScreen.js +9 -6
- package/dist/admin/shell/MembersPage.d.ts +1 -2
- package/dist/admin/shell/MembersPage.js +19 -90
- package/dist/admin/shell/SettingsView.d.ts +1 -2
- package/dist/admin/shell/SettingsView.js +2 -2
- package/dist/admin/shell/SignedOutView.d.ts +5 -0
- package/dist/admin/shell/SignedOutView.js +9 -0
- package/dist/admin/shell/console-links.d.ts +5 -0
- package/dist/admin/shell/console-links.js +11 -0
- package/dist/admin/shell/members-list.d.ts +1 -14
- package/dist/admin/shell/members-list.js +2 -23
- package/dist/admin/shell/pages-tree.d.ts +0 -2
- package/dist/admin/shell/pages-tree.js +1 -1
- package/dist/admin/shell/presence-socket.d.ts +10 -0
- package/dist/admin/shell/presence-socket.js +191 -0
- package/dist/admin/shell/presence.d.ts +6 -18
- package/dist/admin/shell/presence.js +4 -11
- package/dist/admin/shell/session.d.ts +20 -12
- package/dist/admin/shell/session.js +113 -47
- package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
- package/dist/admin/ui/LocaleSwitcher.js +2 -2
- package/dist/admin/ui/primitives.d.ts +4 -14
- package/dist/admin/ui/primitives.js +5 -8
- package/dist/admin/ui/theme.js +13 -2
- package/dist/admin/validate.js +10 -1
- package/dist/api-client.d.ts +26 -0
- package/dist/api-client.js +107 -0
- package/dist/assemble.d.ts +4 -41
- package/dist/assemble.js +4 -302
- package/dist/asset-store.d.ts +28 -30
- package/dist/asset-store.js +26 -121
- package/dist/astro/components/Head.astro +4 -1
- package/dist/astro/components/Image.astro +12 -17
- package/dist/astro/components/Video.astro +5 -2
- package/dist/astro/head.d.ts +3 -0
- package/dist/astro/head.js +24 -0
- package/dist/astro/index.js +54 -25
- package/dist/astro/locale-paths.d.ts +5 -2
- package/dist/astro/locale-paths.js +10 -2
- package/dist/astro/routes/admin.js +9 -7
- package/dist/astro/runtime.d.ts +2 -1
- package/dist/astro/runtime.js +4 -3
- package/dist/astro/server.d.ts +7 -11
- package/dist/astro/server.js +22 -28
- package/dist/astro/shell.d.ts +2 -2
- package/dist/astro/static-media-node.d.ts +4 -0
- package/dist/astro/static-media-node.js +25 -0
- package/dist/astro/static-media.d.ts +17 -0
- package/dist/astro/static-media.js +55 -0
- package/dist/astro/static.d.ts +5 -10
- package/dist/astro/static.js +13 -29
- package/dist/canvas-marker.d.ts +14 -0
- package/dist/canvas-marker.js +36 -0
- package/dist/cli/api.d.ts +14 -0
- package/dist/cli/api.js +48 -0
- package/dist/cli/app-env.d.ts +2 -0
- package/dist/cli/app-env.js +20 -0
- package/dist/cli/export.d.ts +16 -2
- package/dist/cli/export.js +52 -35
- package/dist/cli/import.d.ts +7 -11
- package/dist/cli/import.js +50 -54
- package/dist/cli/load-config.d.ts +0 -1
- package/dist/cli/load-config.js +1 -1
- package/dist/cli/main.d.ts +4 -12
- package/dist/cli/main.js +26 -72
- package/dist/cli/push.d.ts +6 -0
- package/dist/cli/push.js +31 -0
- package/dist/collections.d.ts +0 -1
- package/dist/collections.js +1 -1
- package/dist/config.d.ts +20 -5
- package/dist/config.js +18 -1
- package/dist/content/content-api.d.ts +22 -23
- package/dist/content/content-api.js +46 -56
- package/dist/content/mime.d.ts +1 -0
- package/dist/content/mime.js +6 -0
- package/dist/deploy-store.d.ts +19 -0
- package/dist/deploy-store.js +1 -0
- package/dist/entry-store.d.ts +3 -89
- package/dist/entry-store.js +2 -550
- package/dist/env.d.ts +27 -20
- package/dist/env.js +38 -50
- package/dist/fields.d.ts +11 -3
- package/dist/fields.js +33 -2
- package/dist/global-id.d.ts +2 -0
- package/dist/global-id.js +4 -2
- package/dist/image/SmoodlyImage.d.ts +9 -5
- package/dist/image/SmoodlyImage.js +29 -15
- package/dist/image/SmoodlyVideo.d.ts +1 -1
- package/dist/image/SmoodlyVideo.js +3 -2
- package/dist/image/index.d.ts +2 -3
- package/dist/image/index.js +0 -2
- package/dist/image/markup.d.ts +53 -0
- package/dist/image/markup.js +111 -0
- package/dist/index.d.ts +12 -22
- package/dist/index.js +7 -20
- package/dist/media-cut.d.ts +44 -0
- package/dist/media-cut.js +70 -0
- package/dist/media.d.ts +57 -3
- package/dist/media.js +125 -15
- package/dist/member-store.d.ts +3 -22
- package/dist/member-store.js +1 -53
- package/dist/next/admin-route.d.ts +1 -1
- package/dist/next/admin-route.js +5 -3
- package/dist/next/meta.js +2 -3
- package/dist/next/page-renderer.d.ts +2 -2
- package/dist/next/page-renderer.js +22 -11
- package/dist/next/queries.d.ts +2 -2
- package/dist/path-index.d.ts +0 -24
- package/dist/path-index.js +0 -98
- package/dist/push.d.ts +30 -0
- package/dist/push.js +52 -0
- package/dist/read-counter.d.ts +2 -0
- package/dist/read-counter.js +6 -0
- package/dist/reads.d.ts +7 -7
- package/dist/reads.js +3 -0
- package/dist/registry-json.d.ts +11 -0
- package/dist/registry-json.js +81 -0
- package/dist/render.d.ts +0 -1
- package/dist/render.js +1 -1
- package/dist/resolve.d.ts +12 -6
- package/dist/resolve.js +11 -8
- package/dist/revalidate.d.ts +8 -1
- package/dist/revalidate.js +8 -1
- package/dist/richtext/index.d.ts +1 -1
- package/dist/section-wrapper.d.ts +1 -1
- package/dist/section-wrapper.js +0 -9
- package/dist/signed-message.d.ts +26 -0
- package/dist/signed-message.js +103 -0
- package/dist/site.d.ts +59 -18
- package/dist/site.js +21 -252
- package/dist/space-transfer.d.ts +5 -2
- package/dist/space-transfer.js +17 -5
- package/dist/store.d.ts +2 -80
- package/dist/store.js +3 -489
- package/dist/toolset.d.ts +2 -1
- package/dist/toolset.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +4 -15
- package/dist/admin/ops-impl.d.ts +0 -51
- package/dist/admin/ops-impl.js +0 -1403
- package/dist/admin/shell/InviteModal.d.ts +0 -6
- package/dist/admin/shell/InviteModal.js +0 -28
- package/dist/admin/shell/LoginView.d.ts +0 -5
- package/dist/admin/shell/LoginView.js +0 -80
- package/dist/admin/shell/SchemaNotice.d.ts +0 -4
- package/dist/admin/shell/SchemaNotice.js +0 -4
- package/dist/admin/shell/SetPasswordView.d.ts +0 -7
- package/dist/admin/shell/SetPasswordView.js +0 -41
- package/dist/admin/shell/presence-realtime.d.ts +0 -3
- package/dist/admin/shell/presence-realtime.js +0 -32
- package/dist/admin/supabase-auth.d.ts +0 -7
- package/dist/admin/supabase-auth.js +0 -30
- package/dist/astro/deploy-deps.d.ts +0 -10
- package/dist/astro/deploy-deps.js +0 -8
- package/dist/astro/image.d.ts +0 -27
- package/dist/astro/image.js +0 -29
- package/dist/bind.d.ts +0 -19
- package/dist/bind.js +0 -34
- package/dist/cli/add-admin.d.ts +0 -11
- package/dist/cli/add-admin.js +0 -25
- package/dist/cli/load-collections.d.ts +0 -2
- package/dist/cli/load-collections.js +0 -27
- package/dist/cli/migrate.d.ts +0 -43
- package/dist/cli/migrate.js +0 -138
- package/dist/cli/rebuild.d.ts +0 -23
- package/dist/cli/rebuild.js +0 -22
- package/dist/cli/space-io.d.ts +0 -41
- package/dist/cli/space-io.js +0 -84
- package/dist/client.d.ts +0 -11
- package/dist/client.js +0 -54
- package/dist/current-space.d.ts +0 -4
- package/dist/current-space.js +0 -9
- package/dist/document-stamp.d.ts +0 -7
- package/dist/document-stamp.js +0 -48
- package/dist/image/loaders.d.ts +0 -15
- package/dist/image/loaders.js +0 -5
- package/dist/image/next-config.d.ts +0 -18
- package/dist/image/next-config.js +0 -32
- package/dist/image/remote-pattern.d.ts +0 -1
- package/dist/image/remote-pattern.js +0 -5
- package/dist/migrations.d.ts +0 -37
- package/dist/migrations.js +0 -73
- package/dist/project.d.ts +0 -88
- package/dist/project.js +0 -231
- package/dist/published.d.ts +0 -142
- package/dist/published.js +0 -164
- package/dist/ref-index.d.ts +0 -19
- package/dist/ref-index.js +0 -0
- package/dist/schema-read.d.ts +0 -23
- package/dist/schema-read.js +0 -50
- package/dist/schema-version.d.ts +0 -19
- package/dist/schema-version.js +0 -34
- package/dist/sql-space.d.ts +0 -44
- package/dist/sql-space.js +0 -289
- package/dist/sql.d.ts +0 -15
- package/dist/sql.js +0 -556
- package/dist/supabase-asset-store.d.ts +0 -21
- package/dist/supabase-asset-store.js +0 -170
- package/dist/supabase-entry-store.d.ts +0 -92
- package/dist/supabase-entry-store.js +0 -824
- package/dist/supabase-member-store.d.ts +0 -21
- package/dist/supabase-member-store.js +0 -96
- package/dist/supabase-path-index.d.ts +0 -30
- package/dist/supabase-path-index.js +0 -138
- package/dist/supabase-published-store.d.ts +0 -56
- package/dist/supabase-published-store.js +0 -314
- package/dist/supabase-store.d.ts +0 -103
- package/dist/supabase-store.js +0 -678
- package/migrations/20260926120000_smoodly_v1.sql +0 -535
- package/migrations/20260926130000_smoodly_v2.sql +0 -26
- package/migrations/20260926140000_smoodly_v3.sql +0 -38
- package/migrations/20260926150000_smoodly_v4.sql +0 -41
- package/migrations/20260929120000_smoodly_v5.sql +0 -56
- package/migrations/20260929135523_smoodly_v6.sql +0 -200
- package/migrations/20260930040956_smoodly_v7.sql +0 -84
- package/migrations/20261007120000_smoodly_v8.sql +0 -154
- package/migrations/20261007200000_smoodly_v9.sql +0 -30
- package/migrations/20261008120000_smoodly_v10.sql +0 -47
- package/migrations/20261009120000_smoodly_v11.sql +0 -49
package/dist/astro/static.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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).
|
|
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.
|
|
45
|
-
if (
|
|
46
|
-
|
|
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;
|
package/dist/cli/api.js
ADDED
|
@@ -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,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
|
+
}
|
package/dist/cli/export.d.ts
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
|
-
import { type
|
|
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
|
-
|
|
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>;
|
package/dist/cli/export.js
CHANGED
|
@@ -1,21 +1,50 @@
|
|
|
1
1
|
// packages/smoodly/src/cli/export.ts
|
|
2
|
-
// `smoodly export` (spec 2026-09-28 export/import §8
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
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 {
|
|
11
|
-
import { CONTENT_TABLES } from "../
|
|
12
|
-
import { FORMAT, brokenReferences, countRows, describeExport, emptyTables,
|
|
13
|
-
import {
|
|
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
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
66
|
-
if (
|
|
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:
|
|
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:
|
|
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);
|
package/dist/cli/import.d.ts
CHANGED
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
import { type ContentTable } from "../
|
|
2
|
-
import { type
|
|
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
|
-
|
|
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
|
|
21
|
-
export declare const nothingNew: (
|
|
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>;
|
package/dist/cli/import.js
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
// packages/smoodly/src/cli/import.ts
|
|
2
|
-
// `smoodly import <dir>` (spec 2026-09-28 export/import §9–§10
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// export's rows and both writes
|
|
2
|
+
// `smoodly import <dir>` (spec 2026-09-28 export/import §9–§10; over the API
|
|
3
|
+
// since plan 2026-10-14 the service holds the stores): an export folder into
|
|
4
|
+
// the key's space. Every check runs before the first write; nothing is
|
|
5
|
+
// overwritten or deleted; a run that stops midway is finished by running it
|
|
6
|
+
// again, since the target may hold only this export's rows and both writes
|
|
7
|
+
// skip what already landed. The service writes each row into its own space;
|
|
8
|
+
// a read assembles from what landed, so nothing is rebuilt after.
|
|
7
9
|
import { existsSync } from "node:fs";
|
|
8
10
|
import { readFile } from "node:fs/promises";
|
|
9
11
|
import { join, resolve } from "node:path";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
import { CONTENT_TABLES } from "../space-transfer.js";
|
|
13
|
+
import { FORMAT, describeImport, emptyTables, foreignRows, insertPlan, missingRows, notEmptyMessage, rowKey, versionMismatchMessage, } from "../space-transfer.js";
|
|
14
|
+
import { connectApi } from "./api.js";
|
|
15
|
+
import { mediaUrl, readTable, transferHead } from "./export.js";
|
|
16
|
+
const BATCH_ROWS = 500;
|
|
17
|
+
/** A batch's JSON stays under the API's 4 MB request cap with room for the envelope. */
|
|
18
|
+
const MAX_BATCH_BYTES = 3500000;
|
|
19
|
+
const notAnExport = (dir) => `Refusing: ${dir} is not a Smoodly export (no manifest.json).`;
|
|
20
|
+
const formatMismatch = (format) => `Refusing: this export's format is ${String(format)}; this smoodly reads format ${FORMAT}. Update smoodly.`;
|
|
19
21
|
export const RUN_AGAIN = "Run the same command again to continue: rows and files that landed are skipped.";
|
|
20
|
-
export const MEMBERS_HEADING = "Members in the source —
|
|
21
|
-
export const nothingNew = (
|
|
22
|
+
export const MEMBERS_HEADING = "Members in the source — add them to the space's members in the project at smoodly.io:";
|
|
23
|
+
export const nothingNew = () => "Nothing new: the space already holds everything in this export.";
|
|
22
24
|
export const CACHE_NOTE = "Restart or redeploy the app: it may have cached pages from while the space was empty.";
|
|
25
|
+
/** `list` cut into batches of at most `size` rows, each halved while its JSON is over MAX_BATCH_BYTES. */
|
|
26
|
+
function batches(list, size) {
|
|
27
|
+
const out = [];
|
|
28
|
+
for (let i = 0; i < list.length;) {
|
|
29
|
+
let take = Math.min(size, list.length - i);
|
|
30
|
+
while (take > 1 && Buffer.byteLength(JSON.stringify({ rows: list.slice(i, i + take) })) > MAX_BATCH_BYTES)
|
|
31
|
+
take = Math.ceil(take / 2);
|
|
32
|
+
out.push(list.slice(i, i + take));
|
|
33
|
+
i += take;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
23
37
|
export async function runImport(args, out, deps = {}) {
|
|
24
38
|
const cwd = deps.cwd ?? process.cwd();
|
|
25
39
|
const dir = resolve(cwd, args.dir);
|
|
@@ -33,64 +47,48 @@ export async function runImport(args, out, deps = {}) {
|
|
|
33
47
|
out(formatMismatch(manifest.format));
|
|
34
48
|
return 1;
|
|
35
49
|
}
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
const env = deps.env ?? process.env;
|
|
51
|
+
const api = deps.api ?? connectApi(cwd, env);
|
|
52
|
+
// Read after connectApi: it loads the app's .env files.
|
|
53
|
+
const space = env.SMOODLY_SPACE_ID ?? "";
|
|
54
|
+
const head = await transferHead(api, out);
|
|
55
|
+
if (!head)
|
|
40
56
|
return 1;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
if (schema.version !== manifest.schemaVersion) {
|
|
44
|
-
out(versionMismatchMessage(schema.version, manifest.schemaVersion));
|
|
57
|
+
if (head.schemaVersion !== manifest.schemaVersion) {
|
|
58
|
+
out(versionMismatchMessage(head.schemaVersion, manifest.schemaVersion));
|
|
45
59
|
return 1;
|
|
46
60
|
}
|
|
47
61
|
const rows = emptyTables();
|
|
48
62
|
for (const table of CONTENT_TABLES)
|
|
49
63
|
rows[table] = JSON.parse(await readFile(join(dir, "rows", `${table}.json`), "utf8"));
|
|
50
64
|
const target = {};
|
|
51
|
-
for (const table of CONTENT_TABLES)
|
|
52
|
-
target[table] = (await
|
|
53
|
-
}
|
|
65
|
+
for (const table of CONTENT_TABLES)
|
|
66
|
+
target[table] = (await readTable(api, table, true)).map((row) => rowKey(table, row));
|
|
54
67
|
const foreign = foreignRows(target, rows);
|
|
55
68
|
if (CONTENT_TABLES.some((t) => foreign[t] > 0)) {
|
|
56
69
|
out(notEmptyMessage(foreign));
|
|
57
70
|
return 1;
|
|
58
71
|
}
|
|
72
|
+
// The files that travel (a file missing in the source as well is in manifest.media.missing), each sent whole with its
|
|
73
|
+
// length: the service streams it into the bucket (spec 2026-10-18 media on R2 and Images §4).
|
|
74
|
+
const files = rows.assets.map((asset) => ({ path: String(asset.path), mime: String(asset.mime), file: join(dir, "media", String(asset.path)) }))
|
|
75
|
+
.filter((m) => existsSync(m.file));
|
|
59
76
|
const newRows = missingRows(target, rows);
|
|
60
77
|
let newFiles = 0;
|
|
61
|
-
// The rebuild after the writes assembles with the app's config: a folder without one is refused before them.
|
|
62
|
-
const config = deps.config ?? (await loadConfig(cwd));
|
|
63
|
-
let rebuilt = { pages: 0, entries: 0 };
|
|
64
78
|
try {
|
|
65
79
|
// Media first, so no ready asset row ever points at a missing file.
|
|
66
|
-
for (const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
if (!existsSync(file))
|
|
70
|
-
continue; // missing in the source as well (manifest.media.missing)
|
|
71
|
-
const up = await db.storage.from(MEDIA_BUCKET).upload(`${space}/${path}`, await readFile(file), {
|
|
72
|
-
contentType: String(asset.mime),
|
|
73
|
-
upsert: false,
|
|
74
|
-
cacheControl: String(MEDIA_CACHE_SECONDS),
|
|
75
|
-
});
|
|
76
|
-
if (up.error && !isAlreadyExists(up.error))
|
|
77
|
-
throw new Error(`uploading ${path}: ${up.error.message}`);
|
|
78
|
-
if (!up.error)
|
|
80
|
+
for (const m of files) {
|
|
81
|
+
const up = await api.postBytes(mediaUrl(m.path), new Uint8Array(await readFile(m.file)), m.mime);
|
|
82
|
+
if (up.created)
|
|
79
83
|
newFiles++;
|
|
80
84
|
}
|
|
81
85
|
const size = deps.batchSize ?? BATCH_ROWS;
|
|
82
86
|
for (const { table, rows: list } of insertPlan(rows)) {
|
|
83
|
-
for (
|
|
87
|
+
for (const [i, batch] of batches(list, size).entries()) {
|
|
84
88
|
deps.beforeBatch?.(table, i);
|
|
85
|
-
|
|
86
|
-
const res = await db.from(table).upsert(batch, { onConflict: CONFLICT_COLUMNS[table], ignoreDuplicates: true });
|
|
87
|
-
if (res.error)
|
|
88
|
-
throw new Error(`inserting ${table}: ${res.error.message}`);
|
|
89
|
+
await api.post(`/transfer/rows/${table}`, { rows: batch });
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
|
-
// The delivery tables are derived, never exported: rebuilt from what landed (spec 2026-10-07 delivery model §2).
|
|
92
|
-
// Also on a run that landed nothing new, so a run stopped here is finished by running it again.
|
|
93
|
-
rebuilt = await rebuildSpace(db, config);
|
|
94
92
|
}
|
|
95
93
|
catch (e) {
|
|
96
94
|
out(`Import stopped: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -98,12 +96,10 @@ export async function runImport(args, out, deps = {}) {
|
|
|
98
96
|
return 1;
|
|
99
97
|
}
|
|
100
98
|
if (newRows === 0 && newFiles === 0) {
|
|
101
|
-
out(nothingNew(
|
|
102
|
-
out(rebuiltMessage(rebuilt));
|
|
99
|
+
out(nothingNew());
|
|
103
100
|
return 0;
|
|
104
101
|
}
|
|
105
102
|
out(`Imported ${describeImport(rows)} into space ${space}.`);
|
|
106
|
-
out(rebuiltMessage(rebuilt));
|
|
107
103
|
if (manifest.members.length > 0) {
|
|
108
104
|
out(MEMBERS_HEADING);
|
|
109
105
|
out(` ${manifest.members.map((m) => `${m.email ?? "(no email)"} (${m.role})`).join(", ")}`);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ResolvedConfig } from "../config.ts";
|
|
2
|
-
export declare const CONFIG_FILES: string[];
|
|
3
2
|
export declare const noConfig: (cwd: string) => string;
|
|
4
3
|
export declare function loadConfig(cwd: string): Promise<ResolvedConfig>;
|
|
5
4
|
/** One named config file (the Astro integration's `config` option names it): the project's tsconfig beside `cwd`
|
package/dist/cli/load-config.js
CHANGED
|
@@ -8,7 +8,7 @@ import { existsSync } from "node:fs";
|
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
10
10
|
import { tsImport } from "tsx/esm/api";
|
|
11
|
-
|
|
11
|
+
const CONFIG_FILES = ["smoodly.config.ts", "smoodly/config.ts", "src/smoodly/config.ts"];
|
|
12
12
|
export const noConfig = (cwd) => `smoodly: no ${CONFIG_FILES.join(", ")} in ${cwd} — run this in your app's folder.`;
|
|
13
13
|
export async function loadConfig(cwd) {
|
|
14
14
|
const file = CONFIG_FILES.map((name) => join(cwd, name)).find((path) => existsSync(path));
|
package/dist/cli/main.d.ts
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
|
-
import { type MigrateArgs } from "./migrate.ts";
|
|
2
1
|
import { type ExportArgs } from "./export.ts";
|
|
3
2
|
import { type ImportArgs } from "./import.ts";
|
|
4
|
-
import { type
|
|
5
|
-
|
|
6
|
-
export declare const USAGE = "usage: smoodly <command> (smoodly --version prints the version)\n\n migrate [--check] [--collections <file>] [--migrations <dir>]\n Copies the core schema files this package ships that supabase/migrations/\n lacks, and writes one views file for the collections whose view is new or\n changed. Prints the push or reset command; never runs it.\n\n --check write nothing; exit 1 when anything is pending, or when\n SMOODLY_SECRET_KEY (if set) cannot read the database\n --collections <file> the module exporting `collections` (default\n smoodly/collections.ts, or src/smoodly/collections.ts\n when only that one exists)\n --migrations <dir> the app's migration folder (default supabase/migrations)\n\n export [--out <dir>]\n Writes the space SMOODLY_SECRET_KEY names \u2014 every content row, history\n included, and the media files \u2014 into a folder (default\n smoodly-export-<date>). Reads only.\n\n import <dir>\n Writes an export folder into the key's space, which must be empty or hold\n only this export's rows (an earlier run to continue). Never deletes. Ends\n by rebuilding the delivery tables with the app's config (smoodly.config.ts,\n smoodly/config.ts or src/smoodly/config.ts), so run it in the app's folder.\n\n rebuild\n Rewrites the delivery tables of the key's space: the finished document of\n every published page, entry and global, assembled with the app's config.\n A publish keeps them current and a document behind the content model is\n rebuilt by its first visitor; run this after a deploy for a warm start.\n\n add-admin <email> [--admin-url <url>]\n Makes <email> an admin of the key's space: with SMOODLY_ADMIN_PASSWORD\n set, creates the account with that password; without, mails an invite\n whose link lands on --admin-url (default: SITE_URL + /admin).";
|
|
3
|
+
import { type PushArgs } from "./push.ts";
|
|
4
|
+
export declare const USAGE = "usage: smoodly <command> (smoodly --version prints the version)\nEvery command reads SMOODLY_API_URL, SMOODLY_SPACE_ID and SMOODLY_SECRET_KEY from the app's .env.local or .env.\n\n push\n Sends the app's registry (its smoodly config), its address (SITE_URL) and\n the package version to the space. A Next site's prebuild runs it, Astro's\n build and both dev servers push on their own; run it from CI or a script.\n SMOODLY_STATIC=1 marks a static site. As a prebuild it never fails the build.\n\n export [--out <dir>]\n Writes the space SMOODLY_SECRET_KEY names \u2014 every content row, history\n included, and the media files \u2014 into a folder (default\n smoodly-export-<date>). Reads only.\n\n import <dir>\n Writes an export folder into the key's space, which must be empty or hold\n only this export's rows (an earlier run to continue). Never deletes.";
|
|
7
5
|
export type Parsed = {
|
|
8
|
-
command: "migrate";
|
|
9
|
-
args: MigrateArgs;
|
|
10
|
-
} | {
|
|
11
6
|
command: "export";
|
|
12
7
|
args: ExportArgs;
|
|
13
8
|
} | {
|
|
14
9
|
command: "import";
|
|
15
10
|
args: ImportArgs;
|
|
16
11
|
} | {
|
|
17
|
-
command: "
|
|
18
|
-
args:
|
|
19
|
-
} | {
|
|
20
|
-
command: "rebuild";
|
|
21
|
-
args: RebuildArgs;
|
|
12
|
+
command: "push";
|
|
13
|
+
args: PushArgs;
|
|
22
14
|
} | {
|
|
23
15
|
command: "help";
|
|
24
16
|
} | {
|