@gallop.software/studio 1.5.9 → 2.0.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.
Files changed (60) hide show
  1. package/app/api/studio/[...path]/route.ts +1 -0
  2. package/app/layout.tsx +20 -0
  3. package/app/page.tsx +82 -0
  4. package/bin/studio.mjs +110 -0
  5. package/dist/handlers/index.js +84 -63
  6. package/dist/handlers/index.js.map +1 -1
  7. package/dist/handlers/index.mjs +135 -114
  8. package/dist/handlers/index.mjs.map +1 -1
  9. package/dist/index.d.mts +14 -10
  10. package/dist/index.d.ts +14 -10
  11. package/dist/index.js +2 -177
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.mjs +4 -179
  14. package/dist/index.mjs.map +1 -1
  15. package/next.config.mjs +22 -0
  16. package/package.json +18 -10
  17. package/src/components/AddNewModal.tsx +402 -0
  18. package/src/components/ErrorModal.tsx +89 -0
  19. package/src/components/R2SetupModal.tsx +400 -0
  20. package/src/components/StudioBreadcrumb.tsx +115 -0
  21. package/src/components/StudioButton.tsx +200 -0
  22. package/src/components/StudioContext.tsx +219 -0
  23. package/src/components/StudioDetailView.tsx +714 -0
  24. package/src/components/StudioFileGrid.tsx +704 -0
  25. package/src/components/StudioFileList.tsx +743 -0
  26. package/src/components/StudioFolderPicker.tsx +342 -0
  27. package/src/components/StudioModal.tsx +473 -0
  28. package/src/components/StudioPreview.tsx +399 -0
  29. package/src/components/StudioSettings.tsx +536 -0
  30. package/src/components/StudioToolbar.tsx +1448 -0
  31. package/src/components/StudioUI.tsx +731 -0
  32. package/src/components/styles/common.ts +236 -0
  33. package/src/components/tokens.ts +78 -0
  34. package/src/components/useStudioActions.tsx +497 -0
  35. package/src/config/index.ts +7 -0
  36. package/src/config/workspace.ts +52 -0
  37. package/src/handlers/favicon.ts +152 -0
  38. package/src/handlers/files.ts +784 -0
  39. package/src/handlers/images.ts +949 -0
  40. package/src/handlers/import.ts +190 -0
  41. package/src/handlers/index.ts +168 -0
  42. package/src/handlers/list.ts +627 -0
  43. package/src/handlers/scan.ts +311 -0
  44. package/src/handlers/utils/cdn.ts +234 -0
  45. package/src/handlers/utils/files.ts +64 -0
  46. package/src/handlers/utils/index.ts +4 -0
  47. package/src/handlers/utils/meta.ts +102 -0
  48. package/src/handlers/utils/thumbnails.ts +98 -0
  49. package/src/hooks/useFileList.ts +143 -0
  50. package/src/index.tsx +36 -0
  51. package/src/lib/api.ts +176 -0
  52. package/src/types.ts +119 -0
  53. package/dist/StudioUI-GJK45R3T.js +0 -6500
  54. package/dist/StudioUI-GJK45R3T.js.map +0 -1
  55. package/dist/StudioUI-QZ54STXE.mjs +0 -6500
  56. package/dist/StudioUI-QZ54STXE.mjs.map +0 -1
  57. package/dist/chunk-N6JYTJCB.js +0 -68
  58. package/dist/chunk-N6JYTJCB.js.map +0 -1
  59. package/dist/chunk-RHI3UROE.mjs +0 -68
  60. package/dist/chunk-RHI3UROE.mjs.map +0 -1
@@ -10,13 +10,36 @@ import { NextResponse as NextResponse6 } from "next/server";
10
10
  // src/handlers/list.ts
11
11
  import { NextResponse } from "next/server";
12
12
  import { promises as fs4 } from "fs";
13
- import path5 from "path";
13
+ import path4 from "path";
14
14
 
15
15
  // src/handlers/utils/meta.ts
16
16
  import { promises as fs } from "fs";
17
+
18
+ // src/config/workspace.ts
17
19
  import path from "path";
20
+ var workspacePath = null;
21
+ function getWorkspace() {
22
+ if (workspacePath === null) {
23
+ workspacePath = process.env.STUDIO_WORKSPACE || process.cwd();
24
+ }
25
+ return workspacePath;
26
+ }
27
+ function getPublicPath(...segments) {
28
+ return path.join(getWorkspace(), "public", ...segments);
29
+ }
30
+ function getDataPath(...segments) {
31
+ return path.join(getWorkspace(), "_data", ...segments);
32
+ }
33
+ function getSrcAppPath(...segments) {
34
+ return path.join(getWorkspace(), "src", "app", ...segments);
35
+ }
36
+ function getWorkspacePath(...segments) {
37
+ return path.join(getWorkspace(), ...segments);
38
+ }
39
+
40
+ // src/handlers/utils/meta.ts
18
41
  async function loadMeta() {
19
- const metaPath = path.join(process.cwd(), "_data", "_studio.json");
42
+ const metaPath = getDataPath("_studio.json");
20
43
  try {
21
44
  const content = await fs.readFile(metaPath, "utf-8");
22
45
  return JSON.parse(content);
@@ -25,9 +48,9 @@ async function loadMeta() {
25
48
  }
26
49
  }
27
50
  async function saveMeta(meta) {
28
- const dataDir = path.join(process.cwd(), "_data");
51
+ const dataDir = getDataPath();
29
52
  await fs.mkdir(dataDir, { recursive: true });
30
- const metaPath = path.join(dataDir, "_studio.json");
53
+ const metaPath = getDataPath("_studio.json");
31
54
  const ordered = {};
32
55
  if (meta._cdns) {
33
56
  ordered._cdns = meta._cdns;
@@ -123,7 +146,7 @@ async function processImage(buffer, imageKey) {
123
146
  const baseName = path3.basename(keyWithoutSlash, path3.extname(keyWithoutSlash));
124
147
  const ext = path3.extname(keyWithoutSlash).toLowerCase();
125
148
  const imageDir = path3.dirname(keyWithoutSlash);
126
- const imagesPath = path3.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
149
+ const imagesPath = getPublicPath("images", imageDir === "." ? "" : imageDir);
127
150
  await fs2.mkdir(imagesPath, { recursive: true });
128
151
  const isPng = ext === ".png";
129
152
  const outputExt = isPng ? ".png" : ".jpg";
@@ -131,7 +154,7 @@ async function processImage(buffer, imageKey) {
131
154
  o: { w: originalWidth, h: originalHeight }
132
155
  };
133
156
  const fullFileName = imageDir === "." ? `${baseName}${outputExt}` : `${imageDir}/${baseName}${outputExt}`;
134
- const fullPath = path3.join(process.cwd(), "public", "images", fullFileName);
157
+ const fullPath = getPublicPath("images", fullFileName);
135
158
  let fullWidth = originalWidth;
136
159
  let fullHeight = originalHeight;
137
160
  if (originalWidth > FULL_MAX_WIDTH) {
@@ -158,7 +181,7 @@ async function processImage(buffer, imageKey) {
158
181
  const newHeight = Math.round(maxWidth * ratio);
159
182
  const sizeFileName = `${baseName}${suffix}${outputExt}`;
160
183
  const sizeFilePath = imageDir === "." ? sizeFileName : `${imageDir}/${sizeFileName}`;
161
- const sizePath = path3.join(process.cwd(), "public", "images", sizeFilePath);
184
+ const sizePath = getPublicPath("images", sizeFilePath);
162
185
  if (isPng) {
163
186
  await sharp(buffer).resize(maxWidth, newHeight).png({ quality: 80 }).toFile(sizePath);
164
187
  } else {
@@ -173,7 +196,6 @@ async function processImage(buffer, imageKey) {
173
196
 
174
197
  // src/handlers/utils/cdn.ts
175
198
  import { promises as fs3 } from "fs";
176
- import path4 from "path";
177
199
  import { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand } from "@aws-sdk/client-s3";
178
200
  async function purgeCloudflareCache(urls) {
179
201
  const zoneId = process.env.CLOUDFLARE_ZONE_ID;
@@ -247,7 +269,7 @@ async function uploadToCdn(imageKey) {
247
269
  if (!bucketName) throw new Error("R2 bucket not configured");
248
270
  const r2 = getR2Client();
249
271
  for (const thumbPath of getAllThumbnailPaths(imageKey)) {
250
- const localPath = path4.join(process.cwd(), "public", thumbPath);
272
+ const localPath = getPublicPath(thumbPath);
251
273
  try {
252
274
  const fileBuffer = await fs3.readFile(localPath);
253
275
  await r2.send(
@@ -264,7 +286,7 @@ async function uploadToCdn(imageKey) {
264
286
  }
265
287
  async function deleteLocalThumbnails(imageKey) {
266
288
  for (const thumbPath of getAllThumbnailPaths(imageKey)) {
267
- const localPath = path4.join(process.cwd(), "public", thumbPath);
289
+ const localPath = getPublicPath(thumbPath);
268
290
  try {
269
291
  await fs3.unlink(localPath);
270
292
  } catch {
@@ -295,7 +317,7 @@ async function uploadOriginalToCdn(imageKey) {
295
317
  const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME;
296
318
  if (!bucketName) throw new Error("R2 bucket not configured");
297
319
  const r2 = getR2Client();
298
- const localPath = path4.join(process.cwd(), "public", imageKey);
320
+ const localPath = getPublicPath(imageKey);
299
321
  const fileBuffer = await fs3.readFile(localPath);
300
322
  await r2.send(
301
323
  new PutObjectCommand({
@@ -495,7 +517,7 @@ async function handleList(request) {
495
517
  }
496
518
  return NextResponse.json({ items });
497
519
  }
498
- const absoluteDir = path5.join(process.cwd(), requestedPath);
520
+ const absoluteDir = getWorkspacePath(requestedPath);
499
521
  try {
500
522
  const dirEntries = await fs4.readdir(absoluteDir, { withFileTypes: true });
501
523
  for (const entry of dirEntries) {
@@ -606,7 +628,7 @@ async function handleList(request) {
606
628
  hasThumbnail = true;
607
629
  }
608
630
  } else {
609
- const localThumbPath = path5.join(process.cwd(), "public", thumbPath);
631
+ const localThumbPath = getPublicPath(thumbPath);
610
632
  try {
611
633
  await fs4.access(localThumbPath);
612
634
  thumbnail = thumbPath;
@@ -627,7 +649,7 @@ async function handleList(request) {
627
649
  }
628
650
  if (!isPushedToCloud) {
629
651
  try {
630
- const filePath = path5.join(process.cwd(), "public", key);
652
+ const filePath = getPublicPath(key);
631
653
  const stats = await fs4.stat(filePath);
632
654
  fileSize = stats.size;
633
655
  } catch {
@@ -669,7 +691,7 @@ async function handleSearch(request) {
669
691
  const items = [];
670
692
  for (const [key, entry] of fileEntries) {
671
693
  if (!key.toLowerCase().includes(query)) continue;
672
- const fileName = path5.basename(key);
694
+ const fileName = path4.basename(key);
673
695
  const relativePath = key.slice(1);
674
696
  const isImage = isImageFile(fileName);
675
697
  const isPushedToCloud = entry.c !== void 0;
@@ -687,7 +709,7 @@ async function handleSearch(request) {
687
709
  hasThumbnail = true;
688
710
  }
689
711
  } else {
690
- const localThumbPath = path5.join(process.cwd(), "public", thumbPath);
712
+ const localThumbPath = getPublicPath(thumbPath);
691
713
  try {
692
714
  await fs4.access(localThumbPath);
693
715
  thumbnail = thumbPath;
@@ -745,13 +767,13 @@ async function handleListFolders() {
745
767
  if (entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "images") {
746
768
  const folderRelPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
747
769
  folderSet.add(folderRelPath);
748
- await scanDir(path5.join(dir, entry.name), folderRelPath);
770
+ await scanDir(path4.join(dir, entry.name), folderRelPath);
749
771
  }
750
772
  }
751
773
  } catch {
752
774
  }
753
775
  }
754
- const publicDir = path5.join(process.cwd(), "public");
776
+ const publicDir = getPublicPath();
755
777
  await scanDir(publicDir, "");
756
778
  const folders = [];
757
779
  folders.push({ path: "public", name: "public", depth: 0 });
@@ -777,7 +799,7 @@ async function handleCountImages() {
777
799
  const fileEntries = getFileEntries(meta);
778
800
  const allImages = [];
779
801
  for (const [key] of fileEntries) {
780
- const fileName = path5.basename(key);
802
+ const fileName = path4.basename(key);
781
803
  if (isImageFile(fileName)) {
782
804
  allImages.push(key.slice(1));
783
805
  }
@@ -801,35 +823,34 @@ async function handleFolderImages(request) {
801
823
  const folders = foldersParam.split(",");
802
824
  const meta = await loadMeta();
803
825
  const fileEntries = getFileEntries(meta);
804
- const allImages = [];
826
+ const allFiles = [];
805
827
  const prefixes = folders.map((f) => {
806
828
  const rel = f.replace(/^public\/?/, "");
807
829
  return rel ? `/${rel}/` : "/";
808
830
  });
809
831
  for (const [key] of fileEntries) {
810
- const fileName = path5.basename(key);
811
- if (!isImageFile(fileName)) continue;
812
832
  for (const prefix of prefixes) {
813
833
  if (key.startsWith(prefix) || prefix === "/" && key.startsWith("/")) {
814
- allImages.push(key.slice(1));
834
+ allFiles.push(key.slice(1));
815
835
  break;
816
836
  }
817
837
  }
818
838
  }
819
839
  return NextResponse.json({
820
- count: allImages.length,
821
- images: allImages
840
+ count: allFiles.length,
841
+ images: allFiles
842
+ // Keep as 'images' for backwards compatibility
822
843
  });
823
844
  } catch (error) {
824
- console.error("Failed to get folder images:", error);
825
- return NextResponse.json({ error: "Failed to get folder images" }, { status: 500 });
845
+ console.error("Failed to get folder files:", error);
846
+ return NextResponse.json({ error: "Failed to get folder files" }, { status: 500 });
826
847
  }
827
848
  }
828
849
 
829
850
  // src/handlers/files.ts
830
851
  import { NextResponse as NextResponse2 } from "next/server";
831
852
  import { promises as fs5 } from "fs";
832
- import path6 from "path";
853
+ import path5 from "path";
833
854
  import sharp2 from "sharp";
834
855
  async function handleUpload(request) {
835
856
  try {
@@ -842,7 +863,7 @@ async function handleUpload(request) {
842
863
  const bytes = await file.arrayBuffer();
843
864
  const buffer = Buffer.from(bytes);
844
865
  const fileName = file.name;
845
- const ext = path6.extname(fileName).toLowerCase();
866
+ const ext = path5.extname(fileName).toLowerCase();
846
867
  const isImage = isImageFile(fileName);
847
868
  const isMedia = isMediaFile(fileName);
848
869
  const meta = await loadMeta();
@@ -860,7 +881,7 @@ async function handleUpload(request) {
860
881
  }
861
882
  let imageKey = "/" + (relativeDir ? `${relativeDir}/${fileName}` : fileName);
862
883
  if (meta[imageKey]) {
863
- const baseName = path6.basename(fileName, ext);
884
+ const baseName = path5.basename(fileName, ext);
864
885
  let counter = 1;
865
886
  let newFileName = `${baseName}-${counter}${ext}`;
866
887
  let newKey = "/" + (relativeDir ? `${relativeDir}/${newFileName}` : newFileName);
@@ -871,10 +892,10 @@ async function handleUpload(request) {
871
892
  }
872
893
  imageKey = newKey;
873
894
  }
874
- const actualFileName = path6.basename(imageKey);
875
- const uploadDir = path6.join(process.cwd(), "public", relativeDir);
895
+ const actualFileName = path5.basename(imageKey);
896
+ const uploadDir = getPublicPath(relativeDir);
876
897
  await fs5.mkdir(uploadDir, { recursive: true });
877
- await fs5.writeFile(path6.join(uploadDir, actualFileName), buffer);
898
+ await fs5.writeFile(path5.join(uploadDir, actualFileName), buffer);
878
899
  if (!isMedia) {
879
900
  return NextResponse2.json({
880
901
  success: true,
@@ -921,7 +942,7 @@ async function handleDelete(request) {
921
942
  errors.push(`Invalid path: ${itemPath}`);
922
943
  continue;
923
944
  }
924
- const absolutePath = path6.join(process.cwd(), itemPath);
945
+ const absolutePath = getWorkspacePath(itemPath);
925
946
  const imageKey = "/" + itemPath.replace(/^public\//, "");
926
947
  const entry = meta[imageKey];
927
948
  const isPushedToCloud = entry?.c !== void 0;
@@ -935,7 +956,7 @@ async function handleDelete(request) {
935
956
  const keyEntry = meta[key];
936
957
  if (keyEntry && keyEntry.c === void 0) {
937
958
  for (const thumbPath of getAllThumbnailPaths(key)) {
938
- const absoluteThumbPath = path6.join(process.cwd(), "public", thumbPath);
959
+ const absoluteThumbPath = getPublicPath(thumbPath);
939
960
  try {
940
961
  await fs5.unlink(absoluteThumbPath);
941
962
  } catch {
@@ -951,7 +972,7 @@ async function handleDelete(request) {
951
972
  if (!isInImagesFolder && entry) {
952
973
  if (!isPushedToCloud) {
953
974
  for (const thumbPath of getAllThumbnailPaths(imageKey)) {
954
- const absoluteThumbPath = path6.join(process.cwd(), "public", thumbPath);
975
+ const absoluteThumbPath = getPublicPath(thumbPath);
955
976
  try {
956
977
  await fs5.unlink(absoluteThumbPath);
957
978
  } catch {
@@ -1007,8 +1028,8 @@ async function handleCreateFolder(request) {
1007
1028
  return NextResponse2.json({ error: "Invalid folder name" }, { status: 400 });
1008
1029
  }
1009
1030
  const safePath = (parentPath || "public").replace(/\.\./g, "");
1010
- const folderPath = path6.join(process.cwd(), safePath, sanitizedName);
1011
- if (!folderPath.startsWith(path6.join(process.cwd(), "public"))) {
1031
+ const folderPath = getWorkspacePath(safePath, sanitizedName);
1032
+ if (!folderPath.startsWith(getPublicPath())) {
1012
1033
  return NextResponse2.json({ error: "Invalid path" }, { status: 400 });
1013
1034
  }
1014
1035
  try {
@@ -1017,7 +1038,7 @@ async function handleCreateFolder(request) {
1017
1038
  } catch {
1018
1039
  }
1019
1040
  await fs5.mkdir(folderPath, { recursive: true });
1020
- return NextResponse2.json({ success: true, path: path6.join(safePath, sanitizedName) });
1041
+ return NextResponse2.json({ success: true, path: path5.join(safePath, sanitizedName) });
1021
1042
  } catch (error) {
1022
1043
  console.error("Failed to create folder:", error);
1023
1044
  return NextResponse2.json({ error: "Failed to create folder" }, { status: 500 });
@@ -1034,10 +1055,10 @@ async function handleRename(request) {
1034
1055
  return NextResponse2.json({ error: "Invalid name" }, { status: 400 });
1035
1056
  }
1036
1057
  const safePath = oldPath.replace(/\.\./g, "");
1037
- const absoluteOldPath = path6.join(process.cwd(), safePath);
1038
- const parentDir = path6.dirname(absoluteOldPath);
1039
- const absoluteNewPath = path6.join(parentDir, sanitizedName);
1040
- if (!absoluteOldPath.startsWith(path6.join(process.cwd(), "public"))) {
1058
+ const absoluteOldPath = getWorkspacePath(safePath);
1059
+ const parentDir = path5.dirname(absoluteOldPath);
1060
+ const absoluteNewPath = path5.join(parentDir, sanitizedName);
1061
+ if (!absoluteOldPath.startsWith(getPublicPath())) {
1041
1062
  return NextResponse2.json({ error: "Invalid path" }, { status: 400 });
1042
1063
  }
1043
1064
  try {
@@ -1052,12 +1073,12 @@ async function handleRename(request) {
1052
1073
  }
1053
1074
  const stats = await fs5.stat(absoluteOldPath);
1054
1075
  const isFile = stats.isFile();
1055
- const isImage = isFile && isImageFile(path6.basename(oldPath));
1076
+ const isImage = isFile && isImageFile(path5.basename(oldPath));
1056
1077
  await fs5.rename(absoluteOldPath, absoluteNewPath);
1057
1078
  if (isImage) {
1058
1079
  const meta = await loadMeta();
1059
1080
  const oldRelativePath = safePath.replace(/^public\//, "");
1060
- const newRelativePath = path6.join(path6.dirname(oldRelativePath), sanitizedName);
1081
+ const newRelativePath = path5.join(path5.dirname(oldRelativePath), sanitizedName);
1061
1082
  const oldKey = "/" + oldRelativePath;
1062
1083
  const newKey = "/" + newRelativePath;
1063
1084
  if (meta[oldKey]) {
@@ -1065,9 +1086,9 @@ async function handleRename(request) {
1065
1086
  const oldThumbPaths = getAllThumbnailPaths(oldKey);
1066
1087
  const newThumbPaths = getAllThumbnailPaths(newKey);
1067
1088
  for (let i = 0; i < oldThumbPaths.length; i++) {
1068
- const oldThumbPath = path6.join(process.cwd(), "public", oldThumbPaths[i]);
1069
- const newThumbPath = path6.join(process.cwd(), "public", newThumbPaths[i]);
1070
- await fs5.mkdir(path6.dirname(newThumbPath), { recursive: true });
1089
+ const oldThumbPath = getPublicPath(oldThumbPaths[i]);
1090
+ const newThumbPath = getPublicPath(newThumbPaths[i]);
1091
+ await fs5.mkdir(path5.dirname(newThumbPath), { recursive: true });
1071
1092
  try {
1072
1093
  await fs5.rename(oldThumbPath, newThumbPath);
1073
1094
  } catch {
@@ -1078,7 +1099,7 @@ async function handleRename(request) {
1078
1099
  }
1079
1100
  await saveMeta(meta);
1080
1101
  }
1081
- const newPath = path6.join(path6.dirname(safePath), sanitizedName);
1102
+ const newPath = path5.join(path5.dirname(safePath), sanitizedName);
1082
1103
  return NextResponse2.json({ success: true, newPath });
1083
1104
  } catch (error) {
1084
1105
  console.error("Failed to rename:", error);
@@ -1107,8 +1128,8 @@ async function handleMoveStream(request) {
1107
1128
  return;
1108
1129
  }
1109
1130
  const safeDestination = destination.replace(/\.\./g, "");
1110
- const absoluteDestination = path6.join(process.cwd(), safeDestination);
1111
- if (!absoluteDestination.startsWith(path6.join(process.cwd(), "public"))) {
1131
+ const absoluteDestination = getWorkspacePath(safeDestination);
1132
+ if (!absoluteDestination.startsWith(getPublicPath())) {
1112
1133
  sendEvent({ type: "error", message: "Invalid destination" });
1113
1134
  controller.close();
1114
1135
  return;
@@ -1124,10 +1145,10 @@ async function handleMoveStream(request) {
1124
1145
  for (let i = 0; i < paths.length; i++) {
1125
1146
  const itemPath = paths[i];
1126
1147
  const safePath = itemPath.replace(/\.\./g, "");
1127
- const itemName = path6.basename(safePath);
1128
- const newAbsolutePath = path6.join(absoluteDestination, itemName);
1148
+ const itemName = path5.basename(safePath);
1149
+ const newAbsolutePath = path5.join(absoluteDestination, itemName);
1129
1150
  const oldRelativePath = safePath.replace(/^public\//, "");
1130
- const newRelativePath = path6.join(safeDestination.replace(/^public\//, ""), itemName);
1151
+ const newRelativePath = path5.join(safeDestination.replace(/^public\//, ""), itemName);
1131
1152
  const oldKey = "/" + oldRelativePath;
1132
1153
  const newKey = "/" + newRelativePath;
1133
1154
  sendEvent({
@@ -1152,7 +1173,7 @@ async function handleMoveStream(request) {
1152
1173
  if (isRemote && isImage) {
1153
1174
  const remoteUrl = `${fileCdnUrl}${oldKey}`;
1154
1175
  const buffer = await downloadFromRemoteUrl(remoteUrl);
1155
- await fs5.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
1176
+ await fs5.mkdir(path5.dirname(newAbsolutePath), { recursive: true });
1156
1177
  await fs5.writeFile(newAbsolutePath, buffer);
1157
1178
  const newEntry = {
1158
1179
  o: entry?.o,
@@ -1163,7 +1184,7 @@ async function handleMoveStream(request) {
1163
1184
  moved.push(itemPath);
1164
1185
  } else if (isPushedToR2 && isImage) {
1165
1186
  const buffer = await downloadFromCdn(oldKey);
1166
- await fs5.mkdir(path6.dirname(newAbsolutePath), { recursive: true });
1187
+ await fs5.mkdir(path5.dirname(newAbsolutePath), { recursive: true });
1167
1188
  await fs5.writeFile(newAbsolutePath, buffer);
1168
1189
  let newEntry = {
1169
1190
  o: entry?.o,
@@ -1190,8 +1211,8 @@ async function handleMoveStream(request) {
1190
1211
  meta[newKey] = newEntry;
1191
1212
  moved.push(itemPath);
1192
1213
  } else {
1193
- const absolutePath = path6.join(process.cwd(), safePath);
1194
- if (absoluteDestination.startsWith(absolutePath + path6.sep)) {
1214
+ const absolutePath = getWorkspacePath(safePath);
1215
+ if (absoluteDestination.startsWith(absolutePath + path5.sep)) {
1195
1216
  errors.push(`Cannot move ${itemName} into itself`);
1196
1217
  continue;
1197
1218
  }
@@ -1213,9 +1234,9 @@ async function handleMoveStream(request) {
1213
1234
  const oldThumbPaths = getAllThumbnailPaths(oldKey);
1214
1235
  const newThumbPaths = getAllThumbnailPaths(newKey);
1215
1236
  for (let j = 0; j < oldThumbPaths.length; j++) {
1216
- const oldThumbPath = path6.join(process.cwd(), "public", oldThumbPaths[j]);
1217
- const newThumbPath = path6.join(process.cwd(), "public", newThumbPaths[j]);
1218
- await fs5.mkdir(path6.dirname(newThumbPath), { recursive: true });
1237
+ const oldThumbPath = getPublicPath(oldThumbPaths[j]);
1238
+ const newThumbPath = getPublicPath(newThumbPaths[j]);
1239
+ await fs5.mkdir(path5.dirname(newThumbPath), { recursive: true });
1219
1240
  try {
1220
1241
  await fs5.rename(oldThumbPath, newThumbPath);
1221
1242
  } catch {
@@ -1268,7 +1289,7 @@ async function handleMoveStream(request) {
1268
1289
  // src/handlers/images.ts
1269
1290
  import { NextResponse as NextResponse3 } from "next/server";
1270
1291
  import { promises as fs6 } from "fs";
1271
- import path7 from "path";
1292
+ import path6 from "path";
1272
1293
  import { S3Client as S3Client2, PutObjectCommand as PutObjectCommand2 } from "@aws-sdk/client-s3";
1273
1294
  async function handleSync(request) {
1274
1295
  const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID;
@@ -1320,7 +1341,7 @@ async function handleSync(request) {
1320
1341
  const remoteUrl = `${existingCdnUrl}${imageKey}`;
1321
1342
  originalBuffer = await downloadFromRemoteUrl(remoteUrl);
1322
1343
  } else {
1323
- const originalLocalPath = path7.join(process.cwd(), "public", imageKey);
1344
+ const originalLocalPath = getPublicPath(imageKey);
1324
1345
  try {
1325
1346
  originalBuffer = await fs6.readFile(originalLocalPath);
1326
1347
  } catch {
@@ -1339,7 +1360,7 @@ async function handleSync(request) {
1339
1360
  urlsToPurge.push(`${publicUrl}${imageKey}`);
1340
1361
  if (!isRemote && isProcessed(entry)) {
1341
1362
  for (const thumbPath of getAllThumbnailPaths(imageKey)) {
1342
- const localPath = path7.join(process.cwd(), "public", thumbPath);
1363
+ const localPath = getPublicPath(thumbPath);
1343
1364
  try {
1344
1365
  const fileBuffer = await fs6.readFile(localPath);
1345
1366
  await r2.send(
@@ -1357,9 +1378,9 @@ async function handleSync(request) {
1357
1378
  }
1358
1379
  entry.c = cdnIndex;
1359
1380
  if (!isRemote) {
1360
- const originalLocalPath = path7.join(process.cwd(), "public", imageKey);
1381
+ const originalLocalPath = getPublicPath(imageKey);
1361
1382
  for (const thumbPath of getAllThumbnailPaths(imageKey)) {
1362
- const localPath = path7.join(process.cwd(), "public", thumbPath);
1383
+ const localPath = getPublicPath(thumbPath);
1363
1384
  try {
1364
1385
  await fs6.unlink(localPath);
1365
1386
  } catch {
@@ -1413,19 +1434,19 @@ async function handleReprocess(request) {
1413
1434
  const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
1414
1435
  const isInOurR2 = existingCdnUrl === publicUrl;
1415
1436
  const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
1416
- const originalPath = path7.join(process.cwd(), "public", imageKey);
1437
+ const originalPath = getPublicPath(imageKey);
1417
1438
  try {
1418
1439
  buffer = await fs6.readFile(originalPath);
1419
1440
  } catch {
1420
1441
  if (isInOurR2) {
1421
1442
  buffer = await downloadFromCdn(imageKey);
1422
- const dir = path7.dirname(originalPath);
1443
+ const dir = path6.dirname(originalPath);
1423
1444
  await fs6.mkdir(dir, { recursive: true });
1424
1445
  await fs6.writeFile(originalPath, buffer);
1425
1446
  } else if (isRemote && existingCdnUrl) {
1426
1447
  const remoteUrl = `${existingCdnUrl}${imageKey}`;
1427
1448
  buffer = await downloadFromRemoteUrl(remoteUrl);
1428
- const dir = path7.dirname(originalPath);
1449
+ const dir = path6.dirname(originalPath);
1429
1450
  await fs6.mkdir(dir, { recursive: true });
1430
1451
  await fs6.writeFile(originalPath, buffer);
1431
1452
  } else {
@@ -1623,33 +1644,33 @@ async function handleReprocessStream(request) {
1623
1644
  const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
1624
1645
  const isInOurR2 = existingCdnUrl === publicUrl;
1625
1646
  const isRemote = existingCdnIndex !== void 0 && !isInOurR2;
1626
- const originalPath = path7.join(process.cwd(), "public", imageKey);
1647
+ const originalPath = getPublicPath(imageKey);
1627
1648
  try {
1628
1649
  buffer = await fs6.readFile(originalPath);
1629
1650
  } catch {
1630
1651
  if (isInOurR2) {
1631
1652
  buffer = await downloadFromCdn(imageKey);
1632
- const dir = path7.dirname(originalPath);
1653
+ const dir = path6.dirname(originalPath);
1633
1654
  await fs6.mkdir(dir, { recursive: true });
1634
1655
  await fs6.writeFile(originalPath, buffer);
1635
1656
  } else if (isRemote && existingCdnUrl) {
1636
1657
  const remoteUrl = `${existingCdnUrl}${imageKey}`;
1637
1658
  buffer = await downloadFromRemoteUrl(remoteUrl);
1638
- const dir = path7.dirname(originalPath);
1659
+ const dir = path6.dirname(originalPath);
1639
1660
  await fs6.mkdir(dir, { recursive: true });
1640
1661
  await fs6.writeFile(originalPath, buffer);
1641
1662
  } else {
1642
1663
  throw new Error(`File not found: ${imageKey}`);
1643
1664
  }
1644
1665
  }
1645
- const ext = path7.extname(imageKey).toLowerCase();
1666
+ const ext = path6.extname(imageKey).toLowerCase();
1646
1667
  const isSvg = ext === ".svg";
1647
1668
  if (isSvg) {
1648
- const imageDir = path7.dirname(imageKey.slice(1));
1649
- const imagesPath = path7.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
1669
+ const imageDir = path6.dirname(imageKey.slice(1));
1670
+ const imagesPath = getPublicPath("images", imageDir === "." ? "" : imageDir);
1650
1671
  await fs6.mkdir(imagesPath, { recursive: true });
1651
- const fileName = path7.basename(imageKey);
1652
- const destPath = path7.join(imagesPath, fileName);
1672
+ const fileName = path6.basename(imageKey);
1673
+ const destPath = path6.join(imagesPath, fileName);
1653
1674
  await fs6.writeFile(destPath, buffer);
1654
1675
  meta[imageKey] = {
1655
1676
  ...entry,
@@ -1734,7 +1755,7 @@ async function handleProcessAllStream() {
1734
1755
  let alreadyProcessed = 0;
1735
1756
  const imagesToProcess = [];
1736
1757
  for (const [key, entry] of getFileEntries(meta)) {
1737
- const fileName = path7.basename(key);
1758
+ const fileName = path6.basename(key);
1738
1759
  if (!isImageFile(fileName)) continue;
1739
1760
  if (!isProcessed(entry)) {
1740
1761
  imagesToProcess.push({ key, entry });
@@ -1746,7 +1767,7 @@ async function handleProcessAllStream() {
1746
1767
  sendEvent({ type: "start", total });
1747
1768
  for (let i = 0; i < imagesToProcess.length; i++) {
1748
1769
  const { key, entry } = imagesToProcess[i];
1749
- const fullPath = path7.join(process.cwd(), "public", key);
1770
+ const fullPath = getPublicPath(key);
1750
1771
  const existingCdnIndex = entry.c;
1751
1772
  const existingCdnUrl = existingCdnIndex !== void 0 ? cdnUrls[existingCdnIndex] : void 0;
1752
1773
  const isInOurR2 = existingCdnUrl === publicUrl;
@@ -1763,26 +1784,26 @@ async function handleProcessAllStream() {
1763
1784
  let buffer;
1764
1785
  if (isInOurR2) {
1765
1786
  buffer = await downloadFromCdn(key);
1766
- const dir = path7.dirname(fullPath);
1787
+ const dir = path6.dirname(fullPath);
1767
1788
  await fs6.mkdir(dir, { recursive: true });
1768
1789
  await fs6.writeFile(fullPath, buffer);
1769
1790
  } else if (isRemote && existingCdnUrl) {
1770
1791
  const remoteUrl = `${existingCdnUrl}${key}`;
1771
1792
  buffer = await downloadFromRemoteUrl(remoteUrl);
1772
- const dir = path7.dirname(fullPath);
1793
+ const dir = path6.dirname(fullPath);
1773
1794
  await fs6.mkdir(dir, { recursive: true });
1774
1795
  await fs6.writeFile(fullPath, buffer);
1775
1796
  } else {
1776
1797
  buffer = await fs6.readFile(fullPath);
1777
1798
  }
1778
- const ext = path7.extname(key).toLowerCase();
1799
+ const ext = path6.extname(key).toLowerCase();
1779
1800
  const isSvg = ext === ".svg";
1780
1801
  if (isSvg) {
1781
- const imageDir = path7.dirname(key.slice(1));
1782
- const imagesPath = path7.join(process.cwd(), "public", "images", imageDir === "." ? "" : imageDir);
1802
+ const imageDir = path6.dirname(key.slice(1));
1803
+ const imagesPath = getPublicPath("images", imageDir === "." ? "" : imageDir);
1783
1804
  await fs6.mkdir(imagesPath, { recursive: true });
1784
- const fileName = path7.basename(key);
1785
- const destPath = path7.join(imagesPath, fileName);
1805
+ const fileName = path6.basename(key);
1806
+ const destPath = path6.join(imagesPath, fileName);
1786
1807
  await fs6.writeFile(destPath, buffer);
1787
1808
  meta[key] = {
1788
1809
  ...entry,
@@ -1832,7 +1853,7 @@ async function handleProcessAllStream() {
1832
1853
  const entries = await fs6.readdir(dir, { withFileTypes: true });
1833
1854
  for (const fsEntry of entries) {
1834
1855
  if (fsEntry.name.startsWith(".")) continue;
1835
- const entryFullPath = path7.join(dir, fsEntry.name);
1856
+ const entryFullPath = path6.join(dir, fsEntry.name);
1836
1857
  const relPath = relativePath ? `${relativePath}/${fsEntry.name}` : fsEntry.name;
1837
1858
  if (fsEntry.isDirectory()) {
1838
1859
  await findOrphans(entryFullPath, relPath);
@@ -1851,7 +1872,7 @@ async function handleProcessAllStream() {
1851
1872
  } catch {
1852
1873
  }
1853
1874
  }
1854
- const imagesDir = path7.join(process.cwd(), "public", "images");
1875
+ const imagesDir = getPublicPath("images");
1855
1876
  try {
1856
1877
  await findOrphans(imagesDir);
1857
1878
  } catch {
@@ -1862,7 +1883,7 @@ async function handleProcessAllStream() {
1862
1883
  let isEmpty = true;
1863
1884
  for (const fsEntry of entries) {
1864
1885
  if (fsEntry.isDirectory()) {
1865
- const subDirEmpty = await removeEmptyDirs(path7.join(dir, fsEntry.name));
1886
+ const subDirEmpty = await removeEmptyDirs(path6.join(dir, fsEntry.name));
1866
1887
  if (!subDirEmpty) isEmpty = false;
1867
1888
  } else {
1868
1889
  isEmpty = false;
@@ -1941,8 +1962,8 @@ async function handleDownloadStream(request) {
1941
1962
  }
1942
1963
  try {
1943
1964
  const imageBuffer = await downloadFromCdn(imageKey);
1944
- const localPath = path7.join(process.cwd(), "public", imageKey.replace(/^\//, ""));
1945
- await fs6.mkdir(path7.dirname(localPath), { recursive: true });
1965
+ const localPath = getPublicPath(imageKey.replace(/^\//, ""));
1966
+ await fs6.mkdir(path6.dirname(localPath), { recursive: true });
1946
1967
  await fs6.writeFile(localPath, imageBuffer);
1947
1968
  await deleteThumbnailsFromCdn(imageKey);
1948
1969
  const wasProcessed = isProcessed(entry);
@@ -2007,7 +2028,7 @@ async function handleDownloadStream(request) {
2007
2028
  // src/handlers/scan.ts
2008
2029
  import { NextResponse as NextResponse4 } from "next/server";
2009
2030
  import { promises as fs7 } from "fs";
2010
- import path8 from "path";
2031
+ import path7 from "path";
2011
2032
  import sharp3 from "sharp";
2012
2033
  import { encode as encode2 } from "blurhash";
2013
2034
  async function handleScanStream() {
@@ -2033,7 +2054,7 @@ async function handleScanStream() {
2033
2054
  const entries = await fs7.readdir(dir, { withFileTypes: true });
2034
2055
  for (const entry of entries) {
2035
2056
  if (entry.name.startsWith(".")) continue;
2036
- const fullPath = path8.join(dir, entry.name);
2057
+ const fullPath = path7.join(dir, entry.name);
2037
2058
  const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
2038
2059
  if (relPath === "images" || relPath.startsWith("images/")) continue;
2039
2060
  if (entry.isDirectory()) {
@@ -2045,7 +2066,7 @@ async function handleScanStream() {
2045
2066
  } catch {
2046
2067
  }
2047
2068
  }
2048
- const publicDir = path8.join(process.cwd(), "public");
2069
+ const publicDir = getPublicPath();
2049
2070
  await scanDir(publicDir);
2050
2071
  const total = allFiles.length;
2051
2072
  sendEvent({ type: "start", total });
@@ -2063,7 +2084,7 @@ async function handleScanStream() {
2063
2084
  continue;
2064
2085
  }
2065
2086
  if (meta[imageKey]) {
2066
- const ext = path8.extname(relativePath);
2087
+ const ext = path7.extname(relativePath);
2067
2088
  const baseName = relativePath.slice(0, -ext.length);
2068
2089
  let counter = 1;
2069
2090
  let newKey = `/${baseName}-${counter}${ext}`;
@@ -2072,7 +2093,7 @@ async function handleScanStream() {
2072
2093
  newKey = `/${baseName}-${counter}${ext}`;
2073
2094
  }
2074
2095
  const newRelativePath = `${baseName}-${counter}${ext}`;
2075
- const newFullPath = path8.join(process.cwd(), "public", newRelativePath);
2096
+ const newFullPath = getPublicPath(newRelativePath);
2076
2097
  try {
2077
2098
  await fs7.rename(fullPath, newFullPath);
2078
2099
  renamed.push({ from: relativePath, to: newRelativePath });
@@ -2088,7 +2109,7 @@ async function handleScanStream() {
2088
2109
  try {
2089
2110
  const isImage = isImageFile(relativePath);
2090
2111
  if (isImage) {
2091
- const ext = path8.extname(relativePath).toLowerCase();
2112
+ const ext = path7.extname(relativePath).toLowerCase();
2092
2113
  if (ext === ".svg") {
2093
2114
  meta[imageKey] = { o: { w: 0, h: 0 }, b: "" };
2094
2115
  } else {
@@ -2130,7 +2151,7 @@ async function handleScanStream() {
2130
2151
  const entries = await fs7.readdir(dir, { withFileTypes: true });
2131
2152
  for (const entry of entries) {
2132
2153
  if (entry.name.startsWith(".")) continue;
2133
- const fullPath = path8.join(dir, entry.name);
2154
+ const fullPath = path7.join(dir, entry.name);
2134
2155
  const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
2135
2156
  if (entry.isDirectory()) {
2136
2157
  await findOrphans(fullPath, relPath);
@@ -2144,7 +2165,7 @@ async function handleScanStream() {
2144
2165
  } catch {
2145
2166
  }
2146
2167
  }
2147
- const imagesDir = path8.join(process.cwd(), "public", "images");
2168
+ const imagesDir = getPublicPath("images");
2148
2169
  try {
2149
2170
  await findOrphans(imagesDir);
2150
2171
  } catch {
@@ -2188,7 +2209,7 @@ async function handleDeleteOrphans(request) {
2188
2209
  errors.push(`Invalid path: ${orphanPath}`);
2189
2210
  continue;
2190
2211
  }
2191
- const fullPath = path8.join(process.cwd(), "public", orphanPath);
2212
+ const fullPath = getPublicPath(orphanPath);
2192
2213
  try {
2193
2214
  await fs7.unlink(fullPath);
2194
2215
  deleted.push(orphanPath);
@@ -2197,14 +2218,14 @@ async function handleDeleteOrphans(request) {
2197
2218
  errors.push(orphanPath);
2198
2219
  }
2199
2220
  }
2200
- const imagesDir = path8.join(process.cwd(), "public", "images");
2221
+ const imagesDir = getPublicPath("images");
2201
2222
  async function removeEmptyDirs(dir) {
2202
2223
  try {
2203
2224
  const entries = await fs7.readdir(dir, { withFileTypes: true });
2204
2225
  let isEmpty = true;
2205
2226
  for (const entry of entries) {
2206
2227
  if (entry.isDirectory()) {
2207
- const subDirEmpty = await removeEmptyDirs(path8.join(dir, entry.name));
2228
+ const subDirEmpty = await removeEmptyDirs(path7.join(dir, entry.name));
2208
2229
  if (!subDirEmpty) isEmpty = false;
2209
2230
  } else {
2210
2231
  isEmpty = false;
@@ -2239,8 +2260,8 @@ import { encode as encode3 } from "blurhash";
2239
2260
  function parseImageUrl(url) {
2240
2261
  const parsed = new URL(url);
2241
2262
  const base = `${parsed.protocol}//${parsed.host}`;
2242
- const path10 = parsed.pathname;
2243
- return { base, path: path10 };
2263
+ const path9 = parsed.pathname;
2264
+ return { base, path: path9 };
2244
2265
  }
2245
2266
  async function processRemoteImage(url) {
2246
2267
  const response = await fetch(url);
@@ -2289,20 +2310,20 @@ async function handleImportUrls(request) {
2289
2310
  currentFile: url
2290
2311
  });
2291
2312
  try {
2292
- const { base, path: path10 } = parseImageUrl(url);
2293
- const existingEntry = getMetaEntry(meta, path10);
2313
+ const { base, path: path9 } = parseImageUrl(url);
2314
+ const existingEntry = getMetaEntry(meta, path9);
2294
2315
  if (existingEntry) {
2295
- skipped.push(path10);
2316
+ skipped.push(path9);
2296
2317
  continue;
2297
2318
  }
2298
2319
  const cdnIndex = getOrAddCdnIndex(meta, base);
2299
2320
  const imageData = await processRemoteImage(url);
2300
- setMetaEntry(meta, path10, {
2321
+ setMetaEntry(meta, path9, {
2301
2322
  o: imageData.o,
2302
2323
  b: imageData.b,
2303
2324
  c: cdnIndex
2304
2325
  });
2305
- added.push(path10);
2326
+ added.push(path9);
2306
2327
  } catch (error) {
2307
2328
  console.error(`Failed to import ${url}:`, error);
2308
2329
  errors.push(url);
@@ -2360,7 +2381,7 @@ async function handleUpdateCdns(request) {
2360
2381
  // src/handlers/favicon.ts
2361
2382
  import { NextResponse as NextResponse5 } from "next/server";
2362
2383
  import sharp5 from "sharp";
2363
- import path9 from "path";
2384
+ import path8 from "path";
2364
2385
  import fs8 from "fs/promises";
2365
2386
  var FAVICON_CONFIGS = [
2366
2387
  { name: "favicon.ico", size: 48 },
@@ -2379,13 +2400,13 @@ async function handleGenerateFavicon(request) {
2379
2400
  } catch {
2380
2401
  return NextResponse5.json({ error: "Invalid request body" }, { status: 400 });
2381
2402
  }
2382
- const fileName = path9.basename(imagePath).toLowerCase();
2403
+ const fileName = path8.basename(imagePath).toLowerCase();
2383
2404
  if (fileName !== "favicon.png" && fileName !== "favicon.jpg") {
2384
2405
  return NextResponse5.json({
2385
2406
  error: "Source file must be named favicon.png or favicon.jpg"
2386
2407
  }, { status: 400 });
2387
2408
  }
2388
- const sourcePath = path9.join(process.cwd(), "public", imagePath.replace(/^\//, ""));
2409
+ const sourcePath = getPublicPath(imagePath.replace(/^\//, ""));
2389
2410
  try {
2390
2411
  await fs8.access(sourcePath);
2391
2412
  } catch {
@@ -2397,7 +2418,7 @@ async function handleGenerateFavicon(request) {
2397
2418
  } catch {
2398
2419
  return NextResponse5.json({ error: "Source file is not a valid image" }, { status: 400 });
2399
2420
  }
2400
- const outputDir = path9.join(process.cwd(), "src", "app");
2421
+ const outputDir = getSrcAppPath();
2401
2422
  try {
2402
2423
  await fs8.access(outputDir);
2403
2424
  } catch {
@@ -2431,7 +2452,7 @@ async function handleGenerateFavicon(request) {
2431
2452
  message: `Generating ${config.name} (${config.size}x${config.size})...`
2432
2453
  });
2433
2454
  try {
2434
- const outputPath = path9.join(outputDir, config.name);
2455
+ const outputPath = path8.join(outputDir, config.name);
2435
2456
  await sharp5(sourcePath).resize(config.size, config.size, {
2436
2457
  fit: "cover",
2437
2458
  position: "center"