this repo has no description

chore: cleaned up commands and libs

+100 -210
+49 -78
packages/cli/src/commands/init.ts
··· 53 53 }, 54 54 ); 55 55 56 - const hasImages = await consola.prompt( 57 - "Do you have a separate directory for cover images?", 56 + const imagesDir = await consola.prompt( 57 + "Cover images directory (where cover/og images are stored, leave empty to skip):", 58 58 { 59 - type: "confirm", 60 - initial: false, 59 + type: "text", 60 + placeholder: "./public/images", 61 61 }, 62 62 ); 63 - 64 - let imagesDir: string | undefined; 65 - if (hasImages) { 66 - const imgDir = await consola.prompt( 67 - "Cover images directory (where cover/og images are stored):", 68 - { 69 - type: "text", 70 - placeholder: "./public/images", 71 - }, 72 - ); 73 - imagesDir = imgDir as string; 74 - } 75 63 76 64 // Public/static directory for .well-known files 77 65 const publicDir = await consola.prompt( ··· 104 92 ); 105 93 106 94 // Frontmatter mapping configuration 107 - const customFrontmatter = await consola.prompt( 108 - "Do you use custom frontmatter field names?", 109 - { 110 - type: "confirm", 111 - initial: false, 112 - }, 95 + consola.info( 96 + "Configure your frontmatter field mappings (press Enter to use defaults):", 113 97 ); 114 98 115 - let frontmatterMapping: FrontmatterMapping | undefined; 116 - if (customFrontmatter) { 117 - consola.info( 118 - "Configure your frontmatter field mappings (press Enter to use defaults):", 119 - ); 99 + const titleField = await consola.prompt("Field name for title:", { 100 + type: "text", 101 + default: "title", 102 + placeholder: "title", 103 + }); 120 104 121 - const titleField = await consola.prompt("Field name for title:", { 122 - type: "text", 123 - default: "title", 124 - placeholder: "title", 125 - }); 105 + const descField = await consola.prompt("Field name for description:", { 106 + type: "text", 107 + default: "description", 108 + placeholder: "description", 109 + }); 126 110 127 - const descField = await consola.prompt("Field name for description:", { 128 - type: "text", 129 - default: "description", 130 - placeholder: "description", 131 - }); 111 + const dateField = await consola.prompt("Field name for publish date:", { 112 + type: "text", 113 + default: "publishDate", 114 + placeholder: "publishDate, pubDate, date, etc.", 115 + }); 132 116 133 - const dateField = await consola.prompt("Field name for publish date:", { 134 - type: "text", 135 - default: "publishDate", 136 - placeholder: "publishDate, pubDate, date, etc.", 137 - }); 117 + const coverField = await consola.prompt("Field name for cover image:", { 118 + type: "text", 119 + default: "ogImage", 120 + placeholder: "ogImage, coverImage, image, hero, etc.", 121 + }); 138 122 139 - const coverField = await consola.prompt("Field name for cover image:", { 140 - type: "text", 141 - default: "ogImage", 142 - placeholder: "ogImage, coverImage, image, hero, etc.", 143 - }); 123 + let frontmatterMapping: FrontmatterMapping | undefined = {}; 144 124 145 - frontmatterMapping = {}; 125 + if (titleField && titleField !== "title") { 126 + frontmatterMapping.title = titleField as string; 127 + } 128 + if (descField && descField !== "description") { 129 + frontmatterMapping.description = descField as string; 130 + } 131 + if (dateField && dateField !== "publishDate") { 132 + frontmatterMapping.publishDate = dateField as string; 133 + } 134 + if (coverField && coverField !== "ogImage") { 135 + frontmatterMapping.coverImage = coverField as string; 136 + } 146 137 147 - if (titleField && titleField !== "title") { 148 - frontmatterMapping.title = titleField as string; 149 - } 150 - if (descField && descField !== "description") { 151 - frontmatterMapping.description = descField as string; 152 - } 153 - if (dateField && dateField !== "publishDate") { 154 - frontmatterMapping.publishDate = dateField as string; 155 - } 156 - if (coverField && coverField !== "ogImage") { 157 - frontmatterMapping.coverImage = coverField as string; 158 - } 159 - 160 - // Only keep frontmatterMapping if it has any custom fields 161 - if (Object.keys(frontmatterMapping).length === 0) { 162 - frontmatterMapping = undefined; 163 - } 138 + // Only keep frontmatterMapping if it has any custom fields 139 + if (Object.keys(frontmatterMapping).length === 0) { 140 + frontmatterMapping = undefined; 164 141 } 165 142 166 143 // Publication setup ··· 214 191 }, 215 192 ); 216 193 217 - const hasIcon = await consola.prompt("Add an icon image?", { 218 - type: "confirm", 219 - initial: false, 220 - }); 221 - 222 - let iconPath: string | undefined; 223 - if (hasIcon) { 224 - const icon = await consola.prompt("Icon image path:", { 194 + const iconPath = await consola.prompt( 195 + "Icon image path (leave empty to skip):", 196 + { 225 197 type: "text", 226 198 placeholder: "./icon.png", 227 - }); 228 - iconPath = icon as string; 229 - } 199 + }, 200 + ); 230 201 231 202 const showInDiscover = await consola.prompt("Show in Discover feed?", { 232 203 type: "confirm", ··· 239 210 url: siteUrl as string, 240 211 name: pubName as string, 241 212 description: (pubDescription as string) || undefined, 242 - iconPath, 213 + iconPath: (iconPath as string) || undefined, 243 214 showInDiscover, 244 215 }); 245 216 consola.success(`Publication created: ${publicationUri}`); ··· 267 238 const configContent = generateConfigTemplate({ 268 239 siteUrl: siteUrl as string, 269 240 contentDir: contentDir as string, 270 - imagesDir, 241 + imagesDir: imagesDir || undefined, 271 242 publicDir: publicDir as string, 272 243 outputDir: outputDir as string, 273 244 pathPrefix: pathPrefix as string,
+1 -6
packages/cli/src/commands/publish.ts
··· 84 84 85 85 // Scan for posts 86 86 consola.start("Scanning for posts..."); 87 - const posts = await scanContentDirectory(contentDir, config.include, config.exclude, config.frontmatter); 87 + const posts = await scanContentDirectory(contentDir, config.frontmatter); 88 88 consola.info(`Found ${posts.length} posts`); 89 89 90 90 // Determine which posts need publishing ··· 95 95 }> = []; 96 96 97 97 for (const post of posts) { 98 - // Skip hidden posts 99 - if (post.frontmatter.hidden) { 100 - continue; 101 - } 102 - 103 98 const contentHash = await getContentHash(post.rawContent); 104 99 const relativeFilePath = path.relative(configDir, post.filePath); 105 100 const postState = state.posts[relativeFilePath];
+1 -1
packages/cli/src/commands/sync.ts
··· 86 86 87 87 // Scan local posts 88 88 consola.start("Scanning local content..."); 89 - const localPosts = await scanContentDirectory(contentDir, config.include, config.exclude, config.frontmatter); 89 + const localPosts = await scanContentDirectory(contentDir, config.frontmatter); 90 90 consola.info(`Found ${localPosts.length} local posts`); 91 91 92 92 // Build a map of path -> local post for matching
+1 -16
packages/cli/src/lib/atproto.ts
··· 1 1 import { AtpAgent } from "@atproto/api"; 2 2 import * as path from "path"; 3 - import type { Credentials, BlogPost, BlobObject, PublisherConfig, PublicationRecord } from "./types"; 4 - import { generateTid } from "./tid"; 3 + import type { Credentials, BlogPost, BlobObject, PublisherConfig } from "./types"; 5 4 import { stripMarkdownForText } from "./markdown"; 6 5 7 6 export async function resolveHandleToPDS(handle: string): Promise<string> { ··· 184 183 record.coverImage = coverImage; 185 184 } 186 185 187 - if (config.location) { 188 - record.location = config.location; 189 - } 190 - 191 - const rkey = generateTid(); 192 - 193 186 const response = await agent.com.atproto.repo.createRecord({ 194 187 repo: agent.session!.did, 195 188 collection: "site.standard.document", 196 - rkey, 197 189 record, 198 190 }); 199 191 ··· 233 225 234 226 if (coverImage) { 235 227 record.coverImage = coverImage; 236 - } 237 - 238 - if (config.location) { 239 - record.location = config.location; 240 228 } 241 229 242 230 await agent.com.atproto.repo.putRecord({ ··· 342 330 }; 343 331 } 344 332 345 - const rkey = generateTid(); 346 - 347 333 const response = await agent.com.atproto.repo.createRecord({ 348 334 repo: agent.session!.did, 349 335 collection: "site.standard.publication", 350 - rkey, 351 336 record, 352 337 }); 353 338
-5
packages/cli/src/lib/config.ts
··· 66 66 pathPrefix?: string; 67 67 publicationUri: string; 68 68 pdsUrl?: string; 69 - location?: string; 70 69 frontmatter?: FrontmatterMapping; 71 70 }): string { 72 71 const config: Record<string, unknown> = { ··· 94 93 95 94 if (options.pdsUrl && options.pdsUrl !== "https://bsky.social") { 96 95 config.pdsUrl = options.pdsUrl; 97 - } 98 - 99 - if (options.location) { 100 - config.location = options.location; 101 96 } 102 97 103 98 if (options.frontmatter && Object.keys(options.frontmatter).length > 0) {
+1 -11
packages/cli/src/lib/markdown.ts
··· 82 82 const coverField = mapping?.coverImage || "ogImage"; 83 83 frontmatter.ogImage = raw[coverField] || raw.ogImage; 84 84 85 - // Hidden mapping 86 - const hiddenField = mapping?.hidden || "hidden"; 87 - frontmatter.hidden = raw[hiddenField] || raw.hidden; 88 - 89 85 // Tags mapping 90 86 const tagsField = mapping?.tags || "tags"; 91 87 frontmatter.tags = raw[tagsField] || raw.tags; ··· 113 109 114 110 export async function scanContentDirectory( 115 111 contentDir: string, 116 - include?: string[], 117 - exclude?: string[], 118 112 frontmatterMapping?: FrontmatterMapping 119 113 ): Promise<BlogPost[]> { 120 - const patterns = include || ["**/*.md", "**/*.mdx"]; 114 + const patterns = ["**/*.md", "**/*.mdx"]; 121 115 const posts: BlogPost[] = []; 122 116 123 117 for (const pattern of patterns) { ··· 127 121 cwd: contentDir, 128 122 absolute: false, 129 123 })) { 130 - // Check exclusions 131 - if (exclude?.some((ex) => relativePath.includes(ex))) { 132 - continue; 133 - } 134 124 135 125 const filePath = path.join(contentDir, relativePath); 136 126 const file = Bun.file(filePath);
-41
packages/cli/src/lib/tid.ts
··· 1 - // TID (Timestamp Identifier) generation per ATProto spec 2 - // Format: base32-sortable encoded, 13 characters 3 - // Structure: 53 bits of timestamp (microseconds since epoch) + 10 bits of clock ID 4 - 5 - const S32_CHAR = "234567abcdefghijklmnopqrstuvwxyz"; 6 - 7 - let lastTimestamp = 0; 8 - let clockId = Math.floor(Math.random() * 1024); 9 - 10 - export function generateTid(): string { 11 - // Get current timestamp in microseconds 12 - let timestamp = Date.now() * 1000; 13 - 14 - // Ensure monotonically increasing timestamps 15 - if (timestamp <= lastTimestamp) { 16 - timestamp = lastTimestamp + 1; 17 - } 18 - lastTimestamp = timestamp; 19 - 20 - // Combine timestamp (53 bits) and clock ID (10 bits) 21 - // TID is a 63-bit integer encoded as 13 base32 characters 22 - const tid = (BigInt(timestamp) << 10n) | BigInt(clockId); 23 - 24 - // Convert to base32-sortable 25 - let result = ""; 26 - let value = tid; 27 - for (let i = 0; i < 13; i++) { 28 - result = S32_CHAR[Number(value % 32n)] + result; 29 - value = value / 32n; 30 - } 31 - 32 - return result; 33 - } 34 - 35 - export function isValidTid(tid: string): boolean { 36 - if (tid.length !== 13) return false; 37 - for (const char of tid) { 38 - if (!S32_CHAR.includes(char)) return false; 39 - } 40 - return true; 41 - }
+47 -52
packages/cli/src/lib/types.ts
··· 1 1 export interface FrontmatterMapping { 2 - title?: string; // Field name for title (default: "title") 3 - description?: string; // Field name for description (default: "description") 4 - publishDate?: string; // Field name for publish date (default: "publishDate", also checks "pubDate", "date", "createdAt", "created_at") 5 - coverImage?: string; // Field name for cover image (default: "ogImage") 6 - hidden?: string; // Field name for hidden flag (default: "hidden") 7 - tags?: string; // Field name for tags (default: "tags") 2 + title?: string; // Field name for title (default: "title") 3 + description?: string; // Field name for description (default: "description") 4 + publishDate?: string; // Field name for publish date (default: "publishDate", also checks "pubDate", "date", "createdAt", "created_at") 5 + coverImage?: string; // Field name for cover image (default: "ogImage") 6 + tags?: string; // Field name for tags (default: "tags") 8 7 } 9 8 10 9 export interface PublisherConfig { 11 - siteUrl: string; 12 - contentDir: string; 13 - imagesDir?: string; // Directory containing cover images 14 - publicDir?: string; // Static/public folder for .well-known files (default: public) 15 - outputDir?: string; // Built output directory for inject command 16 - pathPrefix?: string; // URL path prefix for posts (default: /posts) 17 - publicationUri: string; 18 - pdsUrl?: string; 19 - location?: string; 20 - include?: string[]; 21 - exclude?: string[]; 22 - identity?: string; // Which stored identity to use (matches identifier) 23 - frontmatter?: FrontmatterMapping; // Custom frontmatter field mappings 10 + siteUrl: string; 11 + contentDir: string; 12 + imagesDir?: string; // Directory containing cover images 13 + publicDir?: string; // Static/public folder for .well-known files (default: public) 14 + outputDir?: string; // Built output directory for inject command 15 + pathPrefix?: string; // URL path prefix for posts (default: /posts) 16 + publicationUri: string; 17 + pdsUrl?: string; 18 + identity?: string; // Which stored identity to use (matches identifier) 19 + frontmatter?: FrontmatterMapping; // Custom frontmatter field mappings 24 20 } 25 21 26 22 export interface Credentials { 27 - pdsUrl: string; 28 - identifier: string; 29 - password: string; 23 + pdsUrl: string; 24 + identifier: string; 25 + password: string; 30 26 } 31 27 32 28 export interface PostFrontmatter { 33 - title: string; 34 - description?: string; 35 - publishDate: string; 36 - tags?: string[]; 37 - ogImage?: string; 38 - hidden?: boolean; 39 - atUri?: string; 29 + title: string; 30 + description?: string; 31 + publishDate: string; 32 + tags?: string[]; 33 + ogImage?: string; 34 + atUri?: string; 40 35 } 41 36 42 37 export interface BlogPost { 43 - filePath: string; 44 - slug: string; 45 - frontmatter: PostFrontmatter; 46 - content: string; 47 - rawContent: string; 38 + filePath: string; 39 + slug: string; 40 + frontmatter: PostFrontmatter; 41 + content: string; 42 + rawContent: string; 48 43 } 49 44 50 45 export interface BlobRef { 51 - $link: string; 46 + $link: string; 52 47 } 53 48 54 49 export interface BlobObject { 55 - $type: "blob"; 56 - ref: BlobRef; 57 - mimeType: string; 58 - size: number; 50 + $type: "blob"; 51 + ref: BlobRef; 52 + mimeType: string; 53 + size: number; 59 54 } 60 55 61 56 export interface PublisherState { 62 - posts: Record<string, PostState>; 57 + posts: Record<string, PostState>; 63 58 } 64 59 65 60 export interface PostState { 66 - contentHash: string; 67 - atUri?: string; 68 - lastPublished?: string; 61 + contentHash: string; 62 + atUri?: string; 63 + lastPublished?: string; 69 64 } 70 65 71 66 export interface PublicationRecord { 72 - $type: "site.standard.publication"; 73 - url: string; 74 - name: string; 75 - description?: string; 76 - icon?: BlobObject; 77 - createdAt: string; 78 - preferences?: { 79 - showInDiscover?: boolean; 80 - }; 67 + $type: "site.standard.publication"; 68 + url: string; 69 + name: string; 70 + description?: string; 71 + icon?: BlobObject; 72 + createdAt: string; 73 + preferences?: { 74 + showInDiscover?: boolean; 75 + }; 81 76 }