A CLI for publishing standard.site documents to ATProto

feat: add blob images

+26 -37
+26 -37
packages/cli/src/commands/publish-lite.ts
··· 36 ) 37 } 38 39 - async function resolveLocalImagePath( 40 src: string, 41 postFilePath: string, 42 contentDir: string, 43 imagesDir?: string, 44 - ): Promise<string | null> { 45 const candidates = [ 46 path.resolve(path.dirname(postFilePath), src), 47 path.resolve(contentDir, src), 48 ] 49 if (imagesDir) { 50 candidates.push(path.resolve(imagesDir, src)) 51 - // Try stripping a leading directory that matches imagesDir basename 52 const baseName = path.basename(imagesDir) 53 const idx = src.indexOf(baseName) 54 if (idx !== -1) { ··· 56 candidates.push(path.resolve(imagesDir, after)) 57 } 58 } 59 - 60 - for (const candidate of candidates) { 61 - try { 62 - const stat = await fs.stat(candidate) 63 - if (stat.isFile() && stat.size > 0) return candidate 64 - } catch {} 65 - } 66 - return null 67 } 68 69 async function uploadBlob( 70 agent: Agent, 71 - filePath: string, 72 ): Promise<BlobObject | undefined> { 73 - if (!(await fileExists(filePath))) return undefined 74 75 - try { 76 - const imageBuffer = await fs.readFile(filePath) 77 - const mimeType = mimeTypes.lookup(filePath) || "application/octet-stream" 78 - const response = await agent.com.atproto.repo.uploadBlob( 79 - new Uint8Array(imageBuffer), 80 - { encoding: mimeType }, 81 - ) 82 - return { 83 - $type: "blob", 84 - ref: { $link: response.data.blob.ref.toString() }, 85 - mimeType, 86 - size: imageBuffer.byteLength, 87 - } 88 - } catch (error) { 89 - console.error(`Error uploading blob ${filePath}:`, error) 90 - return undefined 91 } 92 } 93 94 async function processImages( ··· 111 const src = match[2]! 112 if (!isLocalPath(src)) continue 113 114 - const resolved = await resolveLocalImagePath( 115 - src, postFilePath, contentDir, imagesDir, 116 - ) 117 - if (!resolved) continue 118 - 119 - let blob = uploadCache.get(resolved) 120 if (!blob) { 121 - blob = await uploadBlob(agent, resolved) 122 if (!blob) continue 123 - uploadCache.set(resolved, blob) 124 } 125 126 images.push({ image: blob, alt: alt || undefined })
··· 36 ) 37 } 38 39 + function getImageCandidates( 40 src: string, 41 postFilePath: string, 42 contentDir: string, 43 imagesDir?: string, 44 + ): string[] { 45 const candidates = [ 46 path.resolve(path.dirname(postFilePath), src), 47 path.resolve(contentDir, src), 48 ] 49 if (imagesDir) { 50 candidates.push(path.resolve(imagesDir, src)) 51 const baseName = path.basename(imagesDir) 52 const idx = src.indexOf(baseName) 53 if (idx !== -1) { ··· 55 candidates.push(path.resolve(imagesDir, after)) 56 } 57 } 58 + return candidates 59 } 60 61 async function uploadBlob( 62 agent: Agent, 63 + candidates: string[], 64 ): Promise<BlobObject | undefined> { 65 + for (const filePath of candidates) { 66 + if (!(await fileExists(filePath))) continue 67 68 + try { 69 + const imageBuffer = await fs.readFile(filePath) 70 + if (imageBuffer.byteLength === 0) continue 71 + const mimeType = mimeTypes.lookup(filePath) || "application/octet-stream" 72 + const response = await agent.com.atproto.repo.uploadBlob( 73 + new Uint8Array(imageBuffer), 74 + { encoding: mimeType }, 75 + ) 76 + return { 77 + $type: "blob", 78 + ref: { $link: response.data.blob.ref.toString() }, 79 + mimeType, 80 + size: imageBuffer.byteLength, 81 + } 82 + } catch {} 83 } 84 + return undefined 85 } 86 87 async function processImages( ··· 104 const src = match[2]! 105 if (!isLocalPath(src)) continue 106 107 + let blob = uploadCache.get(src) 108 if (!blob) { 109 + const candidates = getImageCandidates(src, postFilePath, contentDir, imagesDir) 110 + blob = await uploadBlob(agent, candidates) 111 if (!blob) continue 112 + uploadCache.set(src, blob) 113 } 114 115 images.push({ image: blob, alt: alt || undefined })