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
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});