proto-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +350 -0
- package/README.md +279 -0
- package/components.json +18 -0
- package/package.json +73 -0
- package/scripts/download-mobbin-reference-images.mjs +112 -0
- package/scripts/lib/host-config.example.sh +48 -0
- package/scripts/lib/host-config.sh +25 -0
- package/scripts/lib/resolve-host-root.mjs +18 -0
- package/scripts/lib/source-path.sh +49 -0
- package/scripts/link-source-db.sh +128 -0
- package/scripts/link-source.sh +40 -0
- package/scripts/publish-package.sh +147 -0
- package/scripts/share-prototype.sh +279 -0
- package/scripts/sync-from-source.sh +74 -0
- package/scripts/verify-prototype-component-ids.mjs +151 -0
- package/scripts/verify-prototype-preview-states.mjs +135 -0
- package/src/app.ts +4 -0
- package/src/components/platform-ui/code-block.tsx +21 -0
- package/src/components/platform-ui/controls-panel-options.tsx +71 -0
- package/src/components/platform-ui/controls-panel-select.tsx +438 -0
- package/src/components/platform-ui/empty-state.tsx +15 -0
- package/src/components/platform-ui/icon-button.tsx +18 -0
- package/src/components/platform-ui/panel-select.tsx +123 -0
- package/src/components/platform-ui/panel.tsx +97 -0
- package/src/components/platform-ui/review-dropdown-menu.tsx +35 -0
- package/src/components/platform-ui/sidebar.tsx +293 -0
- package/src/components/platform-ui/toolbar-icon-button.tsx +34 -0
- package/src/components/platform-ui/toolbar.tsx +37 -0
- package/src/components/prototype-gallery-client.tsx +57 -0
- package/src/components/prototype-provider.tsx +20 -0
- package/src/components/prototypes/mini-pill-label.tsx +37 -0
- package/src/components/prototypes/prototype-change-log-panel.tsx +207 -0
- package/src/components/prototypes/prototype-comment-provider.tsx +61 -0
- package/src/components/prototypes/prototype-comment-toolbar.tsx +65 -0
- package/src/components/prototypes/prototype-control.tsx +31 -0
- package/src/components/prototypes/prototype-controls.tsx +553 -0
- package/src/components/prototypes/prototype-create-exploration-modal.tsx +117 -0
- package/src/components/prototypes/prototype-create-state-modal.tsx +117 -0
- package/src/components/prototypes/prototype-default-state-map-page.tsx +66 -0
- package/src/components/prototypes/prototype-design-brief-modal.tsx +128 -0
- package/src/components/prototypes/prototype-design-brief-panel.tsx +525 -0
- package/src/components/prototypes/prototype-design-context-panel.tsx +65 -0
- package/src/components/prototypes/prototype-floating-pill.module.scss +1341 -0
- package/src/components/prototypes/prototype-mobbin-gallery.tsx +330 -0
- package/src/components/prototypes/prototype-reference-docs.tsx +222 -0
- package/src/components/prototypes/prototype-review-chrome.module.scss +630 -0
- package/src/components/prototypes/prototype-review-chrome.tsx +1241 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-select.tsx +4 -0
- package/src/components/prototypes/prototype-review-sidebar.tsx +167 -0
- package/src/components/prototypes/prototype-share-link-restore.tsx +75 -0
- package/src/components/prototypes/prototype-share-mode.tsx +206 -0
- package/src/components/prototypes/prototype-spec-panel-content.tsx +538 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +336 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.tsx +1023 -0
- package/src/components/prototypes/prototype-state-canvas.tsx +38 -0
- package/src/components/prototypes/prototype-state-screenshot-preview.tsx +47 -0
- package/src/components/prototypes/prototype-target.tsx +120 -0
- package/src/components/prototypes/prototype-tweak-highlight.tsx +132 -0
- package/src/components/prototypes/prototype-tweaks.tsx +48 -0
- package/src/components/prototypes/prototype-variant-explorer.tsx +347 -0
- package/src/components/prototypes/prototype-variant-rationale-panel.tsx +49 -0
- package/src/components/prototypes/prototype-variant-tabs.tsx +207 -0
- package/src/components/prototypes/prototype-vercel-preview-modal.tsx +120 -0
- package/src/components/prototypes/state-map-annotation-tooltip.module.scss +89 -0
- package/src/components/prototypes/state-map-annotation-tooltip.tsx +137 -0
- package/src/components/prototypes/state-map-wireframe-highlight.module.scss +25 -0
- package/src/components/prototypes/state-map-wireframe-highlight.tsx +82 -0
- package/src/components/shell/prototype-gallery-shell.tsx +79 -0
- package/src/components/shell/prototype-screenshot-capture.tsx +45 -0
- package/src/components/shell/prototype-shell.module.scss +194 -0
- package/src/components/shell/prototype-shell.tsx +160 -0
- package/src/components/shell/prototype-state-screenshot-capture.tsx +68 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/dialog.module.scss +81 -0
- package/src/components/ui/dialog.tsx +127 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/option-select.tsx +81 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/toggle-group.tsx +80 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/config/create-prototype-gallery-page.tsx +65 -0
- package/src/config/create-prototype-host-app.tsx +121 -0
- package/src/config/create-prototype-page.tsx +41 -0
- package/src/config/create-prototype-site-layout.tsx +28 -0
- package/src/config/define-prototype-config.ts +7 -0
- package/src/config/prototype-icon.tsx +25 -0
- package/src/config/prototype-site-layout-client.tsx +35 -0
- package/src/config/with-prototype.ts +20 -0
- package/src/index.ts +303 -0
- package/src/lib/pr-split/build-pr-agent-prompt.ts +68 -0
- package/src/lib/pr-split/build-pr-split-prototype-url.ts +12 -0
- package/src/lib/pr-split/pr-split-highlight.ts +6 -0
- package/src/lib/pr-split/pr-split-types.ts +58 -0
- package/src/lib/pr-split/resolve-source-preview-url.ts +45 -0
- package/src/lib/pr-split/use-pr-vercel-previews.ts +69 -0
- package/src/lib/prototype-comments/core/annotation-channel.ts +41 -0
- package/src/lib/prototype-comments/core/annotation-status.ts +60 -0
- package/src/lib/prototype-comments/core/annotation-target.ts +567 -0
- package/src/lib/prototype-comments/core/capture-theme.ts +41 -0
- package/src/lib/prototype-comments/core/capture.ts +11 -0
- package/src/lib/prototype-comments/core/comment-capture-blocked.ts +47 -0
- package/src/lib/prototype-comments/core/comment-threads.ts +52 -0
- package/src/lib/prototype-comments/core/element-identification.ts +615 -0
- package/src/lib/prototype-comments/core/export.ts +35 -0
- package/src/lib/prototype-comments/core/freeze-animations.ts +266 -0
- package/src/lib/prototype-comments/core/identify-element.ts +76 -0
- package/src/lib/prototype-comments/core/normalize-viewport.ts +35 -0
- package/src/lib/prototype-comments/core/react-detection.ts +704 -0
- package/src/lib/prototype-comments/core/restore.ts +7 -0
- package/src/lib/prototype-comments/core/screenshot.ts +428 -0
- package/src/lib/prototype-comments/core/source-location.ts +906 -0
- package/src/lib/prototype-comments/core/storage.ts +80 -0
- package/src/lib/prototype-comments/core/types.ts +79 -0
- package/src/lib/prototype-comments/core/validation.ts +17 -0
- package/src/lib/prototype-comments/hooks/useLayoutShift.ts +61 -0
- package/src/lib/prototype-comments/index.ts +58 -0
- package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +1398 -0
- package/src/lib/prototype-comments/react/CommentProvider.tsx +298 -0
- package/src/lib/prototype-comments/scss.d.ts +10 -0
- package/src/lib/prototype-comments/ui/CommentAnnotationOverlay.tsx +253 -0
- package/src/lib/prototype-comments/ui/CommentDetailPage.tsx +56 -0
- package/src/lib/prototype-comments/ui/CommentHeaderCount.tsx +28 -0
- package/src/lib/prototype-comments/ui/CommentThread.tsx +331 -0
- package/src/lib/prototype-comments/ui/CommentsGrid.tsx +780 -0
- package/src/lib/prototype-comments/ui/CommentsListPage.tsx +25 -0
- package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +77 -0
- package/src/lib/prototype-comments/ui/annotation-marker/index.tsx +200 -0
- package/src/lib/prototype-comments/ui/annotation-marker/styles.module.scss +194 -0
- package/src/lib/prototype-comments/ui/annotation-popup/index.tsx +335 -0
- package/src/lib/prototype-comments/ui/annotation-popup/styles.module.scss +469 -0
- package/src/lib/prototype-comments/ui/capture-styles.module.scss +2225 -0
- package/src/lib/prototype-comments/ui/comment-highlight.ts +48 -0
- package/src/lib/prototype-comments/ui/comment-thread.module.scss +203 -0
- package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +253 -0
- package/src/lib/prototype-comments/ui/comments-sidebar.module.scss +123 -0
- package/src/lib/prototype-comments/ui/icon-transitions.module.scss +51 -0
- package/src/lib/prototype-comments/ui/icons.tsx +1086 -0
- package/src/lib/prototype-comments/ui/is-comment-detail-path.ts +7 -0
- package/src/lib/prototypes/capture-prototype-viewport.ts +32 -0
- package/src/lib/prototypes/changelog-meta-context.tsx +162 -0
- package/src/lib/prototypes/changelog-meta-storage.ts +62 -0
- package/src/lib/prototypes/changelog-meta.ts +95 -0
- package/src/lib/prototypes/create-prototype-registry.ts +74 -0
- package/src/lib/prototypes/create-state-screenshot-thumbnail.ts +46 -0
- package/src/lib/prototypes/design-exploration-default-storage.ts +68 -0
- package/src/lib/prototypes/design-exploration-default.ts +51 -0
- package/src/lib/prototypes/design-exploration-types.ts +203 -0
- package/src/lib/prototypes/prototype-comment-registry.tsx +145 -0
- package/src/lib/prototypes/prototype-comment-review-bridge.tsx +40 -0
- package/src/lib/prototypes/prototype-comment-review-state.ts +57 -0
- package/src/lib/prototypes/prototype-component-registry.ts +40 -0
- package/src/lib/prototypes/prototype-config-types.ts +34 -0
- package/src/lib/prototypes/prototype-preview-state-registry.ts +443 -0
- package/src/lib/prototypes/prototype-preview-state-types.ts +89 -0
- package/src/lib/prototypes/prototype-review-context.tsx +905 -0
- package/src/lib/prototypes/prototype-review-url.ts +134 -0
- package/src/lib/prototypes/prototype-share-link.ts +175 -0
- package/src/lib/prototypes/prototype-state-canvas-constants.ts +118 -0
- package/src/lib/prototypes/prototype-state-canvas-types.ts +159 -0
- package/src/lib/prototypes/prototype-state-screenshot.ts +39 -0
- package/src/lib/prototypes/prototype-tweak-registry.ts +96 -0
- package/src/lib/prototypes/prototype-tweak-types.ts +31 -0
- package/src/lib/prototypes/reference-docs.ts +137 -0
- package/src/lib/prototypes/screenshot-manifest.ts +33 -0
- package/src/lib/prototypes/screenshot-src.ts +7 -0
- package/src/lib/prototypes/share-command.ts +8 -0
- package/src/lib/prototypes/state-map-annotation.ts +70 -0
- package/src/lib/prototypes/use-design-exploration-default.ts +114 -0
- package/src/lib/prototypes/use-persisted-local-state.ts +140 -0
- package/src/lib/prototypes/use-prototype-comments.ts +63 -0
- package/src/lib/prototypes/use-prototype-mobile-viewport-frame.ts +46 -0
- package/src/lib/prototypes/use-prototype-state-screenshot-src.ts +71 -0
- package/src/lib/prototypes/use-prototype-tool-theme.tsx +115 -0
- package/src/lib/prototypes/use-prototype-viewport-frame.ts +34 -0
- package/src/lib/prototypes/use-register-prototype-reference-docs.ts +16 -0
- package/src/lib/prototypes/use-snap-corner-position.ts +417 -0
- package/src/lib/prototypes/use-toolbar-hover-panel.ts +98 -0
- package/src/lib/prototypes/variant-set-lucide-icon.tsx +55 -0
- package/src/lib/tool-portal.ts +33 -0
- package/src/lib/use-copy-to-clipboard.ts +81 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vercel-preview/parse-vercel-github-comment.ts +81 -0
- package/src/server/create-host-api-route.ts +100 -0
- package/src/server/create-prototype-api-routes.ts +428 -0
- package/src/server/gallery/create-gallery-api.ts +56 -0
- package/src/server/gallery/folders-route.ts +156 -0
- package/src/server/gallery/gallery-env.ts +39 -0
- package/src/server/gallery/gallery-route.ts +150 -0
- package/src/server/gallery/gallery-types.ts +15 -0
- package/src/server/gallery/image-route.ts +59 -0
- package/src/server/gallery/item-route.ts +92 -0
- package/src/server/pr-vercel-preview-route.ts +162 -0
- package/src/server/redis/client.ts +55 -0
- package/src/server/redis/prototype-changelog-meta.ts +31 -0
- package/src/server/redis/prototype-comments.ts +92 -0
- package/src/server/redis/prototype-design-exploration-default.ts +32 -0
- package/src/server.ts +51 -0
- package/src/styles/globals.css +624 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3
|
+
import path from "path";
|
|
4
|
+
|
|
5
|
+
import { createPrototypeRegistry } from "@prototype/lib/prototypes/create-prototype-registry";
|
|
6
|
+
import type { PrototypeConfig } from "@prototype/lib/prototypes/prototype-config-types";
|
|
7
|
+
import { isValidAnnotation } from "@prototype/lib/prototype-comments/core/validation";
|
|
8
|
+
import {
|
|
9
|
+
isValidChangelogMeta,
|
|
10
|
+
normalizeChangelogMetaPayload,
|
|
11
|
+
} from "@prototype/lib/prototypes/changelog-meta";
|
|
12
|
+
import {
|
|
13
|
+
sanitizeStateIdForFilename,
|
|
14
|
+
stateScreenshotManifestKey,
|
|
15
|
+
} from "@prototype/lib/prototypes/prototype-state-screenshot";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
readScreenshotManifest,
|
|
19
|
+
touchScreenshotManifest,
|
|
20
|
+
touchScreenshotManifestKey,
|
|
21
|
+
} from "../lib/prototypes/screenshot-manifest";
|
|
22
|
+
import { getRedisStatus } from "./redis/client";
|
|
23
|
+
import {
|
|
24
|
+
listPrototypeComments,
|
|
25
|
+
mergePrototypeComments,
|
|
26
|
+
upsertPrototypeComment,
|
|
27
|
+
} from "./redis/prototype-comments";
|
|
28
|
+
import {
|
|
29
|
+
isValidDesignExplorationDefaultRecord,
|
|
30
|
+
normalizeDesignExplorationDefaultPayload,
|
|
31
|
+
} from "@prototype/lib/prototypes/design-exploration-default";
|
|
32
|
+
import {
|
|
33
|
+
getPrototypeChangelogMeta,
|
|
34
|
+
setPrototypeChangelogMeta,
|
|
35
|
+
} from "./redis/prototype-changelog-meta";
|
|
36
|
+
import {
|
|
37
|
+
getPrototypeDesignExplorationDefault,
|
|
38
|
+
setPrototypeDesignExplorationDefault,
|
|
39
|
+
} from "./redis/prototype-design-exploration-default";
|
|
40
|
+
|
|
41
|
+
type SlugRouteContext = {
|
|
42
|
+
params: Promise<{ slug: string }>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type CommentIdRouteContext = {
|
|
46
|
+
params: Promise<{ slug: string; id: string }>;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function commentStorageUnavailableResponse() {
|
|
50
|
+
const status = getRedisStatus();
|
|
51
|
+
return NextResponse.json(
|
|
52
|
+
{
|
|
53
|
+
error: "Comment storage is not configured.",
|
|
54
|
+
missing: status.missing,
|
|
55
|
+
},
|
|
56
|
+
{ status: 503 },
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function changelogStorageUnavailableResponse() {
|
|
61
|
+
const status = getRedisStatus();
|
|
62
|
+
return NextResponse.json(
|
|
63
|
+
{
|
|
64
|
+
error: "Change log storage is not configured.",
|
|
65
|
+
missing: status.missing,
|
|
66
|
+
},
|
|
67
|
+
{ status: 503 },
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function designDefaultStorageUnavailableResponse() {
|
|
72
|
+
const status = getRedisStatus();
|
|
73
|
+
return NextResponse.json(
|
|
74
|
+
{
|
|
75
|
+
error: "Design default storage is not configured.",
|
|
76
|
+
missing: status.missing,
|
|
77
|
+
},
|
|
78
|
+
{ status: 503 },
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function createPrototypeCommentsRoute(config: PrototypeConfig) {
|
|
83
|
+
const registry = createPrototypeRegistry(config);
|
|
84
|
+
|
|
85
|
+
async function GET(_request: NextRequest, { params }: SlugRouteContext) {
|
|
86
|
+
const { slug } = await params;
|
|
87
|
+
const prototype = registry.getPrototype(slug);
|
|
88
|
+
|
|
89
|
+
if (!prototype) {
|
|
90
|
+
return NextResponse.json({ error: "Unknown prototype" }, { status: 404 });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const redisStatus = getRedisStatus();
|
|
94
|
+
if (!redisStatus.ok) {
|
|
95
|
+
return commentStorageUnavailableResponse();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const annotations = await listPrototypeComments(slug);
|
|
100
|
+
return NextResponse.json(annotations);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error("Prototype comments GET error:", error);
|
|
103
|
+
return NextResponse.json({ error: "Failed to load comments" }, { status: 500 });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function PUT(request: NextRequest, { params }: SlugRouteContext) {
|
|
108
|
+
const { slug } = await params;
|
|
109
|
+
const prototype = registry.getPrototype(slug);
|
|
110
|
+
|
|
111
|
+
if (!prototype) {
|
|
112
|
+
return NextResponse.json({ error: "Unknown prototype" }, { status: 404 });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const redisStatus = getRedisStatus();
|
|
116
|
+
if (!redisStatus.ok) {
|
|
117
|
+
return commentStorageUnavailableResponse();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let body: unknown;
|
|
121
|
+
try {
|
|
122
|
+
body = await request.json();
|
|
123
|
+
} catch {
|
|
124
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!Array.isArray(body)) {
|
|
128
|
+
return NextResponse.json({ error: "Body must be an array" }, { status: 400 });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const annotations = body.filter(isValidAnnotation);
|
|
132
|
+
if (annotations.length === 0 && body.length > 0) {
|
|
133
|
+
return NextResponse.json({ error: "No valid comments in body" }, { status: 400 });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const count = await mergePrototypeComments(slug, annotations);
|
|
138
|
+
return NextResponse.json({ ok: true, count });
|
|
139
|
+
} catch (error) {
|
|
140
|
+
console.error("Prototype comments PUT error:", error);
|
|
141
|
+
return NextResponse.json({ error: "Failed to save comments" }, { status: 500 });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { GET, PUT };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function createPrototypeCommentIdRoute(config: PrototypeConfig) {
|
|
149
|
+
const registry = createPrototypeRegistry(config);
|
|
150
|
+
|
|
151
|
+
async function PUT(request: NextRequest, { params }: CommentIdRouteContext) {
|
|
152
|
+
const { slug, id } = await params;
|
|
153
|
+
const prototype = registry.getPrototype(slug);
|
|
154
|
+
|
|
155
|
+
if (!prototype) {
|
|
156
|
+
return NextResponse.json({ error: "Unknown prototype" }, { status: 404 });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const redisStatus = getRedisStatus();
|
|
160
|
+
if (!redisStatus.ok) {
|
|
161
|
+
return commentStorageUnavailableResponse();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let body: unknown;
|
|
165
|
+
try {
|
|
166
|
+
body = await request.json();
|
|
167
|
+
} catch {
|
|
168
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!isValidAnnotation(body)) {
|
|
172
|
+
return NextResponse.json({ error: "Invalid comment annotation" }, { status: 400 });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const annotation = body as Record<string, unknown>;
|
|
176
|
+
if (annotation.id !== id) {
|
|
177
|
+
return NextResponse.json(
|
|
178
|
+
{ error: "Comment id must match the URL path." },
|
|
179
|
+
{ status: 400 },
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
await upsertPrototypeComment(slug, body);
|
|
185
|
+
return NextResponse.json({ ok: true, id });
|
|
186
|
+
} catch (error) {
|
|
187
|
+
console.error("Prototype comment PUT error:", error);
|
|
188
|
+
return NextResponse.json({ error: "Failed to save comment" }, { status: 500 });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { PUT };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function createPrototypeChangelogMetaRoute(config: PrototypeConfig) {
|
|
196
|
+
const registry = createPrototypeRegistry(config);
|
|
197
|
+
|
|
198
|
+
async function GET(_request: NextRequest, { params }: SlugRouteContext) {
|
|
199
|
+
const { slug } = await params;
|
|
200
|
+
const prototype = registry.getPrototype(slug);
|
|
201
|
+
|
|
202
|
+
if (!prototype) {
|
|
203
|
+
return NextResponse.json({ error: "Unknown prototype" }, { status: 404 });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const redisStatus = getRedisStatus();
|
|
207
|
+
if (!redisStatus.ok) {
|
|
208
|
+
return changelogStorageUnavailableResponse();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const meta = await getPrototypeChangelogMeta(slug);
|
|
213
|
+
return NextResponse.json(meta);
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error("Prototype changelog-meta GET error:", error);
|
|
216
|
+
return NextResponse.json(
|
|
217
|
+
{ error: "Failed to load change log overview" },
|
|
218
|
+
{ status: 500 },
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function PUT(request: NextRequest, { params }: SlugRouteContext) {
|
|
224
|
+
const { slug } = await params;
|
|
225
|
+
const prototype = registry.getPrototype(slug);
|
|
226
|
+
|
|
227
|
+
if (!prototype) {
|
|
228
|
+
return NextResponse.json({ error: "Unknown prototype" }, { status: 404 });
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const redisStatus = getRedisStatus();
|
|
232
|
+
if (!redisStatus.ok) {
|
|
233
|
+
return changelogStorageUnavailableResponse();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let body: unknown;
|
|
237
|
+
try {
|
|
238
|
+
body = await request.json();
|
|
239
|
+
} catch {
|
|
240
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!isValidChangelogMeta(body)) {
|
|
244
|
+
return NextResponse.json(
|
|
245
|
+
{ error: "Invalid change log meta body" },
|
|
246
|
+
{ status: 400 },
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const meta = normalizeChangelogMetaPayload(body);
|
|
251
|
+
if (!meta) {
|
|
252
|
+
return NextResponse.json(
|
|
253
|
+
{ error: "Invalid change log meta body" },
|
|
254
|
+
{ status: 400 },
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await setPrototypeChangelogMeta(slug, meta);
|
|
260
|
+
return NextResponse.json({ ok: true });
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error("Prototype changelog-meta PUT error:", error);
|
|
263
|
+
return NextResponse.json(
|
|
264
|
+
{ error: "Failed to save change log overview" },
|
|
265
|
+
{ status: 500 },
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { GET, PUT };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function createPrototypeDesignExplorationDefaultRoute(
|
|
274
|
+
config: PrototypeConfig,
|
|
275
|
+
) {
|
|
276
|
+
const registry = createPrototypeRegistry(config);
|
|
277
|
+
|
|
278
|
+
async function GET(request: NextRequest, { params }: SlugRouteContext) {
|
|
279
|
+
const { slug } = await params;
|
|
280
|
+
const prototype = registry.getPrototype(slug);
|
|
281
|
+
|
|
282
|
+
if (!prototype) {
|
|
283
|
+
return NextResponse.json({ error: "Unknown prototype" }, { status: 404 });
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const storageKeyPrefix = request.nextUrl.searchParams.get("key")?.trim();
|
|
287
|
+
if (!storageKeyPrefix) {
|
|
288
|
+
return NextResponse.json(
|
|
289
|
+
{ error: "Missing key query parameter." },
|
|
290
|
+
{ status: 400 },
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const redisStatus = getRedisStatus();
|
|
295
|
+
if (!redisStatus.ok) {
|
|
296
|
+
return designDefaultStorageUnavailableResponse();
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const record = await getPrototypeDesignExplorationDefault(storageKeyPrefix);
|
|
301
|
+
return NextResponse.json(record);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error("Prototype design-exploration-default GET error:", error);
|
|
304
|
+
return NextResponse.json(
|
|
305
|
+
{ error: "Failed to load design default variant" },
|
|
306
|
+
{ status: 500 },
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function PUT(request: NextRequest, { params }: SlugRouteContext) {
|
|
312
|
+
const { slug } = await params;
|
|
313
|
+
const prototype = registry.getPrototype(slug);
|
|
314
|
+
|
|
315
|
+
if (!prototype) {
|
|
316
|
+
return NextResponse.json({ error: "Unknown prototype" }, { status: 404 });
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const storageKeyPrefix = request.nextUrl.searchParams.get("key")?.trim();
|
|
320
|
+
if (!storageKeyPrefix) {
|
|
321
|
+
return NextResponse.json(
|
|
322
|
+
{ error: "Missing key query parameter." },
|
|
323
|
+
{ status: 400 },
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const redisStatus = getRedisStatus();
|
|
328
|
+
if (!redisStatus.ok) {
|
|
329
|
+
return designDefaultStorageUnavailableResponse();
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let body: unknown;
|
|
333
|
+
try {
|
|
334
|
+
body = await request.json();
|
|
335
|
+
} catch {
|
|
336
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (!isValidDesignExplorationDefaultRecord(body)) {
|
|
340
|
+
return NextResponse.json(
|
|
341
|
+
{ error: "Invalid design default record body" },
|
|
342
|
+
{ status: 400 },
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const record = normalizeDesignExplorationDefaultPayload(body);
|
|
347
|
+
if (!record) {
|
|
348
|
+
return NextResponse.json(
|
|
349
|
+
{ error: "Invalid design default record body" },
|
|
350
|
+
{ status: 400 },
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
await setPrototypeDesignExplorationDefault(storageKeyPrefix, record);
|
|
356
|
+
return NextResponse.json({ ok: true });
|
|
357
|
+
} catch (error) {
|
|
358
|
+
console.error("Prototype design-exploration-default PUT error:", error);
|
|
359
|
+
return NextResponse.json(
|
|
360
|
+
{ error: "Failed to save design default variant" },
|
|
361
|
+
{ status: 500 },
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return { GET, PUT };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function createPrototypeScreenshotRoute(config: PrototypeConfig) {
|
|
370
|
+
const registry = createPrototypeRegistry(config);
|
|
371
|
+
|
|
372
|
+
async function POST(request: NextRequest, { params }: SlugRouteContext) {
|
|
373
|
+
if (process.env.NODE_ENV !== "development") {
|
|
374
|
+
return NextResponse.json({ error: "Not available" }, { status: 404 });
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const { slug } = await params;
|
|
378
|
+
const prototype = registry.getPrototype(slug);
|
|
379
|
+
|
|
380
|
+
if (!prototype) {
|
|
381
|
+
return NextResponse.json({ error: "Unknown prototype" }, { status: 404 });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const body = (await request.json()) as {
|
|
385
|
+
image?: string;
|
|
386
|
+
stateId?: string;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
if (
|
|
390
|
+
typeof body.image !== "string" ||
|
|
391
|
+
!body.image.startsWith("data:image/png;base64,")
|
|
392
|
+
) {
|
|
393
|
+
return NextResponse.json({ error: "Invalid image" }, { status: 400 });
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const buffer = Buffer.from(
|
|
397
|
+
body.image.replace(/^data:image\/png;base64,/, ""),
|
|
398
|
+
"base64",
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const screenshotsDir = path.join(
|
|
402
|
+
process.cwd(),
|
|
403
|
+
"public/prototypes/screenshots",
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
if (typeof body.stateId === "string" && body.stateId.length > 0) {
|
|
407
|
+
const stateDir = path.join(screenshotsDir, slug, "states");
|
|
408
|
+
await mkdir(stateDir, { recursive: true });
|
|
409
|
+
const filename = `${sanitizeStateIdForFilename(body.stateId)}.png`;
|
|
410
|
+
await writeFile(path.join(stateDir, filename), buffer);
|
|
411
|
+
|
|
412
|
+
const version = await touchScreenshotManifestKey(
|
|
413
|
+
stateScreenshotManifestKey(slug, body.stateId),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
return NextResponse.json({ ok: true, version, stateId: body.stateId });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
await mkdir(screenshotsDir, { recursive: true });
|
|
420
|
+
await writeFile(path.join(screenshotsDir, `${slug}.png`), buffer);
|
|
421
|
+
|
|
422
|
+
const version = await touchScreenshotManifest(slug);
|
|
423
|
+
|
|
424
|
+
return NextResponse.json({ ok: true, version });
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return { POST };
|
|
428
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NextRequest } from "next/server";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
DELETE as deleteGallery,
|
|
5
|
+
GET as getGallery,
|
|
6
|
+
POST as postGallery,
|
|
7
|
+
} from "./gallery-route";
|
|
8
|
+
import { createFoldersRouteHandlers } from "./folders-route";
|
|
9
|
+
import { GET as getGalleryImage } from "./image-route";
|
|
10
|
+
import {
|
|
11
|
+
DELETE as deleteGalleryItem,
|
|
12
|
+
PATCH as patchGalleryItem,
|
|
13
|
+
} from "./item-route";
|
|
14
|
+
import type { GalleryConfig } from "./gallery-types";
|
|
15
|
+
|
|
16
|
+
export function createGalleryApi(_config?: GalleryConfig) {
|
|
17
|
+
const foldersRoute = createFoldersRouteHandlers();
|
|
18
|
+
|
|
19
|
+
async function dispatch(request: NextRequest, path: string[]): Promise<Response | null> {
|
|
20
|
+
const method = request.method;
|
|
21
|
+
|
|
22
|
+
if (path[0] === "gallery") {
|
|
23
|
+
if (path.length === 1) {
|
|
24
|
+
if (method === "GET") return getGallery();
|
|
25
|
+
if (method === "POST") return postGallery(request);
|
|
26
|
+
if (method === "DELETE") return deleteGallery();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (path.length === 2 && path[1] === "folders") {
|
|
30
|
+
if (method === "GET") return foldersRoute.GET();
|
|
31
|
+
if (method === "POST") return foldersRoute.POST(request);
|
|
32
|
+
if (method === "PUT") return foldersRoute.PUT(request);
|
|
33
|
+
if (method === "PATCH") return foldersRoute.PATCH(request);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (path.length === 2) {
|
|
37
|
+
const id = path[1]!;
|
|
38
|
+
if (method === "PATCH") {
|
|
39
|
+
return patchGalleryItem(request, { params: Promise.resolve({ id }) });
|
|
40
|
+
}
|
|
41
|
+
if (method === "DELETE") {
|
|
42
|
+
return deleteGalleryItem(request, { params: Promise.resolve({ id }) });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (path.length === 3 && path[2] === "image" && method === "GET") {
|
|
47
|
+
const id = path[1]!;
|
|
48
|
+
return getGalleryImage(request, { params: Promise.resolve({ id }) });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return { dispatch };
|
|
56
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { kv } from "@vercel/kv";
|
|
2
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
3
|
+
|
|
4
|
+
import { getGalleryKvStatus } from "./gallery-env";
|
|
5
|
+
|
|
6
|
+
const FOLDERS_KEY = "gallery:folders";
|
|
7
|
+
const GALLERY_KEY = "gallery:items";
|
|
8
|
+
|
|
9
|
+
export function createFoldersRouteHandlers() {
|
|
10
|
+
async function getFolders(): Promise<string[]> {
|
|
11
|
+
const stored = await kv.get<string[]>(FOLDERS_KEY);
|
|
12
|
+
return stored ?? [];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function GET() {
|
|
16
|
+
const status = getGalleryKvStatus();
|
|
17
|
+
if (!status.ok) {
|
|
18
|
+
return NextResponse.json({ error: "Gallery not configured" }, { status: 503 });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const folders = await getFolders();
|
|
23
|
+
return NextResponse.json(folders);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error("Folders GET error:", error);
|
|
26
|
+
return NextResponse.json({ error: "Failed to fetch folders" }, { status: 500 });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function POST(request: NextRequest) {
|
|
31
|
+
const status = getGalleryKvStatus();
|
|
32
|
+
if (!status.ok) {
|
|
33
|
+
return NextResponse.json({ error: "Gallery not configured" }, { status: 503 });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const body = await request.json();
|
|
38
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
39
|
+
|
|
40
|
+
if (!name) {
|
|
41
|
+
return NextResponse.json({ error: "Folder name is required" }, { status: 400 });
|
|
42
|
+
}
|
|
43
|
+
if (name.length > 50) {
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ error: "Folder name must be 50 characters or fewer" },
|
|
46
|
+
{ status: 400 },
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const folders = await getFolders();
|
|
51
|
+
if (folders.some((f) => f.toLowerCase() === name.toLowerCase())) {
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: "A folder with that name already exists" },
|
|
54
|
+
{ status: 409 },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const updated = [...folders, name];
|
|
59
|
+
await kv.set(FOLDERS_KEY, updated);
|
|
60
|
+
|
|
61
|
+
return NextResponse.json(updated);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error("Folders POST error:", error);
|
|
64
|
+
const message = error instanceof Error ? error.message : "Failed to create folder";
|
|
65
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function PUT(request: NextRequest) {
|
|
70
|
+
const status = getGalleryKvStatus();
|
|
71
|
+
if (!status.ok) {
|
|
72
|
+
return NextResponse.json({ error: "Gallery not configured" }, { status: 503 });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const body = await request.json();
|
|
77
|
+
const folders = body.folders;
|
|
78
|
+
if (
|
|
79
|
+
!Array.isArray(folders) ||
|
|
80
|
+
folders.some((f: unknown) => typeof f !== "string" || !(f as string).trim())
|
|
81
|
+
) {
|
|
82
|
+
return NextResponse.json(
|
|
83
|
+
{ error: "folders must be an array of non-empty strings" },
|
|
84
|
+
{ status: 400 },
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
await kv.set(FOLDERS_KEY, folders);
|
|
88
|
+
return NextResponse.json(folders);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error("Folders PUT error:", error);
|
|
91
|
+
const message = error instanceof Error ? error.message : "Failed to reorder folders";
|
|
92
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function PATCH(request: NextRequest) {
|
|
97
|
+
const status = getGalleryKvStatus();
|
|
98
|
+
if (!status.ok) {
|
|
99
|
+
return NextResponse.json({ error: "Gallery not configured" }, { status: 503 });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const body = await request.json();
|
|
104
|
+
const oldName = typeof body.oldName === "string" ? body.oldName.trim() : "";
|
|
105
|
+
const newName = typeof body.newName === "string" ? body.newName.trim() : "";
|
|
106
|
+
|
|
107
|
+
if (!oldName || !newName) {
|
|
108
|
+
return NextResponse.json({ error: "oldName and newName are required" }, { status: 400 });
|
|
109
|
+
}
|
|
110
|
+
if (newName.length > 50) {
|
|
111
|
+
return NextResponse.json(
|
|
112
|
+
{ error: "Folder name must be 50 characters or fewer" },
|
|
113
|
+
{ status: 400 },
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const folders = await getFolders();
|
|
118
|
+
const idx = folders.findIndex((f) => f === oldName);
|
|
119
|
+
if (idx === -1) {
|
|
120
|
+
return NextResponse.json({ error: "Folder not found" }, { status: 404 });
|
|
121
|
+
}
|
|
122
|
+
if (oldName !== newName && folders.some((f) => f.toLowerCase() === newName.toLowerCase())) {
|
|
123
|
+
return NextResponse.json(
|
|
124
|
+
{ error: "A folder with that name already exists" },
|
|
125
|
+
{ status: 409 },
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
folders[idx] = newName;
|
|
130
|
+
await kv.set(FOLDERS_KEY, folders);
|
|
131
|
+
|
|
132
|
+
const items = (await kv.get<Record<string, unknown>[]>(GALLERY_KEY)) ?? [];
|
|
133
|
+
let changed = false;
|
|
134
|
+
for (const item of items) {
|
|
135
|
+
if (Array.isArray(item.tags)) {
|
|
136
|
+
const tagIdx = (item.tags as string[]).indexOf(oldName);
|
|
137
|
+
if (tagIdx !== -1) {
|
|
138
|
+
(item.tags as string[])[tagIdx] = newName;
|
|
139
|
+
changed = true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (changed) {
|
|
144
|
+
await kv.set(GALLERY_KEY, items);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return NextResponse.json(folders);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error("Folders PATCH error:", error);
|
|
150
|
+
const message = error instanceof Error ? error.message : "Failed to rename folder";
|
|
151
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { GET, POST, PUT, PATCH };
|
|
156
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function normalizeGalleryEnv(): void {
|
|
2
|
+
if (!process.env.KV_REST_API_URL) {
|
|
3
|
+
const urlKey = Object.keys(process.env).find((k) => k.endsWith("_KV_REST_API_URL"));
|
|
4
|
+
if (urlKey) process.env.KV_REST_API_URL = process.env[urlKey];
|
|
5
|
+
}
|
|
6
|
+
if (!process.env.KV_REST_API_TOKEN) {
|
|
7
|
+
const tokenKey = Object.keys(process.env).find((k) => k.endsWith("_KV_REST_API_TOKEN"));
|
|
8
|
+
if (tokenKey) process.env.KV_REST_API_TOKEN = process.env[tokenKey];
|
|
9
|
+
}
|
|
10
|
+
if (!process.env.BLOB_READ_WRITE_TOKEN) {
|
|
11
|
+
const blobKey = Object.keys(process.env).find(
|
|
12
|
+
(k) => k.includes("BLOB_READ_WRITE_TOKEN") || k.includes("BLOB_READ_WRITE"),
|
|
13
|
+
);
|
|
14
|
+
if (blobKey) process.env.BLOB_READ_WRITE_TOKEN = process.env[blobKey];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getGalleryStorageStatus(): {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
missing: string[];
|
|
21
|
+
} {
|
|
22
|
+
normalizeGalleryEnv();
|
|
23
|
+
const missing: string[] = [];
|
|
24
|
+
if (!process.env.KV_REST_API_URL) missing.push("KV_REST_API_URL");
|
|
25
|
+
if (!process.env.KV_REST_API_TOKEN) missing.push("KV_REST_API_TOKEN");
|
|
26
|
+
if (!process.env.BLOB_READ_WRITE_TOKEN) missing.push("BLOB_READ_WRITE_TOKEN");
|
|
27
|
+
return { ok: missing.length === 0, missing };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getGalleryKvStatus(): {
|
|
31
|
+
ok: boolean;
|
|
32
|
+
missing: string[];
|
|
33
|
+
} {
|
|
34
|
+
normalizeGalleryEnv();
|
|
35
|
+
const missing: string[] = [];
|
|
36
|
+
if (!process.env.KV_REST_API_URL) missing.push("KV_REST_API_URL");
|
|
37
|
+
if (!process.env.KV_REST_API_TOKEN) missing.push("KV_REST_API_TOKEN");
|
|
38
|
+
return { ok: missing.length === 0, missing };
|
|
39
|
+
}
|