@gallop.software/studio 2.3.173 → 2.4.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/config/workspace.ts","../../src/config/index.ts","../../src/handlers/utils/meta.ts","../../src/handlers/utils/files.ts","../../src/handlers/utils/thumbnails.ts","../../src/types.ts","../../src/handlers/utils/cdn.ts","../../src/handlers/utils/index.ts","../../src/cli/scan.ts","../../src/cli/process.ts","../../src/handlers/utils/folders.ts","../../src/cli/push.ts","../../src/cli/download.ts","../../src/handlers/utils/response.ts","../../src/handlers/utils/errors.ts","../../src/handlers/utils/cancellation.ts","../../src/handlers/fonts.ts","../../src/cli/fonts.ts","../../src/cli/index.ts"],"sourcesContent":["import path from 'path'\n\nlet workspacePath: string | null = null\n\n/**\n * Get the workspace root path.\n * In standalone mode, this comes from STUDIO_WORKSPACE env var.\n * In embedded mode, this defaults to process.cwd().\n */\nexport function getWorkspace(): string {\n if (workspacePath === null) {\n workspacePath = process.env.STUDIO_WORKSPACE || process.cwd()\n }\n return workspacePath\n}\n\n/**\n * Get an absolute path within the public folder.\n */\nexport function getPublicPath(...segments: string[]): string {\n return path.join(getWorkspace(), 'public', ...segments)\n}\n\n/**\n * Get an absolute path within the _data folder.\n */\nexport function getDataPath(...segments: string[]): string {\n return path.join(getWorkspace(), '_data', ...segments)\n}\n\n/**\n * Get an absolute path within the src/app folder.\n */\nexport function getSrcAppPath(...segments: string[]): string {\n return path.join(getWorkspace(), 'src', 'app', ...segments)\n}\n\n/**\n * Get an absolute path within the workspace root.\n */\nexport function getWorkspacePath(...segments: string[]): string {\n return path.join(getWorkspace(), ...segments)\n}\n","export {\n getWorkspace,\n getPublicPath,\n getDataPath,\n getSrcAppPath,\n getWorkspacePath,\n} from './workspace'\n","import { promises as fs } from 'fs'\nimport path from 'path'\nimport type { FullMeta, MetaEntry } from '../../types'\nimport { getDataPath } from '../../config'\n\nexport async function loadMeta(): Promise<FullMeta> {\n const metaPath = getDataPath('_studio.json')\n\n try {\n const content = await fs.readFile(metaPath, 'utf-8')\n return JSON.parse(content) as FullMeta\n } catch (err: unknown) {\n // File doesn't exist yet — expected on first run\n if (err && typeof err === 'object' && 'code' in err && err.code === 'ENOENT') {\n return {}\n }\n // JSON parse error — corrupted file, backup and warn\n if (err instanceof SyntaxError) {\n const backupPath = metaPath + '.corrupt.' + Date.now()\n try {\n await fs.rename(metaPath, backupPath)\n console.warn(`[studio] _studio.json was corrupted. Backed up to ${path.basename(backupPath)}`)\n } catch {\n console.warn('[studio] _studio.json was corrupted and could not be backed up')\n }\n return {}\n }\n // Other I/O errors (permissions, etc.) should not be silently swallowed\n throw err\n }\n}\n\nexport async function saveMeta(meta: FullMeta): Promise<void> {\n const dataDir = getDataPath()\n await fs.mkdir(dataDir, { recursive: true })\n const metaPath = getDataPath('_studio.json')\n \n // Ensure _cdns is at the top by creating ordered object\n const ordered: FullMeta = {}\n if (meta._cdns) {\n ordered._cdns = meta._cdns\n }\n // Add all other entries\n for (const [key, value] of Object.entries(meta)) {\n if (key !== '_cdns') {\n ordered[key] = value\n }\n }\n \n const tempPath = metaPath + '.tmp'\n await fs.writeFile(tempPath, JSON.stringify(ordered, null, 2))\n await fs.rename(tempPath, metaPath)\n}\n\n/**\n * Get the CDN URLs array from meta\n */\nexport function getCdnUrls(meta: FullMeta): string[] {\n return meta._cdns || []\n}\n\n/**\n * Set the CDN URLs array in meta\n */\nexport function setCdnUrls(meta: FullMeta, urls: string[]): void {\n meta._cdns = urls\n}\n\n/**\n * Get or add a CDN URL, returning its index\n */\nexport function getOrAddCdnIndex(meta: FullMeta, cdnUrl: string): number {\n if (!meta._cdns) {\n meta._cdns = []\n }\n \n // Normalize URL (remove trailing slash)\n const normalizedUrl = cdnUrl.replace(/\\/$/, '')\n \n const existingIndex = meta._cdns.indexOf(normalizedUrl)\n if (existingIndex >= 0) {\n return existingIndex\n }\n \n // Add new CDN URL\n meta._cdns.push(normalizedUrl)\n return meta._cdns.length - 1\n}\n\n/**\n * Get a meta entry (excludes special keys like _cdns)\n */\nexport function getMetaEntry(meta: FullMeta, key: string): MetaEntry | undefined {\n if (key.startsWith('_')) return undefined\n const value = meta[key]\n if (Array.isArray(value)) return undefined\n return value as MetaEntry | undefined\n}\n\n/**\n * Set a meta entry\n */\nexport function setMetaEntry(meta: FullMeta, key: string, entry: MetaEntry): void {\n meta[key] = entry\n}\n\n/**\n * Delete a meta entry\n */\nexport function deleteMetaEntry(meta: FullMeta, key: string): void {\n delete meta[key]\n}\n\n/**\n * Get all file entries (excludes special keys like _cdns)\n */\nexport function getFileEntries(meta: FullMeta): Array<[string, MetaEntry]> {\n return Object.entries(meta).filter(\n ([key, value]) => !key.startsWith('_') && !Array.isArray(value)\n ) as Array<[string, MetaEntry]>\n}\n","import { promises as fs } from 'fs'\nimport path from 'path'\n\n/**\n * Convert a filename to a slug-friendly format:\n * - lowercase\n * - spaces and underscores to hyphens\n * - remove special characters except hyphens and dots\n * - collapse multiple hyphens\n * - preserve file extension\n */\nexport function slugifyFilename(filename: string): string {\n const ext = path.extname(filename).toLowerCase()\n const baseName = path.basename(filename, path.extname(filename))\n \n const slugged = baseName\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '') // Remove diacritics\n .replace(/[_\\s]+/g, '-') // Replace spaces and underscores with hyphens\n .replace(/[^a-z0-9-]/g, '') // Remove non-alphanumeric except hyphens\n .replace(/-+/g, '-') // Collapse multiple hyphens\n .replace(/^-|-$/g, '') // Trim leading/trailing hyphens\n \n // If the slug is empty after processing, use a fallback\n const finalSlug = slugged || 'file'\n \n return finalSlug + ext\n}\n\n/**\n * Slugify a folder name (no extension handling)\n */\nexport function slugifyFolderName(name: string): string {\n const slugged = name\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '') // Remove diacritics\n .replace(/[_\\s]+/g, '-') // Replace spaces and underscores with hyphens\n .replace(/[^a-z0-9-]/g, '') // Remove non-alphanumeric except hyphens\n .replace(/-+/g, '-') // Collapse multiple hyphens\n .replace(/^-|-$/g, '') // Trim leading/trailing hyphens\n \n return slugged || 'folder'\n}\n\nexport function isImageFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n return ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)\n}\n\nexport function isMediaFile(filename: string): boolean {\n const ext = path.extname(filename).toLowerCase()\n // Images\n if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff', '.tif'].includes(ext)) return true\n // Videos\n if (['.mp4', '.webm', '.mov', '.avi', '.mkv', '.m4v'].includes(ext)) return true\n // Audio\n if (['.mp3', '.wav', '.ogg', '.m4a', '.flac', '.aac'].includes(ext)) return true\n // Documents\n if (['.pdf', '.json'].includes(ext)) return true\n return false\n}\n\nexport function getContentType(filePath: string): string {\n const ext = path.extname(filePath).toLowerCase()\n switch (ext) {\n case '.jpg':\n case '.jpeg':\n return 'image/jpeg'\n case '.png':\n return 'image/png'\n case '.gif':\n return 'image/gif'\n case '.webp':\n return 'image/webp'\n case '.svg':\n return 'image/svg+xml'\n default:\n return 'application/octet-stream'\n }\n}\n\nexport async function getFolderStats(folderPath: string): Promise<{ fileCount: number; totalSize: number }> {\n let fileCount = 0\n let totalSize = 0\n\n async function scanFolder(dir: string): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n const fullPath = path.join(dir, entry.name)\n if (entry.isDirectory()) {\n await scanFolder(fullPath)\n } else if (isMediaFile(entry.name)) {\n fileCount++\n const stats = await fs.stat(fullPath)\n totalSize += stats.size\n }\n }\n } catch { /* ignore errors */ }\n }\n\n await scanFolder(folderPath)\n return { fileCount, totalSize }\n}\n","import { promises as fs } from \"fs\";\nimport path from \"path\";\nimport sharp from \"sharp\";\nimport type { MetaEntry, Dimensions } from \"../../types\";\nimport { getPublicPath } from \"../../config\";\n\nexport const FULL_MAX_WIDTH = 2560;\n\nexport const DEFAULT_SIZES: Record<\n string,\n { width: number; suffix: string; key: \"sm\" | \"md\" | \"lg\" }\n> = {\n small: { width: 300, suffix: \"-sm\", key: \"sm\" },\n medium: { width: 700, suffix: \"-md\", key: \"md\" },\n large: { width: 1400, suffix: \"-lg\", key: \"lg\" },\n};\n\nexport async function processImage(\n buffer: Buffer,\n imageKey: string\n): Promise<MetaEntry> {\n // Apply EXIF rotation first to get correct dimensions\n // Many cameras store images rotated with EXIF metadata to display correctly\n const rotatedBuffer = await sharp(buffer).rotate().toBuffer();\n const metadata = await sharp(rotatedBuffer).metadata();\n const originalWidth = metadata.width || 0;\n const originalHeight = metadata.height || 0;\n const ratio = originalHeight / originalWidth;\n\n // Remove leading slash for path operations\n const keyWithoutSlash = imageKey.startsWith(\"/\")\n ? imageKey.slice(1)\n : imageKey;\n const baseName = path.basename(\n keyWithoutSlash,\n path.extname(keyWithoutSlash)\n );\n const ext = path.extname(keyWithoutSlash).toLowerCase();\n const imageDir = path.dirname(keyWithoutSlash);\n\n const imagesPath = getPublicPath(\"images\", imageDir === \".\" ? \"\" : imageDir);\n await fs.mkdir(imagesPath, { recursive: true });\n\n const isPng = ext === \".png\";\n const outputExt = isPng ? \".png\" : \".jpg\";\n\n // Build the result entry\n const entry: MetaEntry = {\n o: { w: originalWidth, h: originalHeight },\n };\n\n // Generate full size (capped at FULL_MAX_WIDTH)\n const fullFileName =\n imageDir === \".\"\n ? `${baseName}${outputExt}`\n : `${imageDir}/${baseName}${outputExt}`;\n const fullPath = getPublicPath(\"images\", fullFileName);\n\n let fullWidth = originalWidth;\n let fullHeight = originalHeight;\n\n if (originalWidth > FULL_MAX_WIDTH) {\n fullWidth = FULL_MAX_WIDTH;\n fullHeight = Math.round(FULL_MAX_WIDTH * ratio);\n if (isPng) {\n await sharp(rotatedBuffer)\n .resize(fullWidth, fullHeight)\n .png({ quality: 85 })\n .toFile(fullPath);\n } else {\n await sharp(rotatedBuffer)\n .resize(fullWidth, fullHeight)\n .jpeg({ quality: 85 })\n .toFile(fullPath);\n }\n } else {\n if (isPng) {\n await sharp(rotatedBuffer).png({ quality: 85 }).toFile(fullPath);\n } else {\n await sharp(rotatedBuffer).jpeg({ quality: 85 }).toFile(fullPath);\n }\n }\n entry.f = { w: fullWidth, h: fullHeight };\n\n // Generate thumbnail sizes\n for (const [, sizeConfig] of Object.entries(DEFAULT_SIZES)) {\n const { width: maxWidth, suffix, key } = sizeConfig;\n if (originalWidth <= maxWidth) {\n continue; // Skip if original is smaller than this size\n }\n\n const newHeight = Math.round(maxWidth * ratio);\n const sizeFileName = `${baseName}${suffix}${outputExt}`;\n const sizeFilePath =\n imageDir === \".\" ? sizeFileName : `${imageDir}/${sizeFileName}`;\n const sizePath = getPublicPath(\"images\", sizeFilePath);\n\n if (isPng) {\n await sharp(rotatedBuffer)\n .resize(maxWidth, newHeight)\n .png({ quality: 80 })\n .toFile(sizePath);\n } else {\n await sharp(rotatedBuffer)\n .resize(maxWidth, newHeight)\n .jpeg({ quality: 80 })\n .toFile(sizePath);\n }\n\n entry[key] = { w: maxWidth, h: newHeight };\n }\n\n return entry;\n}\n","/**\n * Dimensions object {w, h}\n */\nexport interface Dimensions {\n w: number;\n h: number;\n}\n\n/**\n * Meta entry - works for images and non-images\n * o: original dimensions, c: CDN index\n * sm/md/lg/f: thumbnail dimensions (presence implies processed)\n */\nexport interface MetaEntry {\n o?: Dimensions; // original dimensions {w, h}\n sm?: Dimensions; // small thumbnail (300px width)\n md?: Dimensions; // medium thumbnail (700px width)\n lg?: Dimensions; // large thumbnail (1400px width)\n f?: Dimensions; // full size (capped at 2560px width)\n c?: number; // CDN index - index into _cdns array\n u?: 1; // update pending - local file overrides cloud file\n b?: string; // blur hash or base64 placeholder\n}\n\n/**\n * Full meta schema including special keys\n * _cdns: Array of CDN base URLs\n * Other keys: file paths from public folder\n */\nexport interface FullMeta {\n _cdns?: string[]; // Array of CDN base URLs\n [key: string]: MetaEntry | string[] | undefined;\n}\n\n/**\n * Meta schema - keyed by path from public folder\n * Example: { \"/portfolio/photo.jpg\": { o: {w:2400,h:1600}, sm: {w:300,h:200}, ... } }\n */\nexport type LeanMeta = Record<string, MetaEntry>;\n\n// Alias for compatibility\nexport type LeanImageEntry = MetaEntry;\n\n/**\n * File/folder item for browser\n */\nexport interface FileItem {\n name: string;\n path: string;\n type: \"file\" | \"folder\";\n size?: number;\n dimensions?: { width: number; height: number };\n isProcessed?: boolean;\n cdnPushed?: boolean;\n cdnBaseUrl?: string; // CDN base URL when pushed to cloud\n isRemote?: boolean; // true if CDN URL doesn't match R2 (external import)\n isCloud?: boolean; // true if file exists in cloud (CDN pushed or remote)\n isProtected?: boolean; // true for images folder and its contents (cannot select/modify)\n // Folder-specific properties\n fileCount?: number;\n totalSize?: number;\n cloudCount?: number; // Number of R2 cloud files in folder\n remoteCount?: number; // Number of remote (imported URL) files in folder\n localCount?: number; // Number of local files in folder\n // For showing thumbnails - path to -sm version if exists\n thumbnail?: string;\n // Whether a processed thumbnail exists\n hasThumbnail?: boolean;\n // Which thumbnail sizes exist\n hasSm?: boolean;\n hasMd?: boolean;\n hasLg?: boolean;\n hasFull?: boolean;\n // Update pending - local file overrides cloud file\n hasUpdate?: boolean;\n // Number of files with pending updates in folder\n updateCount?: number;\n}\n\n/**\n * Studio configuration\n */\nexport interface StudioConfig {\n r2AccountId?: string;\n r2AccessKeyId?: string;\n r2SecretAccessKey?: string;\n r2BucketName?: string;\n r2PublicUrl?: string;\n thumbnailSizes?: {\n small: number;\n medium: number;\n large: number;\n };\n}\n\n/**\n * Get thumbnail path from original image path\n */\nexport function getThumbnailPath(\n originalPath: string,\n size: \"sm\" | \"md\" | \"lg\" | \"full\"\n): string {\n if (size === \"full\") {\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || \".jpg\";\n const base = originalPath.replace(/\\.\\w+$/, \"\");\n const outputExt = ext.toLowerCase() === \".png\" ? \".png\" : \".jpg\";\n return `/images${base}${outputExt}`;\n }\n const ext = originalPath.match(/\\.\\w+$/)?.[0] || \".jpg\";\n const base = originalPath.replace(/\\.\\w+$/, \"\");\n const outputExt = ext.toLowerCase() === \".png\" ? \".png\" : \".jpg\";\n return `/images${base}-${size}${outputExt}`;\n}\n\n/**\n * Get all thumbnail paths for an image\n */\nexport function getAllThumbnailPaths(originalPath: string): string[] {\n return [\n getThumbnailPath(originalPath, \"full\"),\n getThumbnailPath(originalPath, \"lg\"),\n getThumbnailPath(originalPath, \"md\"),\n getThumbnailPath(originalPath, \"sm\"),\n ];\n}\n\n/**\n * Check if an image entry is processed (has any thumbnail dimensions)\n */\nexport function isProcessed(entry: MetaEntry | undefined): boolean {\n if (!entry) return false;\n return !!(entry.f || entry.lg || entry.md || entry.sm);\n}\n","import { promises as fs } from 'fs'\nimport { S3Client, GetObjectCommand, PutObjectCommand, DeleteObjectCommand, CopyObjectCommand } from '@aws-sdk/client-s3'\nimport { getAllThumbnailPaths } from '../../types'\nimport { getContentType } from './files'\nimport { getPublicPath } from '../../config'\n\nfunction getR2Client() {\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n\n if (!accountId || !accessKeyId || !secretAccessKey) {\n throw new Error('R2 not configured')\n }\n\n return new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n}\n\nexport async function downloadFromCdn(originalPath: string): Promise<Buffer> {\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n if (!bucketName) throw new Error('R2 bucket not configured')\n\n const r2 = getR2Client()\n const maxRetries = 3\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n const response = await r2.send(\n new GetObjectCommand({\n Bucket: bucketName,\n Key: originalPath.replace(/^\\//, ''),\n })\n )\n\n const stream = response.Body as NodeJS.ReadableStream\n const chunks: Buffer[] = []\n for await (const chunk of stream) {\n chunks.push(Buffer.from(chunk))\n }\n return Buffer.concat(chunks)\n } catch (error) {\n lastError = error as Error\n // Wait before retry (exponential backoff: 500ms, 1s)\n if (attempt < maxRetries - 1) {\n await new Promise(resolve => setTimeout(resolve, 500 * (attempt + 1)))\n }\n }\n }\n\n throw lastError || new Error(`Failed to download ${originalPath} after ${maxRetries} attempts`)\n}\n\nexport async function uploadToCdn(imageKey: string): Promise<void> {\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n if (!bucketName) throw new Error('R2 bucket not configured')\n\n const r2 = getR2Client()\n\n // Upload all thumbnail sizes derived from imageKey\n for (const thumbPath of getAllThumbnailPaths(imageKey)) {\n const localPath = getPublicPath(thumbPath)\n try {\n const fileBuffer = await fs.readFile(localPath)\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: thumbPath.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(thumbPath),\n })\n )\n } catch {\n // File might not exist (e.g., if image is smaller than thumbnail size)\n }\n }\n}\n\nexport async function deleteLocalThumbnails(imageKey: string): Promise<void> {\n for (const thumbPath of getAllThumbnailPaths(imageKey)) {\n const localPath = getPublicPath(thumbPath)\n try {\n await fs.unlink(localPath)\n } catch {\n // File might not exist\n }\n }\n}\n\n/**\n * Download image from a remote URL (not R2)\n */\nexport async function downloadFromRemoteUrl(url: string): Promise<Buffer> {\n const maxRetries = 3\n let lastError: Error | undefined\n\n for (let attempt = 0; attempt < maxRetries; attempt++) {\n try {\n const response = await fetch(url)\n if (!response.ok) {\n throw new Error(`Failed to download from ${url}: ${response.status}`)\n }\n const arrayBuffer = await response.arrayBuffer()\n return Buffer.from(arrayBuffer)\n } catch (error) {\n lastError = error as Error\n // Wait before retry (exponential backoff: 500ms, 1s)\n if (attempt < maxRetries - 1) {\n await new Promise(resolve => setTimeout(resolve, 500 * (attempt + 1)))\n }\n }\n }\n\n throw lastError || new Error(`Failed to download from ${url} after ${maxRetries} attempts`)\n}\n\n/**\n * Upload original image to R2 CDN\n */\nexport async function uploadOriginalToCdn(imageKey: string): Promise<void> {\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n if (!bucketName) throw new Error('R2 bucket not configured')\n\n const r2 = getR2Client()\n const localPath = getPublicPath(imageKey)\n const fileBuffer = await fs.readFile(localPath)\n \n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: imageKey.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(imageKey),\n })\n )\n}\n\n/**\n * Delete original and thumbnails from R2 CDN\n */\nexport async function deleteFromCdn(imageKey: string, hasThumbnails: boolean): Promise<void> {\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n if (!bucketName) throw new Error('R2 bucket not configured')\n\n const r2 = getR2Client()\n\n // Delete original\n try {\n await r2.send(\n new DeleteObjectCommand({\n Bucket: bucketName,\n Key: imageKey.replace(/^\\//, ''),\n })\n )\n } catch {\n // May not exist\n }\n\n // Delete thumbnails if they exist\n if (hasThumbnails) {\n for (const thumbPath of getAllThumbnailPaths(imageKey)) {\n try {\n await r2.send(\n new DeleteObjectCommand({\n Bucket: bucketName,\n Key: thumbPath.replace(/^\\//, ''),\n })\n )\n } catch {\n // May not exist\n }\n }\n }\n}\n\n/**\n * Delete only thumbnails from R2 CDN (keeps original)\n */\nexport async function deleteThumbnailsFromCdn(imageKey: string): Promise<void> {\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n if (!bucketName) throw new Error('R2 bucket not configured')\n\n const r2 = getR2Client()\n\n for (const thumbPath of getAllThumbnailPaths(imageKey)) {\n try {\n await r2.send(\n new DeleteObjectCommand({\n Bucket: bucketName,\n Key: thumbPath.replace(/^\\//, ''),\n })\n )\n } catch {\n // May not exist\n }\n }\n}\n\n/**\n * Delete only original from R2 CDN (keeps thumbnails)\n * Used for cache busting before re-upload\n */\nexport async function deleteOriginalFromCdn(imageKey: string): Promise<void> {\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n if (!bucketName) throw new Error('R2 bucket not configured')\n\n const r2 = getR2Client()\n\n try {\n await r2.send(\n new DeleteObjectCommand({\n Bucket: bucketName,\n Key: imageKey.replace(/^\\//, ''),\n })\n )\n } catch {\n // May not exist\n }\n}\n\n/**\n * Copy a file within R2 CDN (server-side, no download/upload)\n * Used for rename/move operations - much faster than download+upload\n */\nexport async function copyInCdn(oldKey: string, newKey: string): Promise<void> {\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n if (!bucketName) throw new Error('R2 bucket not configured')\n\n const r2 = getR2Client()\n const oldKeyClean = oldKey.replace(/^\\//, '')\n const newKeyClean = newKey.replace(/^\\//, '')\n\n await r2.send(\n new CopyObjectCommand({\n Bucket: bucketName,\n CopySource: `${bucketName}/${oldKeyClean}`,\n Key: newKeyClean,\n })\n )\n}\n\n/**\n * Move a file within R2 CDN (copy + delete, server-side)\n * Handles original and thumbnails\n */\nexport async function moveInCdn(oldKey: string, newKey: string, hasThumbnails: boolean): Promise<void> {\n // Copy original\n await copyInCdn(oldKey, newKey)\n \n // Copy thumbnails if they exist\n if (hasThumbnails) {\n const oldThumbPaths = getAllThumbnailPaths(oldKey)\n const newThumbPaths = getAllThumbnailPaths(newKey)\n \n for (let i = 0; i < oldThumbPaths.length; i++) {\n try {\n await copyInCdn(oldThumbPaths[i], newThumbPaths[i])\n } catch {\n // Thumbnail might not exist\n }\n }\n }\n \n // Delete old files\n await deleteFromCdn(oldKey, hasThumbnails)\n}\n","export { loadMeta, saveMeta, getCdnUrls, setCdnUrls, getOrAddCdnIndex, getMetaEntry, setMetaEntry, deleteMetaEntry, getFileEntries } from './meta'\nexport { isImageFile, isMediaFile, getContentType, getFolderStats, slugifyFilename, slugifyFolderName } from './files'\nexport { processImage, DEFAULT_SIZES } from './thumbnails'\nexport { downloadFromCdn, uploadToCdn, deleteLocalThumbnails, downloadFromRemoteUrl, uploadOriginalToCdn, deleteFromCdn, deleteThumbnailsFromCdn, deleteOriginalFromCdn, copyInCdn, moveInCdn } from './cdn'\n","import { promises as fs } from 'fs'\nimport path from 'path'\nimport sharp from 'sharp'\nimport { printProgress, printComplete, printError } from './index'\nimport {\n loadMeta,\n saveMeta,\n isMediaFile,\n isImageFile,\n getFileEntries,\n slugifyFilename,\n} from '../handlers/utils'\nimport { getAllThumbnailPaths, isProcessed } from '../types'\nimport { getPublicPath } from '../config'\n\nexport async function runScan(_args: string[]) {\n console.log('Scanning for media files...')\n\n const meta = await loadMeta()\n const existingCount = Object.keys(meta).filter(k => !k.startsWith('_')).length\n const existingKeys = new Set(Object.keys(meta))\n const added: string[] = []\n const renamed: Array<{ from: string; to: string }> = []\n const errors: string[] = []\n const orphanedFiles: string[] = []\n const pendingUpdates: string[] = []\n\n // Collect all files first\n const allFiles: Array<{ relativePath: string; fullPath: string }> = []\n\n async function scanDir(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n // Skip the images folder (generated thumbnails)\n if (relPath === 'images' || relPath.startsWith('images/')) continue\n\n if (entry.isDirectory()) {\n await scanDir(fullPath, relPath)\n } else if (isMediaFile(entry.name)) {\n allFiles.push({ relativePath: relPath, fullPath })\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const publicDir = getPublicPath()\n await scanDir(publicDir)\n\n const total = allFiles.length\n\n for (let i = 0; i < allFiles.length; i++) {\n let { relativePath, fullPath } = allFiles[i]\n let imageKey = '/' + relativePath\n\n printProgress(i + 1, total, relativePath)\n\n // Check if already in meta\n if (existingKeys.has(imageKey)) {\n const entry = meta[imageKey] as { c?: number; u?: 1 } | undefined\n if (entry?.c !== undefined && !entry?.u) {\n entry.u = 1\n pendingUpdates.push(imageKey)\n }\n continue\n }\n\n // Slugify filename to be URL-safe\n const dirName = path.dirname(relativePath)\n const originalFileName = path.basename(relativePath)\n const sluggedFileName = slugifyFilename(originalFileName)\n\n if (sluggedFileName !== originalFileName) {\n const newRelativePath = dirName === '.' ? sluggedFileName : `${dirName}/${sluggedFileName}`\n const newFullPath = getPublicPath(newRelativePath)\n const newKey = '/' + newRelativePath\n\n if (!meta[newKey] && !existingKeys.has(newKey)) {\n try {\n await fs.mkdir(path.dirname(newFullPath), { recursive: true })\n await fs.rename(fullPath, newFullPath)\n renamed.push({ from: relativePath, to: newRelativePath })\n relativePath = newRelativePath\n fullPath = newFullPath\n imageKey = newKey\n } catch (err) {\n console.error(`\\nFailed to slugify ${relativePath}:`, err)\n }\n }\n }\n\n // Check for collision\n if (meta[imageKey]) {\n const ext = path.extname(relativePath)\n const baseName = relativePath.slice(0, -ext.length)\n let counter = 1\n let newKey = `/${baseName}-${counter}${ext}`\n\n while (meta[newKey]) {\n counter++\n newKey = `/${baseName}-${counter}${ext}`\n }\n\n const newRelativePath = `${baseName}-${counter}${ext}`\n const newFullPath = getPublicPath(newRelativePath)\n\n try {\n await fs.rename(fullPath, newFullPath)\n renamed.push({ from: relativePath, to: newRelativePath })\n relativePath = newRelativePath\n fullPath = newFullPath\n imageKey = newKey\n } catch (err) {\n console.error(`\\nFailed to rename ${relativePath}:`, err)\n errors.push(`Failed to rename ${relativePath}`)\n continue\n }\n }\n\n try {\n const isImage = isImageFile(relativePath)\n\n if (isImage) {\n const ext = path.extname(relativePath).toLowerCase()\n\n if (ext === '.svg') {\n meta[imageKey] = { o: { w: 0, h: 0 } }\n } else {\n try {\n const buffer = await fs.readFile(fullPath)\n const rotatedBuffer = await sharp(buffer).rotate().toBuffer()\n const metadata = await sharp(rotatedBuffer).metadata()\n\n meta[imageKey] = {\n o: { w: metadata.width || 0, h: metadata.height || 0 },\n }\n } catch {\n meta[imageKey] = { o: { w: 0, h: 0 } }\n }\n }\n } else {\n meta[imageKey] = {}\n }\n\n existingKeys.add(imageKey)\n added.push(imageKey)\n\n // Save meta periodically\n if (added.length % 10 === 0) {\n await saveMeta(meta)\n }\n } catch (error) {\n console.error(`\\nFailed to process ${relativePath}:`, error)\n errors.push(relativePath)\n }\n }\n\n // Check for orphaned thumbnails\n process.stdout.write('\\n')\n console.log(' Checking for orphaned thumbnails...')\n\n const expectedThumbnails = new Set<string>()\n const fileEntries = getFileEntries(meta)\n for (const [imageKey, entry] of fileEntries) {\n if (entry.c === undefined && isProcessed(entry)) {\n for (const thumbPath of getAllThumbnailPaths(imageKey)) {\n expectedThumbnails.add(thumbPath)\n }\n }\n }\n\n async function findOrphans(dir: string, relativePath: string = ''): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n\n const fullPath = path.join(dir, entry.name)\n const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name\n\n if (entry.isDirectory()) {\n await findOrphans(fullPath, relPath)\n } else if (isImageFile(entry.name)) {\n const publicPath = `/images/${relPath}`\n if (!expectedThumbnails.has(publicPath)) {\n orphanedFiles.push(publicPath)\n }\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n const imagesDir = getPublicPath('images')\n try {\n await findOrphans(imagesDir)\n } catch {\n // images dir might not exist\n }\n\n // Clean up empty folders\n console.log(' Cleaning up empty folders...')\n let emptyFoldersDeleted = 0\n\n async function cleanEmptyFolders(dir: string): Promise<void> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n\n for (const entry of entries) {\n if (entry.name.startsWith('.')) continue\n if (!entry.isDirectory()) continue\n\n const fullPath = path.join(dir, entry.name)\n if (fullPath === imagesDir) continue\n\n await cleanEmptyFolders(fullPath)\n\n try {\n const subEntries = await fs.readdir(fullPath)\n const meaningfulEntries = subEntries.filter(e => !e.startsWith('.'))\n if (meaningfulEntries.length === 0) {\n await fs.rm(fullPath, { recursive: true })\n emptyFoldersDeleted++\n }\n } catch {\n // Folder might already be deleted\n }\n }\n } catch {\n // Directory might not exist\n }\n }\n\n await cleanEmptyFolders(getPublicPath())\n\n // Clean empty folders inside images directory\n async function cleanImagesEmptyFolders(dir: string): Promise<boolean> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n let isEmpty = true\n\n for (const entry of entries) {\n if (entry.isDirectory()) {\n const subDirEmpty = await cleanImagesEmptyFolders(path.join(dir, entry.name))\n if (!subDirEmpty) isEmpty = false\n } else if (!entry.name.startsWith('.')) {\n isEmpty = false\n }\n }\n\n if (isEmpty && dir !== imagesDir) {\n await fs.rm(dir, { recursive: true })\n emptyFoldersDeleted++\n }\n\n return isEmpty\n } catch {\n return true\n }\n }\n\n try {\n await cleanImagesEmptyFolders(imagesDir)\n } catch {\n // images dir might not exist\n }\n\n // Clean up orphaned meta entries\n console.log(' Checking for orphaned entries...')\n const orphanedEntries: string[] = []\n\n for (const key of Object.keys(meta)) {\n if (key.startsWith('_')) continue\n\n const entry = meta[key] as { c?: number; u?: 1 } | undefined\n if (!entry) continue\n\n // Skip cloud files\n if (entry.c !== undefined) {\n if (entry.u === 1) {\n const localPath = getPublicPath(key)\n try {\n await fs.access(localPath)\n } catch {\n delete entry.u\n }\n }\n continue\n }\n\n // For local files, check if they still exist\n const localPath = getPublicPath(key)\n try {\n await fs.access(localPath)\n } catch {\n orphanedEntries.push(key)\n delete meta[key]\n }\n }\n\n await saveMeta(meta)\n\n // Print summary\n const parts: string[] = []\n parts.push(`${existingCount} existing`)\n if (added.length > 0) parts.push(`${added.length} added`)\n if (renamed.length > 0) parts.push(`${renamed.length} renamed`)\n if (errors.length > 0) parts.push(`${errors.length} errors`)\n if (orphanedFiles.length > 0) parts.push(`${orphanedFiles.length} orphaned thumbnails`)\n if (orphanedEntries.length > 0) parts.push(`${orphanedEntries.length} orphaned entries removed`)\n if (pendingUpdates.length > 0) parts.push(`${pendingUpdates.length} pending updates`)\n if (emptyFoldersDeleted > 0) parts.push(`${emptyFoldersDeleted} empty folders removed`)\n\n printComplete(`Scan complete. ${parts.join(', ')}.`)\n}\n","import { promises as fs } from 'fs'\nimport path from 'path'\nimport { printProgress, printComplete, printError } from './index'\nimport {\n loadMeta,\n saveMeta,\n isImageFile,\n getFileEntries,\n processImage,\n} from '../handlers/utils'\nimport { isProcessed } from '../types'\nimport { getPublicPath } from '../config'\n\nexport async function runProcess(args: string[]) {\n const prefix = args[0] || ''\n\n if (prefix) {\n console.log(`Processing unprocessed images matching \"/${prefix}\"...`)\n } else {\n console.log('Processing all unprocessed images...')\n }\n\n const meta = await loadMeta()\n const processed: string[] = []\n const errors: string[] = []\n let alreadyProcessed = 0\n\n // Get all images from meta that need processing\n const imagesToProcess: Array<{ key: string }> = []\n\n for (const [key, entry] of getFileEntries(meta)) {\n const fileName = path.basename(key)\n if (!isImageFile(fileName)) continue\n\n // Apply prefix filter\n if (prefix && !key.startsWith(`/${prefix}`)) continue\n\n if (!isProcessed(entry)) {\n imagesToProcess.push({ key })\n } else {\n alreadyProcessed++\n }\n }\n\n if (imagesToProcess.length === 0) {\n console.log(`No unprocessed images found${prefix ? ` matching \"/${prefix}\"` : ''}. ${alreadyProcessed} already processed.`)\n return\n }\n\n const total = imagesToProcess.length\n console.log(`Found ${total} unprocessed image${total !== 1 ? 's' : ''} (${alreadyProcessed} already processed)`)\n\n for (let i = 0; i < imagesToProcess.length; i++) {\n const { key } = imagesToProcess[i]\n const fullPath = getPublicPath(key)\n\n printProgress(i + 1, total, key.slice(1))\n\n try {\n let buffer: Buffer\n\n try {\n buffer = await fs.readFile(fullPath)\n } catch {\n printError(`File not found: ${key}`)\n errors.push(key)\n continue\n }\n\n const ext = path.extname(key).toLowerCase()\n const isSvg = ext === '.svg'\n\n if (isSvg) {\n const imageDir = path.dirname(key.slice(1))\n const imagesPath = getPublicPath('images', imageDir === '.' ? '' : imageDir)\n await fs.mkdir(imagesPath, { recursive: true })\n\n const fileName = path.basename(key)\n const destPath = path.join(imagesPath, fileName)\n await fs.writeFile(destPath, buffer)\n\n const existingEntry = meta[key]\n meta[key] = {\n ...(typeof existingEntry === 'object' && !Array.isArray(existingEntry) ? existingEntry : {}),\n o: { w: 0, h: 0 },\n f: { w: 0, h: 0 },\n }\n } else {\n const updatedEntry = await processImage(buffer, key)\n meta[key] = updatedEntry\n }\n\n // Save meta after each\n await saveMeta(meta)\n processed.push(key)\n } catch (error) {\n console.error(`\\nFailed to process ${key}:`, error)\n errors.push(key)\n }\n }\n\n await saveMeta(meta)\n\n // Print summary\n const parts: string[] = []\n parts.push(`${processed.length} processed`)\n if (errors.length > 0) parts.push(`${errors.length} failed`)\n\n if (errors.length > 0) {\n printError(`Processing complete. ${parts.join(', ')}.`)\n } else {\n printComplete(`Processing complete. ${parts.join(', ')}.`)\n }\n}\n","import { promises as fs } from 'fs'\nimport path from 'path'\nimport { getPublicPath } from '../../config'\n\n/**\n * Check if a file is a hidden/system file that should be ignored\n * - Mac/Linux: files starting with .\n * - Windows: Thumbs.db, desktop.ini, etc.\n */\nexport function isHiddenOrSystemFile(filename: string): boolean {\n // Hidden files on Mac/Linux start with .\n if (filename.startsWith('.')) return true\n \n // Windows system files\n const windowsSystemFiles = ['thumbs.db', 'desktop.ini', 'ehthumbs.db', 'ehthumbs_vista.db']\n if (windowsSystemFiles.includes(filename.toLowerCase())) return true\n \n return false\n}\n\n/**\n * Recursively delete empty folders starting from the given path\n * Stops at the public folder boundary\n * Includes the images folder - it can be deleted if empty and will be recreated when needed\n * Ignores hidden and system files (deletes them if they're the only contents)\n */\nexport async function deleteEmptyFolders(folderPath: string): Promise<void> {\n const publicPath = getPublicPath()\n \n // Normalize paths for comparison\n const normalizedFolder = path.resolve(folderPath)\n const normalizedPublic = path.resolve(publicPath)\n \n // Don't delete the public folder itself\n if (normalizedFolder === normalizedPublic) {\n return\n }\n \n // Check if folder is inside public\n if (!normalizedFolder.startsWith(normalizedPublic)) {\n return\n }\n \n try {\n const entries = await fs.readdir(folderPath)\n \n // Filter out hidden/system files\n const meaningfulEntries = entries.filter(e => !isHiddenOrSystemFile(e))\n \n // If folder only contains hidden/system files (or is empty), delete it\n if (meaningfulEntries.length === 0) {\n // First delete any hidden/system files\n for (const entry of entries) {\n if (isHiddenOrSystemFile(entry)) {\n try {\n await fs.unlink(path.join(folderPath, entry))\n } catch {\n // Ignore deletion errors for system files\n }\n }\n }\n \n // Now delete the folder\n await fs.rmdir(folderPath)\n \n // Recursively check parent folder\n const parentFolder = path.dirname(folderPath)\n await deleteEmptyFolders(parentFolder)\n }\n } catch {\n // Folder doesn't exist or can't be read, ignore\n }\n}\n\n/**\n * Ensure a folder exists, creating it if necessary\n */\nexport async function ensureFolderExists(folderPath: string): Promise<void> {\n try {\n await fs.mkdir(folderPath, { recursive: true })\n } catch {\n // Already exists or can't be created\n }\n}\n\n/**\n * Recursively clean up all empty folders within a directory\n * Also deletes the directory itself if it becomes empty\n * Ignores hidden/system files (deletes them if they're the only contents)\n */\nexport async function cleanupEmptyFoldersRecursive(dir: string): Promise<boolean> {\n try {\n const entries = await fs.readdir(dir, { withFileTypes: true })\n let isEmpty = true\n \n for (const entry of entries) {\n if (entry.isDirectory()) {\n const subDirEmpty = await cleanupEmptyFoldersRecursive(path.join(dir, entry.name))\n if (!subDirEmpty) isEmpty = false\n } else if (!isHiddenOrSystemFile(entry.name)) {\n // Non-hidden file exists\n isEmpty = false\n }\n }\n \n // Delete empty folder\n if (isEmpty) {\n // First delete any hidden/system files\n for (const entry of entries) {\n if (!entry.isDirectory() && isHiddenOrSystemFile(entry.name)) {\n try {\n await fs.unlink(path.join(dir, entry.name))\n } catch {\n // Ignore deletion errors\n }\n }\n }\n await fs.rmdir(dir)\n }\n \n return isEmpty\n } catch {\n return true\n }\n}\n","import { promises as fs } from 'fs'\nimport path from 'path'\nimport { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'\nimport { printProgress, printComplete, printError } from './index'\nimport {\n loadMeta,\n saveMeta,\n getFileEntries,\n getContentType,\n getCdnUrls,\n getOrAddCdnIndex,\n getMetaEntry,\n downloadFromRemoteUrl,\n} from '../handlers/utils'\nimport { getAllThumbnailPaths, isProcessed } from '../types'\nimport { getPublicPath } from '../config'\nimport { deleteEmptyFolders } from '../handlers/utils/folders'\n\nexport async function runPush(args: string[]) {\n const prefix = args[0] || ''\n\n const accountId = process.env.CLOUDFLARE_R2_ACCOUNT_ID\n const accessKeyId = process.env.CLOUDFLARE_R2_ACCESS_KEY_ID\n const secretAccessKey = process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY\n const bucketName = process.env.CLOUDFLARE_R2_BUCKET_NAME\n const publicUrl = process.env.CLOUDFLARE_R2_PUBLIC_URL?.replace(/\\/\\s*$/, '')\n\n if (!accountId || !accessKeyId || !secretAccessKey || !bucketName || !publicUrl) {\n printError('R2 not configured. Set CLOUDFLARE_R2_* environment variables in .env.local')\n process.exit(1)\n }\n\n if (prefix) {\n console.log(`Pushing local images matching \"/${prefix}\" to CDN...`)\n } else {\n console.log('Pushing all local images to CDN...')\n }\n\n const meta = await loadMeta()\n const cdnUrls = getCdnUrls(meta)\n const cdnIndex = getOrAddCdnIndex(meta, publicUrl)\n\n const r2 = new S3Client({\n region: 'auto',\n endpoint: `https://${accountId}.r2.cloudflarestorage.com`,\n credentials: { accessKeyId, secretAccessKey },\n })\n\n // Find local images to push\n const imagesToPush: string[] = []\n\n for (const [key, entry] of getFileEntries(meta)) {\n // Apply prefix filter\n if (prefix && !key.startsWith(`/${prefix}`)) continue\n\n // Skip already-pushed files\n const existingCdnUrl = entry.c !== undefined ? cdnUrls[entry.c] : undefined\n if (existingCdnUrl === publicUrl) continue\n\n // Only push local files (no c flag) or remote files (different CDN)\n imagesToPush.push(key)\n }\n\n if (imagesToPush.length === 0) {\n console.log(`No local images to push${prefix ? ` matching \"/${prefix}\"` : ''}.`)\n return\n }\n\n const total = imagesToPush.length\n console.log(`Pushing ${total} image${total !== 1 ? 's' : ''} to CDN...`)\n\n const pushed: string[] = []\n const errors: string[] = []\n const sourceFolders = new Set<string>()\n\n for (let i = 0; i < imagesToPush.length; i++) {\n const imageKey = imagesToPush[i]\n const entry = getMetaEntry(meta, imageKey)\n\n printProgress(i + 1, total, path.basename(imageKey))\n\n if (!entry) {\n errors.push(`Not in meta: ${imageKey}`)\n continue\n }\n\n const existingCdnUrl = entry.c !== undefined ? cdnUrls[entry.c] : undefined\n const isRemote = entry.c !== undefined && existingCdnUrl !== publicUrl\n\n try {\n let originalBuffer: Buffer\n\n if (isRemote && existingCdnUrl) {\n const remoteUrl = `${existingCdnUrl}${imageKey}`\n originalBuffer = await downloadFromRemoteUrl(remoteUrl)\n } else {\n const originalLocalPath = getPublicPath(imageKey)\n try {\n originalBuffer = await fs.readFile(originalLocalPath)\n } catch {\n errors.push(`File not found: ${imageKey}`)\n continue\n }\n }\n\n // Upload original to R2\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: imageKey.replace(/^\\//, ''),\n Body: originalBuffer,\n ContentType: getContentType(imageKey),\n })\n )\n\n // Upload thumbnails (only if processed locally, not for remote imports)\n if (!isRemote && isProcessed(entry)) {\n for (const thumbPath of getAllThumbnailPaths(imageKey)) {\n const localPath = getPublicPath(thumbPath)\n try {\n const fileBuffer = await fs.readFile(localPath)\n await r2.send(\n new PutObjectCommand({\n Bucket: bucketName,\n Key: thumbPath.replace(/^\\//, ''),\n Body: fileBuffer,\n ContentType: getContentType(thumbPath),\n })\n )\n } catch {\n // Thumbnail might not exist\n }\n }\n }\n\n entry.c = cdnIndex\n\n // Delete local files (only for non-remote)\n if (!isRemote) {\n const originalLocalPath = getPublicPath(imageKey)\n sourceFolders.add(path.dirname(originalLocalPath))\n\n // Delete local thumbnails\n for (const thumbPath of getAllThumbnailPaths(imageKey)) {\n const localPath = getPublicPath(thumbPath)\n sourceFolders.add(path.dirname(localPath))\n try {\n await fs.unlink(localPath)\n } catch {\n /* ignore */\n }\n }\n\n // Delete local original\n try {\n await fs.unlink(originalLocalPath)\n } catch {\n /* ignore */\n }\n }\n\n await saveMeta(meta)\n pushed.push(imageKey)\n } catch (error) {\n console.error(`\\nFailed to push ${imageKey}:`, error)\n errors.push(`Failed: ${imageKey}`)\n }\n }\n\n // Clean up empty source folders\n for (const folder of sourceFolders) {\n await deleteEmptyFolders(folder)\n }\n\n // Print summary\n const parts: string[] = []\n parts.push(`${pushed.length} pushed`)\n if (errors.length > 0) parts.push(`${errors.length} failed`)\n\n if (errors.length > 0) {\n printError(`Push complete. ${parts.join(', ')}.`)\n } else {\n printComplete(`Push complete. ${parts.join(', ')}.`)\n }\n}\n","import { promises as fs } from 'fs'\nimport path from 'path'\nimport { printProgress, printComplete, printError } from './index'\nimport {\n loadMeta,\n saveMeta,\n getFileEntries,\n getMetaEntry,\n processImage,\n downloadFromCdn,\n deleteOriginalFromCdn,\n deleteThumbnailsFromCdn,\n} from '../handlers/utils'\nimport { isProcessed } from '../types'\nimport { getPublicPath } from '../config'\n\nexport async function runDownload(args: string[]) {\n const prefix = args[0] || ''\n\n if (prefix) {\n console.log(`Downloading cloud images matching \"/${prefix}\" to local...`)\n } else {\n console.log('Downloading all cloud images to local...')\n }\n\n const meta = await loadMeta()\n\n // Find cloud images to download\n const imagesToDownload: string[] = []\n\n for (const [key, entry] of getFileEntries(meta)) {\n // Apply prefix filter\n if (prefix && !key.startsWith(`/${prefix}`)) continue\n\n // Only download cloud files\n if (entry.c === undefined) continue\n\n imagesToDownload.push(key)\n }\n\n if (imagesToDownload.length === 0) {\n console.log(`No cloud images to download${prefix ? ` matching \"/${prefix}\"` : ''}.`)\n return\n }\n\n const total = imagesToDownload.length\n console.log(`Downloading ${total} image${total !== 1 ? 's' : ''} from CDN...`)\n\n const downloaded: string[] = []\n const errors: string[] = []\n\n for (let i = 0; i < imagesToDownload.length; i++) {\n const imageKey = imagesToDownload[i]\n const entry = getMetaEntry(meta, imageKey)\n\n printProgress(i + 1, total, path.basename(imageKey))\n\n if (!entry || entry.c === undefined) {\n errors.push(`Not on cloud: ${imageKey}`)\n continue\n }\n\n try {\n // Download original from R2\n const imageBuffer = await downloadFromCdn(imageKey)\n\n // Ensure directory exists\n const localPath = getPublicPath(imageKey.replace(/^\\//, ''))\n await fs.mkdir(path.dirname(localPath), { recursive: true })\n\n // Write to local filesystem\n await fs.writeFile(localPath, imageBuffer)\n\n // Delete original and thumbnails from R2\n await deleteOriginalFromCdn(imageKey)\n await deleteThumbnailsFromCdn(imageKey)\n\n // Check if image was processed\n const wasProcessed = isProcessed(entry)\n\n // Remove the c property (no longer on CDN)\n delete entry.c\n\n // If it was processed, regenerate thumbnails locally\n if (wasProcessed) {\n const processedEntry = await processImage(imageBuffer, imageKey)\n entry.sm = processedEntry.sm\n entry.md = processedEntry.md\n entry.lg = processedEntry.lg\n entry.f = processedEntry.f\n }\n\n // Save meta after each successful download\n await saveMeta(meta)\n\n downloaded.push(imageKey)\n } catch (error) {\n console.error(`\\nFailed to download ${imageKey}:`, error)\n errors.push(imageKey)\n }\n }\n\n await saveMeta(meta)\n\n // Print summary\n const parts: string[] = []\n parts.push(`${downloaded.length} downloaded`)\n if (errors.length > 0) parts.push(`${errors.length} failed`)\n\n if (errors.length > 0) {\n printError(`Download complete. ${parts.join(', ')}.`)\n } else {\n printComplete(`Download complete. ${parts.join(', ')}.`)\n }\n}\n","/**\n * Utility functions to replace NextResponse with standard Response\n * This allows handlers to work without Next.js dependency\n */\n\n/**\n * Create a JSON response (mimics NextResponse.json)\n */\nexport function jsonResponse<T>(\n data: T,\n init?: { status?: number; headers?: Record<string, string> }\n): Response {\n const headers = new Headers({\n 'Content-Type': 'application/json',\n ...init?.headers,\n })\n\n return new Response(JSON.stringify(data), {\n status: init?.status ?? 200,\n headers,\n })\n}\n\n/**\n * Create a streaming response for Server-Sent Events\n */\nexport function streamResponse(\n stream: ReadableStream,\n init?: { headers?: Record<string, string> }\n): Response {\n const headers = new Headers({\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n ...init?.headers,\n })\n\n return new Response(stream, {\n status: 200,\n headers,\n })\n}\n\n/**\n * Create an SSE encoder for streaming responses\n */\nexport function createSSEStream() {\n const encoder = new TextEncoder()\n let controller: ReadableStreamDefaultController<Uint8Array> | null = null\n\n const stream = new ReadableStream<Uint8Array>({\n start(c) {\n controller = c\n },\n })\n\n return {\n stream,\n send(event: string, data: unknown) {\n if (controller) {\n const message = `event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`\n controller.enqueue(encoder.encode(message))\n }\n },\n close() {\n if (controller) {\n controller.close()\n }\n },\n }\n}\n","/**\n * Check if an error is an ENOENT (file/directory not found) error\n */\nexport function isFileNotFound(err: unknown): boolean {\n return err !== null && typeof err === 'object' && 'code' in err && err.code === 'ENOENT'\n}\n","// Global cancellation tokens for streaming operations\nconst cancelledOperations = new Set<string>()\n\nexport function cancelOperation(operationId: string) {\n cancelledOperations.add(operationId)\n // Clean up after 60 seconds\n setTimeout(() => cancelledOperations.delete(operationId), 60000)\n}\n\nexport function isOperationCancelled(operationId: string): boolean {\n return cancelledOperations.has(operationId)\n}\n\nexport function clearCancelledOperation(operationId: string) {\n cancelledOperations.delete(operationId)\n}\n","import { promises as fs } from 'fs'\nimport path from 'path'\nimport { getWorkspacePath } from '../config'\nimport { jsonResponse } from './utils/response'\nimport { slugifyFolderName } from './utils/files'\nimport { isFileNotFound } from './utils/errors'\nimport { isOperationCancelled, clearCancelledOperation } from './utils/cancellation'\nimport type { FileItem } from '../types'\n\n// Weight name to number mapping (including common aliases)\nexport const weightMap: Record<string, string> = {\n thin: '100',\n hairline: '100',\n extralight: '200',\n ultralight: '200',\n light: '300',\n regular: '400',\n book: '400',\n medium: '500',\n semibold: '600',\n demibold: '600',\n bold: '700',\n extrabold: '800',\n ultrabold: '800',\n black: '900',\n heavy: '900',\n}\n\n/**\n * Parse font weight and style from filename\n */\nexport function parseFontMetadata(filename: string): { weight: string; style: string; isVariable: boolean } {\n const name = filename.toLowerCase()\n\n let weight = '400'\n let style = 'normal'\n const isVariable = name.includes('variable')\n\n if (isVariable) {\n weight = '100 900'\n } else {\n // Check for numeric weight in filename (e.g. font-500.woff2, font-w500.woff2)\n const numericMatch = name.match(/[-_]w?(\\d{3})(?:[-_.]|$)/)\n if (numericMatch) {\n const num = parseInt(numericMatch[1])\n if (num >= 100 && num <= 900 && num % 100 === 0) {\n weight = String(num)\n }\n } else {\n // Order matters - check longer strings first\n if (name.includes('ultralight')) weight = weightMap.ultralight\n else if (name.includes('extralight')) weight = weightMap.extralight\n else if (name.includes('ultrabold')) weight = weightMap.ultrabold\n else if (name.includes('extrabold')) weight = weightMap.extrabold\n else if (name.includes('demibold')) weight = weightMap.demibold\n else if (name.includes('semibold')) weight = weightMap.semibold\n else if (name.includes('hairline')) weight = weightMap.hairline\n else if (name.includes('thin')) weight = weightMap.thin\n else if (name.includes('light')) weight = weightMap.light\n else if (name.includes('heavy')) weight = weightMap.heavy\n else if (name.includes('black')) weight = weightMap.black\n else if (name.includes('bold')) weight = weightMap.bold\n else if (name.includes('medium')) weight = weightMap.medium\n else if (name.includes('book')) weight = weightMap.book\n else if (name.includes('regular')) weight = weightMap.regular\n }\n }\n\n if (name.includes('italic')) style = 'italic'\n\n return { weight, style, isVariable }\n}\n\n/**\n * Get human-readable weight name\n */\nexport function getWeightName(weight: string): string {\n if (weight === '100 900') return 'Variable'\n const names: Record<string, string> = {\n '100': 'Thin',\n '200': 'ExtraLight',\n '300': 'Light',\n '400': 'Regular',\n '500': 'Medium',\n '600': 'SemiBold',\n '700': 'Bold',\n '800': 'ExtraBold',\n '900': 'Black',\n }\n return names[weight] || weight\n}\n\n// ---- Internal helpers ----\n\n/** Get all files in a directory recursively */\nasync function getFilesInDir(dirPath: string, basePath: string): Promise<string[]> {\n const files: string[] = []\n try {\n const entries = await fs.readdir(dirPath, { withFileTypes: true })\n for (const entry of entries) {\n const fullPath = path.join(dirPath, entry.name)\n const relativePath = path.join(basePath, entry.name)\n if (entry.isDirectory()) {\n files.push(...await getFilesInDir(fullPath, relativePath))\n } else {\n files.push(relativePath)\n }\n }\n } catch (err) {\n if (!isFileNotFound(err)) console.error(`Error reading directory ${dirPath}:`, err)\n }\n return files\n}\n\n/** Expand font paths (which may include folders) into individual file paths */\nasync function expandFontPaths(paths: string[]): Promise<{ files: Array<{ path: string; parentFolder?: string }>; folders: string[] }> {\n const files: Array<{ path: string; parentFolder?: string }> = []\n const folders: string[] = []\n\n for (const p of paths) {\n const fullPath = getWorkspacePath(p)\n try {\n const stat = await fs.stat(fullPath)\n if (stat.isDirectory()) {\n folders.push(p)\n const filesInDir = await getFilesInDir(fullPath, p)\n for (const f of filesInDir) {\n files.push({ path: f, parentFolder: p })\n }\n } else {\n files.push({ path: p })\n }\n } catch (err) {\n if (!isFileNotFound(err)) console.error(`Error accessing ${p}:`, err)\n }\n }\n\n return { files, folders }\n}\n\n/** Rename files inside a folder to match new folder name convention */\nasync function renameFolderContents(\n folderFullPath: string,\n oldFolderName: string,\n newFolderName: string,\n onProgress?: (entry: string, newFileName: string, current: number, total: number) => void\n): Promise<number> {\n const entries = await fs.readdir(folderFullPath)\n const filesToRename = entries.filter(entry => {\n const entryLower = entry.toLowerCase()\n return entryLower.startsWith(oldFolderName + '-') || entryLower.startsWith(oldFolderName + '_')\n })\n\n let renamed = 0\n for (const entry of filesToRename) {\n const suffix = entry.substring(oldFolderName.length)\n const newFileName = newFolderName + suffix.toLowerCase()\n\n onProgress?.(entry, newFileName, renamed + 1, filesToRename.length)\n\n const oldFilePath = path.join(folderFullPath, entry)\n const newFilePath = path.join(folderFullPath, newFileName)\n\n await fs.rename(oldFilePath, newFilePath)\n renamed++\n }\n\n return renamed\n}\n\n/** Validate rename inputs, resolve paths, check existence. Returns null on error (response already sent). */\nasync function validateRename(oldPath: string, newName: string): Promise<{\n oldFullPath: string\n newFullPath: string\n newPath: string\n oldFolderName: string\n newFolderName: string\n isDirectory: boolean\n} | null> {\n const oldFullPath = getWorkspacePath(oldPath)\n const parentDir = path.dirname(oldPath)\n const oldFolderName = path.basename(oldPath).toLowerCase()\n const newFolderName = newName.toLowerCase()\n const newPath = `${parentDir}/${newFolderName}`\n const newFullPath = getWorkspacePath(newPath)\n\n let isDirectory = false\n try {\n const stat = await fs.stat(oldFullPath)\n isDirectory = stat.isDirectory()\n } catch {\n return null // caller handles 404\n }\n\n try {\n await fs.stat(newFullPath)\n return null // already exists, caller handles 400\n } catch {\n // Good, it doesn't exist\n }\n\n return { oldFullPath, newFullPath, newPath, oldFolderName, newFolderName, isDirectory }\n}\n\n// ---- Handlers ----\n\n/**\n * List files and folders for fonts paths\n * Works like the regular list handler but only for _fonts/\n */\nexport async function handleFontsList(request: Request): Promise<Response> {\n const searchParams = new URL(request.url).searchParams\n const requestedPath = searchParams.get('path') || '_fonts'\n\n try {\n const items: FileItem[] = []\n\n // Only allow paths within _fonts/\n const isAllowed = requestedPath === '_fonts' || requestedPath.startsWith('_fonts/')\n\n if (!isAllowed) {\n return jsonResponse({ items: [], error: 'Path not allowed' }, { status: 400 })\n }\n\n const fsPath = getWorkspacePath(requestedPath)\n\n // Check if directory exists\n try {\n const stat = await fs.stat(fsPath)\n if (!stat.isDirectory()) {\n return jsonResponse({ items: [] })\n }\n } catch {\n // Directory doesn't exist\n return jsonResponse({ items: [], canCreate: true })\n }\n\n // Read directory contents\n const entries = await fs.readdir(fsPath, { withFileTypes: true })\n\n for (const entry of entries) {\n const itemPath = `${requestedPath}/${entry.name}`\n\n if (entry.isDirectory()) {\n // Count files in folder\n let fileCount = 0\n try {\n const subEntries = await fs.readdir(path.join(fsPath, entry.name))\n fileCount = subEntries.filter(f =>\n f.match(/\\.(ttf|woff2?|otf|ts|tsx|js)$/i)\n ).length\n } catch (err) {\n if (!isFileNotFound(err)) console.error(`Error counting files in ${entry.name}:`, err)\n }\n\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'folder',\n fileCount,\n })\n } else {\n // Only show font files and ts/js files\n const ext = path.extname(entry.name).toLowerCase()\n const allowedExts = ['.ttf', '.woff', '.woff2', '.otf', '.ts', '.tsx', '.js']\n\n if (allowedExts.includes(ext)) {\n // Get file size\n let size = 0\n try {\n const fileStat = await fs.stat(path.join(fsPath, entry.name))\n size = fileStat.size\n } catch (err) {\n if (!isFileNotFound(err)) console.error(`Error reading stat for ${entry.name}:`, err)\n }\n\n items.push({\n name: entry.name,\n path: itemPath,\n type: 'file',\n size,\n })\n }\n }\n }\n\n // Sort: folders first, then alphabetically\n items.sort((a, b) => {\n if (a.type === 'folder' && b.type !== 'folder') return -1\n if (a.type !== 'folder' && b.type === 'folder') return 1\n return a.name.localeCompare(b.name)\n })\n\n return jsonResponse({ items })\n } catch (error) {\n console.error('Error listing fonts:', error)\n return jsonResponse({ error: 'Failed to list fonts' }, { status: 500 })\n }\n}\n\n/**\n * Upload TTF font files\n * Automatically creates a folder based on the filename prefix (before the first \"-\")\n */\nexport async function handleFontsUpload(request: Request): Promise<Response> {\n try {\n const formData = await request.formData()\n const file = formData.get('file') as File | null\n const basePath = formData.get('path') as string || '_fonts'\n\n if (!file) {\n return jsonResponse({ error: 'No file provided' }, { status: 400 })\n }\n\n // Only accept TTF and OTF files\n const ext = file.name.toLowerCase()\n if (!ext.endsWith('.ttf') && !ext.endsWith('.otf')) {\n return jsonResponse({ error: 'Only TTF and OTF files are supported' }, { status: 400 })\n }\n\n // Validate path\n if (!basePath.startsWith('_fonts')) {\n return jsonResponse({ error: 'Can only upload to _fonts/' }, { status: 400 })\n }\n\n const bytes = await file.arrayBuffer()\n const buffer = Buffer.from(bytes)\n\n // Extract folder name from filename (part before the first \"-\")\n const fileName = file.name.toLowerCase()\n const dashIndex = fileName.indexOf('-')\n let rawFolderName: string\n\n if (dashIndex > 0) {\n rawFolderName = fileName.substring(0, dashIndex)\n } else {\n // No dash found, use filename without extension as folder name\n rawFolderName = fileName.replace(/\\.(ttf|otf)$/, '')\n }\n const folderName = slugifyFolderName(rawFolderName)\n\n // Determine target folder - if we're at _fonts root, create subfolder\n // If we're already in a subfolder, use that\n let targetPath: string\n if (basePath === '_fonts') {\n targetPath = `_fonts/${folderName}`\n } else {\n targetPath = basePath\n }\n\n // Ensure directory exists\n const uploadDir = getWorkspacePath(targetPath)\n await fs.mkdir(uploadDir, { recursive: true })\n\n // Save file\n const filePath = path.join(uploadDir, fileName)\n await fs.writeFile(filePath, buffer)\n\n return jsonResponse({\n success: true,\n path: `${targetPath}/${fileName}`,\n folder: targetPath,\n })\n } catch (error) {\n console.error('Error uploading font:', error)\n return jsonResponse({ error: 'Failed to upload font' }, { status: 500 })\n }\n}\n\n/**\n * Create a folder in _fonts\n */\nexport async function handleFontsCreateFolder(request: Request): Promise<Response> {\n try {\n const { path: targetPath, name } = await request.json()\n\n if (!targetPath || !name) {\n return jsonResponse({ error: 'Path and name are required' }, { status: 400 })\n }\n\n // Only allow paths within _fonts/\n const isAllowed = targetPath === '_fonts' || targetPath.startsWith('_fonts/')\n\n if (!isAllowed) {\n return jsonResponse({ error: 'Path not allowed' }, { status: 400 })\n }\n\n const safeName = slugifyFolderName(name)\n const folderPath = getWorkspacePath(targetPath, safeName)\n await fs.mkdir(folderPath, { recursive: true })\n\n return jsonResponse({\n success: true,\n path: `${targetPath}/${safeName}`,\n })\n } catch (error) {\n console.error('Error creating folder:', error)\n return jsonResponse({ error: 'Failed to create folder' }, { status: 500 })\n }\n}\n\n/**\n * Delete files or folders from _fonts\n */\nexport async function handleFontsDelete(request: Request): Promise<Response> {\n try {\n const { paths } = await request.json()\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return jsonResponse({ error: 'Paths are required' }, { status: 400 })\n }\n\n for (const p of paths) {\n if (!p.startsWith('_fonts/')) {\n return jsonResponse({ error: `Path not allowed: ${p}` }, { status: 400 })\n }\n }\n\n const { files, folders } = await expandFontPaths(paths)\n const deleted: string[] = []\n const errors: string[] = []\n\n for (const file of files) {\n try {\n await fs.unlink(getWorkspacePath(file.path))\n deleted.push(file.path)\n } catch (err) {\n errors.push(`Failed to delete ${file.path}: ${err instanceof Error ? err.message : 'Unknown error'}`)\n }\n }\n\n for (const folder of folders) {\n try {\n await fs.rm(getWorkspacePath(folder), { recursive: true })\n } catch (err) {\n if (!isFileNotFound(err)) console.error(`Error removing folder ${folder}:`, err)\n }\n }\n\n return jsonResponse({\n success: true,\n deleted,\n errors: errors.length > 0 ? errors : undefined,\n })\n } catch (error) {\n console.error('Error deleting:', error)\n return jsonResponse({ error: 'Failed to delete' }, { status: 500 })\n }\n}\n\n/**\n * Delete files or folders from _fonts with streaming progress\n */\nexport async function handleFontsDeleteStream(request: Request): Promise<Response> {\n try {\n const { paths, operationId } = await request.json()\n\n if (!paths || !Array.isArray(paths) || paths.length === 0) {\n return jsonResponse({ error: 'Paths are required' }, { status: 400 })\n }\n\n for (const p of paths) {\n if (!p.startsWith('_fonts/')) {\n return jsonResponse({ error: `Path not allowed: ${p}` }, { status: 400 })\n }\n }\n\n const isCancelled = () => operationId ? isOperationCancelled(operationId) : false\n\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n async start(controller) {\n const send = (data: Record<string, unknown>) => {\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\\n\\n`))\n }\n\n const { files: allFiles, folders } = await expandFontPaths(paths)\n const total = allFiles.length\n const deleted: string[] = []\n const errors: string[] = []\n\n send({ type: 'start', total })\n\n for (let i = 0; i < allFiles.length; i++) {\n if (isCancelled()) {\n if (operationId) clearCancelledOperation(operationId)\n send({\n type: 'complete',\n deleted: deleted.length,\n errors: errors.length,\n message: `Stopped. Deleted ${deleted.length} file${deleted.length !== 1 ? 's' : ''}.`,\n cancelled: true,\n })\n controller.close()\n return\n }\n\n const file = allFiles[i]\n const fileName = file.path.split('/').pop() || file.path\n\n send({ type: 'progress', message: `Deleting ${fileName}...`, current: i + 1, total, currentFile: fileName })\n\n try {\n await fs.unlink(getWorkspacePath(file.path))\n deleted.push(file.path)\n } catch (err) {\n errors.push(`Failed to delete ${file.path}: ${err instanceof Error ? err.message : 'Unknown error'}`)\n }\n }\n\n for (const folder of folders) {\n try {\n await fs.rm(getWorkspacePath(folder), { recursive: true })\n } catch (err) {\n if (!isFileNotFound(err)) console.error(`Error removing folder ${folder}:`, err)\n }\n }\n\n if (operationId) clearCancelledOperation(operationId)\n send({\n type: 'complete',\n deleted: deleted.length,\n errors: errors.length,\n message: `Deleted ${deleted.length} file${deleted.length !== 1 ? 's' : ''}`,\n })\n\n controller.close()\n }\n })\n\n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n },\n })\n } catch (error) {\n console.error('Error deleting:', error)\n return jsonResponse({ error: 'Failed to delete' }, { status: 500 })\n }\n}\n\n/**\n * Rename a folder in _fonts\n * Also renames all files inside to match the new folder name convention\n */\nexport async function handleFontsRename(request: Request): Promise<Response> {\n try {\n const { oldPath, newName } = await request.json()\n\n if (!oldPath || !newName) {\n return jsonResponse({ error: 'oldPath and newName are required' }, { status: 400 })\n }\n if (!oldPath.startsWith('_fonts/')) {\n return jsonResponse({ error: 'Can only rename items in _fonts/' }, { status: 400 })\n }\n if (newName.includes('/') || newName.includes('\\\\')) {\n return jsonResponse({ error: 'Invalid folder name' }, { status: 400 })\n }\n\n const result = await validateRename(oldPath, newName)\n if (!result) {\n // Check which error: path not found or already exists\n try {\n await fs.stat(getWorkspacePath(oldPath))\n return jsonResponse({ error: 'A folder with that name already exists' }, { status: 400 })\n } catch {\n return jsonResponse({ error: 'Path not found' }, { status: 404 })\n }\n }\n\n const { oldFullPath, newFullPath, newPath, oldFolderName, newFolderName, isDirectory } = result\n\n await fs.rename(oldFullPath, newFullPath)\n\n if (isDirectory) {\n try {\n await renameFolderContents(newFullPath, oldFolderName, newFolderName)\n } catch (err) {\n console.error('Error renaming files inside folder:', err)\n }\n }\n\n return jsonResponse({ success: true, oldPath, newPath })\n } catch (error) {\n console.error('Error renaming:', error)\n return jsonResponse({ error: 'Failed to rename' }, { status: 500 })\n }\n}\n\n/**\n * Rename a folder in _fonts with streaming progress\n * Also renames all files inside to match the new folder name convention\n */\nexport async function handleFontsRenameStream(request: Request): Promise<Response> {\n try {\n const { oldPath, newName } = await request.json()\n\n if (!oldPath || !newName) {\n return jsonResponse({ error: 'oldPath and newName are required' }, { status: 400 })\n }\n if (!oldPath.startsWith('_fonts/')) {\n return jsonResponse({ error: 'Can only rename items in _fonts/' }, { status: 400 })\n }\n if (newName.includes('/') || newName.includes('\\\\')) {\n return jsonResponse({ error: 'Invalid folder name' }, { status: 400 })\n }\n\n const result = await validateRename(oldPath, newName)\n if (!result) {\n try {\n await fs.stat(getWorkspacePath(oldPath))\n return jsonResponse({ error: 'A folder with that name already exists' }, { status: 400 })\n } catch {\n return jsonResponse({ error: 'Path not found' }, { status: 404 })\n }\n }\n\n const { oldFullPath, newFullPath, newPath, oldFolderName, newFolderName, isDirectory } = result\n\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n async start(controller) {\n const send = (data: Record<string, unknown>) => {\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\\n\\n`))\n }\n\n try {\n send({ type: 'progress', message: `Renaming folder to ${newFolderName}...`, current: 0, total: 1 })\n await fs.rename(oldFullPath, newFullPath)\n\n if (isDirectory) {\n await renameFolderContents(newFullPath, oldFolderName, newFolderName, (entry, newFileName, current, total) => {\n send({ type: 'progress', message: `Renaming ${entry} → ${newFileName}...`, current, total })\n })\n }\n\n send({ type: 'complete', message: `Renamed to ${newFolderName}`, renamed: 1, oldPath, newPath })\n } catch (err) {\n send({ type: 'error', message: String(err) })\n }\n\n controller.close()\n }\n })\n\n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n },\n })\n } catch (error) {\n console.error('Error renaming:', error)\n return jsonResponse({ error: 'Failed to rename' }, { status: 500 })\n }\n}\n\n/**\n * Scan a font folder to detect TTF/woff2 files and parse metadata\n */\nexport async function handleFontsScan(request: Request): Promise<Response> {\n try {\n const { folder } = await request.json()\n \n if (!folder || !folder.startsWith('_fonts/')) {\n return jsonResponse({ error: 'Invalid folder path' }, { status: 400 })\n }\n \n const folderPath = getWorkspacePath(folder)\n const folderName = path.basename(folder)\n \n // Check folder exists\n try {\n const stat = await fs.stat(folderPath)\n if (!stat.isDirectory()) {\n return jsonResponse({ error: 'Path is not a folder' }, { status: 400 })\n }\n } catch {\n return jsonResponse({ error: 'Folder not found' }, { status: 404 })\n }\n \n const entries = await fs.readdir(folderPath)\n \n const ttfFiles: string[] = []\n const woff2Files: string[] = []\n const detectedFonts: Array<{ file: string; weight: string; weightName: string; style: string }> = []\n \n const otfFiles: string[] = []\n\n for (const entry of entries) {\n const ext = path.extname(entry).toLowerCase()\n if (ext === '.ttf') {\n ttfFiles.push(entry)\n } else if (ext === '.otf') {\n otfFiles.push(entry)\n } else if (ext === '.woff2') {\n woff2Files.push(entry)\n const baseName = path.basename(entry, '.woff2')\n const { weight, style } = parseFontMetadata(baseName)\n detectedFonts.push({\n file: entry,\n weight,\n weightName: getWeightName(weight),\n style,\n })\n }\n }\n\n // Check if woff2 generation is needed (from TTF or OTF sources)\n const needsGeneration = (ttfFiles.length > 0 || otfFiles.length > 0) && woff2Files.length === 0\n \n // Check which src/fonts/*.ts files reference this folder\n const srcFontsPath = getWorkspacePath('src/fonts')\n const assignments: string[] = []\n \n try {\n const srcEntries = await fs.readdir(srcFontsPath)\n for (const entry of srcEntries) {\n if (entry.endsWith('.ts')) {\n const filePath = path.join(srcFontsPath, entry)\n const content = await fs.readFile(filePath, 'utf8')\n // Check if this file references the folder\n if (content.includes(`/_fonts/${folderName}/`)) {\n assignments.push(path.basename(entry, '.ts'))\n }\n }\n }\n } catch (err) {\n if (!isFileNotFound(err)) console.error('Error scanning src/fonts:', err)\n }\n\n return jsonResponse({\n folder,\n folderName,\n ttfFiles,\n otfFiles,\n woff2Files,\n detectedFonts,\n needsGeneration,\n assignments,\n })\n } catch (error) {\n console.error('Error scanning fonts:', error)\n return jsonResponse({ error: 'Failed to scan fonts' }, { status: 500 })\n }\n}\n\n/**\n * List all font assignments in src/fonts/\n */\nexport async function handleFontsListAssignments(): Promise<Response> {\n try {\n const srcFontsPath = getWorkspacePath('src/fonts')\n const assignments: Array<{ name: string; folder: string }> = []\n \n try {\n const entries = await fs.readdir(srcFontsPath)\n \n for (const entry of entries) {\n if (entry.endsWith('.ts')) {\n const name = path.basename(entry, '.ts')\n const filePath = path.join(srcFontsPath, entry)\n const content = await fs.readFile(filePath, 'utf8')\n \n // Extract folder name from path like '../../_fonts/inter/'\n const match = content.match(/\\/_fonts\\/([^/]+)\\//)\n const folder = match ? match[1] : 'unknown'\n \n assignments.push({ name, folder })\n }\n }\n } catch (err) {\n if (!isFileNotFound(err)) console.error('Error listing font assignments:', err)\n }\n\n return jsonResponse({ assignments })\n } catch (error) {\n console.error('Error listing assignments:', error)\n return jsonResponse({ error: 'Failed to list assignments' }, { status: 500 })\n }\n}\n\n/**\n * Delete a font assignment file\n */\nexport async function handleFontsDeleteAssignment(request: Request): Promise<Response> {\n try {\n const { name } = await request.json()\n \n if (!name || typeof name !== 'string') {\n return jsonResponse({ error: 'Assignment name is required' }, { status: 400 })\n }\n \n // Validate name (no path traversal)\n if (name.includes('/') || name.includes('\\\\') || name.includes('..')) {\n return jsonResponse({ error: 'Invalid assignment name' }, { status: 400 })\n }\n \n const filePath = getWorkspacePath('src/fonts', `${name}.ts`)\n \n try {\n await fs.unlink(filePath)\n return jsonResponse({ success: true })\n } catch {\n return jsonResponse({ error: 'Assignment not found' }, { status: 404 })\n }\n } catch (error) {\n console.error('Error deleting assignment:', error)\n return jsonResponse({ error: 'Failed to delete assignment' }, { status: 500 })\n }\n}\n\n/**\n * Assign web fonts - generates woff2 if needed and writes src/fonts/*.ts files\n * This is a streaming endpoint for progress updates\n * \n * Supports two modes:\n * - folder: path to a folder in _fonts/ (scans for woff2/ttf, generates if needed)\n * - files: array of woff2 file paths (uses these directly, no generation)\n */\nexport async function handleFontsAssign(request: Request): Promise<Response> {\n try {\n const { folder, files, assignments, operationId } = await request.json()\n\n // Validate: need either folder or files\n const isFileMode = files && Array.isArray(files) && files.length > 0\n const isFolderMode = folder && folder.startsWith('_fonts/')\n \n if (!isFileMode && !isFolderMode) {\n return jsonResponse({ error: 'Either folder or files must be provided' }, { status: 400 })\n }\n \n if (!assignments || !Array.isArray(assignments) || assignments.length === 0) {\n return jsonResponse({ error: 'At least one assignment is required' }, { status: 400 })\n }\n \n // Validate assignment names\n for (const name of assignments) {\n if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(name)) {\n return jsonResponse({ error: `Invalid assignment name: ${name}` }, { status: 400 })\n }\n }\n \n // Validate file paths if in file mode\n if (isFileMode) {\n for (const filePath of files) {\n if (!filePath.startsWith('_fonts/') || !filePath.toLowerCase().endsWith('.woff2')) {\n return jsonResponse({ error: `Invalid file path: ${filePath}. Must be a woff2 file in _fonts/` }, { status: 400 })\n }\n }\n }\n \n // Set up SSE streaming\n const encoder = new TextEncoder()\n const stream = new ReadableStream({\n async start(controller) {\n const send = (data: Record<string, unknown>) => {\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(data)}\\n\\n`))\n }\n \n const isCancelled = () => operationId ? isOperationCancelled(operationId) : false\n\n try {\n let fontMap: Array<{ path: string; weight: string; style: string }>\n\n if (isFileMode) {\n // File mode: use provided woff2 files directly\n send({ type: 'progress', message: `Processing ${files.length} selected file${files.length > 1 ? 's' : ''}...`, current: 0, total: files.length })\n\n fontMap = files.map((filePath: string) => {\n const relativePath = filePath.replace(/^_fonts\\//, '')\n const baseName = path.basename(filePath, '.woff2')\n const { weight, style } = parseFontMetadata(baseName)\n return { path: relativePath, weight, style }\n })\n } else {\n // Folder mode: scan folder and generate woff2 if needed\n const folderPath = getWorkspacePath(folder)\n const folderName = path.basename(folder)\n\n const entries = await fs.readdir(folderPath)\n const sourceFiles = entries.filter(f => {\n const lower = f.toLowerCase()\n return lower.endsWith('.ttf') || lower.endsWith('.otf')\n })\n let woff2Files = entries.filter(f => f.toLowerCase().endsWith('.woff2'))\n\n if (sourceFiles.length === 0 && woff2Files.length === 0) {\n send({ type: 'error', message: 'No font files found in folder' })\n controller.close()\n return\n }\n\n // Generate woff2 if needed\n if (woff2Files.length === 0 && sourceFiles.length > 0) {\n send({ type: 'progress', message: 'Generating woff2 files...', current: 0, total: sourceFiles.length })\n\n const ttf2woff2Module = await import('ttf2woff2')\n const ttf2woff2 = ttf2woff2Module.default\n\n for (let i = 0; i < sourceFiles.length; i++) {\n if (isCancelled()) {\n if (operationId) clearCancelledOperation(operationId)\n send({ type: 'complete', processed: 0, message: 'Stopped.', cancelled: true })\n controller.close()\n return\n }\n\n const sourceFile = sourceFiles[i]\n const sourceExt = path.extname(sourceFile)\n const baseName = path.basename(sourceFile, sourceExt)\n const woff2Name = baseName + '.woff2'\n\n send({ type: 'progress', message: `Compressing ${sourceFile}...`, current: i + 1, total: sourceFiles.length, currentFile: sourceFile })\n\n try {\n const sourcePath = path.join(folderPath, sourceFile)\n const input = await fs.readFile(sourcePath)\n const woff2Data = ttf2woff2(input)\n await fs.writeFile(path.join(folderPath, woff2Name), woff2Data)\n woff2Files.push(woff2Name)\n } catch (err) {\n send({ type: 'progress', message: `Failed to compress ${sourceFile}`, error: String(err) })\n }\n }\n }\n\n if (woff2Files.length === 0) {\n send({ type: 'error', message: 'No woff2 files available' })\n controller.close()\n return\n }\n\n fontMap = woff2Files.map(file => {\n const baseName = path.basename(file, '.woff2')\n const { weight, style } = parseFontMetadata(baseName)\n return { path: `${folderName}/${file}`, weight, style }\n })\n }\n\n // Sort fontMap by weight (ascending), then style (normal before italic)\n fontMap.sort((a, b) => {\n const wa = parseInt(a.weight) || 400\n const wb = parseInt(b.weight) || 400\n if (wa !== wb) return wa - wb\n return a.style === 'normal' ? -1 : 1\n })\n\n // Write assignment files\n const srcFontsPath = getWorkspacePath('src/fonts')\n await fs.mkdir(srcFontsPath, { recursive: true })\n\n const created: string[] = []\n const errors: string[] = []\n\n for (let i = 0; i < assignments.length; i++) {\n const assignmentName = assignments[i]\n const fileName = `${assignmentName}.ts`\n const filePath = path.join(srcFontsPath, fileName)\n\n // Check if file already exists (for overwrite info)\n let overwritten = false\n try {\n await fs.stat(filePath)\n overwritten = true\n } catch {\n // File doesn't exist, that's fine\n }\n\n const action = overwritten ? 'Overwriting' : 'Writing'\n send({ type: 'progress', message: `${action} ${fileName}...`, current: i + 1, total: assignments.length, currentFile: fileName })\n\n try {\n const variableName = `${assignmentName}Font`\n\n const srcArray = fontMap\n .map(font => ` { path: '../../_fonts/${font.path}', weight: '${font.weight}', style: '${font.style}' },`)\n .join('\\n')\n\n const template = `import localFont from 'next/font/local'\n\nexport const ${variableName} = localFont({\n src: [\n${srcArray}\n ],\n display: 'swap',\n})\n`\n\n await fs.writeFile(filePath, template, 'utf8')\n created.push(assignmentName)\n } catch (err) {\n errors.push(`Failed to write ${fileName}: ${err}`)\n }\n }\n\n if (operationId) clearCancelledOperation(operationId)\n send({\n type: 'complete',\n message: `Created ${created.length} font assignment${created.length !== 1 ? 's' : ''}`,\n processed: created.length,\n errors: errors.length,\n })\n } catch (err) {\n send({ type: 'error', message: String(err) })\n }\n \n controller.close()\n }\n })\n \n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n },\n })\n } catch (error) {\n console.error('Error assigning fonts:', error)\n return jsonResponse({ error: 'Failed to assign fonts' }, { status: 500 })\n }\n}\n","import { promises as fs } from 'fs'\nimport path from 'path'\nimport { printProgress, printComplete, printError } from './index'\nimport { parseFontMetadata } from '../handlers/fonts'\nimport { getWorkspacePath } from '../config'\n\nexport async function runFonts(args: string[]) {\n const subcommand = args[0]\n\n if (!subcommand) {\n console.error('Usage:')\n console.error(' studio fonts woff2 <folder> Convert TTF/OTF to woff2')\n console.error(' studio fonts assign <folder> --name <name> Generate src/fonts/<name>.ts')\n process.exit(1)\n }\n\n switch (subcommand) {\n case 'woff2':\n await runFontsWoff2(args.slice(1))\n break\n case 'assign':\n await runFontsAssign(args.slice(1))\n break\n default:\n console.error(`Unknown fonts subcommand: ${subcommand}`)\n console.error('Available: woff2, assign')\n process.exit(1)\n }\n}\n\nasync function runFontsWoff2(args: string[]) {\n const folder = args[0]\n\n if (!folder) {\n console.error('Usage: studio fonts woff2 <folder>')\n console.error(' <folder> is the folder name inside _fonts/')\n process.exit(1)\n }\n\n const folderPath = getWorkspacePath('_fonts', folder)\n\n // Check folder exists\n try {\n const stat = await fs.stat(folderPath)\n if (!stat.isDirectory()) {\n printError(`Not a directory: _fonts/${folder}`)\n process.exit(1)\n }\n } catch {\n printError(`Folder not found: _fonts/${folder}`)\n process.exit(1)\n }\n\n const entries = await fs.readdir(folderPath)\n const sourceFiles = entries.filter(f => {\n const lower = f.toLowerCase()\n return lower.endsWith('.ttf') || lower.endsWith('.otf')\n })\n\n if (sourceFiles.length === 0) {\n console.log(`No TTF/OTF files found in _fonts/${folder}/`)\n return\n }\n\n console.log(`Converting ${sourceFiles.length} font file${sourceFiles.length !== 1 ? 's' : ''} to woff2...`)\n\n const ttf2woff2Module = await import('ttf2woff2')\n const ttf2woff2 = ttf2woff2Module.default\n\n const converted: string[] = []\n const errors: string[] = []\n\n for (let i = 0; i < sourceFiles.length; i++) {\n const sourceFile = sourceFiles[i]\n const sourceExt = path.extname(sourceFile)\n const baseName = path.basename(sourceFile, sourceExt)\n const woff2Name = baseName + '.woff2'\n\n printProgress(i + 1, sourceFiles.length, sourceFile)\n\n try {\n const sourcePath = path.join(folderPath, sourceFile)\n const input = await fs.readFile(sourcePath)\n const woff2Data = ttf2woff2(input)\n await fs.writeFile(path.join(folderPath, woff2Name), woff2Data)\n converted.push(woff2Name)\n } catch (error) {\n console.error(`\\nFailed to convert ${sourceFile}:`, error)\n errors.push(sourceFile)\n }\n }\n\n if (errors.length > 0) {\n printError(`Converted ${converted.length} files, ${errors.length} failed.`)\n } else {\n printComplete(`Converted ${converted.length} file${converted.length !== 1 ? 's' : ''} to woff2.`)\n }\n}\n\nasync function runFontsAssign(args: string[]) {\n const folder = args[0]\n\n // Parse --name flag\n let name = ''\n for (let i = 0; i < args.length; i++) {\n if (args[i] === '--name' && args[i + 1]) {\n name = args[i + 1]\n break\n }\n }\n\n if (!folder || !name) {\n console.error('Usage: studio fonts assign <folder> --name <name>')\n console.error(' <folder> is the folder name inside _fonts/')\n console.error(' <name> is the variable name for src/fonts/<name>.ts')\n process.exit(1)\n }\n\n // Validate name\n if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(name)) {\n printError(`Invalid assignment name: ${name}. Must start with a letter and contain only letters/numbers.`)\n process.exit(1)\n }\n\n const folderPath = getWorkspacePath('_fonts', folder)\n\n // Check folder exists\n try {\n const stat = await fs.stat(folderPath)\n if (!stat.isDirectory()) {\n printError(`Not a directory: _fonts/${folder}`)\n process.exit(1)\n }\n } catch {\n printError(`Folder not found: _fonts/${folder}`)\n process.exit(1)\n }\n\n const entries = await fs.readdir(folderPath)\n const woff2Files = entries.filter(f => f.toLowerCase().endsWith('.woff2'))\n\n if (woff2Files.length === 0) {\n printError(`No woff2 files found in _fonts/${folder}/. Run 'studio fonts woff2 ${folder}' first.`)\n process.exit(1)\n }\n\n console.log(`Generating font assignment from ${woff2Files.length} woff2 file${woff2Files.length !== 1 ? 's' : ''}...`)\n\n // Parse font metadata from filenames\n const fontMap = woff2Files.map(file => {\n const baseName = path.basename(file, '.woff2')\n const { weight, style } = parseFontMetadata(baseName)\n return { path: `${folder}/${file}`, weight, style }\n })\n\n // Sort by weight, then style\n fontMap.sort((a, b) => {\n const wa = parseInt(a.weight) || 400\n const wb = parseInt(b.weight) || 400\n if (wa !== wb) return wa - wb\n return a.style === 'normal' ? -1 : 1\n })\n\n // Generate the template\n const variableName = `${name}Font`\n const srcArray = fontMap\n .map(font => ` { path: '../../_fonts/${font.path}', weight: '${font.weight}', style: '${font.style}' },`)\n .join('\\n')\n\n const template = `import localFont from 'next/font/local'\n\nexport const ${variableName} = localFont({\n src: [\n${srcArray}\n ],\n display: 'swap',\n})\n`\n\n // Write the file\n const srcFontsPath = getWorkspacePath('src/fonts')\n await fs.mkdir(srcFontsPath, { recursive: true })\n\n const filePath = path.join(srcFontsPath, `${name}.ts`)\n\n // Check if file already exists\n let overwritten = false\n try {\n await fs.stat(filePath)\n overwritten = true\n } catch {\n // File doesn't exist\n }\n\n await fs.writeFile(filePath, template, 'utf8')\n\n if (overwritten) {\n printComplete(`Overwrote src/fonts/${name}.ts with ${woff2Files.length} font source${woff2Files.length !== 1 ? 's' : ''}.`)\n } else {\n printComplete(`Created src/fonts/${name}.ts with ${woff2Files.length} font source${woff2Files.length !== 1 ? 's' : ''}.`)\n }\n\n // Print detected weights\n for (const font of fontMap) {\n const weightName = getWeightNameLocal(font.weight)\n console.log(` ${font.path} -> ${weightName} ${font.style}`)\n }\n}\n\nfunction getWeightNameLocal(weight: string): string {\n if (weight === '100 900') return 'Variable'\n const names: Record<string, string> = {\n '100': 'Thin',\n '200': 'ExtraLight',\n '300': 'Light',\n '400': 'Regular',\n '500': 'Medium',\n '600': 'SemiBold',\n '700': 'Bold',\n '800': 'ExtraBold',\n '900': 'Black',\n }\n return names[weight] || weight\n}\n","import { config as loadEnv } from 'dotenv'\nimport { join } from 'path'\nimport { existsSync } from 'fs'\n\nexport function printProgress(current: number, total: number, message: string) {\n const pct = Math.round((current / total) * 100)\n process.stdout.write(`\\r [${current}/${total}] ${pct}% ${message}`)\n}\n\nexport function printComplete(message: string) {\n process.stdout.write('\\n')\n console.log(`\\u2713 ${message}`)\n}\n\nexport function printError(message: string) {\n process.stderr.write('\\n')\n console.error(`\\u2717 ${message}`)\n}\n\nexport async function run(command: string, workspace: string, args: string[]) {\n process.env.STUDIO_WORKSPACE = workspace\n\n const envPath = join(workspace, '.env.local')\n if (existsSync(envPath)) {\n loadEnv({ path: envPath })\n }\n\n try {\n switch (command) {\n case 'scan': {\n const { runScan } = await import('./scan')\n await runScan(args)\n break\n }\n case 'process': {\n const { runProcess } = await import('./process')\n await runProcess(args)\n break\n }\n case 'push': {\n const { runPush } = await import('./push')\n await runPush(args)\n break\n }\n case 'download': {\n const { runDownload } = await import('./download')\n await runDownload(args)\n break\n }\n case 'fonts': {\n const { runFonts } = await import('./fonts')\n await runFonts(args)\n break\n }\n default:\n console.error(`Unknown command: ${command}`)\n console.error('Available commands: scan, process, push, download, fonts')\n process.exit(1)\n }\n } catch (error) {\n console.error('Command failed:', error instanceof Error ? error.message : error)\n process.exit(1)\n }\n}\n"],"mappings":"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","names":["path","path","fs","path","ext","base","outputExt","fs","path","sharp","localPath","fs","path","printError","fs","path","fs","path","S3Client","PutObjectCommand","printError","fs","path","printError","fs","path","printError","init_fonts","printError","runScan","runProcess","runPush","runDownload","runFonts"]}