this repo has no description
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();