this repo has no description
at design-docs 283 lines 8.0 kB view raw
1#!/usr/bin/env bun 2import { access, mkdir, writeFile } from "node:fs/promises"; 3import { dirname, join, resolve } from "node:path"; 4import { 5 exportFromConfig, 6 findConfigFile, 7 loadExportConfig, 8} from "@sitebase/core"; 9import { Command } from "commander"; 10import { getAuthenticatedAgent, login, logout, setVerbose } from "./auth.ts"; 11import { formatPublication } from "./publication.ts"; 12 13const program = new Command(); 14 15program 16 .name("sitebase") 17 .description("CLI tools for standard.site publications") 18 .version("0.0.1") 19 .option("-v, --verbose", "Enable verbose debug output") 20 .hook("preAction", () => { 21 if (program.opts().verbose) { 22 setVerbose(true); 23 } 24 }); 25 26program 27 .command("export") 28 .description("Export a publication to markdown files") 29 .option( 30 "-c, --config <file>", 31 "Path to config file (auto-discovers sitebase.config.{ts,js} if not specified)", 32 ) 33 .action(async (options: { config?: string }) => { 34 try { 35 // Find config file 36 let configPath = options.config ? resolve(options.config) : null; 37 38 if (!configPath) { 39 // Auto-discover 40 configPath = await findConfigFile(process.cwd()); 41 if (!configPath) { 42 console.error( 43 "No config file found. Create sitebase.config.ts or specify with --config", 44 ); 45 process.exit(1); 46 } 47 } 48 49 console.log(`Using config: ${configPath}`); 50 const config = await loadExportConfig(configPath); 51 52 console.log(`Exporting publication: ${config.publicationUri}`); 53 console.log(`Export targets: ${config.exports.length}`); 54 55 const configDir = dirname(configPath); 56 const exportResults = await exportFromConfig(config, configDir); 57 58 // Print results for each target 59 for (const [i, result] of exportResults.entries()) { 60 const target = config.exports[i]; 61 console.log(`\nTarget ${i + 1}: ${target?.outputDir}`); 62 console.log(` Documents processed: ${result.documentsProcessed}`); 63 console.log(` Documents skipped: ${result.documentsSkipped}`); 64 console.log(` Files written: ${result.filesWritten.length}`); 65 66 if (result.warnings.length > 0) { 67 console.log(` Warnings:`); 68 for (const warning of result.warnings) { 69 console.log(` - ${warning}`); 70 } 71 } 72 73 if (result.filesWritten.length > 0) { 74 console.log(` Files:`); 75 for (const file of result.filesWritten) { 76 console.log(` - ${file}`); 77 } 78 } 79 } 80 } catch (error) { 81 console.error( 82 `Error: ${error instanceof Error ? error.message : String(error)}`, 83 ); 84 process.exit(1); 85 } 86 }); 87 88program 89 .command("init") 90 .description("Create a sitebase.config.ts file with starter configuration") 91 .action(async () => { 92 const cwd = process.cwd(); 93 const configPath = join(cwd, "sitebase.config.ts"); 94 const templatesDir = join(cwd, "templates"); 95 const templatePath = join(templatesDir, "post.hbs"); 96 97 // Check if config already exists 98 try { 99 await access(configPath); 100 console.error("Error: sitebase.config.ts already exists"); 101 process.exit(1); 102 } catch { 103 // File doesn't exist, we can proceed 104 } 105 106 // Config file template with comments 107 const configContent = `// sitebase.config.ts 108import type { ExportConfig } from "@sitebase/core"; 109import { slugify } from "@sitebase/core"; 110 111/** 112 * Sitebase Export Configuration 113 * 114 * This file configures how your AT Protocol publication is exported to markdown files. 115 * For more information, see: https://github.com/sethetter/sitebase 116 */ 117const config: ExportConfig = { 118 // The AT URI of your publication (required) 119 // Format: at://did:plc:xxx/site.standard.publication/rkey 120 // Find this in your PDS or use the standard.site dashboard 121 publicationUri: "at://YOUR_DID/site.standard.publication/YOUR_RKEY", 122 123 // Export targets - each entry exports documents to a different location/format 124 // You can have multiple targets to export the same publication different ways 125 exports: [ 126 { 127 // Directory where markdown files will be written (relative to this config file) 128 outputDir: "./content", 129 130 // Optional: Only include documents with at least one of these tags 131 // If not specified, all documents are included (except those matching excludeTags) 132 // includeTags: ["post", "article"], 133 134 // Optional: Exclude documents with any of these tags 135 // Defaults to ["draft"] if includeTags is not specified 136 // excludeTags: ["draft", "private"], 137 138 // Function to generate the filename for each document (required) 139 // Receives an object with: title, path, publishedAt, updatedAt, tags, content, etc. 140 filename: (data) => { 141 // Example: "2024-01-15_my-post-title.md" 142 const date = data.publishedAt?.slice(0, 10) || "undated"; 143 return \`\${date}_\${slugify(data.title)}.md\`; 144 }, 145 146 // Optional: Path to a Handlebars template for content generation 147 // The template receives the same data object as the filename function 148 contentTemplate: "./templates/post.hbs", 149 150 // Optional: Function to generate content (overrides contentTemplate if both specified) 151 // content: (data) => \`--- 152 // title: "\${data.title}" 153 // date: \${data.publishedAt} 154 // --- 155 // 156 // \${data.content} 157 // \`, 158 }, 159 ], 160}; 161 162export default config; 163`; 164 165 // Handlebars template for posts 166 const templateContent = `--- 167title: "{{title}}" 168{{#if description}} 169description: "{{description}}" 170{{/if}} 171{{#if publishedAt}} 172date: {{publishedAt}} 173{{/if}} 174{{#if updatedAt}} 175updated: {{updatedAt}} 176{{/if}} 177{{#if tags.length}} 178tags: 179{{#each tags}} 180 - {{this}} 181{{/each}} 182{{/if}} 183--- 184 185{{{content}}} 186`; 187 188 try { 189 // Write config file 190 await writeFile(configPath, configContent, "utf-8"); 191 console.log("Created sitebase.config.ts"); 192 193 // Create templates directory and template file 194 await mkdir(templatesDir, { recursive: true }); 195 await writeFile(templatePath, templateContent, "utf-8"); 196 console.log("Created templates/post.hbs"); 197 198 console.log("\nNext steps:"); 199 console.log( 200 " 1. Update publicationUri in sitebase.config.ts with your publication's AT URI", 201 ); 202 console.log(" 2. Customize the export targets as needed"); 203 console.log(" 3. Run: sitebase export"); 204 } catch (error) { 205 console.error( 206 `Error: ${error instanceof Error ? error.message : String(error)}`, 207 ); 208 process.exit(1); 209 } 210 }); 211 212const auth = program.command("auth").description("Manage authentication"); 213 214auth 215 .command("login") 216 .description("Authenticate with ATProto via OAuth") 217 .argument("<handle>", "Your ATProto handle (e.g. user.bsky.social)") 218 .action(async (handle: string) => { 219 try { 220 await login(handle); 221 } catch (error) { 222 console.error( 223 `Error: ${error instanceof Error ? error.message : String(error)}`, 224 ); 225 process.exit(1); 226 } 227 }); 228 229auth 230 .command("logout") 231 .description("Clear stored session") 232 .action(async () => { 233 await logout(); 234 }); 235 236const pub = program.command("pub").description("Manage publications"); 237 238pub 239 .command("list") 240 .description("List all of your publications") 241 .action(async () => { 242 try { 243 const { agent, did } = await getAuthenticatedAgent(); 244 const response = await agent.com.atproto.repo.listRecords({ 245 repo: did, 246 collection: "site.standard.publication", 247 }); 248 } catch (e) {} 249 }); 250 251pub 252 .command("view") 253 .description("View your publication") 254 .action(async () => { 255 try { 256 const { agent, did } = await getAuthenticatedAgent(); 257 const response = await agent.com.atproto.repo.listRecords({ 258 repo: did, 259 collection: "site.standard.publication", 260 limit: 1, 261 }); 262 263 if (!response.data.records.length) { 264 console.log("No publication found."); 265 return; 266 } 267 268 const record = response.data.records[0]!; 269 console.log( 270 formatPublication({ 271 uri: record.uri, 272 value: record.value as Record<string, unknown>, 273 }), 274 ); 275 } catch (error) { 276 console.error( 277 `Error: ${error instanceof Error ? error.message : String(error)}`, 278 ); 279 process.exit(1); 280 } 281 }); 282 283program.parse();