WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at main 155 lines 4.4 kB view raw
1import { defineCommand } from "citty"; 2import consola from "consola"; 3import { input } from "@inquirer/prompts"; 4import postgres from "postgres"; 5import { drizzle } from "drizzle-orm/postgres-js"; 6import * as schema from "@atbb/db"; 7import { ForumAgent } from "@atbb/atproto"; 8import { loadCliConfig } from "../lib/config.js"; 9import { checkEnvironment } from "../lib/preflight.js"; 10import { createCategory } from "../lib/steps/create-category.js"; 11import { isProgrammingError } from "../lib/errors.js"; 12import { logger } from "../lib/logger.js"; 13 14const categoryAddCommand = defineCommand({ 15 meta: { 16 name: "add", 17 description: "Add a new category to the forum", 18 }, 19 args: { 20 name: { 21 type: "string", 22 description: "Category name", 23 }, 24 description: { 25 type: "string", 26 description: "Category description (optional)", 27 }, 28 slug: { 29 type: "string", 30 description: "URL-friendly identifier (auto-derived from name if omitted)", 31 }, 32 "sort-order": { 33 type: "string", 34 description: "Numeric sort position — lower values appear first", 35 }, 36 }, 37 async run({ args }) { 38 consola.box("atBB — Add Category"); 39 40 const config = loadCliConfig(); 41 const envCheck = checkEnvironment(config); 42 43 if (!envCheck.ok) { 44 consola.error("Missing required environment variables:"); 45 for (const name of envCheck.errors) { 46 consola.error(` - ${name}`); 47 } 48 consola.info("Set these in your .env file or environment, then re-run."); 49 process.exit(1); 50 } 51 52 const sql = postgres(config.databaseUrl); 53 const db = drizzle(sql, { schema }); 54 55 async function cleanup() { 56 await sql.end(); 57 } 58 59 try { 60 await sql`SELECT 1`; 61 consola.success("Database connection successful"); 62 } catch (error) { 63 consola.error( 64 "Failed to connect to database:", 65 error instanceof Error ? error.message : String(error) 66 ); 67 await cleanup(); 68 process.exit(1); 69 } 70 71 consola.start("Authenticating as Forum DID..."); 72 const forumAgent = new ForumAgent( 73 config.pdsUrl, 74 config.forumHandle, 75 config.forumPassword, 76 logger 77 ); 78 try { 79 await forumAgent.initialize(); 80 } catch (error) { 81 consola.error( 82 "Failed to reach PDS during authentication:", 83 error instanceof Error ? error.message : String(error) 84 ); 85 try { await forumAgent.shutdown(); } catch {} 86 await cleanup(); 87 process.exit(1); 88 } 89 90 if (!forumAgent.isAuthenticated()) { 91 const status = forumAgent.getStatus(); 92 consola.error(`Failed to authenticate: ${status.error}`); 93 await forumAgent.shutdown(); 94 await cleanup(); 95 process.exit(1); 96 } 97 98 const agent = forumAgent.getAgent()!; 99 consola.success(`Authenticated as ${config.forumHandle}`); 100 101 const name = 102 args.name ?? 103 (await input({ message: "Category name:", default: "General" })); 104 105 const description = 106 args.description ?? 107 (await input({ message: "Category description (optional):" })); 108 109 const sortOrderRaw = args["sort-order"]; 110 const sortOrder = 111 sortOrderRaw !== undefined ? parseInt(sortOrderRaw, 10) : undefined; 112 113 try { 114 const result = await createCategory(db, agent, config.forumDid, { 115 name, 116 ...(description && { description }), 117 ...(args.slug && { slug: args.slug }), 118 ...(sortOrder !== undefined && !isNaN(sortOrder) && { sortOrder }), 119 }); 120 121 if (result.skipped) { 122 consola.warn(`Category "${result.existingName}" already exists: ${result.uri}`); 123 } else { 124 consola.success(`Created category "${name}"`); 125 consola.info(`URI: ${result.uri}`); 126 } 127 } catch (error) { 128 if (isProgrammingError(error)) throw error; 129 consola.error( 130 "Failed to create category:", 131 JSON.stringify({ 132 name, 133 forumDid: config.forumDid, 134 error: error instanceof Error ? error.message : String(error), 135 }) 136 ); 137 await forumAgent.shutdown(); 138 await cleanup(); 139 process.exit(1); 140 } 141 142 await forumAgent.shutdown(); 143 await cleanup(); 144 }, 145}); 146 147export const categoryCommand = defineCommand({ 148 meta: { 149 name: "category", 150 description: "Manage forum categories", 151 }, 152 subCommands: { 153 add: categoryAddCommand, 154 }, 155});