| 2 |
2 |
|
import { command, flag } from "cmd-ts"; |
| 3 |
3 |
|
import { select, spinner, log } from "@clack/prompts"; |
| 4 |
4 |
|
import * as path from "node:path"; |
| 5 |
|
- |
import { loadConfig, loadState, saveState, findConfig } from "../lib/config"; |
|
5 |
+ |
import { CONFIG_FILENAME, loadConfig, loadState, saveState, findConfig } from "../lib/config"; |
| 6 |
6 |
|
import { |
| 7 |
7 |
|
loadCredentials, |
| 8 |
8 |
|
listAllCredentials, |
|
| 17 |
17 |
|
resolveImagePath, |
| 18 |
18 |
|
createBlueskyPost, |
| 19 |
19 |
|
addBskyPostRefToDocument, |
|
20 |
+ |
COVER_IMAGE_MAX_SIZE, |
| 20 |
21 |
|
} from "../lib/atproto"; |
| 21 |
22 |
|
import { |
| 22 |
23 |
|
scanContentDirectory, |
|
| 52 |
53 |
|
// Load config |
| 53 |
54 |
|
const configPath = await findConfig(); |
| 54 |
55 |
|
if (!configPath) { |
| 55 |
|
- |
log.error("No publisher.config.ts found. Run 'publisher init' first."); |
|
56 |
+ |
log.error(`No ${CONFIG_FILENAME} found. Run 'sequoia init' first.`); |
| 56 |
57 |
|
process.exit(1); |
| 57 |
58 |
|
} |
| 58 |
59 |
|
|
|
| 261 |
262 |
|
const cutoffDate = new Date(); |
| 262 |
263 |
|
cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays); |
| 263 |
264 |
|
|
|
265 |
+ |
let isValid = true; |
| 264 |
266 |
|
for (const { post, action, reason } of postsToPublish) { |
| 265 |
267 |
|
const icon = action === "create" ? "+" : "~"; |
| 266 |
268 |
|
const relativeFilePath = path.relative(configDir, post.filePath); |
| 267 |
269 |
|
const existingBskyPostRef = state.posts[relativeFilePath]?.bskyPostRef; |
|
270 |
+ |
|
|
271 |
+ |
if (post.frontmatter.ogImage) { |
|
272 |
+ |
post.coverImagePath = await resolveImagePath( |
|
273 |
+ |
post.frontmatter.ogImage, |
|
274 |
+ |
imagesDir, |
|
275 |
+ |
contentDir, |
|
276 |
+ |
); |
|
277 |
+ |
} |
| 268 |
278 |
|
|
| 269 |
279 |
|
let bskyNote = ""; |
| 270 |
280 |
|
if (blueskyEnabled) { |
|
| 292 |
302 |
|
log.message( |
| 293 |
303 |
|
` ${icon} ${post.frontmatter.title} (${reason})${bskyNote}${postUrl}`, |
| 294 |
304 |
|
); |
|
305 |
+ |
|
|
306 |
+ |
const postValid = await validatePost(post); |
|
307 |
+ |
isValid &&= postValid; |
|
308 |
+ |
} |
|
309 |
+ |
|
|
310 |
+ |
if (!isValid) { |
|
311 |
+ |
return; |
| 295 |
312 |
|
} |
| 296 |
313 |
|
|
| 297 |
314 |
|
if (dryRun) { |
|
| 329 |
346 |
|
try { |
| 330 |
347 |
|
// Handle cover image upload |
| 331 |
348 |
|
let coverImage: BlobObject | undefined; |
| 332 |
|
- |
if (post.frontmatter.ogImage) { |
| 333 |
|
- |
const imagePath = await resolveImagePath( |
| 334 |
|
- |
post.frontmatter.ogImage, |
| 335 |
|
- |
imagesDir, |
| 336 |
|
- |
contentDir, |
| 337 |
|
- |
); |
| 338 |
|
- |
|
| 339 |
|
- |
if (imagePath) { |
| 340 |
|
- |
log.info(` Uploading cover image: ${path.basename(imagePath)}`); |
| 341 |
|
- |
coverImage = await uploadImage(agent, imagePath); |
| 342 |
|
- |
if (coverImage) { |
| 343 |
|
- |
log.info(` Uploaded image blob: ${coverImage.ref.$link}`); |
| 344 |
|
- |
} |
| 345 |
|
- |
} else { |
| 346 |
|
- |
log.warn(` Cover image not found: ${post.frontmatter.ogImage}`); |
|
349 |
+ |
if (post.coverImagePath) { |
|
350 |
+ |
log.info(` Uploading cover image: ${path.basename(post.coverImagePath)}`); |
|
351 |
+ |
coverImage = await uploadImage(agent, post.coverImagePath); |
|
352 |
+ |
if (coverImage) { |
|
353 |
+ |
log.info(` Uploaded image blob: ${coverImage.ref.$link}`); |
| 347 |
354 |
|
} |
|
355 |
+ |
} else { |
|
356 |
+ |
log.warn(` Cover image not found: ${post.frontmatter.ogImage}`); |
| 348 |
357 |
|
} |
| 349 |
358 |
|
|
| 350 |
359 |
|
// Track atUri, content for state saving, and bskyPostRef |
|
| 372 |
381 |
|
contentForHash = updatedContent; |
| 373 |
382 |
|
publishedCount++; |
| 374 |
383 |
|
} else { |
|
384 |
+ |
|
|
385 |
+ |
// Validate post. |
| 375 |
386 |
|
atUri = post.frontmatter.atUri!; |
| 376 |
387 |
|
await updateDocument(agent, post, atUri, config, coverImage); |
| 377 |
388 |
|
s.stop(`Updated: ${atUri}`); |
|
| 455 |
466 |
|
} |
| 456 |
467 |
|
}, |
| 457 |
468 |
|
}); |
|
469 |
+ |
|
|
470 |
+ |
async function validatePost(post: BlogPost): Promise<boolean> { |
|
471 |
+ |
if (post.coverImagePath) { |
|
472 |
+ |
const stat = await fs.stat(post.coverImagePath); |
|
473 |
+ |
if (stat.size >= COVER_IMAGE_MAX_SIZE) { |
|
474 |
+ |
log.error(` Cover image "${post.coverImagePath}" must be less than 1MB`); |
|
475 |
+ |
return false; |
|
476 |
+ |
} |
|
477 |
+ |
} |
|
478 |
+ |
|
|
479 |
+ |
return true; |
|
480 |
+ |
} |
|
481 |
+ |
|