smoodly 0.0.42 → 0.0.43
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/dist/admin/auth.js +2 -1
- package/dist/admin/client-ops.d.ts +4 -1
- package/dist/admin/client-ops.js +9 -2
- package/dist/admin/forms/useRefOptions.d.ts +5 -0
- package/dist/admin/forms/useRefOptions.js +17 -4
- package/dist/admin/ops.d.ts +2 -0
- package/dist/admin/shell/AdminApp.js +2 -1
- package/dist/admin/shell/EntryForm.js +19 -2
- package/dist/admin/shell/GlobalForm.js +1 -1
- package/dist/admin/ui/primitives.d.ts +5 -1
- package/dist/admin/ui/primitives.js +8 -3
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +2 -1
- package/dist/cli/migrate.d.ts +7 -0
- package/dist/cli/migrate.js +43 -2
- package/dist/schema-read.d.ts +19 -3
- package/dist/schema-read.js +36 -1
- package/dist/supabase-store.d.ts +2 -2
- package/dist/supabase-store.js +10 -18
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/admin/auth.js
CHANGED
|
@@ -15,8 +15,9 @@ export async function gateAdminOp(auth, call, run) {
|
|
|
15
15
|
let stored;
|
|
16
16
|
try {
|
|
17
17
|
user = await auth.verify(token);
|
|
18
|
+
// Marked, so the browser signs itself out instead of showing this on every screen (2026-10-02).
|
|
18
19
|
if (!user)
|
|
19
|
-
return unauthorized
|
|
20
|
+
return { ok: false, code: "unauthorized", message: "smoodly: your session has expired — sign in again.", expired: true };
|
|
20
21
|
stored = await auth.memberOf(user.userId);
|
|
21
22
|
}
|
|
22
23
|
catch (e) {
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import type { AdminOpAction, AdminOps } from "./ops.ts";
|
|
2
|
-
|
|
2
|
+
/** `onExpired` runs when the server refuses the token outright (gateAdminOp's `expired`): the browser
|
|
3
|
+
* still holds a session the auth project no longer knows — after a database reset, a deleted user —
|
|
4
|
+
* and only a local sign-out brings the sign-in form back (2026-10-02). */
|
|
5
|
+
export declare function makeClientOps(action: AdminOpAction, token?: () => string | null, onExpired?: () => void): AdminOps;
|
package/dist/admin/client-ops.js
CHANGED
|
@@ -9,14 +9,21 @@ const GROUPS = {
|
|
|
9
9
|
schema: ["status"],
|
|
10
10
|
preview: ["enable", "disable"],
|
|
11
11
|
};
|
|
12
|
-
|
|
12
|
+
/** `onExpired` runs when the server refuses the token outright (gateAdminOp's `expired`): the browser
|
|
13
|
+
* still holds a session the auth project no longer knows — after a database reset, a deleted user —
|
|
14
|
+
* and only a local sign-out brings the sign-in form back (2026-10-02). */
|
|
15
|
+
export function makeClientOps(action, token, onExpired) {
|
|
13
16
|
const out = {};
|
|
14
17
|
for (const [group, methods] of Object.entries(GROUPS)) {
|
|
15
18
|
out[group] = {};
|
|
16
19
|
for (const method of methods) {
|
|
17
20
|
out[group][method] = (...args) => {
|
|
18
21
|
const t = token?.() ?? null;
|
|
19
|
-
return action({ path: `${group}.${method}`, args, ...(t ? { token: t } : {}) })
|
|
22
|
+
return action({ path: `${group}.${method}`, args, ...(t ? { token: t } : {}) }).then((r) => {
|
|
23
|
+
if (!r.ok && r.expired)
|
|
24
|
+
onExpired?.();
|
|
25
|
+
return r;
|
|
26
|
+
});
|
|
20
27
|
};
|
|
21
28
|
}
|
|
22
29
|
}
|
|
@@ -3,5 +3,10 @@ import type { AdminOps } from "../ops.ts";
|
|
|
3
3
|
import type { AdminCollection, AdminRegistry } from "../serialize.ts";
|
|
4
4
|
import type { RefOption } from "./ref-picker.ts";
|
|
5
5
|
export declare function refOptionsOf(records: EntryRecord[], meta: AdminCollection | undefined, registry: AdminRegistry, locale: string): RefOption[];
|
|
6
|
+
/** What the options are read from, by content: the registry crosses the server → client boundary
|
|
7
|
+
* and is re-serialized on every autosave (a Server Action re-renders the route), so a key on its
|
|
8
|
+
* identity emptied the picker to "Loading…" and refetched every target on each save (2026-10-02;
|
|
9
|
+
* the richtext editor's toolset and elements are keyed the same way, 2026-09-26). */
|
|
10
|
+
export declare function refOptionsKey(registry: AdminRegistry, locale: string, targets: readonly string[]): string;
|
|
6
11
|
/** Options by target collection; a target is `undefined` until its list arrives. */
|
|
7
12
|
export declare function useRefOptions(ops: AdminOps, registry: AdminRegistry, locale: string, targets: readonly string[]): Record<string, RefOption[] | undefined>;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// collection, loaded once per locale and filtered in the browser. One hook
|
|
4
4
|
// for the entry form, the Globals form and the page editor — the picker
|
|
5
5
|
// takes options as a prop, so a move to server search changes this file only.
|
|
6
|
-
import { useEffect, useState } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
7
|
import { entryTitle } from "../shell/entries-list.js";
|
|
8
8
|
import { localeStatusOf } from "../shell/pages-tree.js";
|
|
9
9
|
export function refOptionsOf(records, meta, registry, locale) {
|
|
@@ -13,14 +13,27 @@ export function refOptionsOf(records, meta, registry, locale) {
|
|
|
13
13
|
return row ? { id: e.id, title, status: localeStatusOf(row) } : { id: e.id, title, status: "", absent: true };
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
|
+
/** What the options are read from, by content: the registry crosses the server → client boundary
|
|
17
|
+
* and is re-serialized on every autosave (a Server Action re-renders the route), so a key on its
|
|
18
|
+
* identity emptied the picker to "Loading…" and refetched every target on each save (2026-10-02;
|
|
19
|
+
* the richtext editor's toolset and elements are keyed the same way, 2026-09-26). */
|
|
20
|
+
export function refOptionsKey(registry, locale, targets) {
|
|
21
|
+
const names = [...new Set(targets)].sort();
|
|
22
|
+
const metas = names.map((n) => registry.collections.find((c) => c.name === n) ?? null);
|
|
23
|
+
return JSON.stringify([locale, registry.locales.default, names, metas]);
|
|
24
|
+
}
|
|
16
25
|
/** Options by target collection; a target is `undefined` until its list arrives. */
|
|
17
26
|
export function useRefOptions(ops, registry, locale, targets) {
|
|
18
27
|
const [loaded, setLoaded] = useState({});
|
|
19
|
-
const key =
|
|
28
|
+
const key = refOptionsKey(registry, locale, targets);
|
|
29
|
+
// The effect reads the render's registry, but reloads only when the key says its content changed.
|
|
30
|
+
const latest = useRef({ registry, locale, targets });
|
|
31
|
+
latest.current = { registry, locale, targets };
|
|
20
32
|
useEffect(() => {
|
|
33
|
+
const { registry, locale, targets } = latest.current;
|
|
21
34
|
let cancelled = false;
|
|
22
35
|
setLoaded({}); // the previous locale's options must not show under this one
|
|
23
|
-
for (const target of
|
|
36
|
+
for (const target of new Set(targets)) {
|
|
24
37
|
const meta = registry.collections.find((c) => c.name === target);
|
|
25
38
|
ops.entries.list(target).then((r) => {
|
|
26
39
|
if (cancelled)
|
|
@@ -31,6 +44,6 @@ export function useRefOptions(ops, registry, locale, targets) {
|
|
|
31
44
|
setLoaded((prev) => ({ ...prev, [target]: [] })); });
|
|
32
45
|
}
|
|
33
46
|
return () => { cancelled = true; };
|
|
34
|
-
}, [ops,
|
|
47
|
+
}, [ops, key]);
|
|
35
48
|
return loaded;
|
|
36
49
|
}
|
package/dist/admin/ops.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type OpError = {
|
|
|
20
20
|
/** Who wrote it last and when — the freeze notice's two facts. */
|
|
21
21
|
by?: string | null;
|
|
22
22
|
at?: number;
|
|
23
|
+
/** An `unauthorized` for a token the auth project rejects: the browser signs itself out (2026-10-02). */
|
|
24
|
+
expired?: true;
|
|
23
25
|
};
|
|
24
26
|
export type OpResult<T> = {
|
|
25
27
|
ok: true;
|
|
@@ -41,7 +41,8 @@ import { connectRealtime } from "./presence-realtime.js";
|
|
|
41
41
|
import { PresenceProvider } from "./presence-context.js";
|
|
42
42
|
export function AdminApp({ registry, op, auth, presenceConfig }) {
|
|
43
43
|
const session = useAdminSession(auth);
|
|
44
|
-
|
|
44
|
+
// An expired session signs out here, locally: the user it names may no longer exist to log out.
|
|
45
|
+
const [ops] = useState(() => makeClientOps(op, session.token, () => { void session.client.auth.signOut({ scope: "local" }); }));
|
|
45
46
|
const route = useRoute(registry.adminPath);
|
|
46
47
|
const [head = "pages", tail] = route.segments.length ? route.segments : ["pages"];
|
|
47
48
|
const isCollection = registry.collections.some((c) => c.name === head);
|
|
@@ -102,6 +102,17 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
102
102
|
useTrackPresence(target);
|
|
103
103
|
const { others } = usePresenceState();
|
|
104
104
|
const presence = usePresence();
|
|
105
|
+
/** After a publish, an unpublish or a slug change only the address indexes are stale: re-read the URLs
|
|
106
|
+
* ("View live"), the redirects and the live findings — never the fields, which may be ahead of the store
|
|
107
|
+
* (as the page editor's reload after a rename, 2026-10-02). */
|
|
108
|
+
const reloadIndexes = async () => {
|
|
109
|
+
const r = await ops.entries.get(collection, id, locale).catch(() => null);
|
|
110
|
+
if (!r?.ok)
|
|
111
|
+
return;
|
|
112
|
+
setUrls(r.data.urls);
|
|
113
|
+
setRedirects(r.data.redirects);
|
|
114
|
+
setLiveFindings(r.data.liveFindings);
|
|
115
|
+
};
|
|
105
116
|
const { saveState, mark, flush, reset } = useDraftAutosave({
|
|
106
117
|
read: () => fieldsRef.current,
|
|
107
118
|
persist: async (value) => {
|
|
@@ -140,11 +151,14 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
140
151
|
}
|
|
141
152
|
const r = await ops.entries.save(collection, id, locale, value, token(), { redirect: redirectRef.current });
|
|
142
153
|
if (r.ok) {
|
|
154
|
+
const slugBefore = recordRef.current?.locales[locale]?.slug;
|
|
143
155
|
redirectRef.current = true;
|
|
144
156
|
recordRef.current = r.data;
|
|
145
157
|
setRecord(r.data);
|
|
146
158
|
setSaveErrors({});
|
|
147
159
|
presence?.announce(target, r.data.locales[locale]?.draftSavedAt ?? null);
|
|
160
|
+
if (r.data.locales[locale]?.slug !== slugBefore)
|
|
161
|
+
void reloadIndexes();
|
|
148
162
|
}
|
|
149
163
|
else if (isStale(r))
|
|
150
164
|
setStale(staleInfoOf(r));
|
|
@@ -271,6 +285,7 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
271
285
|
setRecord(r.data);
|
|
272
286
|
presence?.announce(target, r.data.locales[locale]?.draftSavedAt ?? null);
|
|
273
287
|
setFieldErrors({});
|
|
288
|
+
void reloadIndexes();
|
|
274
289
|
setPublished(true);
|
|
275
290
|
setTimeout(() => setPublished(false), 1600);
|
|
276
291
|
};
|
|
@@ -285,8 +300,10 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
285
300
|
r = { ok: false, code: "internal", message: "Couldn't reach the server." };
|
|
286
301
|
}
|
|
287
302
|
setBusy(null);
|
|
288
|
-
if (r.ok)
|
|
303
|
+
if (r.ok) {
|
|
289
304
|
setRecord(r.data);
|
|
305
|
+
void reloadIndexes();
|
|
306
|
+
}
|
|
290
307
|
else
|
|
291
308
|
fail(r);
|
|
292
309
|
};
|
|
@@ -474,7 +491,7 @@ export function EntryForm({ ops, registry, collection, id }) {
|
|
|
474
491
|
trailing: multilingual && (_jsx(LocaleSwitcher, { registry: registry, present: present, parentLocales: parentLocales, value: locale, adding: adding, onSwitch: (l) => { void switchLocale(l); }, onAdd: (l) => void doAddLocale(l) })),
|
|
475
492
|
},
|
|
476
493
|
{ label: heading },
|
|
477
|
-
] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error" || saveState === "refused") && _jsx(SaveIndicator, { state: saveState, pending: dirty }), urls[locale] && (_jsx(LinkButton, { icon: true, href: urls[locale], target: "_blank", rel: "noreferrer", "aria-label": "View live", title: "View live \u2014 opens the site route. While the admin's draft-mode cookie is on, this shows the DRAFT \u2014 open it in a private window to see what visitors see.", children: _jsx(Eye, { size: 16, strokeWidth: 1.5, "aria-hidden": "true" }) })), _jsx(PublishControl, { status: status, pending: dirty || saveState !== "saved", busy: busy, flash: published, disabled: !row, onPublish: () => { void publish(); }, onUnpublish: () => { void unpublish(); }, onDiscard: () => { void discard(); }, onHistory: () => { void openHistory(); } })] }) }), history && row && (_jsx(HistoryModal, { rows: history, draftId: row.draftVersionId, onSelect: (v) => { void loadSnapshot(v); }, onClose: () => setHistory(null), onRestore: restore, render: (versionId) => snapshot?.id === versionId ? (_jsx("fieldset", { disabled: true, className: "sm-history__form", children: Object.entries(meta.fields).map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: snapshot.fields[key], refOptions: refTargets[key] ? refOptions[refTargets[key]] : undefined, prefix: d.type === "slug" ? slugPrefix : undefined, onChange: () => { } }, key))) })) : _jsx("p", { className: "sm-hint", style: { padding: 16 }, children: "Loading\u2026" }) })), _jsx(PresenceLine, { names: editorsOf(others, target), noun: "entry" }), stale && _jsx(StaleBanner, { info: stale, onReload: loadTheirs }), _jsx(DriftNotice, { draft: findings, live: liveFindings }), _jsxs("div", { className: "sm-editor", inert: stale ? true : undefined, children: [_jsxs("div", { className: "sm-form", ref: formRef, children: [notice && (_jsxs(Banner, { tone: notice.tone, onDismiss: () => setNotice(null), children: [_jsx("p", { style: { margin: 0 }, children: notice.text }), _jsx(FieldErrorList, { fields: notice.fields ?? [], onReveal: (path) => { if (formRef.current)
|
|
494
|
+
] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error" || saveState === "refused") && _jsx(SaveIndicator, { state: saveState, pending: dirty }), urls[locale] && (_jsx(LinkButton, { icon: true, href: urls[locale], target: "_blank", rel: "noreferrer", "aria-label": "View live", title: "View live \u2014 opens the site route. While the admin's draft-mode cookie is on, this shows the DRAFT \u2014 open it in a private window to see what visitors see.", children: _jsx(Eye, { size: 16, strokeWidth: 1.5, "aria-hidden": "true" }) })), _jsx(PublishControl, { status: status, pending: dirty || saveState !== "saved", busy: busy, flash: published, disabled: !row, onPublish: () => { void publish(); }, onUnpublish: () => { void unpublish(); }, onDiscard: () => { void discard(); }, onHistory: () => { void openHistory(); } })] }) }), history && row && (_jsx(HistoryModal, { rows: history, draftId: row.draftVersionId, onSelect: (v) => { void loadSnapshot(v); }, onClose: () => setHistory(null), onRestore: restore, render: (versionId) => snapshot?.id === versionId ? (_jsx("fieldset", { disabled: true, className: "sm-history__form", children: Object.entries(meta.fields).map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: snapshot.fields[key], refOptions: refTargets[key] ? refOptions[refTargets[key]] : undefined, prefix: d.type === "slug" ? slugPrefix : undefined, onChange: () => { } }, key))) })) : _jsx("p", { className: "sm-hint", style: { padding: 16 }, children: "Loading\u2026" }) })), _jsx(PresenceLine, { names: editorsOf(others, target), noun: "entry" }), stale && _jsx(StaleBanner, { info: stale, onReload: loadTheirs }), _jsx(DriftNotice, { draft: findings, live: liveFindings }), _jsxs("div", { className: "sm-editor", inert: stale ? true : undefined, children: [_jsxs("div", { className: "sm-form", ref: formRef, children: [notice && (_jsxs(Banner, { tone: notice.tone, onDismiss: () => setNotice(null), revealKey: `${notice.tone}:${notice.text}`, children: [_jsx("p", { style: { margin: 0 }, children: notice.text }), _jsx(FieldErrorList, { fields: notice.fields ?? [], onReveal: (path) => { if (formRef.current)
|
|
478
495
|
revealField(formRef.current, path); } })] })), absent ? (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [_jsxs("p", { children: ["This entry doesn't exist in ", locale, " yet. Adding it copies the ", entrySourceLocale(record, registry), " fields as the first ", locale, " draft."] }), _jsxs("div", { style: { display: "flex", gap: 8 }, children: [_jsxs(Button, { onClick: () => void doAddLocale(locale), disabled: adding || !parentLocales.includes(locale), title: parentLocales.includes(locale) ? undefined : `add ${locale} to the parent first`, children: ["Add ", locale] }), _jsxs(Button, { variant: "secondary", onClick: () => { void switchLocale(entrySourceLocale(record, registry)); }, children: ["Back to ", entrySourceLocale(record, registry)] })] })] })) : fields !== null ? (_jsxs(_Fragment, { children: [Object.entries(meta.fields).map(widget), _jsx(OrphanFields, { orphans: findings, onClear: (path) => {
|
|
479
496
|
setFields((f) => unsetAtPath(f ?? {}, path));
|
|
480
497
|
setDirty(true);
|
|
@@ -319,6 +319,6 @@ export function GlobalForm({ ops, registry, name }) {
|
|
|
319
319
|
trailing: multilingual && (_jsx(LocaleSwitcher, { registry: registry, present: registry.locales.supported, parentLocales: [], value: locale, adding: false, onSwitch: (l) => { void switchLocale(l); }, onAdd: () => undefined })),
|
|
320
320
|
},
|
|
321
321
|
{ label: item.title },
|
|
322
|
-
] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error" || saveState === "refused") && _jsx(SaveIndicator, { state: saveState, pending: dirty }), _jsx(PublishControl, { status: status, pending: dirty || saveState !== "saved", busy: busy, flash: published, disabled: !row, onPublish: () => { void publish(); }, onUnpublish: () => { void unpublish(); }, onDiscard: () => { void discard(); }, onHistory: () => { void openHistory(); } })] }) }), history && row && (_jsx(HistoryModal, { rows: history, draftId: row.draftVersionId, onSelect: (v) => { void loadSnapshot(v); }, onClose: () => setHistory(null), onRestore: restore, render: (versionId) => snapshot?.id === versionId ? (_jsxs("fieldset", { disabled: true, className: "sm-history__form", children: [_jsx(GlobalFields, { item: item, groups: contentGroups, fields: snapshot.fields, refOptions: refOptions, multilingual: multilingual, onChange: () => { } }), settingGroups.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Settings" }), _jsx(GlobalFields, { item: item, groups: settingGroups, fields: snapshot.fields, refOptions: refOptions, multilingual: multilingual, onChange: () => { } })] }))] })) : _jsx("p", { className: "sm-hint", style: { padding: 16 }, children: "Loading\u2026" }) })), _jsx(PresenceLine, { names: editorsOf(others, target), noun: "item" }), stale && _jsx(StaleBanner, { info: stale, onReload: loadTheirs }), _jsx(DriftNotice, { draft: findings, live: liveFindings }), _jsxs("div", { className: "sm-editor", inert: stale ? true : undefined, children: [_jsxs("div", { className: "sm-form", ref: formRef, children: [notice && (_jsxs(Banner, { tone: notice.tone, onDismiss: () => setNotice(null), children: [_jsx("p", { style: { margin: 0 }, children: notice.text }), _jsx(FieldErrorList, { fields: notice.fields ?? [], onReveal: (path) => { if (formRef.current)
|
|
322
|
+
] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error" || saveState === "refused") && _jsx(SaveIndicator, { state: saveState, pending: dirty }), _jsx(PublishControl, { status: status, pending: dirty || saveState !== "saved", busy: busy, flash: published, disabled: !row, onPublish: () => { void publish(); }, onUnpublish: () => { void unpublish(); }, onDiscard: () => { void discard(); }, onHistory: () => { void openHistory(); } })] }) }), history && row && (_jsx(HistoryModal, { rows: history, draftId: row.draftVersionId, onSelect: (v) => { void loadSnapshot(v); }, onClose: () => setHistory(null), onRestore: restore, render: (versionId) => snapshot?.id === versionId ? (_jsxs("fieldset", { disabled: true, className: "sm-history__form", children: [_jsx(GlobalFields, { item: item, groups: contentGroups, fields: snapshot.fields, refOptions: refOptions, multilingual: multilingual, onChange: () => { } }), settingGroups.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Settings" }), _jsx(GlobalFields, { item: item, groups: settingGroups, fields: snapshot.fields, refOptions: refOptions, multilingual: multilingual, onChange: () => { } })] }))] })) : _jsx("p", { className: "sm-hint", style: { padding: 16 }, children: "Loading\u2026" }) })), _jsx(PresenceLine, { names: editorsOf(others, target), noun: "item" }), stale && _jsx(StaleBanner, { info: stale, onReload: loadTheirs }), _jsx(DriftNotice, { draft: findings, live: liveFindings }), _jsxs("div", { className: "sm-editor", inert: stale ? true : undefined, children: [_jsxs("div", { className: "sm-form", ref: formRef, children: [notice && (_jsxs(Banner, { tone: notice.tone, onDismiss: () => setNotice(null), revealKey: `${notice.tone}:${notice.text}`, children: [_jsx("p", { style: { margin: 0 }, children: notice.text }), _jsx(FieldErrorList, { fields: notice.fields ?? [], onReveal: (path) => { if (formRef.current)
|
|
323
323
|
revealField(formRef.current, path); } })] })), record && (_jsxs("p", { className: "sm-hint", children: [pages.size === 0 ? "Not placed on any page yet." : `Used on ${pages.size} page${pages.size === 1 ? "" : "s"}: `, [...pages].map((id, i) => (_jsxs("span", { children: [i > 0 && ", ", _jsx("a", { href: adminHref(`/pages/${encodeURIComponent(id)}?locale=${encodeURIComponent(locale)}`), children: id })] }, id)))] })), fields !== null ? (_jsxs(_Fragment, { children: [_jsx(GlobalFields, { item: item, groups: contentGroups, fields: fields, errors: markerErrors, refOptions: refOptions, multilingual: multilingual, onChange: onChange }), settingGroups.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Settings" }), _jsx(GlobalFields, { item: item, groups: settingGroups, fields: fields, errors: markerErrors, refOptions: refOptions, multilingual: multilingual, onChange: onChange })] })), _jsx(OrphanFields, { orphans: findings, onClear: clearOrphan })] })) : !notice && _jsx("p", { style: { color: "var(--text-muted)" }, children: "Loading\u2026" })] }), _jsx(ResizeHandle, {}), _jsx("aside", { className: "sm-inspector", children: record && row && (_jsxs("div", { className: "sm-card", children: [_jsx(SectionLabel, { children: "Document info" }), _jsx(InfoRow, { label: "Kind", value: item.section !== undefined ? `Visual · ${item.section}` : "Fields" }), _jsx(InfoRow, { label: "Updated", value: updatedLine(row.updatedAt, row.updatedBy) }), _jsx(InfoRow, { label: "ID", value: record.id, mono: true })] })) })] })] }));
|
|
324
324
|
}
|
|
@@ -142,10 +142,14 @@ export declare function FieldErrorList({ fields, onReveal }: {
|
|
|
142
142
|
fields: FieldError[];
|
|
143
143
|
onReveal?(path: string): void;
|
|
144
144
|
}): import("react").JSX.Element | null;
|
|
145
|
-
export declare function Banner({ tone, children, onDismiss }: {
|
|
145
|
+
export declare function Banner({ tone, children, onDismiss, revealKey }: {
|
|
146
146
|
tone: "ok" | "error" | "warn";
|
|
147
147
|
children: ReactNode;
|
|
148
148
|
onDismiss?: () => void;
|
|
149
|
+
/** Scrolls the banner into view when it appears and whenever this changes: a refused publish
|
|
150
|
+
* clicked at the foot of a long form shows its list of fields (2026-10-02). Unchanged while the
|
|
151
|
+
* list shrinks as fields are fixed, so typing never scrolls. */
|
|
152
|
+
revealKey?: string;
|
|
149
153
|
}): import("react").JSX.Element;
|
|
150
154
|
export declare function cx(...parts: (string | false | null | undefined)[]): string;
|
|
151
155
|
/** One line of a form's info block: a muted label, the value right-aligned and clipped. */
|
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// components/ set (design_handoff_smoodly_cms). Structure and dynamic
|
|
5
5
|
// values live here; every static rule, and all hover/focus state, lives in
|
|
6
6
|
// theme.tsx's stylesheet.
|
|
7
|
-
import { Fragment } from "react";
|
|
7
|
+
import { Fragment, useEffect, useRef } from "react";
|
|
8
8
|
import { GlobeOff, GripVertical, Moon, Sun } from "lucide-react";
|
|
9
9
|
import { THEME_KEY } from "./theme.js";
|
|
10
10
|
import { RouteLink } from "../shell/RouteLink.js";
|
|
@@ -153,8 +153,13 @@ export function FieldErrorList({ fields, onReveal }) {
|
|
|
153
153
|
? _jsx("button", { type: "button", className: "sm-fielderr__field", onClick: () => onReveal(f.field), children: f.field })
|
|
154
154
|
: f.field, ": ", f.message] }, f.field))) }));
|
|
155
155
|
}
|
|
156
|
-
export function Banner({ tone, children, onDismiss }) {
|
|
157
|
-
|
|
156
|
+
export function Banner({ tone, children, onDismiss, revealKey }) {
|
|
157
|
+
const ref = useRef(null);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (revealKey !== undefined)
|
|
160
|
+
ref.current?.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
|
161
|
+
}, [revealKey]);
|
|
162
|
+
return (_jsxs("div", { ref: ref, className: `sm-banner sm-banner--${tone}`, role: tone === "error" ? "alert" : undefined, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: children }), onDismiss && (_jsx("button", { type: "button", onClick: onDismiss, "aria-label": "Dismiss", style: { background: "transparent", border: 0, color: "inherit", cursor: "pointer", font: "inherit", padding: 0 }, children: "\u00D7" }))] }));
|
|
158
163
|
}
|
|
159
164
|
export function cx(...parts) {
|
|
160
165
|
return parts.filter(Boolean).join(" ");
|
package/dist/cli/main.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { type MigrateArgs } from "./migrate.ts";
|
|
|
2
2
|
import { type ExportArgs } from "./export.ts";
|
|
3
3
|
import { type ImportArgs } from "./import.ts";
|
|
4
4
|
import { type AddAdminArgs } from "./add-admin.ts";
|
|
5
|
-
export declare const USAGE = "usage: smoodly <command>\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\n --collections <file> the module exporting `collections` (default smoodly/collections.ts)\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.\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).";
|
|
5
|
+
export declare const USAGE = "usage: smoodly <command>\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 smoodly/collections.ts)\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.\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).";
|
|
6
6
|
export type Parsed = {
|
|
7
7
|
command: "migrate";
|
|
8
8
|
args: MigrateArgs;
|
package/dist/cli/main.js
CHANGED
|
@@ -16,7 +16,8 @@ export const USAGE = `usage: smoodly <command>
|
|
|
16
16
|
lacks, and writes one views file for the collections whose view is new or
|
|
17
17
|
changed. Prints the push or reset command; never runs it.
|
|
18
18
|
|
|
19
|
-
--check write nothing; exit 1 when anything is pending
|
|
19
|
+
--check write nothing; exit 1 when anything is pending, or when
|
|
20
|
+
SMOODLY_SECRET_KEY (if set) cannot read the database
|
|
20
21
|
--collections <file> the module exporting \`collections\` (default smoodly/collections.ts)
|
|
21
22
|
--migrations <dir> the app's migration folder (default supabase/migrations)
|
|
22
23
|
|
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CollectionSchema } from "../collections.ts";
|
|
2
|
+
import { type SchemaRead } from "../schema-read.ts";
|
|
2
3
|
import { type MigrationFile, type MigratePlan } from "../migrations.ts";
|
|
3
4
|
export type MigrateArgs = {
|
|
4
5
|
check: boolean;
|
|
@@ -10,6 +11,8 @@ export declare const PACKAGE_MIGRATIONS_DIR: string;
|
|
|
10
11
|
export declare const NO_FOLDER = "No supabase/migrations folder here \u2014 nothing to do. (Smoodly Cloud keeps the schema current; a self-host app gets the folder from `supabase init`.)";
|
|
11
12
|
export declare const NEXT_STEP = "Next: apply it \u2014 `supabase db push` (hosted) or `supabase migration up` (local).";
|
|
12
13
|
export declare const upToDate: (version: number, n: number) => string;
|
|
14
|
+
export declare const KEY_NOT_CHECKED = "Key not checked: SMOODLY_URL and SMOODLY_SECRET_KEY are not both set here.";
|
|
15
|
+
export declare const keyChecked: (version: number) => string;
|
|
13
16
|
export declare const modifiedMessage: (name: string) => string;
|
|
14
17
|
export declare function readSqlFiles(dir: string): Promise<MigrationFile[]>;
|
|
15
18
|
/** One line per thing the plan does; "Pending" wording under --check. */
|
|
@@ -19,5 +22,9 @@ export type MigrateDeps = {
|
|
|
19
22
|
/** Test seam: replaces the tsx loader. */
|
|
20
23
|
load?: (file: string, cwd: string) => Promise<CollectionSchema<any>[]>;
|
|
21
24
|
now?: () => Date;
|
|
25
|
+
/** The env the key check reads; `.env.local` and `.env` fill what it lacks, as Next does. */
|
|
26
|
+
env?: Record<string, string | undefined>;
|
|
27
|
+
/** Test seam: replaces the database read behind the key check. */
|
|
28
|
+
readKey?: (env: Record<string, string | undefined>) => Promise<SchemaRead>;
|
|
22
29
|
};
|
|
23
30
|
export declare function runMigrate(args: MigrateArgs, out: (line: string) => void, deps?: MigrateDeps): Promise<number>;
|
package/dist/cli/migrate.js
CHANGED
|
@@ -8,7 +8,11 @@ import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { collectionSQL } from "../sql.js";
|
|
11
|
-
import { coreFileVersion, SCHEMA_VERSION } from "../schema-version.js";
|
|
11
|
+
import { coreFileVersion, SCHEMA_VERSION, secretKeyRefusedMessage } from "../schema-version.js";
|
|
12
|
+
import { readSchemaVersion } from "../schema-read.js";
|
|
13
|
+
import { createSmoodlyClient } from "../client.js";
|
|
14
|
+
import { resolveSmoodlyEnv } from "../env.js";
|
|
15
|
+
import { loadAppEnv } from "./space-io.js";
|
|
12
16
|
import { planIsEmpty, planMigrate } from "../migrations.js";
|
|
13
17
|
import { loadCollections } from "./load-collections.js";
|
|
14
18
|
/** The folder shipped in the package: `../../migrations/` from dist/cli/ and from src/cli/ alike. */
|
|
@@ -16,6 +20,8 @@ export const PACKAGE_MIGRATIONS_DIR = fileURLToPath(new URL("../../migrations/",
|
|
|
16
20
|
export const NO_FOLDER = "No supabase/migrations folder here — nothing to do. (Smoodly Cloud keeps the schema current; a self-host app gets the folder from `supabase init`.)";
|
|
17
21
|
export const NEXT_STEP = "Next: apply it — `supabase db push` (hosted) or `supabase migration up` (local).";
|
|
18
22
|
export const upToDate = (version, n) => `Up to date: core schema v${version}, every collection's view is current (${n} ${n === 1 ? "collection" : "collections"}).`;
|
|
23
|
+
export const KEY_NOT_CHECKED = "Key not checked: SMOODLY_URL and SMOODLY_SECRET_KEY are not both set here.";
|
|
24
|
+
export const keyChecked = (version) => `SMOODLY_SECRET_KEY reads the database (schema v${version}).`;
|
|
19
25
|
export const modifiedMessage = (name) => `Refusing: ${name} differs from the package's copy. A published migration is never edited — restore it from the package, or move your change to a new file.`;
|
|
20
26
|
export async function readSqlFiles(dir) {
|
|
21
27
|
const names = (await readdir(dir)).filter((n) => n.endsWith(".sql")).sort();
|
|
@@ -34,6 +40,41 @@ export function describePlan(plan, check) {
|
|
|
34
40
|
}
|
|
35
41
|
return lines;
|
|
36
42
|
}
|
|
43
|
+
/** Errors that are a verdict on the env itself, not on the network: they fail the build. */
|
|
44
|
+
const ENV_VERDICT = /SMOODLY_(SECRET|PUBLISHABLE)_KEY|SMOODLY_URL "/;
|
|
45
|
+
/** The build's key check (decided 2026-10-02): a key the site would be refused with fails the
|
|
46
|
+
* deploy, so the running deployment keeps serving. No key here, or no route to the database
|
|
47
|
+
* (a CI build), passes without a verdict. */
|
|
48
|
+
async function checkKey(out, cwd, deps) {
|
|
49
|
+
const env = deps.env ?? process.env;
|
|
50
|
+
loadAppEnv(cwd, env);
|
|
51
|
+
if (!env.SMOODLY_URL || !env.SMOODLY_SECRET_KEY) {
|
|
52
|
+
out(KEY_NOT_CHECKED);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
let read;
|
|
56
|
+
try {
|
|
57
|
+
read = await (deps.readKey ?? ((e) => readSchemaVersion(createSmoodlyClient(resolveSmoodlyEnv(e)))))(env);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
61
|
+
if (ENV_VERDICT.test(message)) {
|
|
62
|
+
out(message);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
out(`Key not checked: ${message}`);
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
if ("version" in read)
|
|
69
|
+
out(keyChecked(read.version));
|
|
70
|
+
else if (read.missing === "key") {
|
|
71
|
+
out(secretKeyRefusedMessage);
|
|
72
|
+
return 1;
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
out("Key not checked: the database has no Smoodly schema yet — apply the migrations before the site goes live.");
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
37
78
|
export async function runMigrate(args, out, deps = {}) {
|
|
38
79
|
const cwd = deps.cwd ?? process.cwd();
|
|
39
80
|
const appDir = resolve(cwd, args.migrations);
|
|
@@ -58,7 +99,7 @@ export async function runMigrate(args, out, deps = {}) {
|
|
|
58
99
|
return 1;
|
|
59
100
|
if (planIsEmpty(plan)) {
|
|
60
101
|
out(upToDate(SCHEMA_VERSION, collections.length));
|
|
61
|
-
return 0;
|
|
102
|
+
return args.check ? checkKey(out, cwd, deps) : 0;
|
|
62
103
|
}
|
|
63
104
|
if (args.check)
|
|
64
105
|
return 1;
|
package/dist/schema-read.d.ts
CHANGED
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
|
|
3
|
-
export declare function readSchemaVersion(db: SupabaseClient): Promise<{
|
|
2
|
+
export type SchemaRead = {
|
|
4
3
|
version: number;
|
|
5
4
|
} | {
|
|
6
5
|
missing: "key" | "schema";
|
|
7
|
-
}
|
|
6
|
+
};
|
|
7
|
+
/** The policies show `smoodly_meta` to a write key only, so no visible row means the key is not a secret key (spec §5). */
|
|
8
|
+
export declare function readSchemaVersion(db: SupabaseClient): Promise<SchemaRead>;
|
|
9
|
+
/** How long a refused key is believed before it is read again. */
|
|
10
|
+
export declare const KEY_RECHECK_MS = 30000;
|
|
11
|
+
/** The site's schema check, once per process (spec 2026-09-26 §5). An old or missing schema warns
|
|
12
|
+
* and never blocks. A key that cannot see the version row reads every table as empty rather than
|
|
13
|
+
* refused, so lookups fail instead of serving empty pages and 404s (decided 2026-10-02) — but only
|
|
14
|
+
* for {@link KEY_RECHECK_MS}: a fixed key or a briefly missing row recovers without a restart. A
|
|
15
|
+
* failed read refuses nothing and is tried again; the lookup itself names that error. */
|
|
16
|
+
export declare class SecretKeyCheck {
|
|
17
|
+
private readonly read;
|
|
18
|
+
private readonly now;
|
|
19
|
+
private pending;
|
|
20
|
+
private refusedAt;
|
|
21
|
+
constructor(read: () => Promise<SchemaRead>, now?: () => number);
|
|
22
|
+
refused(): Promise<boolean>;
|
|
23
|
+
}
|
package/dist/schema-read.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SCHEMA_VERSION_KEY } from "./schema-version.js";
|
|
1
|
+
import { SCHEMA_VERSION_KEY, schemaBehind, schemaBehindMessage } from "./schema-version.js";
|
|
2
2
|
/** The policies show `smoodly_meta` to a write key only, so no visible row means the key is not a secret key (spec §5). */
|
|
3
3
|
export async function readSchemaVersion(db) {
|
|
4
4
|
const res = await db.from("smoodly_meta").select("value").eq("key", SCHEMA_VERSION_KEY).maybeSingle();
|
|
@@ -13,3 +13,38 @@ export async function readSchemaVersion(db) {
|
|
|
13
13
|
const n = Number(res.data?.value);
|
|
14
14
|
return res.data && Number.isInteger(n) ? { version: n } : { missing: "key" };
|
|
15
15
|
}
|
|
16
|
+
/** How long a refused key is believed before it is read again. */
|
|
17
|
+
export const KEY_RECHECK_MS = 30000;
|
|
18
|
+
/** The site's schema check, once per process (spec 2026-09-26 §5). An old or missing schema warns
|
|
19
|
+
* and never blocks. A key that cannot see the version row reads every table as empty rather than
|
|
20
|
+
* refused, so lookups fail instead of serving empty pages and 404s (decided 2026-10-02) — but only
|
|
21
|
+
* for {@link KEY_RECHECK_MS}: a fixed key or a briefly missing row recovers without a restart. A
|
|
22
|
+
* failed read refuses nothing and is tried again; the lookup itself names that error. */
|
|
23
|
+
export class SecretKeyCheck {
|
|
24
|
+
constructor(read, now = Date.now) {
|
|
25
|
+
this.read = read;
|
|
26
|
+
this.now = now;
|
|
27
|
+
this.pending = null;
|
|
28
|
+
this.refusedAt = null;
|
|
29
|
+
}
|
|
30
|
+
refused() {
|
|
31
|
+
if (this.refusedAt !== null && this.now() - this.refusedAt >= KEY_RECHECK_MS)
|
|
32
|
+
this.pending = null;
|
|
33
|
+
this.pending ?? (this.pending = this.read().then((read) => {
|
|
34
|
+
if ("missing" in read && read.missing === "key") {
|
|
35
|
+
this.refusedAt = this.now();
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
this.refusedAt = null;
|
|
39
|
+
if ("missing" in read)
|
|
40
|
+
console.warn(schemaBehindMessage(null));
|
|
41
|
+
else if (schemaBehind(read.version))
|
|
42
|
+
console.warn(schemaBehindMessage(read.version));
|
|
43
|
+
return false;
|
|
44
|
+
}, () => {
|
|
45
|
+
this.pending = null;
|
|
46
|
+
return false;
|
|
47
|
+
}));
|
|
48
|
+
return this.pending;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/supabase-store.d.ts
CHANGED
|
@@ -62,8 +62,8 @@ export declare class SupabasePageStore implements PageStore {
|
|
|
62
62
|
getPage(id: string): Promise<PageRecord | null>;
|
|
63
63
|
scanTrees(): Promise<PageScan[]>;
|
|
64
64
|
listPages(): Promise<PageRecord[]>;
|
|
65
|
-
/**
|
|
66
|
-
private
|
|
65
|
+
/** Once per store; a refused key fails lookups for 30 s at a time (schema-read.ts). */
|
|
66
|
+
private readonly keyCheck;
|
|
67
67
|
resolvePath(locale: string, path: string): Promise<import("./path-index.ts").PathHit | null>;
|
|
68
68
|
schemaVersion(): Promise<number | null>;
|
|
69
69
|
pathsOf(id: string): Promise<{}>;
|
package/dist/supabase-store.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
// rewritten through ONE RPC per write, so a collision rolls back.
|
|
9
9
|
import { assertPublishable, assertSupportedLocale, localeRow, pageErrors, pageRefEdges, pathOptions, refSections, storeAnchors, } from "./store.js";
|
|
10
10
|
import { SupabasePathIndex } from "./supabase-path-index.js";
|
|
11
|
-
import {
|
|
12
|
-
import { readSchemaVersion } from "./schema-read.js";
|
|
11
|
+
import { secretKeyRefusedMessage } from "./schema-version.js";
|
|
12
|
+
import { readSchemaVersion, SecretKeyCheck } from "./schema-read.js";
|
|
13
13
|
import { groupRedirects } from "./store.js";
|
|
14
14
|
import { assertFresh, draftToken, StaleDraftError } from "./draft-token.js";
|
|
15
15
|
import { assertDepth, assertSegment, isHomeNode, pagePathRows, rewriteTargets } from "./paths.js";
|
|
@@ -46,8 +46,8 @@ export class SupabasePageStore {
|
|
|
46
46
|
constructor(db, options = {}) {
|
|
47
47
|
this.db = db;
|
|
48
48
|
this.options = options;
|
|
49
|
-
/**
|
|
50
|
-
this.
|
|
49
|
+
/** Once per store; a refused key fails lookups for 30 s at a time (schema-read.ts). */
|
|
50
|
+
this.keyCheck = new SecretKeyCheck(() => readSchemaVersion(this.db));
|
|
51
51
|
this.options = { ...options, sections: refSections(options) };
|
|
52
52
|
this.opts = pathOptions(options);
|
|
53
53
|
// Alone, the store's own index reads the addresses it was given (spec 2026-09-29 anchored paths §2).
|
|
@@ -379,20 +379,12 @@ export class SupabasePageStore {
|
|
|
379
379
|
this.fail(res.error.message);
|
|
380
380
|
return res.data.map(toRecord);
|
|
381
381
|
}
|
|
382
|
-
resolvePath(locale, path) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if ("missing" in read)
|
|
389
|
-
console.warn(read.missing === "key" ? secretKeyRefusedMessage : schemaBehindMessage(null));
|
|
390
|
-
else if (schemaBehind(read.version))
|
|
391
|
-
console.warn(schemaBehindMessage(read.version));
|
|
392
|
-
})
|
|
393
|
-
.catch(() => { });
|
|
394
|
-
}
|
|
395
|
-
return this.paths.resolve(locale, path);
|
|
382
|
+
async resolvePath(locale, path) {
|
|
383
|
+
// In parallel: the check costs the first request no extra round trip.
|
|
384
|
+
const [refused, hit] = await Promise.all([this.keyCheck.refused(), this.paths.resolve(locale, path)]);
|
|
385
|
+
if (refused)
|
|
386
|
+
throw new Error(secretKeyRefusedMessage);
|
|
387
|
+
return hit;
|
|
396
388
|
}
|
|
397
389
|
async schemaVersion() {
|
|
398
390
|
const read = await readSchemaVersion(this.db);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SMOODLY_VERSION = "0.0.
|
|
1
|
+
export declare const SMOODLY_VERSION = "0.0.43";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/write-version.mjs from package.json — do not edit.
|
|
2
|
-
export const SMOODLY_VERSION = "0.0.
|
|
2
|
+
export const SMOODLY_VERSION = "0.0.43";
|
package/package.json
CHANGED