A CLI for publishing standard.site documents to ATProto sequoia.pub
standard site lexicon cli publishing
59
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 180 lines 4.3 kB view raw
1import * as fs from "node:fs/promises"; 2import * as path from "node:path"; 3import type { 4 PublisherConfig, 5 PublisherState, 6 FrontmatterMapping, 7 BlueskyConfig, 8} from "./types"; 9 10export const CONFIG_FILENAME = "sequoia.json"; 11const STATE_FILENAME = ".sequoia-state.json"; 12 13async function fileExists(filePath: string): Promise<boolean> { 14 try { 15 await fs.access(filePath); 16 return true; 17 } catch { 18 return false; 19 } 20} 21 22export async function findConfig( 23 startDir: string = process.cwd(), 24): Promise<string | null> { 25 let currentDir = startDir; 26 27 while (true) { 28 const configPath = path.join(currentDir, CONFIG_FILENAME); 29 30 if (await fileExists(configPath)) { 31 return configPath; 32 } 33 34 const parentDir = path.dirname(currentDir); 35 if (parentDir === currentDir) { 36 // Reached root 37 return null; 38 } 39 currentDir = parentDir; 40 } 41} 42 43export async function loadConfig( 44 configPath?: string, 45): Promise<PublisherConfig> { 46 const resolvedPath = configPath || (await findConfig()); 47 48 if (!resolvedPath) { 49 throw new Error( 50 `Could not find ${CONFIG_FILENAME}. Run 'sequoia init' to create one.`, 51 ); 52 } 53 54 try { 55 const content = await fs.readFile(resolvedPath, "utf-8"); 56 const config = JSON.parse(content) as PublisherConfig; 57 58 // Validate required fields 59 if (!config.siteUrl) throw new Error("siteUrl is required in config"); 60 if (!config.contentDir) throw new Error("contentDir is required in config"); 61 if (!config.publicationUri) 62 throw new Error("publicationUri is required in config"); 63 64 return config; 65 } catch (error) { 66 if (error instanceof Error && error.message.includes("required")) { 67 throw error; 68 } 69 throw new Error(`Failed to load config from ${resolvedPath}: ${error}`); 70 } 71} 72 73export function generateConfigTemplate(options: { 74 siteUrl: string; 75 contentDir: string; 76 imagesDir?: string; 77 publicDir?: string; 78 outputDir?: string; 79 pathPrefix?: string; 80 publicationUri: string; 81 pdsUrl?: string; 82 frontmatter?: FrontmatterMapping; 83 ignore?: string[]; 84 removeIndexFromSlug?: boolean; 85 stripDatePrefix?: boolean; 86 pathTemplate?: string; 87 textContentField?: string; 88 publishContent?: boolean; 89 bluesky?: BlueskyConfig; 90}): string { 91 const config: Record<string, unknown> = { 92 $schema: 93 "https://tangled.org/stevedylan.dev/sequoia/raw/main/sequoia.schema.json", 94 siteUrl: options.siteUrl, 95 contentDir: options.contentDir, 96 }; 97 98 if (options.imagesDir) { 99 config.imagesDir = options.imagesDir; 100 } 101 102 if (options.publicDir && options.publicDir !== "./public") { 103 config.publicDir = options.publicDir; 104 } 105 106 if (options.outputDir) { 107 config.outputDir = options.outputDir; 108 } 109 110 if (options.pathPrefix && options.pathPrefix !== "/posts") { 111 config.pathPrefix = options.pathPrefix; 112 } 113 114 config.publicationUri = options.publicationUri; 115 116 if (options.pdsUrl && options.pdsUrl !== "https://bsky.social") { 117 config.pdsUrl = options.pdsUrl; 118 } 119 120 if (options.frontmatter && Object.keys(options.frontmatter).length > 0) { 121 config.frontmatter = options.frontmatter; 122 } 123 124 if (options.ignore && options.ignore.length > 0) { 125 config.ignore = options.ignore; 126 } 127 128 if (options.removeIndexFromSlug) { 129 config.removeIndexFromSlug = options.removeIndexFromSlug; 130 } 131 132 if (options.stripDatePrefix) { 133 config.stripDatePrefix = options.stripDatePrefix; 134 } 135 136 if (options.pathTemplate) { 137 config.pathTemplate = options.pathTemplate; 138 } 139 140 if (options.textContentField) { 141 config.textContentField = options.textContentField; 142 } 143 144 if (options.publishContent) { 145 config.publishContent = options.publishContent; 146 } 147 148 if (options.bluesky) { 149 config.bluesky = options.bluesky; 150 } 151 152 return JSON.stringify(config, null, 2); 153} 154 155export async function loadState(configDir: string): Promise<PublisherState> { 156 const statePath = path.join(configDir, STATE_FILENAME); 157 158 if (!(await fileExists(statePath))) { 159 return { posts: {} }; 160 } 161 162 try { 163 const content = await fs.readFile(statePath, "utf-8"); 164 return JSON.parse(content) as PublisherState; 165 } catch { 166 return { posts: {} }; 167 } 168} 169 170export async function saveState( 171 configDir: string, 172 state: PublisherState, 173): Promise<void> { 174 const statePath = path.join(configDir, STATE_FILENAME); 175 await fs.writeFile(statePath, JSON.stringify(state, null, 2)); 176} 177 178export function getStatePath(configDir: string): string { 179 return path.join(configDir, STATE_FILENAME); 180}