@imjp/writenex-astro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +539 -0
  2. package/dist/chunk-5PM6EQE5.js +151 -0
  3. package/dist/chunk-5PM6EQE5.js.map +1 -0
  4. package/dist/chunk-7XU5X6CW.js +1331 -0
  5. package/dist/chunk-7XU5X6CW.js.map +1 -0
  6. package/dist/chunk-AAOQHQPU.js +574 -0
  7. package/dist/chunk-AAOQHQPU.js.map +1 -0
  8. package/dist/chunk-CF2XXJFF.js +1410 -0
  9. package/dist/chunk-CF2XXJFF.js.map +1 -0
  10. package/dist/chunk-CRPZUUDU.js +52 -0
  11. package/dist/chunk-CRPZUUDU.js.map +1 -0
  12. package/dist/chunk-CYLDJ3HZ.js +310 -0
  13. package/dist/chunk-CYLDJ3HZ.js.map +1 -0
  14. package/dist/chunk-KIKIPIFA.js +1 -0
  15. package/dist/chunk-KIKIPIFA.js.map +1 -0
  16. package/dist/chunk-XNTQTTJU.js +145 -0
  17. package/dist/chunk-XNTQTTJU.js.map +1 -0
  18. package/dist/client/index.css +2 -0
  19. package/dist/client/index.css.map +1 -0
  20. package/dist/client/index.js +375 -0
  21. package/dist/client/index.js.map +1 -0
  22. package/dist/client/styles.css +584 -0
  23. package/dist/client/variables.css +304 -0
  24. package/dist/config/index.d.ts +54 -0
  25. package/dist/config/index.js +38 -0
  26. package/dist/config/index.js.map +1 -0
  27. package/dist/config-BmEdBDo_.d.ts +220 -0
  28. package/dist/content-BWR52vD-.d.ts +64 -0
  29. package/dist/discovery/index.d.ts +310 -0
  30. package/dist/discovery/index.js +38 -0
  31. package/dist/discovery/index.js.map +1 -0
  32. package/dist/errors-C0iYiDTv.d.ts +107 -0
  33. package/dist/filesystem/index.d.ts +1292 -0
  34. package/dist/filesystem/index.js +203 -0
  35. package/dist/filesystem/index.js.map +1 -0
  36. package/dist/image-FP7w5ZIs.d.ts +47 -0
  37. package/dist/index.d.ts +64 -0
  38. package/dist/index.js +151 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/loader-55LWCXHA.js +12 -0
  41. package/dist/loader-55LWCXHA.js.map +1 -0
  42. package/dist/loader-CrdnaAWR.d.ts +327 -0
  43. package/dist/server/index.d.ts +357 -0
  44. package/dist/server/index.js +37 -0
  45. package/dist/server/index.js.map +1 -0
  46. package/package.json +94 -0
  47. package/src/client/App.tsx +900 -0
  48. package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
  49. package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
  50. package/src/client/components/ConfigPanel/index.ts +6 -0
  51. package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
  52. package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
  53. package/src/client/components/CreateContentModal/index.ts +7 -0
  54. package/src/client/components/Editor/Editor.css +885 -0
  55. package/src/client/components/Editor/Editor.tsx +484 -0
  56. package/src/client/components/Editor/ImageDialog.css +344 -0
  57. package/src/client/components/Editor/ImageDialog.tsx +367 -0
  58. package/src/client/components/Editor/LinkDialog.css +326 -0
  59. package/src/client/components/Editor/LinkDialog.tsx +332 -0
  60. package/src/client/components/Editor/index.ts +6 -0
  61. package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
  62. package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
  63. package/src/client/components/FrontmatterForm/index.ts +7 -0
  64. package/src/client/components/Header/Header.css +300 -0
  65. package/src/client/components/Header/Header.tsx +300 -0
  66. package/src/client/components/Header/index.ts +7 -0
  67. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
  68. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
  69. package/src/client/components/KeyboardShortcuts/index.ts +6 -0
  70. package/src/client/components/LazyEditor.tsx +75 -0
  71. package/src/client/components/LiveRegion/LiveRegion.css +19 -0
  72. package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
  73. package/src/client/components/LiveRegion/index.ts +7 -0
  74. package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
  75. package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
  76. package/src/client/components/SearchReplace/index.ts +7 -0
  77. package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
  78. package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
  79. package/src/client/components/SelectCollectionModal/index.ts +7 -0
  80. package/src/client/components/Sidebar/Sidebar.css +570 -0
  81. package/src/client/components/Sidebar/Sidebar.tsx +617 -0
  82. package/src/client/components/Sidebar/index.ts +7 -0
  83. package/src/client/components/SkipLink/SkipLink.css +51 -0
  84. package/src/client/components/SkipLink/SkipLink.tsx +67 -0
  85. package/src/client/components/SkipLink/index.ts +7 -0
  86. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
  87. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
  88. package/src/client/components/UnsavedChangesModal/index.ts +1 -0
  89. package/src/client/components/VersionHistory/DiffViewer.css +430 -0
  90. package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
  91. package/src/client/components/VersionHistory/VersionActions.css +318 -0
  92. package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
  93. package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
  94. package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
  95. package/src/client/components/VersionHistory/index.ts +9 -0
  96. package/src/client/context/ApiContext.tsx +154 -0
  97. package/src/client/context/ThemeContext.tsx +172 -0
  98. package/src/client/hooks/useAnnounce.ts +201 -0
  99. package/src/client/hooks/useApi.ts +374 -0
  100. package/src/client/hooks/useArrowNavigation.ts +286 -0
  101. package/src/client/hooks/useAutosave.ts +241 -0
  102. package/src/client/hooks/useFocusTrap.ts +178 -0
  103. package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
  104. package/src/client/hooks/useSearch.ts +206 -0
  105. package/src/client/hooks/useVersionHistory.ts +451 -0
  106. package/src/client/index.tsx +70 -0
  107. package/src/client/styles.css +584 -0
  108. package/src/client/utils/focus.ts +57 -0
  109. package/src/client/utils/openInEditor.ts +130 -0
  110. package/src/client/variables.css +304 -0
  111. package/src/config/defaults.ts +109 -0
  112. package/src/config/index.ts +32 -0
  113. package/src/config/loader.ts +174 -0
  114. package/src/config/schema.ts +161 -0
  115. package/src/core/constants.ts +39 -0
  116. package/src/core/errors.ts +739 -0
  117. package/src/core/index.ts +11 -0
  118. package/src/discovery/collections.ts +216 -0
  119. package/src/discovery/index.ts +33 -0
  120. package/src/discovery/patterns.ts +702 -0
  121. package/src/discovery/schema.ts +453 -0
  122. package/src/filesystem/images.ts +798 -0
  123. package/src/filesystem/index.ts +107 -0
  124. package/src/filesystem/reader.ts +452 -0
  125. package/src/filesystem/version-config.ts +390 -0
  126. package/src/filesystem/versions.ts +1339 -0
  127. package/src/filesystem/watcher.ts +226 -0
  128. package/src/filesystem/writer.ts +540 -0
  129. package/src/index.ts +61 -0
  130. package/src/integration.ts +228 -0
  131. package/src/server/assets.ts +254 -0
  132. package/src/server/cache.ts +355 -0
  133. package/src/server/index.ts +33 -0
  134. package/src/server/middleware.ts +209 -0
  135. package/src/server/routes.ts +1428 -0
  136. package/src/types/api.ts +61 -0
  137. package/src/types/config.ts +134 -0
  138. package/src/types/content.ts +64 -0
  139. package/src/types/image.ts +48 -0
  140. package/src/types/index.ts +58 -0
  141. package/src/types/version.ts +117 -0
@@ -0,0 +1,107 @@
1
+ /**
2
+ * @fileoverview Filesystem module exports for @writenex/astro
3
+ *
4
+ * This module provides the public API for filesystem operations,
5
+ * including reading, writing, watching content files, version history,
6
+ * and image handling.
7
+ *
8
+ * @module @writenex/astro/filesystem
9
+ */
10
+
11
+ // Reader functions and types
12
+ export {
13
+ readContentFile,
14
+ readCollection,
15
+ getCollectionSummaries,
16
+ getCollectionCount,
17
+ checkCollection,
18
+ getFileStats,
19
+ isContentFile,
20
+ extractSlug,
21
+ generateExcerpt,
22
+ toContentSummary,
23
+ getContentFilePath,
24
+ } from "./reader";
25
+ export type { ReadContentOptions, ReadFileResult } from "./reader";
26
+
27
+ // Writer functions and types
28
+ export {
29
+ createContent,
30
+ updateContent,
31
+ deleteContent,
32
+ generateSlug,
33
+ generateUniqueSlug,
34
+ } from "./writer";
35
+ export type {
36
+ CreateContentOptions,
37
+ UpdateContentOptions,
38
+ WriteResult,
39
+ } from "./writer";
40
+
41
+ // Watcher functions and types
42
+ export {
43
+ ContentWatcher,
44
+ FileModificationTracker,
45
+ createContentWatcher,
46
+ } from "./watcher";
47
+ export type {
48
+ FileChangeType,
49
+ FileChangeEvent,
50
+ WatcherOptions,
51
+ } from "./watcher";
52
+
53
+ // Version history functions
54
+ export {
55
+ saveVersion,
56
+ getVersions,
57
+ getVersion,
58
+ deleteVersion,
59
+ clearVersions,
60
+ pruneVersions,
61
+ restoreVersion,
62
+ generateVersionId,
63
+ parseVersionId,
64
+ getVersionStoragePath,
65
+ getVersionFilePath,
66
+ getManifestPath,
67
+ generatePreview,
68
+ readManifest,
69
+ writeManifest,
70
+ createEmptyManifest,
71
+ recoverManifest,
72
+ getOrRecoverManifest,
73
+ ensureGitignore,
74
+ ensureStorageDirectory,
75
+ } from "./versions";
76
+
77
+ // Version config helpers
78
+ export {
79
+ resolveVersionConfig,
80
+ isVersionHistoryEnabled,
81
+ saveVersionWithConfig,
82
+ getVersionsWithConfig,
83
+ getVersionWithConfig,
84
+ deleteVersionWithConfig,
85
+ clearVersionsWithConfig,
86
+ pruneVersionsWithConfig,
87
+ restoreVersionWithConfig,
88
+ } from "./version-config";
89
+
90
+ // Image functions and types
91
+ export {
92
+ uploadImage,
93
+ parseMultipartFormData,
94
+ isValidImageFile,
95
+ discoverContentImages,
96
+ getContentImageFolder,
97
+ detectContentStructure,
98
+ calculateRelativePath,
99
+ scanDirectoryForImages,
100
+ DEFAULT_IMAGE_CONFIG,
101
+ } from "./images";
102
+ export type {
103
+ ImageUploadResult,
104
+ ImageUploadOptions,
105
+ ContentStructure,
106
+ ContentStructureResult,
107
+ } from "./images";
@@ -0,0 +1,452 @@
1
+ /**
2
+ * @fileoverview Filesystem reader for content collections
3
+ *
4
+ * This module provides functions for reading content files from the filesystem,
5
+ * parsing frontmatter, and extracting content metadata.
6
+ *
7
+ * ## Features:
8
+ * - Read individual content files with frontmatter parsing
9
+ * - List all content files in a collection
10
+ * - Generate content summaries for listing
11
+ * - Support for .md and .mdx files
12
+ *
13
+ * @module @writenex/astro/filesystem/reader
14
+ */
15
+
16
+ import { readFile, readdir, stat } from "node:fs/promises";
17
+ import { existsSync } from "node:fs";
18
+ import { join, basename, extname, relative } from "node:path";
19
+ import matter from "gray-matter";
20
+ import type { ContentItem, ContentSummary } from "@/types";
21
+
22
+ /**
23
+ * Supported content file extensions
24
+ */
25
+ const CONTENT_EXTENSIONS = [".md", ".mdx"];
26
+
27
+ /**
28
+ * Maximum excerpt length in characters
29
+ */
30
+ const EXCERPT_LENGTH = 150;
31
+
32
+ /**
33
+ * Options for reading content
34
+ */
35
+ export interface ReadContentOptions {
36
+ /** Include draft content in listings */
37
+ includeDrafts?: boolean;
38
+ /** Sort field for listings */
39
+ sortBy?: string;
40
+ /** Sort order */
41
+ sortOrder?: "asc" | "desc";
42
+ }
43
+
44
+ /**
45
+ * Result of reading a content file
46
+ */
47
+ export interface ReadFileResult {
48
+ /** Whether the read was successful */
49
+ success: boolean;
50
+ /** The content item (if successful) */
51
+ content?: ContentItem;
52
+ /** Error message (if failed) */
53
+ error?: string;
54
+ }
55
+
56
+ /**
57
+ * Check if a file is a content file based on extension
58
+ *
59
+ * @param filename - The filename to check
60
+ * @returns True if the file is a content file
61
+ */
62
+ export function isContentFile(filename: string): boolean {
63
+ const ext = extname(filename).toLowerCase();
64
+ return CONTENT_EXTENSIONS.includes(ext);
65
+ }
66
+
67
+ /**
68
+ * Extract slug from a content file path
69
+ *
70
+ * Handles various file patterns:
71
+ * - `my-post.md` -> `my-post`
72
+ * - `2024-01-15-my-post.md` -> `2024-01-15-my-post`
73
+ * - `my-post/index.md` -> `my-post`
74
+ *
75
+ * @param filePath - Path to the content file
76
+ * @param collectionPath - Path to the collection directory
77
+ * @returns The extracted slug
78
+ */
79
+ export function extractSlug(filePath: string, collectionPath: string): string {
80
+ const relativePath = relative(collectionPath, filePath);
81
+ const filename = basename(relativePath);
82
+ const ext = extname(filename);
83
+
84
+ // Handle index files (folder-based content)
85
+ if (filename === "index.md" || filename === "index.mdx") {
86
+ const parts = relativePath.split("/");
87
+ if (parts.length >= 2) {
88
+ const slug = parts[parts.length - 2];
89
+ if (slug) return slug;
90
+ }
91
+ }
92
+
93
+ // Remove extension to get slug
94
+ return filename.slice(0, -ext.length);
95
+ }
96
+
97
+ /**
98
+ * Generate an excerpt from markdown content
99
+ *
100
+ * @param body - The markdown body content
101
+ * @param maxLength - Maximum excerpt length
102
+ * @returns The generated excerpt
103
+ */
104
+ export function generateExcerpt(
105
+ body: string,
106
+ maxLength: number = EXCERPT_LENGTH
107
+ ): string {
108
+ // Remove markdown formatting for cleaner excerpt
109
+ const cleaned = body
110
+ // Remove headers
111
+ .replace(/^#{1,6}\s+/gm, "")
112
+ // Remove bold/italic
113
+ .replace(/\*\*([^*]+)\*\*/g, "$1")
114
+ .replace(/\*([^*]+)\*/g, "$1")
115
+ .replace(/__([^_]+)__/g, "$1")
116
+ .replace(/_([^_]+)_/g, "$1")
117
+ // Remove links but keep text
118
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
119
+ // Remove images
120
+ .replace(/!\[([^\]]*)\]\([^)]+\)/g, "")
121
+ // Remove code blocks
122
+ .replace(/```[\s\S]*?```/g, "")
123
+ .replace(/`([^`]+)`/g, "$1")
124
+ // Remove blockquotes
125
+ .replace(/^>\s+/gm, "")
126
+ // Remove horizontal rules
127
+ .replace(/^[-*_]{3,}$/gm, "")
128
+ // Collapse whitespace
129
+ .replace(/\s+/g, " ")
130
+ .trim();
131
+
132
+ if (cleaned.length <= maxLength) {
133
+ return cleaned;
134
+ }
135
+
136
+ // Truncate at word boundary
137
+ const truncated = cleaned.slice(0, maxLength);
138
+ const lastSpace = truncated.lastIndexOf(" ");
139
+
140
+ if (lastSpace > maxLength * 0.7) {
141
+ return truncated.slice(0, lastSpace) + "...";
142
+ }
143
+
144
+ return truncated + "...";
145
+ }
146
+
147
+ /**
148
+ * Read and parse a single content file
149
+ *
150
+ * @param filePath - Absolute path to the content file
151
+ * @param collectionPath - Path to the collection directory
152
+ * @returns ReadFileResult with the parsed content or error
153
+ *
154
+ * @example
155
+ * ```typescript
156
+ * const result = await readContentFile(
157
+ * '/project/src/content/blog/my-post.md',
158
+ * '/project/src/content/blog'
159
+ * );
160
+ *
161
+ * if (result.success) {
162
+ * console.log(result.content.frontmatter.title);
163
+ * }
164
+ * ```
165
+ */
166
+ export async function readContentFile(
167
+ filePath: string,
168
+ collectionPath: string
169
+ ): Promise<ReadFileResult> {
170
+ try {
171
+ // Check if file exists
172
+ if (!existsSync(filePath)) {
173
+ return {
174
+ success: false,
175
+ error: `File not found: ${filePath}`,
176
+ };
177
+ }
178
+
179
+ // Read file content and stats in parallel
180
+ const [raw, stats] = await Promise.all([
181
+ readFile(filePath, "utf-8"),
182
+ stat(filePath),
183
+ ]);
184
+
185
+ // Parse frontmatter
186
+ const { data: frontmatter, content: body } = matter(raw);
187
+
188
+ // Extract slug
189
+ const id = extractSlug(filePath, collectionPath);
190
+
191
+ return {
192
+ success: true,
193
+ content: {
194
+ id,
195
+ path: filePath,
196
+ frontmatter,
197
+ body: body.trim(),
198
+ raw,
199
+ mtime: stats.mtimeMs,
200
+ },
201
+ };
202
+ } catch (error) {
203
+ const message = error instanceof Error ? error.message : String(error);
204
+ return {
205
+ success: false,
206
+ error: `Failed to read content file: ${message}`,
207
+ };
208
+ }
209
+ }
210
+
211
+ /**
212
+ * List all content files in a directory recursively
213
+ *
214
+ * @param dirPath - Path to the directory to scan
215
+ * @returns Array of absolute file paths
216
+ */
217
+ async function listFilesRecursive(dirPath: string): Promise<string[]> {
218
+ const files: string[] = [];
219
+
220
+ if (!existsSync(dirPath)) {
221
+ return files;
222
+ }
223
+
224
+ const entries = await readdir(dirPath, { withFileTypes: true });
225
+
226
+ for (const entry of entries) {
227
+ const fullPath = join(dirPath, entry.name);
228
+
229
+ if (entry.isDirectory()) {
230
+ // Recursively scan subdirectories
231
+ const subFiles = await listFilesRecursive(fullPath);
232
+ files.push(...subFiles);
233
+ } else if (entry.isFile() && isContentFile(entry.name)) {
234
+ files.push(fullPath);
235
+ }
236
+ }
237
+
238
+ return files;
239
+ }
240
+
241
+ /**
242
+ * Read all content files in a collection
243
+ *
244
+ * @param collectionPath - Absolute path to the collection directory
245
+ * @param options - Read options
246
+ * @returns Array of content items
247
+ *
248
+ * @example
249
+ * ```typescript
250
+ * const items = await readCollection('/project/src/content/blog', {
251
+ * includeDrafts: false,
252
+ * sortBy: 'pubDate',
253
+ * sortOrder: 'desc',
254
+ * });
255
+ * ```
256
+ */
257
+ export async function readCollection(
258
+ collectionPath: string,
259
+ options: ReadContentOptions = {}
260
+ ): Promise<ContentItem[]> {
261
+ const { includeDrafts = true, sortBy, sortOrder = "desc" } = options;
262
+
263
+ // Get all content files
264
+ const filePaths = await listFilesRecursive(collectionPath);
265
+
266
+ // Read and parse all files
267
+ const results = await Promise.all(
268
+ filePaths.map((fp) => readContentFile(fp, collectionPath))
269
+ );
270
+
271
+ // Filter successful reads and optionally filter drafts
272
+ let items = results
273
+ .filter(
274
+ (r): r is { success: true; content: ContentItem } =>
275
+ r.success && !!r.content
276
+ )
277
+ .map((r) => r.content)
278
+ .filter((item) => {
279
+ if (!includeDrafts && item.frontmatter.draft === true) {
280
+ return false;
281
+ }
282
+ return true;
283
+ });
284
+
285
+ // Sort if requested
286
+ if (sortBy) {
287
+ items = items.sort((a, b) => {
288
+ const aVal = a.frontmatter[sortBy];
289
+ const bVal = b.frontmatter[sortBy];
290
+
291
+ // Handle undefined values
292
+ if (aVal === undefined && bVal === undefined) return 0;
293
+ if (aVal === undefined) return sortOrder === "asc" ? -1 : 1;
294
+ if (bVal === undefined) return sortOrder === "asc" ? 1 : -1;
295
+
296
+ // Compare values (convert to string for comparison)
297
+ const aStr = String(aVal);
298
+ const bStr = String(bVal);
299
+ if (aStr < bStr) return sortOrder === "asc" ? -1 : 1;
300
+ if (aStr > bStr) return sortOrder === "asc" ? 1 : -1;
301
+ return 0;
302
+ });
303
+ }
304
+
305
+ return items;
306
+ }
307
+
308
+ /**
309
+ * Convert a content item to a summary for listing
310
+ *
311
+ * @param item - The full content item
312
+ * @returns Content summary with essential fields
313
+ */
314
+ export function toContentSummary(item: ContentItem): ContentSummary {
315
+ const { id, path, frontmatter, body } = item;
316
+
317
+ // Support both pubDate and publishDate naming conventions
318
+ const dateValue =
319
+ frontmatter.pubDate ?? frontmatter.publishDate ?? frontmatter.date;
320
+
321
+ return {
322
+ id,
323
+ path,
324
+ title: String(frontmatter.title ?? id),
325
+ pubDate: dateValue ? String(dateValue) : undefined,
326
+ draft: frontmatter.draft === true,
327
+ excerpt: generateExcerpt(body),
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Get content summaries for a collection
333
+ *
334
+ * @param collectionPath - Absolute path to the collection directory
335
+ * @param options - Read options
336
+ * @returns Array of content summaries
337
+ */
338
+ export async function getCollectionSummaries(
339
+ collectionPath: string,
340
+ options: ReadContentOptions = {}
341
+ ): Promise<ContentSummary[]> {
342
+ const items = await readCollection(collectionPath, options);
343
+ return items.map(toContentSummary);
344
+ }
345
+
346
+ /**
347
+ * Get the count of content files in a collection
348
+ *
349
+ * @param collectionPath - Absolute path to the collection directory
350
+ * @returns Number of content files
351
+ */
352
+ export async function getCollectionCount(
353
+ collectionPath: string
354
+ ): Promise<number> {
355
+ const filePaths = await listFilesRecursive(collectionPath);
356
+ return filePaths.length;
357
+ }
358
+
359
+ /**
360
+ * Check if a collection directory exists and contains content
361
+ *
362
+ * @param collectionPath - Absolute path to the collection directory
363
+ * @returns Object with exists and hasContent flags
364
+ */
365
+ export async function checkCollection(collectionPath: string): Promise<{
366
+ exists: boolean;
367
+ hasContent: boolean;
368
+ count: number;
369
+ }> {
370
+ if (!existsSync(collectionPath)) {
371
+ return { exists: false, hasContent: false, count: 0 };
372
+ }
373
+
374
+ const count = await getCollectionCount(collectionPath);
375
+
376
+ return {
377
+ exists: true,
378
+ hasContent: count > 0,
379
+ count,
380
+ };
381
+ }
382
+
383
+ /**
384
+ * Get file stats for a content file
385
+ *
386
+ * @param filePath - Path to the content file
387
+ * @returns File stats or null if file doesn't exist
388
+ */
389
+ export async function getFileStats(filePath: string): Promise<{
390
+ size: number;
391
+ mtime: Date;
392
+ ctime: Date;
393
+ } | null> {
394
+ try {
395
+ const stats = await stat(filePath);
396
+ return {
397
+ size: stats.size,
398
+ mtime: stats.mtime,
399
+ ctime: stats.birthtime,
400
+ };
401
+ } catch {
402
+ return null;
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Get the file path for a content item by ID
408
+ *
409
+ * Searches for the content file in the collection directory,
410
+ * handling different content structures:
411
+ * - Folder-based: `slug/index.md` or `slug/index.mdx`
412
+ * - Flat file: `slug.md` or `slug.mdx`
413
+ *
414
+ * @param collectionPath - Path to the collection directory
415
+ * @param contentId - Content ID (slug)
416
+ * @returns File path if found, null otherwise
417
+ *
418
+ * @example
419
+ * ```typescript
420
+ * const filePath = getContentFilePath('/project/src/content/blog', 'my-post');
421
+ * // Returns: '/project/src/content/blog/my-post.md' or
422
+ * // '/project/src/content/blog/my-post/index.md'
423
+ * ```
424
+ */
425
+ export function getContentFilePath(
426
+ collectionPath: string,
427
+ contentId: string
428
+ ): string | null {
429
+ // Try folder-based structure first (slug/index.md or slug/index.mdx)
430
+ const indexMdPath = join(collectionPath, contentId, "index.md");
431
+ if (existsSync(indexMdPath)) {
432
+ return indexMdPath;
433
+ }
434
+
435
+ const indexMdxPath = join(collectionPath, contentId, "index.mdx");
436
+ if (existsSync(indexMdxPath)) {
437
+ return indexMdxPath;
438
+ }
439
+
440
+ // Try flat file structure (slug.md or slug.mdx)
441
+ const flatMdPath = join(collectionPath, `${contentId}.md`);
442
+ if (existsSync(flatMdPath)) {
443
+ return flatMdPath;
444
+ }
445
+
446
+ const flatMdxPath = join(collectionPath, `${contentId}.mdx`);
447
+ if (existsSync(flatMdxPath)) {
448
+ return flatMdxPath;
449
+ }
450
+
451
+ return null;
452
+ }