chore: cleaned up commands and libs 544483ba
Steve · 2026-01-28 23:57 8 file(s) · +100 −210
packages/cli/src/commands/init.ts +49 −78
53 53
			},
54 54
		);
55 55
56 -
		const hasImages = await consola.prompt(
57 -
			"Do you have a separate directory for cover images?",
56 +
		const imagesDir = await consola.prompt(
57 +
			"Cover images directory (where cover/og images are stored, leave empty to skip):",
58 58
			{
59 -
				type: "confirm",
60 -
				initial: false,
59 +
				type: "text",
60 +
				placeholder: "./public/images",
61 61
			},
62 62
		);
63 -
64 -
		let imagesDir: string | undefined;
65 -
		if (hasImages) {
66 -
			const imgDir = await consola.prompt(
67 -
				"Cover images directory (where cover/og images are stored):",
68 -
				{
69 -
					type: "text",
70 -
					placeholder: "./public/images",
71 -
				},
72 -
			);
73 -
			imagesDir = imgDir as string;
74 -
		}
75 63
76 64
		// Public/static directory for .well-known files
77 65
		const publicDir = await consola.prompt(
104 92
		);
105 93
106 94
		// Frontmatter mapping configuration
107 -
		const customFrontmatter = await consola.prompt(
108 -
			"Do you use custom frontmatter field names?",
109 -
			{
110 -
				type: "confirm",
111 -
				initial: false,
112 -
			},
95 +
		consola.info(
96 +
			"Configure your frontmatter field mappings (press Enter to use defaults):",
113 97
		);
114 98
115 -
		let frontmatterMapping: FrontmatterMapping | undefined;
116 -
		if (customFrontmatter) {
117 -
			consola.info(
118 -
				"Configure your frontmatter field mappings (press Enter to use defaults):",
119 -
			);
99 +
		const titleField = await consola.prompt("Field name for title:", {
100 +
			type: "text",
101 +
			default: "title",
102 +
			placeholder: "title",
103 +
		});
120 104
121 -
			const titleField = await consola.prompt("Field name for title:", {
122 -
				type: "text",
123 -
				default: "title",
124 -
				placeholder: "title",
125 -
			});
105 +
		const descField = await consola.prompt("Field name for description:", {
106 +
			type: "text",
107 +
			default: "description",
108 +
			placeholder: "description",
109 +
		});
126 110
127 -
			const descField = await consola.prompt("Field name for description:", {
128 -
				type: "text",
129 -
				default: "description",
130 -
				placeholder: "description",
131 -
			});
111 +
		const dateField = await consola.prompt("Field name for publish date:", {
112 +
			type: "text",
113 +
			default: "publishDate",
114 +
			placeholder: "publishDate, pubDate, date, etc.",
115 +
		});
132 116
133 -
			const dateField = await consola.prompt("Field name for publish date:", {
134 -
				type: "text",
135 -
				default: "publishDate",
136 -
				placeholder: "publishDate, pubDate, date, etc.",
137 -
			});
117 +
		const coverField = await consola.prompt("Field name for cover image:", {
118 +
			type: "text",
119 +
			default: "ogImage",
120 +
			placeholder: "ogImage, coverImage, image, hero, etc.",
121 +
		});
138 122
139 -
			const coverField = await consola.prompt("Field name for cover image:", {
140 -
				type: "text",
141 -
				default: "ogImage",
142 -
				placeholder: "ogImage, coverImage, image, hero, etc.",
143 -
			});
123 +
		let frontmatterMapping: FrontmatterMapping | undefined = {};
144 124
145 -
			frontmatterMapping = {};
125 +
		if (titleField && titleField !== "title") {
126 +
			frontmatterMapping.title = titleField as string;
127 +
		}
128 +
		if (descField && descField !== "description") {
129 +
			frontmatterMapping.description = descField as string;
130 +
		}
131 +
		if (dateField && dateField !== "publishDate") {
132 +
			frontmatterMapping.publishDate = dateField as string;
133 +
		}
134 +
		if (coverField && coverField !== "ogImage") {
135 +
			frontmatterMapping.coverImage = coverField as string;
136 +
		}
146 137
147 -
			if (titleField && titleField !== "title") {
148 -
				frontmatterMapping.title = titleField as string;
149 -
			}
150 -
			if (descField && descField !== "description") {
151 -
				frontmatterMapping.description = descField as string;
152 -
			}
153 -
			if (dateField && dateField !== "publishDate") {
154 -
				frontmatterMapping.publishDate = dateField as string;
155 -
			}
156 -
			if (coverField && coverField !== "ogImage") {
157 -
				frontmatterMapping.coverImage = coverField as string;
158 -
			}
159 -
160 -
			// Only keep frontmatterMapping if it has any custom fields
161 -
			if (Object.keys(frontmatterMapping).length === 0) {
162 -
				frontmatterMapping = undefined;
163 -
			}
138 +
		// Only keep frontmatterMapping if it has any custom fields
139 +
		if (Object.keys(frontmatterMapping).length === 0) {
140 +
			frontmatterMapping = undefined;
164 141
		}
165 142
166 143
		// Publication setup
214 191
				},
215 192
			);
216 193
217 -
			const hasIcon = await consola.prompt("Add an icon image?", {
218 -
				type: "confirm",
219 -
				initial: false,
220 -
			});
221 -
222 -
			let iconPath: string | undefined;
223 -
			if (hasIcon) {
224 -
				const icon = await consola.prompt("Icon image path:", {
194 +
			const iconPath = await consola.prompt(
195 +
				"Icon image path (leave empty to skip):",
196 +
				{
225 197
					type: "text",
226 198
					placeholder: "./icon.png",
227 -
				});
228 -
				iconPath = icon as string;
229 -
			}
199 +
				},
200 +
			);
230 201
231 202
			const showInDiscover = await consola.prompt("Show in Discover feed?", {
232 203
				type: "confirm",
239 210
					url: siteUrl as string,
240 211
					name: pubName as string,
241 212
					description: (pubDescription as string) || undefined,
242 -
					iconPath,
213 +
					iconPath: (iconPath as string) || undefined,
243 214
					showInDiscover,
244 215
				});
245 216
				consola.success(`Publication created: ${publicationUri}`);
267 238
		const configContent = generateConfigTemplate({
268 239
			siteUrl: siteUrl as string,
269 240
			contentDir: contentDir as string,
270 -
			imagesDir,
241 +
			imagesDir: imagesDir || undefined,
271 242
			publicDir: publicDir as string,
272 243
			outputDir: outputDir as string,
273 244
			pathPrefix: pathPrefix as string,
packages/cli/src/commands/publish.ts +1 −6
84 84
85 85
    // Scan for posts
86 86
    consola.start("Scanning for posts...");
87 -
    const posts = await scanContentDirectory(contentDir, config.include, config.exclude, config.frontmatter);
87 +
    const posts = await scanContentDirectory(contentDir, config.frontmatter);
88 88
    consola.info(`Found ${posts.length} posts`);
89 89
90 90
    // Determine which posts need publishing
95 95
    }> = [];
96 96
97 97
    for (const post of posts) {
98 -
      // Skip hidden posts
99 -
      if (post.frontmatter.hidden) {
100 -
        continue;
101 -
      }
102 -
103 98
      const contentHash = await getContentHash(post.rawContent);
104 99
      const relativeFilePath = path.relative(configDir, post.filePath);
105 100
      const postState = state.posts[relativeFilePath];
packages/cli/src/commands/sync.ts +1 −1
86 86
87 87
    // Scan local posts
88 88
    consola.start("Scanning local content...");
89 -
    const localPosts = await scanContentDirectory(contentDir, config.include, config.exclude, config.frontmatter);
89 +
    const localPosts = await scanContentDirectory(contentDir, config.frontmatter);
90 90
    consola.info(`Found ${localPosts.length} local posts`);
91 91
92 92
    // Build a map of path -> local post for matching
packages/cli/src/lib/atproto.ts +1 −16
1 1
import { AtpAgent } from "@atproto/api";
2 2
import * as path from "path";
3 -
import type { Credentials, BlogPost, BlobObject, PublisherConfig, PublicationRecord } from "./types";
4 -
import { generateTid } from "./tid";
3 +
import type { Credentials, BlogPost, BlobObject, PublisherConfig } from "./types";
5 4
import { stripMarkdownForText } from "./markdown";
6 5
7 6
export async function resolveHandleToPDS(handle: string): Promise<string> {
184 183
    record.coverImage = coverImage;
185 184
  }
186 185
187 -
  if (config.location) {
188 -
    record.location = config.location;
189 -
  }
190 -
191 -
  const rkey = generateTid();
192 -
193 186
  const response = await agent.com.atproto.repo.createRecord({
194 187
    repo: agent.session!.did,
195 188
    collection: "site.standard.document",
196 -
    rkey,
197 189
    record,
198 190
  });
199 191
233 225
234 226
  if (coverImage) {
235 227
    record.coverImage = coverImage;
236 -
  }
237 -
238 -
  if (config.location) {
239 -
    record.location = config.location;
240 228
  }
241 229
242 230
  await agent.com.atproto.repo.putRecord({
342 330
    };
343 331
  }
344 332
345 -
  const rkey = generateTid();
346 -
347 333
  const response = await agent.com.atproto.repo.createRecord({
348 334
    repo: agent.session!.did,
349 335
    collection: "site.standard.publication",
350 -
    rkey,
351 336
    record,
352 337
  });
353 338
packages/cli/src/lib/config.ts +0 −5
66 66
	pathPrefix?: string;
67 67
	publicationUri: string;
68 68
	pdsUrl?: string;
69 -
	location?: string;
70 69
	frontmatter?: FrontmatterMapping;
71 70
}): string {
72 71
	const config: Record<string, unknown> = {
94 93
95 94
	if (options.pdsUrl && options.pdsUrl !== "https://bsky.social") {
96 95
		config.pdsUrl = options.pdsUrl;
97 -
	}
98 -
99 -
	if (options.location) {
100 -
		config.location = options.location;
101 96
	}
102 97
103 98
	if (options.frontmatter && Object.keys(options.frontmatter).length > 0) {
packages/cli/src/lib/markdown.ts +1 −11
82 82
  const coverField = mapping?.coverImage || "ogImage";
83 83
  frontmatter.ogImage = raw[coverField] || raw.ogImage;
84 84
85 -
  // Hidden mapping
86 -
  const hiddenField = mapping?.hidden || "hidden";
87 -
  frontmatter.hidden = raw[hiddenField] || raw.hidden;
88 -
89 85
  // Tags mapping
90 86
  const tagsField = mapping?.tags || "tags";
91 87
  frontmatter.tags = raw[tagsField] || raw.tags;
113 109
114 110
export async function scanContentDirectory(
115 111
  contentDir: string,
116 -
  include?: string[],
117 -
  exclude?: string[],
118 112
  frontmatterMapping?: FrontmatterMapping
119 113
): Promise<BlogPost[]> {
120 -
  const patterns = include || ["**/*.md", "**/*.mdx"];
114 +
  const patterns = ["**/*.md", "**/*.mdx"];
121 115
  const posts: BlogPost[] = [];
122 116
123 117
  for (const pattern of patterns) {
127 121
      cwd: contentDir,
128 122
      absolute: false,
129 123
    })) {
130 -
      // Check exclusions
131 -
      if (exclude?.some((ex) => relativePath.includes(ex))) {
132 -
        continue;
133 -
      }
134 124
135 125
      const filePath = path.join(contentDir, relativePath);
136 126
      const file = Bun.file(filePath);
packages/cli/src/lib/tid.ts (deleted) +0 −41
1 -
// TID (Timestamp Identifier) generation per ATProto spec
2 -
// Format: base32-sortable encoded, 13 characters
3 -
// Structure: 53 bits of timestamp (microseconds since epoch) + 10 bits of clock ID
4 -
5 -
const S32_CHAR = "234567abcdefghijklmnopqrstuvwxyz";
6 -
7 -
let lastTimestamp = 0;
8 -
let clockId = Math.floor(Math.random() * 1024);
9 -
10 -
export function generateTid(): string {
11 -
  // Get current timestamp in microseconds
12 -
  let timestamp = Date.now() * 1000;
13 -
14 -
  // Ensure monotonically increasing timestamps
15 -
  if (timestamp <= lastTimestamp) {
16 -
    timestamp = lastTimestamp + 1;
17 -
  }
18 -
  lastTimestamp = timestamp;
19 -
20 -
  // Combine timestamp (53 bits) and clock ID (10 bits)
21 -
  // TID is a 63-bit integer encoded as 13 base32 characters
22 -
  const tid = (BigInt(timestamp) << 10n) | BigInt(clockId);
23 -
24 -
  // Convert to base32-sortable
25 -
  let result = "";
26 -
  let value = tid;
27 -
  for (let i = 0; i < 13; i++) {
28 -
    result = S32_CHAR[Number(value % 32n)] + result;
29 -
    value = value / 32n;
30 -
  }
31 -
32 -
  return result;
33 -
}
34 -
35 -
export function isValidTid(tid: string): boolean {
36 -
  if (tid.length !== 13) return false;
37 -
  for (const char of tid) {
38 -
    if (!S32_CHAR.includes(char)) return false;
39 -
  }
40 -
  return true;
41 -
}
packages/cli/src/lib/types.ts +47 −52
1 1
export interface FrontmatterMapping {
2 -
  title?: string;       // Field name for title (default: "title")
3 -
  description?: string; // Field name for description (default: "description")
4 -
  publishDate?: string; // Field name for publish date (default: "publishDate", also checks "pubDate", "date", "createdAt", "created_at")
5 -
  coverImage?: string;  // Field name for cover image (default: "ogImage")
6 -
  hidden?: string;      // Field name for hidden flag (default: "hidden")
7 -
  tags?: string;        // Field name for tags (default: "tags")
2 +
	title?: string; // Field name for title (default: "title")
3 +
	description?: string; // Field name for description (default: "description")
4 +
	publishDate?: string; // Field name for publish date (default: "publishDate", also checks "pubDate", "date", "createdAt", "created_at")
5 +
	coverImage?: string; // Field name for cover image (default: "ogImage")
6 +
	tags?: string; // Field name for tags (default: "tags")
8 7
}
9 8
10 9
export interface PublisherConfig {
11 -
  siteUrl: string;
12 -
  contentDir: string;
13 -
  imagesDir?: string;   // Directory containing cover images
14 -
  publicDir?: string;   // Static/public folder for .well-known files (default: public)
15 -
  outputDir?: string;   // Built output directory for inject command
16 -
  pathPrefix?: string;  // URL path prefix for posts (default: /posts)
17 -
  publicationUri: string;
18 -
  pdsUrl?: string;
19 -
  location?: string;
20 -
  include?: string[];
21 -
  exclude?: string[];
22 -
  identity?: string;    // Which stored identity to use (matches identifier)
23 -
  frontmatter?: FrontmatterMapping; // Custom frontmatter field mappings
10 +
	siteUrl: string;
11 +
	contentDir: string;
12 +
	imagesDir?: string; // Directory containing cover images
13 +
	publicDir?: string; // Static/public folder for .well-known files (default: public)
14 +
	outputDir?: string; // Built output directory for inject command
15 +
	pathPrefix?: string; // URL path prefix for posts (default: /posts)
16 +
	publicationUri: string;
17 +
	pdsUrl?: string;
18 +
	identity?: string; // Which stored identity to use (matches identifier)
19 +
	frontmatter?: FrontmatterMapping; // Custom frontmatter field mappings
24 20
}
25 21
26 22
export interface Credentials {
27 -
  pdsUrl: string;
28 -
  identifier: string;
29 -
  password: string;
23 +
	pdsUrl: string;
24 +
	identifier: string;
25 +
	password: string;
30 26
}
31 27
32 28
export interface PostFrontmatter {
33 -
  title: string;
34 -
  description?: string;
35 -
  publishDate: string;
36 -
  tags?: string[];
37 -
  ogImage?: string;
38 -
  hidden?: boolean;
39 -
  atUri?: string;
29 +
	title: string;
30 +
	description?: string;
31 +
	publishDate: string;
32 +
	tags?: string[];
33 +
	ogImage?: string;
34 +
	atUri?: string;
40 35
}
41 36
42 37
export interface BlogPost {
43 -
  filePath: string;
44 -
  slug: string;
45 -
  frontmatter: PostFrontmatter;
46 -
  content: string;
47 -
  rawContent: string;
38 +
	filePath: string;
39 +
	slug: string;
40 +
	frontmatter: PostFrontmatter;
41 +
	content: string;
42 +
	rawContent: string;
48 43
}
49 44
50 45
export interface BlobRef {
51 -
  $link: string;
46 +
	$link: string;
52 47
}
53 48
54 49
export interface BlobObject {
55 -
  $type: "blob";
56 -
  ref: BlobRef;
57 -
  mimeType: string;
58 -
  size: number;
50 +
	$type: "blob";
51 +
	ref: BlobRef;
52 +
	mimeType: string;
53 +
	size: number;
59 54
}
60 55
61 56
export interface PublisherState {
62 -
  posts: Record<string, PostState>;
57 +
	posts: Record<string, PostState>;
63 58
}
64 59
65 60
export interface PostState {
66 -
  contentHash: string;
67 -
  atUri?: string;
68 -
  lastPublished?: string;
61 +
	contentHash: string;
62 +
	atUri?: string;
63 +
	lastPublished?: string;
69 64
}
70 65
71 66
export interface PublicationRecord {
72 -
  $type: "site.standard.publication";
73 -
  url: string;
74 -
  name: string;
75 -
  description?: string;
76 -
  icon?: BlobObject;
77 -
  createdAt: string;
78 -
  preferences?: {
79 -
    showInDiscover?: boolean;
80 -
  };
67 +
	$type: "site.standard.publication";
68 +
	url: string;
69 +
	name: string;
70 +
	description?: string;
71 +
	icon?: BlobObject;
72 +
	createdAt: string;
73 +
	preferences?: {
74 +
		showInDiscover?: boolean;
75 +
	};
81 76
}