Something went wrong. Try again.
A CLI for publishing standard.site documents to ATProto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195import * as fs from "node:fs/promises";import * as path from "node:path";import type { PublisherConfig, PublisherState, FrontmatterMapping, BlueskyConfig,} from "./types";
export const CONFIG_FILENAME = "sequoia.json";const STATE_FILENAME = ".sequoia-state.json";
/** * Default values for optional `PublisherConfig` fields that have documented defaults. * Must be kept in sync with the field comments on `PublisherConfig` in `types.ts`. */export const DEFAULT_PUBLISHER_CONFIG = { publicDir: "./public", outputDir: "./dist", pathPrefix: "/posts", publishContent: true, removeIndexFromSlug: false, stripDatePrefix: false, autoSync: true,} satisfies Partial<PublisherConfig>;
async function fileExists(filePath: string): Promise<boolean> { try { await fs.access(filePath); return true; } catch { return false; }}
export async function findConfig( startDir: string = process.cwd(),): Promise<string | null> { let currentDir = startDir;
while (true) { const configPath = path.join(currentDir, CONFIG_FILENAME);
if (await fileExists(configPath)) { return configPath; }
const parentDir = path.dirname(currentDir); if (parentDir === currentDir) { // Reached root return null; } currentDir = parentDir; }}
export async function loadConfig( configPath?: string,): Promise<PublisherConfig> { const resolvedPath = configPath || (await findConfig());
if (!resolvedPath) { throw new Error( `Could not find ${CONFIG_FILENAME}. Run 'sequoia init' to create one.`, ); }
try { const content = await fs.readFile(resolvedPath, "utf-8"); const config = JSON.parse(content) as PublisherConfig;
// Validate required fields if (!config.siteUrl) throw new Error("siteUrl is required in config"); if (!config.contentDir) throw new Error("contentDir is required in config"); if (!config.publicationUri) throw new Error("publicationUri is required in config");
return { ...DEFAULT_PUBLISHER_CONFIG, ...config }; } catch (error) { if (error instanceof Error && error.message.includes("required")) { throw error; } throw new Error(`Failed to load config from ${resolvedPath}: ${error}`); }}
export function generateConfigTemplate(options: { siteUrl: string; contentDir: string; imagesDir?: string; publicDir?: string; outputDir?: string; pathPrefix?: string; publicationUri: string; pdsUrl?: string; frontmatter?: FrontmatterMapping; ignore?: string[]; removeIndexFromSlug?: boolean; stripDatePrefix?: boolean; pathTemplate?: string; textContentField?: string; publishContent?: boolean; bluesky?: BlueskyConfig;}): string { const config: Record<string, unknown> = { $schema: "https://tangled.org/stevedylan.dev/sequoia/raw/main/sequoia.schema.json", siteUrl: options.siteUrl, contentDir: options.contentDir, };
if (options.imagesDir) { config.imagesDir = options.imagesDir; }
if (options.publicDir && options.publicDir !== "./public") { config.publicDir = options.publicDir; }
if (options.outputDir) { config.outputDir = options.outputDir; }
if (options.pathPrefix && options.pathPrefix !== "/posts") { config.pathPrefix = options.pathPrefix; }
config.publicationUri = options.publicationUri;
if (options.pdsUrl && options.pdsUrl !== "https://bsky.social") { config.pdsUrl = options.pdsUrl; }
if (options.frontmatter && Object.keys(options.frontmatter).length > 0) { config.frontmatter = options.frontmatter; }
if (options.ignore && options.ignore.length > 0) { config.ignore = options.ignore; }
if (options.removeIndexFromSlug) { config.removeIndexFromSlug = options.removeIndexFromSlug; }
if (options.stripDatePrefix) { config.stripDatePrefix = options.stripDatePrefix; }
if (options.pathTemplate) { config.pathTemplate = options.pathTemplate; }
if (options.textContentField) { config.textContentField = options.textContentField; }
if (options.publishContent !== undefined) { config.publishContent = options.publishContent; }
if (options.bluesky) { config.bluesky = options.bluesky; }
return JSON.stringify(config, null, 2);}
export async function loadState(configDir: string): Promise<PublisherState> { const statePath = path.join(configDir, STATE_FILENAME);
if (!(await fileExists(statePath))) { return { posts: {} }; }
try { const content = await fs.readFile(statePath, "utf-8"); return JSON.parse(content) as PublisherState; } catch { return { posts: {} }; }}
export async function saveState( configDir: string, state: PublisherState,): Promise<void> { const statePath = path.join(configDir, STATE_FILENAME); await fs.writeFile(statePath, JSON.stringify(state, null, 2));}
export function getStatePath(configDir: string): string { return path.join(configDir, STATE_FILENAME);}