@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 @@
1
+ {"version":3,"sources":["../src/core/errors.ts","../src/filesystem/versions.ts","../src/filesystem/writer.ts","../src/filesystem/images.ts"],"sourcesContent":["/**\n * @fileoverview Custom error classes for @writenex/astro\n *\n * This module provides categorized error types for better error handling,\n * debugging, and user-facing error messages across the integration.\n *\n * ## Error Categories:\n * - Configuration errors (invalid config, missing files)\n * - Filesystem errors (read/write failures, permissions)\n * - Content errors (parsing, validation, not found)\n * - API errors (request handling, validation)\n * - Version history errors (manifest, storage)\n *\n * @module @writenex/astro/core/errors\n */\n\n/**\n * Error codes for categorization and i18n support\n */\nexport enum WritenexErrorCode {\n // Configuration errors (1xx)\n CONFIG_NOT_FOUND = \"CONFIG_NOT_FOUND\",\n CONFIG_INVALID = \"CONFIG_INVALID\",\n CONFIG_PARSE_ERROR = \"CONFIG_PARSE_ERROR\",\n\n // Filesystem errors (2xx)\n FS_READ_ERROR = \"FS_READ_ERROR\",\n FS_WRITE_ERROR = \"FS_WRITE_ERROR\",\n FS_DELETE_ERROR = \"FS_DELETE_ERROR\",\n FS_PERMISSION_DENIED = \"FS_PERMISSION_DENIED\",\n FS_PATH_NOT_FOUND = \"FS_PATH_NOT_FOUND\",\n FS_PATH_TRAVERSAL = \"FS_PATH_TRAVERSAL\",\n\n // Content errors (3xx)\n CONTENT_NOT_FOUND = \"CONTENT_NOT_FOUND\",\n CONTENT_PARSE_ERROR = \"CONTENT_PARSE_ERROR\",\n CONTENT_VALIDATION_ERROR = \"CONTENT_VALIDATION_ERROR\",\n CONTENT_ALREADY_EXISTS = \"CONTENT_ALREADY_EXISTS\",\n CONTENT_INVALID_SLUG = \"CONTENT_INVALID_SLUG\",\n CONTENT_CONFLICT = \"CONTENT_CONFLICT\",\n\n // Collection errors (4xx)\n COLLECTION_NOT_FOUND = \"COLLECTION_NOT_FOUND\",\n COLLECTION_EMPTY = \"COLLECTION_EMPTY\",\n COLLECTION_DISCOVERY_ERROR = \"COLLECTION_DISCOVERY_ERROR\",\n\n // API errors (5xx)\n API_BAD_REQUEST = \"API_BAD_REQUEST\",\n API_METHOD_NOT_ALLOWED = \"API_METHOD_NOT_ALLOWED\",\n API_INTERNAL_ERROR = \"API_INTERNAL_ERROR\",\n API_TIMEOUT = \"API_TIMEOUT\",\n\n // Image errors (6xx)\n IMAGE_INVALID_TYPE = \"IMAGE_INVALID_TYPE\",\n IMAGE_TOO_LARGE = \"IMAGE_TOO_LARGE\",\n IMAGE_UPLOAD_ERROR = \"IMAGE_UPLOAD_ERROR\",\n IMAGE_NOT_FOUND = \"IMAGE_NOT_FOUND\",\n\n // Version history errors (7xx)\n VERSION_NOT_FOUND = \"VERSION_NOT_FOUND\",\n VERSION_MANIFEST_CORRUPT = \"VERSION_MANIFEST_CORRUPT\",\n VERSION_LOCK_TIMEOUT = \"VERSION_LOCK_TIMEOUT\",\n VERSION_SAVE_ERROR = \"VERSION_SAVE_ERROR\",\n VERSION_RESTORE_ERROR = \"VERSION_RESTORE_ERROR\",\n\n // Pattern errors (8xx)\n PATTERN_INVALID = \"PATTERN_INVALID\",\n PATTERN_MISSING_TOKEN = \"PATTERN_MISSING_TOKEN\",\n\n // Unknown\n UNKNOWN_ERROR = \"UNKNOWN_ERROR\",\n}\n\n/**\n * HTTP status codes mapped to error categories\n */\nexport const ERROR_HTTP_STATUS: Record<WritenexErrorCode, number> = {\n // Configuration errors - 500 (server misconfiguration)\n [WritenexErrorCode.CONFIG_NOT_FOUND]: 500,\n [WritenexErrorCode.CONFIG_INVALID]: 500,\n [WritenexErrorCode.CONFIG_PARSE_ERROR]: 500,\n\n // Filesystem errors\n [WritenexErrorCode.FS_READ_ERROR]: 500,\n [WritenexErrorCode.FS_WRITE_ERROR]: 500,\n [WritenexErrorCode.FS_DELETE_ERROR]: 500,\n [WritenexErrorCode.FS_PERMISSION_DENIED]: 403,\n [WritenexErrorCode.FS_PATH_NOT_FOUND]: 404,\n [WritenexErrorCode.FS_PATH_TRAVERSAL]: 400,\n\n // Content errors\n [WritenexErrorCode.CONTENT_NOT_FOUND]: 404,\n [WritenexErrorCode.CONTENT_PARSE_ERROR]: 500,\n [WritenexErrorCode.CONTENT_VALIDATION_ERROR]: 400,\n [WritenexErrorCode.CONTENT_ALREADY_EXISTS]: 409,\n [WritenexErrorCode.CONTENT_INVALID_SLUG]: 400,\n [WritenexErrorCode.CONTENT_CONFLICT]: 409,\n\n // Collection errors\n [WritenexErrorCode.COLLECTION_NOT_FOUND]: 404,\n [WritenexErrorCode.COLLECTION_EMPTY]: 404,\n [WritenexErrorCode.COLLECTION_DISCOVERY_ERROR]: 500,\n\n // API errors\n [WritenexErrorCode.API_BAD_REQUEST]: 400,\n [WritenexErrorCode.API_METHOD_NOT_ALLOWED]: 405,\n [WritenexErrorCode.API_INTERNAL_ERROR]: 500,\n [WritenexErrorCode.API_TIMEOUT]: 504,\n\n // Image errors\n [WritenexErrorCode.IMAGE_INVALID_TYPE]: 400,\n [WritenexErrorCode.IMAGE_TOO_LARGE]: 413,\n [WritenexErrorCode.IMAGE_UPLOAD_ERROR]: 500,\n [WritenexErrorCode.IMAGE_NOT_FOUND]: 404,\n\n // Version history errors\n [WritenexErrorCode.VERSION_NOT_FOUND]: 404,\n [WritenexErrorCode.VERSION_MANIFEST_CORRUPT]: 500,\n [WritenexErrorCode.VERSION_LOCK_TIMEOUT]: 503,\n [WritenexErrorCode.VERSION_SAVE_ERROR]: 500,\n [WritenexErrorCode.VERSION_RESTORE_ERROR]: 500,\n\n // Pattern errors\n [WritenexErrorCode.PATTERN_INVALID]: 400,\n [WritenexErrorCode.PATTERN_MISSING_TOKEN]: 400,\n\n // Unknown\n [WritenexErrorCode.UNKNOWN_ERROR]: 500,\n};\n\n/**\n * Base error class for all Writenex errors\n *\n * Provides structured error information including error code,\n * HTTP status, and optional context data for debugging.\n */\nexport class WritenexError extends Error {\n /** Error code for categorization */\n readonly code: WritenexErrorCode;\n\n /** HTTP status code for API responses */\n readonly httpStatus: number;\n\n /** Additional context data for debugging */\n readonly context?: Record<string, unknown>;\n\n /** Original error if this wraps another error */\n readonly cause?: Error;\n\n constructor(\n code: WritenexErrorCode,\n message: string,\n options?: {\n context?: Record<string, unknown>;\n cause?: Error;\n }\n ) {\n super(message);\n this.name = \"WritenexError\";\n this.code = code;\n this.httpStatus = ERROR_HTTP_STATUS[code];\n this.context = options?.context;\n this.cause = options?.cause;\n\n // Maintains proper stack trace for where error was thrown\n if (Error.captureStackTrace) {\n Error.captureStackTrace(this, WritenexError);\n }\n }\n\n /**\n * Convert error to JSON for API responses\n */\n toJSON(): Record<string, unknown> {\n return {\n error: this.message,\n code: this.code,\n ...(this.context ? { context: this.context } : {}),\n };\n }\n\n /**\n * Create a user-friendly error message\n */\n toUserMessage(): string {\n return this.message;\n }\n}\n\n// =============================================================================\n// Configuration Errors\n// =============================================================================\n\n/**\n * Error thrown when configuration file is not found\n */\nexport class ConfigNotFoundError extends WritenexError {\n constructor(searchPaths: string[]) {\n super(\n WritenexErrorCode.CONFIG_NOT_FOUND,\n `Configuration file not found. Searched: ${searchPaths.join(\", \")}`,\n { context: { searchPaths } }\n );\n this.name = \"ConfigNotFoundError\";\n }\n}\n\n/**\n * Error thrown when configuration is invalid\n */\nexport class ConfigInvalidError extends WritenexError {\n constructor(errors: string[], configPath?: string) {\n super(\n WritenexErrorCode.CONFIG_INVALID,\n `Invalid configuration: ${errors.join(\"; \")}`,\n { context: { errors, configPath } }\n );\n this.name = \"ConfigInvalidError\";\n }\n}\n\n/**\n * Error thrown when configuration file cannot be parsed\n */\nexport class ConfigParseError extends WritenexError {\n constructor(configPath: string, cause?: Error) {\n super(\n WritenexErrorCode.CONFIG_PARSE_ERROR,\n `Failed to parse configuration file: ${configPath}`,\n { context: { configPath }, cause }\n );\n this.name = \"ConfigParseError\";\n }\n}\n\n// =============================================================================\n// Filesystem Errors\n// =============================================================================\n\n/**\n * Error thrown when file read operation fails\n */\nexport class FileReadError extends WritenexError {\n constructor(filePath: string, cause?: Error) {\n super(WritenexErrorCode.FS_READ_ERROR, `Failed to read file: ${filePath}`, {\n context: { filePath },\n cause,\n });\n this.name = \"FileReadError\";\n }\n}\n\n/**\n * Error thrown when file write operation fails\n */\nexport class FileWriteError extends WritenexError {\n constructor(filePath: string, cause?: Error) {\n super(\n WritenexErrorCode.FS_WRITE_ERROR,\n `Failed to write file: ${filePath}`,\n { context: { filePath }, cause }\n );\n this.name = \"FileWriteError\";\n }\n}\n\n/**\n * Error thrown when file delete operation fails\n */\nexport class FileDeleteError extends WritenexError {\n constructor(filePath: string, cause?: Error) {\n super(\n WritenexErrorCode.FS_DELETE_ERROR,\n `Failed to delete file: ${filePath}`,\n { context: { filePath }, cause }\n );\n this.name = \"FileDeleteError\";\n }\n}\n\n/**\n * Error thrown when path is not found\n */\nexport class PathNotFoundError extends WritenexError {\n constructor(path: string, type: \"file\" | \"directory\" = \"file\") {\n super(\n WritenexErrorCode.FS_PATH_NOT_FOUND,\n `${type === \"directory\" ? \"Directory\" : \"File\"} not found: ${path}`,\n { context: { path, type } }\n );\n this.name = \"PathNotFoundError\";\n }\n}\n\n/**\n * Error thrown when path traversal attack is detected\n */\nexport class PathTraversalError extends WritenexError {\n constructor(requestedPath: string, basePath: string) {\n super(\n WritenexErrorCode.FS_PATH_TRAVERSAL,\n \"Invalid path: attempted path traversal detected\",\n { context: { requestedPath, basePath } }\n );\n this.name = \"PathTraversalError\";\n }\n}\n\n// =============================================================================\n// Content Errors\n// =============================================================================\n\n/**\n * Error thrown when content item is not found\n */\nexport class ContentNotFoundError extends WritenexError {\n constructor(collection: string, contentId: string) {\n super(\n WritenexErrorCode.CONTENT_NOT_FOUND,\n `Content '${contentId}' not found in collection '${collection}'`,\n { context: { collection, contentId } }\n );\n this.name = \"ContentNotFoundError\";\n }\n}\n\n/**\n * Error thrown when content parsing fails\n */\nexport class ContentParseError extends WritenexError {\n constructor(filePath: string, cause?: Error) {\n super(\n WritenexErrorCode.CONTENT_PARSE_ERROR,\n `Failed to parse content file: ${filePath}`,\n { context: { filePath }, cause }\n );\n this.name = \"ContentParseError\";\n }\n}\n\n/**\n * Error thrown when content validation fails\n */\nexport class ContentValidationError extends WritenexError {\n constructor(errors: string[], contentId?: string) {\n super(\n WritenexErrorCode.CONTENT_VALIDATION_ERROR,\n `Content validation failed: ${errors.join(\"; \")}`,\n { context: { errors, contentId } }\n );\n this.name = \"ContentValidationError\";\n }\n}\n\n/**\n * Error thrown when content already exists\n */\nexport class ContentAlreadyExistsError extends WritenexError {\n constructor(collection: string, slug: string) {\n super(\n WritenexErrorCode.CONTENT_ALREADY_EXISTS,\n `Content with slug '${slug}' already exists in collection '${collection}'`,\n { context: { collection, slug } }\n );\n this.name = \"ContentAlreadyExistsError\";\n }\n}\n\n/**\n * Error thrown when content was modified externally (conflict detection)\n *\n * This error includes both the server version and the client's expected mtime\n * to help resolve the conflict.\n */\nexport class ContentConflictError extends WritenexError {\n /** Current content on disk */\n readonly serverContent: string;\n /** Server's current mtime */\n readonly serverMtime: number;\n /** Client's expected mtime */\n readonly clientMtime: number;\n\n constructor(\n collection: string,\n contentId: string,\n serverContent: string,\n serverMtime: number,\n clientMtime: number\n ) {\n super(\n WritenexErrorCode.CONTENT_CONFLICT,\n `Content '${contentId}' in '${collection}' was modified externally. ` +\n `Expected mtime: ${clientMtime}, actual: ${serverMtime}`,\n {\n context: {\n collection,\n contentId,\n serverMtime,\n clientMtime,\n timeDiff: serverMtime - clientMtime,\n },\n }\n );\n this.name = \"ContentConflictError\";\n this.serverContent = serverContent;\n this.serverMtime = serverMtime;\n this.clientMtime = clientMtime;\n }\n\n /**\n * Override toJSON to include conflict-specific data\n */\n override toJSON(): Record<string, unknown> {\n return {\n ...super.toJSON(),\n serverContent: this.serverContent,\n serverMtime: this.serverMtime,\n clientMtime: this.clientMtime,\n };\n }\n}\n\n// =============================================================================\n// Collection Errors\n// =============================================================================\n\n/**\n * Error thrown when collection is not found\n */\nexport class CollectionNotFoundError extends WritenexError {\n constructor(collectionName: string) {\n super(\n WritenexErrorCode.COLLECTION_NOT_FOUND,\n `Collection '${collectionName}' not found`,\n { context: { collectionName } }\n );\n this.name = \"CollectionNotFoundError\";\n }\n}\n\n/**\n * Error thrown when collection discovery fails\n */\nexport class CollectionDiscoveryError extends WritenexError {\n constructor(contentPath: string, cause?: Error) {\n super(\n WritenexErrorCode.COLLECTION_DISCOVERY_ERROR,\n `Failed to discover collections in: ${contentPath}`,\n { context: { contentPath }, cause }\n );\n this.name = \"CollectionDiscoveryError\";\n }\n}\n\n// =============================================================================\n// API Errors\n// =============================================================================\n\n/**\n * Error thrown for bad API requests\n */\nexport class ApiBadRequestError extends WritenexError {\n constructor(message: string, details?: Record<string, unknown>) {\n super(WritenexErrorCode.API_BAD_REQUEST, message, { context: details });\n this.name = \"ApiBadRequestError\";\n }\n}\n\n/**\n * Error thrown when HTTP method is not allowed\n */\nexport class ApiMethodNotAllowedError extends WritenexError {\n constructor(method: string, allowedMethods: string[]) {\n super(\n WritenexErrorCode.API_METHOD_NOT_ALLOWED,\n `Method ${method} not allowed. Allowed: ${allowedMethods.join(\", \")}`,\n { context: { method, allowedMethods } }\n );\n this.name = \"ApiMethodNotAllowedError\";\n }\n}\n\n/**\n * Error thrown for API timeout\n */\nexport class ApiTimeoutError extends WritenexError {\n constructor(operation: string, timeoutMs: number) {\n super(\n WritenexErrorCode.API_TIMEOUT,\n `Operation '${operation}' timed out after ${timeoutMs}ms`,\n { context: { operation, timeoutMs } }\n );\n this.name = \"ApiTimeoutError\";\n }\n}\n\n// =============================================================================\n// Image Errors\n// =============================================================================\n\n/**\n * Error thrown when image type is invalid\n */\nexport class ImageInvalidTypeError extends WritenexError {\n constructor(filename: string, supportedTypes: string[]) {\n super(\n WritenexErrorCode.IMAGE_INVALID_TYPE,\n `Invalid image type for '${filename}'. Supported: ${supportedTypes.join(\", \")}`,\n { context: { filename, supportedTypes } }\n );\n this.name = \"ImageInvalidTypeError\";\n }\n}\n\n/**\n * Error thrown when image is too large\n */\nexport class ImageTooLargeError extends WritenexError {\n constructor(filename: string, size: number, maxSize: number) {\n super(\n WritenexErrorCode.IMAGE_TOO_LARGE,\n `Image '${filename}' is too large (${formatBytes(size)}). Maximum: ${formatBytes(maxSize)}`,\n { context: { filename, size, maxSize } }\n );\n this.name = \"ImageTooLargeError\";\n }\n}\n\n/**\n * Error thrown when image upload fails\n */\nexport class ImageUploadError extends WritenexError {\n constructor(filename: string, cause?: Error) {\n super(\n WritenexErrorCode.IMAGE_UPLOAD_ERROR,\n `Failed to upload image: ${filename}`,\n { context: { filename }, cause }\n );\n this.name = \"ImageUploadError\";\n }\n}\n\n/**\n * Error thrown when image is not found\n */\nexport class ImageNotFoundError extends WritenexError {\n constructor(imagePath: string) {\n super(WritenexErrorCode.IMAGE_NOT_FOUND, `Image not found: ${imagePath}`, {\n context: { imagePath },\n });\n this.name = \"ImageNotFoundError\";\n }\n}\n\n// =============================================================================\n// Version History Errors\n// =============================================================================\n\n/**\n * Error thrown when version is not found\n */\nexport class VersionNotFoundError extends WritenexError {\n constructor(collection: string, contentId: string, versionId: string) {\n super(\n WritenexErrorCode.VERSION_NOT_FOUND,\n `Version '${versionId}' not found for content '${contentId}' in '${collection}'`,\n { context: { collection, contentId, versionId } }\n );\n this.name = \"VersionNotFoundError\";\n }\n}\n\n/**\n * Error thrown when version manifest is corrupt\n */\nexport class VersionManifestCorruptError extends WritenexError {\n constructor(manifestPath: string, cause?: Error) {\n super(\n WritenexErrorCode.VERSION_MANIFEST_CORRUPT,\n `Version manifest is corrupt: ${manifestPath}`,\n { context: { manifestPath }, cause }\n );\n this.name = \"VersionManifestCorruptError\";\n }\n}\n\n/**\n * Error thrown when version lock times out\n */\nexport class VersionLockTimeoutError extends WritenexError {\n constructor(storagePath: string, timeoutMs: number) {\n super(\n WritenexErrorCode.VERSION_LOCK_TIMEOUT,\n `Timeout waiting for version lock on ${storagePath} after ${timeoutMs}ms`,\n { context: { storagePath, timeoutMs } }\n );\n this.name = \"VersionLockTimeoutError\";\n }\n}\n\n/**\n * Error thrown when version save fails\n */\nexport class VersionSaveError extends WritenexError {\n constructor(collection: string, contentId: string, cause?: Error) {\n super(\n WritenexErrorCode.VERSION_SAVE_ERROR,\n `Failed to save version for '${contentId}' in '${collection}'`,\n { context: { collection, contentId }, cause }\n );\n this.name = \"VersionSaveError\";\n }\n}\n\n/**\n * Error thrown when version restore fails\n */\nexport class VersionRestoreError extends WritenexError {\n constructor(\n collection: string,\n contentId: string,\n versionId: string,\n cause?: Error\n ) {\n super(\n WritenexErrorCode.VERSION_RESTORE_ERROR,\n `Failed to restore version '${versionId}' for '${contentId}' in '${collection}'`,\n { context: { collection, contentId, versionId }, cause }\n );\n this.name = \"VersionRestoreError\";\n }\n}\n\n// =============================================================================\n// Pattern Errors\n// =============================================================================\n\n/**\n * Error thrown when file pattern is invalid\n */\nexport class PatternInvalidError extends WritenexError {\n constructor(pattern: string, reason: string) {\n super(\n WritenexErrorCode.PATTERN_INVALID,\n `Invalid file pattern '${pattern}': ${reason}`,\n { context: { pattern, reason } }\n );\n this.name = \"PatternInvalidError\";\n }\n}\n\n/**\n * Error thrown when required pattern token is missing\n */\nexport class PatternMissingTokenError extends WritenexError {\n constructor(pattern: string, missingToken: string) {\n super(\n WritenexErrorCode.PATTERN_MISSING_TOKEN,\n `Pattern '${pattern}' is missing required token: {${missingToken}}`,\n { context: { pattern, missingToken } }\n );\n this.name = \"PatternMissingTokenError\";\n }\n}\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Format bytes to human-readable string\n */\nfunction formatBytes(bytes: number): string {\n if (bytes === 0) return \"0 Bytes\";\n const k = 1024;\n const sizes = [\"Bytes\", \"KB\", \"MB\", \"GB\"];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + \" \" + sizes[i];\n}\n\n/**\n * Check if an error is a WritenexError\n */\nexport function isWritenexError(error: unknown): error is WritenexError {\n return error instanceof WritenexError;\n}\n\n/**\n * Wrap an unknown error as a WritenexError\n *\n * Useful for catch blocks where the error type is unknown.\n */\nexport function wrapError(\n error: unknown,\n defaultCode: WritenexErrorCode = WritenexErrorCode.UNKNOWN_ERROR\n): WritenexError {\n if (isWritenexError(error)) {\n return error;\n }\n\n const message = error instanceof Error ? error.message : String(error);\n const cause = error instanceof Error ? error : undefined;\n\n return new WritenexError(defaultCode, message, { cause });\n}\n\n/**\n * Create error from Node.js filesystem error\n */\nexport function fromNodeError(\n error: NodeJS.ErrnoException,\n filePath: string\n): WritenexError {\n switch (error.code) {\n case \"ENOENT\":\n return new PathNotFoundError(filePath);\n case \"EACCES\":\n case \"EPERM\":\n return new WritenexError(\n WritenexErrorCode.FS_PERMISSION_DENIED,\n `Permission denied: ${filePath}`,\n { context: { filePath, errno: error.code }, cause: error }\n );\n case \"EISDIR\":\n return new WritenexError(\n WritenexErrorCode.FS_READ_ERROR,\n `Expected file but found directory: ${filePath}`,\n { context: { filePath }, cause: error }\n );\n case \"ENOTDIR\":\n return new WritenexError(\n WritenexErrorCode.FS_READ_ERROR,\n `Expected directory but found file: ${filePath}`,\n { context: { filePath }, cause: error }\n );\n default:\n return new FileReadError(filePath, error);\n }\n}\n","/**\n * @fileoverview Version history management for content files\n *\n * This module provides functions for creating, reading, and managing\n * version history (shadow copies) of content files. Versions are stored\n * as markdown files in a hidden directory structure with a JSON manifest\n * tracking metadata.\n *\n * ## Storage Structure:\n * ```\n * .writenex/versions/\n * ├── .gitignore # Contains \"*\" to exclude from Git\n * └── {collection}/\n * └── {contentId}/\n * ├── manifest.json # Version metadata\n * └── {timestamp}.md # Version files\n * ```\n *\n * @module @writenex/astro/filesystem/versions\n * @see {@link VersionEntry} - Version metadata type\n * @see {@link VersionManifest} - Manifest structure type\n */\n\nimport {\n readFile,\n writeFile,\n mkdir,\n readdir,\n stat,\n unlink,\n} from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, basename } from \"node:path\";\nimport matter from \"gray-matter\";\nimport type {\n VersionEntry,\n VersionManifest,\n VersionHistoryConfig,\n Version,\n VersionResult,\n SaveVersionOptions,\n RestoreVersionOptions,\n RestoreResult,\n} from \"@/types\";\n\n// =============================================================================\n// Constants\n// =============================================================================\n\n/** Maximum characters for content preview */\nconst PREVIEW_MAX_LENGTH = 100;\n\n/** Default gitignore content for version storage */\nconst GITIGNORE_CONTENT = \"*\\n\";\n\n/** Frontmatter key for storing version label (prefixed to avoid conflicts) */\nconst LABEL_FRONTMATTER_KEY = \"_writenex_label\";\n\n/** Lock timeout in milliseconds */\nconst LOCK_TIMEOUT_MS = 30000;\n\n/** Lock retry interval in milliseconds */\nconst LOCK_RETRY_INTERVAL_MS = 50;\n\n// =============================================================================\n// Locking Mechanism\n// =============================================================================\n\n/**\n * In-memory lock manager for preventing concurrent manifest operations.\n *\n * Uses a Map to track locks per storage path, with each lock containing\n * a promise that resolves when the lock is released.\n */\ninterface LockEntry {\n /** Promise that resolves when lock is released */\n promise: Promise<void>;\n /** Function to release the lock */\n release: () => void;\n /** Timestamp when lock was acquired */\n acquiredAt: number;\n}\n\n/** Map of storage paths to their lock entries */\nconst locks = new Map<string, LockEntry>();\n\n/**\n * Acquire a lock for a specific storage path.\n *\n * If the path is already locked, waits until the lock is released\n * or timeout is reached.\n *\n * @param storagePath - Path to lock\n * @param timeoutMs - Maximum time to wait for lock (default: 30s)\n * @returns Release function to call when done\n * @throws Error if lock cannot be acquired within timeout\n */\nasync function acquireLock(\n storagePath: string,\n timeoutMs: number = LOCK_TIMEOUT_MS\n): Promise<() => void> {\n const startTime = Date.now();\n\n // Wait for existing lock to be released\n while (locks.has(storagePath)) {\n const existingLock = locks.get(storagePath)!;\n\n // Check for stale lock (acquired more than timeout ago)\n if (Date.now() - existingLock.acquiredAt > timeoutMs) {\n console.warn(\n `[writenex] Releasing stale lock for ${storagePath} (held for ${Date.now() - existingLock.acquiredAt}ms)`\n );\n existingLock.release();\n locks.delete(storagePath);\n break;\n }\n\n // Check if we've exceeded our timeout\n if (Date.now() - startTime > timeoutMs) {\n throw new Error(\n `[writenex] Timeout waiting for lock on ${storagePath} after ${timeoutMs}ms`\n );\n }\n\n // Wait for lock to be released or retry interval\n await Promise.race([\n existingLock.promise,\n new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS)),\n ]);\n }\n\n // Create new lock\n let releaseFunc: () => void;\n const lockPromise = new Promise<void>((resolve) => {\n releaseFunc = resolve;\n });\n\n const lockEntry: LockEntry = {\n promise: lockPromise,\n release: releaseFunc!,\n acquiredAt: Date.now(),\n };\n\n locks.set(storagePath, lockEntry);\n\n // Return release function that also cleans up the map\n return () => {\n lockEntry.release();\n locks.delete(storagePath);\n };\n}\n\n/**\n * Execute a function with an exclusive lock on the storage path.\n *\n * Ensures only one operation can modify the manifest at a time,\n * preventing race conditions during concurrent saves.\n *\n * @param storagePath - Path to lock\n * @param fn - Function to execute while holding the lock\n * @returns Result of the function\n */\nasync function withLock<T>(\n storagePath: string,\n fn: () => Promise<T>\n): Promise<T> {\n const release = await acquireLock(storagePath);\n try {\n return await fn();\n } finally {\n release();\n }\n}\n\n// =============================================================================\n// Utility Functions\n// =============================================================================\n\n/**\n * Generate a unique version ID based on current timestamp with random suffix.\n *\n * The ID is an ISO-8601 timestamp with colons replaced by hyphens,\n * plus a 4-character random suffix to ensure uniqueness even when\n * multiple versions are created within the same millisecond.\n *\n * Format: YYYY-MM-DDTHH-MM-SS.mmmZ-xxxx\n * Where xxxx is a random alphanumeric suffix.\n *\n * @returns Version ID string (e.g., \"2024-12-11T10-30-00.000Z-a1b2\")\n *\n * @example\n * ```typescript\n * const id = generateVersionId();\n * // Returns: \"2024-12-11T10-30-00.000Z-a1b2\"\n * ```\n */\nexport function generateVersionId(): string {\n const timestamp = new Date().toISOString().replace(/:/g, \"-\");\n const randomSuffix = Math.random().toString(36).substring(2, 6);\n return `${timestamp}-${randomSuffix}`;\n}\n\n/**\n * Parse a version ID back to a Date object.\n *\n * Handles both old format (without suffix) and new format (with random suffix).\n *\n * @param versionId - Version ID string\n * @returns Date object or null if invalid\n */\nexport function parseVersionId(versionId: string): Date | null {\n // Remove random suffix if present (format: ...Z-xxxx)\n const withoutSuffix = versionId.replace(/-[a-z0-9]{4}$/, \"\");\n\n // Convert hyphens back to colons for ISO parsing\n // Format: 2024-12-11T10-30-00.000Z -> 2024-12-11T10:30:00.000Z\n // Note: The dot before milliseconds is preserved by generateVersionId()\n const isoString = withoutSuffix.replace(\n /T(\\d{2})-(\\d{2})-(\\d{2})\\.(\\d{3})Z/,\n \"T$1:$2:$3.$4Z\"\n );\n\n const date = new Date(isoString);\n return isNaN(date.getTime()) ? null : date;\n}\n\n/**\n * Get the storage path for version files of a content item.\n *\n * @param projectRoot - Absolute path to project root\n * @param collection - Collection name\n * @param contentId - Content item ID (slug)\n * @param config - Version history configuration\n * @returns Absolute path to version storage directory\n *\n * @example\n * ```typescript\n * const path = getVersionStoragePath(\n * '/project',\n * 'blog',\n * 'my-post',\n * { storagePath: '.writenex/versions' }\n * );\n * // Returns: \"/project/.writenex/versions/blog/my-post\"\n * ```\n */\nexport function getVersionStoragePath(\n projectRoot: string,\n collection: string,\n contentId: string,\n config: Required<VersionHistoryConfig>\n): string {\n return join(projectRoot, config.storagePath, collection, contentId);\n}\n\n/**\n * Get the path to a specific version file.\n *\n * @param storagePath - Version storage directory path\n * @param versionId - Version ID\n * @returns Absolute path to version file\n */\nexport function getVersionFilePath(\n storagePath: string,\n versionId: string\n): string {\n return join(storagePath, `${versionId}.md`);\n}\n\n/**\n * Get the path to the manifest file for a content item.\n *\n * @param storagePath - Version storage directory path\n * @returns Absolute path to manifest file\n */\nexport function getManifestPath(storagePath: string): string {\n return join(storagePath, \"manifest.json\");\n}\n\n/**\n * Generate a preview string from content.\n *\n * Extracts the first 100 characters of the content body,\n * stripping frontmatter if present.\n *\n * @param content - Full markdown content\n * @returns Preview string (max 100 characters)\n *\n * @example\n * ```typescript\n * const preview = generatePreview(\"---\\ntitle: Test\\n---\\n\\n# Hello World\\n\\nThis is content.\");\n * // Returns: \"# Hello World\\n\\nThis is content.\"\n * ```\n */\nexport function generatePreview(content: string): string {\n // Parse frontmatter to get body only\n try {\n const { content: body } = matter(content);\n const trimmed = body.trim();\n\n if (trimmed.length <= PREVIEW_MAX_LENGTH) {\n return trimmed;\n }\n\n return trimmed.substring(0, PREVIEW_MAX_LENGTH);\n } catch {\n // If parsing fails, use raw content\n const trimmed = content.trim();\n return trimmed.length <= PREVIEW_MAX_LENGTH\n ? trimmed\n : trimmed.substring(0, PREVIEW_MAX_LENGTH);\n }\n}\n\n/**\n * Extract label from version file content.\n *\n * Reads the special _writenex_label frontmatter field that stores\n * the version label for recovery purposes.\n *\n * @param content - Full markdown content of version file\n * @returns Label string or undefined if not present\n */\nexport function extractLabelFromContent(content: string): string | undefined {\n try {\n const { data } = matter(content);\n const label = data[LABEL_FRONTMATTER_KEY];\n return typeof label === \"string\" ? label : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Inject label into content as frontmatter for persistence.\n *\n * Adds the _writenex_label field to frontmatter so the label\n * can be recovered if the manifest is lost or corrupted.\n *\n * @param content - Original markdown content\n * @param label - Label to inject\n * @returns Content with label injected in frontmatter\n */\nexport function injectLabelIntoContent(content: string, label: string): string {\n try {\n const { data, content: body } = matter(content);\n\n // Add label to frontmatter\n const newData = { ...data, [LABEL_FRONTMATTER_KEY]: label };\n\n // Reconstruct the file with updated frontmatter\n return matter.stringify(body, newData);\n } catch {\n // If parsing fails, prepend frontmatter with just the label\n return `---\\n${LABEL_FRONTMATTER_KEY}: \"${label}\"\\n---\\n\\n${content}`;\n }\n}\n\n/**\n * Remove the internal label field from content for user-facing operations.\n *\n * Strips the _writenex_label field from frontmatter when returning\n * content to users (e.g., during restore).\n *\n * @param content - Content that may contain internal label field\n * @returns Content with internal label field removed\n */\nexport function stripLabelFromContent(content: string): string {\n try {\n const { data, content: body } = matter(content);\n\n // Remove the internal label field\n if (LABEL_FRONTMATTER_KEY in data) {\n const { [LABEL_FRONTMATTER_KEY]: _, ...cleanData } = data;\n\n // If no other frontmatter, return just the body\n if (Object.keys(cleanData).length === 0) {\n return body.startsWith(\"\\n\") ? body.slice(1) : body;\n }\n\n return matter.stringify(body, cleanData);\n }\n\n return content;\n } catch {\n return content;\n }\n}\n\n/**\n * Ensure the .gitignore file exists in the version storage root.\n *\n * Creates a .gitignore file with \"*\" pattern to exclude all version\n * files from Git tracking.\n *\n * @param projectRoot - Absolute path to project root\n * @param config - Version history configuration\n *\n * @example\n * ```typescript\n * await ensureGitignore('/project', { storagePath: '.writenex/versions' });\n * // Creates: /project/.writenex/versions/.gitignore with content \"*\"\n * ```\n */\nexport async function ensureGitignore(\n projectRoot: string,\n config: Required<VersionHistoryConfig>\n): Promise<void> {\n const storageRoot = join(projectRoot, config.storagePath);\n const gitignorePath = join(storageRoot, \".gitignore\");\n\n // Ensure storage directory exists\n if (!existsSync(storageRoot)) {\n await mkdir(storageRoot, { recursive: true });\n }\n\n // Create .gitignore if it doesn't exist\n if (!existsSync(gitignorePath)) {\n await writeFile(gitignorePath, GITIGNORE_CONTENT, \"utf-8\");\n }\n}\n\n/**\n * Ensure the version storage directory exists for a content item.\n *\n * @param storagePath - Version storage directory path\n */\nexport async function ensureStorageDirectory(\n storagePath: string\n): Promise<void> {\n if (!existsSync(storagePath)) {\n await mkdir(storagePath, { recursive: true });\n }\n}\n\n// =============================================================================\n// Manifest Operations\n// =============================================================================\n\n/**\n * Read the version manifest for a content item.\n *\n * @param storagePath - Version storage directory path\n * @returns Version manifest or null if not found/corrupted\n *\n * @example\n * ```typescript\n * const manifest = await readManifest('/project/.writenex/versions/blog/my-post');\n * if (manifest) {\n * console.log(`Found ${manifest.versions.length} versions`);\n * }\n * ```\n */\nexport async function readManifest(\n storagePath: string\n): Promise<VersionManifest | null> {\n const manifestPath = getManifestPath(storagePath);\n\n if (!existsSync(manifestPath)) {\n return null;\n }\n\n try {\n const content = await readFile(manifestPath, \"utf-8\");\n const data = JSON.parse(content) as VersionManifest;\n\n // Validate required fields\n if (!data.contentId || !data.collection || !Array.isArray(data.versions)) {\n console.warn(`[writenex] Corrupted manifest at ${manifestPath}`);\n return null;\n }\n\n return data;\n } catch (error) {\n console.warn(\n `[writenex] Failed to read manifest at ${manifestPath}:`,\n error\n );\n return null;\n }\n}\n\n/**\n * Write the version manifest for a content item.\n *\n * @param storagePath - Version storage directory path\n * @param manifest - Version manifest to write\n *\n * @example\n * ```typescript\n * await writeManifest('/project/.writenex/versions/blog/my-post', {\n * contentId: 'my-post',\n * collection: 'blog',\n * versions: [],\n * updatedAt: new Date().toISOString(),\n * });\n * ```\n */\nexport async function writeManifest(\n storagePath: string,\n manifest: VersionManifest\n): Promise<void> {\n await ensureStorageDirectory(storagePath);\n\n const manifestPath = getManifestPath(storagePath);\n const content = JSON.stringify(manifest, null, 2);\n\n await writeFile(manifestPath, content, \"utf-8\");\n}\n\n/**\n * Create an empty manifest for a content item.\n *\n * @param collection - Collection name\n * @param contentId - Content item ID\n * @returns New empty manifest\n */\nexport function createEmptyManifest(\n collection: string,\n contentId: string\n): VersionManifest {\n return {\n contentId,\n collection,\n versions: [],\n updatedAt: new Date().toISOString(),\n };\n}\n\n/**\n * Recover manifest by scanning version files in the storage directory.\n *\n * This function rebuilds the manifest from existing version files\n * when the manifest is corrupted or missing.\n *\n * @param storagePath - Version storage directory path\n * @param collection - Collection name\n * @param contentId - Content item ID\n * @returns Recovered manifest\n *\n * @example\n * ```typescript\n * const manifest = await recoverManifest(\n * '/project/.writenex/versions/blog/my-post',\n * 'blog',\n * 'my-post'\n * );\n * ```\n */\nexport async function recoverManifest(\n storagePath: string,\n collection: string,\n contentId: string\n): Promise<VersionManifest> {\n const manifest = createEmptyManifest(collection, contentId);\n\n if (!existsSync(storagePath)) {\n return manifest;\n }\n\n try {\n const files = await readdir(storagePath);\n const versionFiles = files.filter(\n (f) => f.endsWith(\".md\") && f !== \"manifest.json\"\n );\n\n for (const file of versionFiles) {\n const versionId = basename(file, \".md\");\n const filePath = join(storagePath, file);\n\n try {\n const content = await readFile(filePath, \"utf-8\");\n const stats = await stat(filePath);\n const timestamp = parseVersionId(versionId);\n\n if (timestamp) {\n // Extract label from content if present (for recovery)\n const label = extractLabelFromContent(content);\n\n const entry: VersionEntry = {\n id: versionId,\n timestamp: timestamp.toISOString(),\n preview: generatePreview(content),\n size: stats.size,\n ...(label ? { label } : {}),\n };\n\n manifest.versions.push(entry);\n }\n } catch {\n // Skip files that can't be read\n console.warn(`[writenex] Skipping unreadable version file: ${file}`);\n }\n }\n\n // Sort by timestamp descending (newest first)\n manifest.versions.sort(\n (a, b) =>\n new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()\n );\n\n manifest.updatedAt = new Date().toISOString();\n\n // Write recovered manifest\n await writeManifest(storagePath, manifest);\n\n return manifest;\n } catch (error) {\n console.warn(`[writenex] Failed to recover manifest:`, error);\n return manifest;\n }\n}\n\n/**\n * Get or recover manifest for a content item.\n *\n * Attempts to read existing manifest, falls back to recovery if corrupted.\n *\n * @param storagePath - Version storage directory path\n * @param collection - Collection name\n * @param contentId - Content item ID\n * @returns Version manifest\n */\nexport async function getOrRecoverManifest(\n storagePath: string,\n collection: string,\n contentId: string\n): Promise<VersionManifest> {\n const manifest = await readManifest(storagePath);\n\n if (manifest) {\n return manifest;\n }\n\n // Try to recover from version files\n return recoverManifest(storagePath, collection, contentId);\n}\n\n// =============================================================================\n// Version CRUD Operations\n// =============================================================================\n\n/**\n * Save a version snapshot of content.\n *\n * Creates a new version file with the provided content and updates\n * the manifest. Automatically prunes old versions if the limit is exceeded.\n *\n * @param projectRoot - Absolute path to project root\n * @param collection - Collection name\n * @param contentId - Content item ID (slug)\n * @param content - Full markdown content to save\n * @param config - Version history configuration\n * @param options - Save options\n * @returns Result of the save operation\n *\n * @example\n * ```typescript\n * const result = await saveVersion(\n * '/project',\n * 'blog',\n * 'my-post',\n * '---\\ntitle: My Post\\n---\\n\\nContent here...',\n * { enabled: true, maxVersions: 20, storagePath: '.writenex/versions' }\n * );\n *\n * if (result.success) {\n * console.log(`Created version: ${result.version?.id}`);\n * }\n * ```\n */\nexport async function saveVersion(\n projectRoot: string,\n collection: string,\n contentId: string,\n content: string,\n config: Required<VersionHistoryConfig>,\n options: SaveVersionOptions = {}\n): Promise<VersionResult> {\n const { label, skipIfIdentical = false } = options;\n\n // Check if version history is enabled\n if (!config.enabled) {\n return { success: true };\n }\n\n // Get storage path for this content item\n const storagePath = getVersionStoragePath(\n projectRoot,\n collection,\n contentId,\n config\n );\n\n // Use lock to prevent concurrent manifest modifications\n return withLock(storagePath, async () => {\n try {\n // Ensure gitignore exists in storage root\n await ensureGitignore(projectRoot, config);\n\n await ensureStorageDirectory(storagePath);\n\n // Get or create manifest\n const manifest = await getOrRecoverManifest(\n storagePath,\n collection,\n contentId\n );\n\n // Check if content is identical to last version (if skipIfIdentical is true)\n if (skipIfIdentical && manifest.versions.length > 0) {\n const lastVersion = manifest.versions[0];\n if (lastVersion) {\n const lastVersionPath = getVersionFilePath(\n storagePath,\n lastVersion.id\n );\n if (existsSync(lastVersionPath)) {\n try {\n const lastContent = await readFile(lastVersionPath, \"utf-8\");\n if (lastContent === content) {\n return { success: true, version: lastVersion };\n }\n } catch {\n // If we can't read the last version, proceed with saving\n }\n }\n }\n }\n\n // Generate version ID and create version file\n // Use the same timestamp for both id and timestamp to ensure consistency\n const now = new Date();\n const versionId = now.toISOString().replace(/:/g, \"-\");\n const versionPath = getVersionFilePath(storagePath, versionId);\n\n // If label is provided, inject it into content for recovery purposes\n const contentToSave = label\n ? injectLabelIntoContent(content, label)\n : content;\n\n // Write version file\n await writeFile(versionPath, contentToSave, \"utf-8\");\n\n // Get file stats for size\n const stats = await stat(versionPath);\n\n // Create version entry\n const entry: VersionEntry = {\n id: versionId,\n timestamp: now.toISOString(),\n preview: generatePreview(content),\n size: stats.size,\n ...(label ? { label } : {}),\n };\n\n // Add to manifest (newest first)\n manifest.versions.unshift(entry);\n manifest.updatedAt = new Date().toISOString();\n\n // Write updated manifest\n await writeManifest(storagePath, manifest);\n\n // Prune old versions (inside lock to prevent race)\n await pruneVersionsInternal(storagePath, config);\n\n return { success: true, version: entry };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`[writenex] Failed to save version:`, error);\n return { success: false, error: `Failed to save version: ${message}` };\n }\n });\n}\n\n/**\n * Get all versions for a content item.\n *\n * Returns versions sorted by timestamp in descending order (newest first).\n * Handles missing or corrupted manifests gracefully.\n *\n * @param projectRoot - Absolute path to project root\n * @param collection - Collection name\n * @param contentId - Content item ID (slug)\n * @param config - Version history configuration\n * @returns Array of version entries\n *\n * @example\n * ```typescript\n * const versions = await getVersions(\n * '/project',\n * 'blog',\n * 'my-post',\n * { enabled: true, maxVersions: 20, storagePath: '.writenex/versions' }\n * );\n *\n * console.log(`Found ${versions.length} versions`);\n * ```\n */\nexport async function getVersions(\n projectRoot: string,\n collection: string,\n contentId: string,\n config: Required<VersionHistoryConfig>\n): Promise<VersionEntry[]> {\n // Check if version history is enabled\n if (!config.enabled) {\n return [];\n }\n\n try {\n const storagePath = getVersionStoragePath(\n projectRoot,\n collection,\n contentId,\n config\n );\n\n // Check if storage directory exists\n if (!existsSync(storagePath)) {\n return [];\n }\n\n // Get or recover manifest\n const manifest = await getOrRecoverManifest(\n storagePath,\n collection,\n contentId\n );\n\n // Return versions sorted by timestamp descending (newest first)\n return [...manifest.versions].sort(\n (a, b) =>\n new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()\n );\n } catch (error) {\n console.warn(`[writenex] Failed to get versions:`, error);\n return [];\n }\n}\n\n/**\n * Get a specific version with full content.\n *\n * Reads the version file and parses it to return structured data\n * with frontmatter and body separated.\n *\n * @param projectRoot - Absolute path to project root\n * @param collection - Collection name\n * @param contentId - Content item ID (slug)\n * @param versionId - Version ID to retrieve\n * @param config - Version history configuration\n * @returns Full version data or null if not found\n *\n * @example\n * ```typescript\n * const version = await getVersion(\n * '/project',\n * 'blog',\n * 'my-post',\n * '2024-12-11T10-30-00-000Z',\n * { enabled: true, maxVersions: 20, storagePath: '.writenex/versions' }\n * );\n *\n * if (version) {\n * console.log(`Title: ${version.frontmatter.title}`);\n * console.log(`Body: ${version.body}`);\n * }\n * ```\n */\nexport async function getVersion(\n projectRoot: string,\n collection: string,\n contentId: string,\n versionId: string,\n config: Required<VersionHistoryConfig>\n): Promise<Version | null> {\n // Check if version history is enabled\n if (!config.enabled) {\n return null;\n }\n\n try {\n const storagePath = getVersionStoragePath(\n projectRoot,\n collection,\n contentId,\n config\n );\n const versionPath = getVersionFilePath(storagePath, versionId);\n\n // Check if version file exists\n if (!existsSync(versionPath)) {\n return null;\n }\n\n // Read version file\n const rawContent = await readFile(versionPath, \"utf-8\");\n const stats = await stat(versionPath);\n\n // Extract label from content (stored for recovery)\n const labelFromContent = extractLabelFromContent(rawContent);\n\n // Strip internal label field before returning to user\n const content = stripLabelFromContent(rawContent);\n\n // Parse frontmatter from cleaned content\n const { data: frontmatter, content: body } = matter(content);\n\n // Get timestamp from version ID\n const timestamp = parseVersionId(versionId);\n if (!timestamp) {\n return null;\n }\n\n // Get label from manifest first, fall back to content-embedded label\n const manifest = await readManifest(storagePath);\n const manifestEntry = manifest?.versions.find((v) => v.id === versionId);\n const label = manifestEntry?.label ?? labelFromContent;\n\n return {\n id: versionId,\n timestamp: timestamp.toISOString(),\n preview: generatePreview(content),\n size: stats.size,\n content,\n frontmatter,\n body: body.trim(),\n ...(label ? { label } : {}),\n };\n } catch (error) {\n console.warn(`[writenex] Failed to get version ${versionId}:`, error);\n return null;\n }\n}\n\n/**\n * Delete a specific version.\n *\n * Removes the version file from the filesystem and updates the manifest.\n *\n * @param projectRoot - Absolute path to project root\n * @param collection - Collection name\n * @param contentId - Content item ID (slug)\n * @param versionId - Version ID to delete\n * @param config - Version history configuration\n * @returns Result of the delete operation\n *\n * @example\n * ```typescript\n * const result = await deleteVersion(\n * '/project',\n * 'blog',\n * 'my-post',\n * '2024-12-11T10-30-00-000Z',\n * { enabled: true, maxVersions: 20, storagePath: '.writenex/versions' }\n * );\n *\n * if (result.success) {\n * console.log('Version deleted');\n * }\n * ```\n */\nexport async function deleteVersion(\n projectRoot: string,\n collection: string,\n contentId: string,\n versionId: string,\n config: Required<VersionHistoryConfig>\n): Promise<VersionResult> {\n const storagePath = getVersionStoragePath(\n projectRoot,\n collection,\n contentId,\n config\n );\n\n // Use lock to prevent concurrent manifest modifications\n return withLock(storagePath, async () => {\n try {\n const versionPath = getVersionFilePath(storagePath, versionId);\n\n // Check if version file exists\n if (!existsSync(versionPath)) {\n return { success: false, error: `Version not found: ${versionId}` };\n }\n\n // Get manifest\n const manifest = await getOrRecoverManifest(\n storagePath,\n collection,\n contentId\n );\n\n // Find version entry\n const entryIndex = manifest.versions.findIndex((v) => v.id === versionId);\n const entry = entryIndex >= 0 ? manifest.versions[entryIndex] : undefined;\n\n // Delete version file\n await unlink(versionPath);\n\n // Update manifest\n if (entryIndex >= 0) {\n manifest.versions.splice(entryIndex, 1);\n manifest.updatedAt = new Date().toISOString();\n await writeManifest(storagePath, manifest);\n }\n\n return { success: true, version: entry };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`[writenex] Failed to delete version:`, error);\n return { success: false, error: `Failed to delete version: ${message}` };\n }\n });\n}\n\n/**\n * Clear all versions for a content item.\n *\n * Deletes all version files and resets the manifest to empty state.\n *\n * @param projectRoot - Absolute path to project root\n * @param collection - Collection name\n * @param contentId - Content item ID (slug)\n * @param config - Version history configuration\n * @returns Result of the clear operation\n *\n * @example\n * ```typescript\n * const result = await clearVersions(\n * '/project',\n * 'blog',\n * 'my-post',\n * { enabled: true, maxVersions: 20, storagePath: '.writenex/versions' }\n * );\n *\n * if (result.success) {\n * console.log('All versions cleared');\n * }\n * ```\n */\nexport async function clearVersions(\n projectRoot: string,\n collection: string,\n contentId: string,\n config: Required<VersionHistoryConfig>\n): Promise<VersionResult> {\n const storagePath = getVersionStoragePath(\n projectRoot,\n collection,\n contentId,\n config\n );\n\n // Check if storage directory exists (no lock needed for this check)\n if (!existsSync(storagePath)) {\n return { success: true };\n }\n\n // Use lock to prevent concurrent manifest modifications\n return withLock(storagePath, async () => {\n try {\n // Get all version files\n const files = await readdir(storagePath);\n const versionFiles = files.filter(\n (f) => f.endsWith(\".md\") && f !== \"manifest.json\"\n );\n\n // Delete all version files\n for (const file of versionFiles) {\n const filePath = join(storagePath, file);\n try {\n await unlink(filePath);\n } catch {\n // Ignore errors for individual files\n }\n }\n\n // Reset manifest\n const manifest = createEmptyManifest(collection, contentId);\n await writeManifest(storagePath, manifest);\n\n return { success: true };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`[writenex] Failed to clear versions:`, error);\n return { success: false, error: `Failed to clear versions: ${message}` };\n }\n });\n}\n\n/**\n * Prune old versions to maintain the maximum limit.\n *\n * Deletes the oldest unlabeled versions when the count exceeds maxVersions.\n * Labeled versions are preserved regardless of count.\n *\n * @param projectRoot - Absolute path to project root\n * @param collection - Collection name\n * @param contentId - Content item ID (slug)\n * @param config - Version history configuration\n * @returns Result of the prune operation\n *\n * @example\n * ```typescript\n * const result = await pruneVersions(\n * '/project',\n * 'blog',\n * 'my-post',\n * { enabled: true, maxVersions: 20, storagePath: '.writenex/versions' }\n * );\n * ```\n */\n/**\n * Internal prune function that assumes lock is already held.\n *\n * @param storagePath - Version storage directory path\n * @param config - Version history configuration\n * @returns Result of the prune operation\n */\nasync function pruneVersionsInternal(\n storagePath: string,\n config: Required<VersionHistoryConfig>\n): Promise<VersionResult> {\n try {\n // Check if storage directory exists\n if (!existsSync(storagePath)) {\n return { success: true };\n }\n\n // Get manifest (read fresh to ensure we have latest data)\n const manifest = await readManifest(storagePath);\n if (!manifest) {\n return { success: true };\n }\n\n // Separate labeled and unlabeled versions\n const labeledVersions = manifest.versions.filter((v) => v.label);\n const unlabeledVersions = manifest.versions.filter((v) => !v.label);\n\n // Check if pruning is needed\n if (unlabeledVersions.length <= config.maxVersions) {\n return { success: true };\n }\n\n // Sort unlabeled versions by timestamp (oldest first for deletion)\n const sortedUnlabeled = [...unlabeledVersions].sort(\n (a, b) =>\n new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()\n );\n\n // Calculate how many to delete\n const toDelete = sortedUnlabeled.slice(\n 0,\n unlabeledVersions.length - config.maxVersions\n );\n\n // Delete old version files\n for (const version of toDelete) {\n const versionPath = getVersionFilePath(storagePath, version.id);\n try {\n if (existsSync(versionPath)) {\n await unlink(versionPath);\n }\n } catch {\n // Ignore errors for individual files\n }\n }\n\n // Update manifest - keep labeled + remaining unlabeled\n const remainingUnlabeled = sortedUnlabeled.slice(\n unlabeledVersions.length - config.maxVersions\n );\n manifest.versions = [...labeledVersions, ...remainingUnlabeled].sort(\n (a, b) =>\n new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()\n );\n manifest.updatedAt = new Date().toISOString();\n\n await writeManifest(storagePath, manifest);\n\n return { success: true };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`[writenex] Failed to prune versions:`, error);\n return { success: false, error: `Failed to prune versions: ${message}` };\n }\n}\n\nexport async function pruneVersions(\n projectRoot: string,\n collection: string,\n contentId: string,\n config: Required<VersionHistoryConfig>\n): Promise<VersionResult> {\n const storagePath = getVersionStoragePath(\n projectRoot,\n collection,\n contentId,\n config\n );\n\n // Check if storage directory exists (no lock needed for this check)\n if (!existsSync(storagePath)) {\n return { success: true };\n }\n\n // Use lock to prevent concurrent manifest modifications\n return withLock(storagePath, () =>\n pruneVersionsInternal(storagePath, config)\n );\n}\n\n// =============================================================================\n// Restore Operations\n// =============================================================================\n\n/**\n * Restore a version to current content.\n *\n * This function:\n * 1. Creates a safety snapshot of the current content before restoring\n * 2. Reads the version content to restore\n * 3. Overwrites the current content file with the version content\n *\n * Note: Cache invalidation should be handled by the caller (API route)\n * since the cache is managed at the server level.\n *\n * @param projectRoot - Absolute path to project root\n * @param collection - Collection name\n * @param contentId - Content item ID (slug)\n * @param versionId - Version ID to restore\n * @param contentFilePath - Absolute path to the current content file\n * @param config - Version history configuration\n * @param options - Restore options\n * @returns Result of the restore operation\n *\n * @example\n * ```typescript\n * const result = await restoreVersion(\n * '/project',\n * 'blog',\n * 'my-post',\n * '2024-12-11T10-30-00-000Z',\n * '/project/src/content/blog/my-post.md',\n * { enabled: true, maxVersions: 20, storagePath: '.writenex/versions' }\n * );\n *\n * if (result.success) {\n * console.log('Restored content:', result.content);\n * if (result.safetySnapshot) {\n * console.log('Safety snapshot created:', result.safetySnapshot.id);\n * }\n * }\n * ```\n */\nexport async function restoreVersion(\n projectRoot: string,\n collection: string,\n contentId: string,\n versionId: string,\n contentFilePath: string,\n config: Required<VersionHistoryConfig>,\n options: RestoreVersionOptions = {}\n): Promise<RestoreResult> {\n const { safetySnapshotLabel = \"Before restore\", skipSafetySnapshot = false } =\n options;\n\n try {\n // Step 1: Get the version to restore\n const versionToRestore = await getVersion(\n projectRoot,\n collection,\n contentId,\n versionId,\n config\n );\n\n if (!versionToRestore) {\n return {\n success: false,\n error: `Version not found: ${versionId}`,\n };\n }\n\n // Step 2: Read current content and create safety snapshot\n let safetySnapshot: VersionEntry | undefined;\n\n if (!skipSafetySnapshot && existsSync(contentFilePath)) {\n try {\n const currentContent = await readFile(contentFilePath, \"utf-8\");\n\n // Create safety snapshot with label\n const snapshotResult = await saveVersion(\n projectRoot,\n collection,\n contentId,\n currentContent,\n config,\n { label: safetySnapshotLabel }\n );\n\n if (snapshotResult.success && snapshotResult.version) {\n safetySnapshot = snapshotResult.version;\n }\n } catch (error) {\n // Log warning but continue with restore\n console.warn(\n `[writenex] Failed to create safety snapshot before restore:`,\n error\n );\n }\n }\n\n // Step 3: Overwrite current content file with version content\n await writeFile(contentFilePath, versionToRestore.content, \"utf-8\");\n\n return {\n success: true,\n version: {\n id: versionToRestore.id,\n timestamp: versionToRestore.timestamp,\n preview: versionToRestore.preview,\n size: versionToRestore.size,\n ...(versionToRestore.label ? { label: versionToRestore.label } : {}),\n },\n content: versionToRestore.content,\n safetySnapshot,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`[writenex] Failed to restore version:`, error);\n return {\n success: false,\n error: `Failed to restore version: ${message}`,\n };\n }\n}\n","/**\n * @fileoverview Filesystem writer for content operations\n *\n * This module provides functions for creating, updating, and deleting\n * content files in Astro content collections.\n *\n * ## Features:\n * - Create new content files with frontmatter\n * - Update existing content files\n * - Delete content files\n * - Generate unique slugs to avoid collisions\n * - Support for different file patterns (flat, folder-based, date-prefixed)\n * - Automatic version history creation before updates\n *\n * @module @writenex/astro/filesystem/writer\n */\n\nimport { writeFile, unlink, mkdir, readFile, stat } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, dirname, basename } from \"node:path\";\nimport slugify from \"slugify\";\nimport { readContentFile } from \"./reader\";\nimport { saveVersion } from \"./versions\";\nimport {\n generatePathFromPattern,\n resolvePatternTokens,\n isValidPattern,\n} from \"@/discovery/patterns\";\nimport type { VersionHistoryConfig } from \"@/types\";\nimport { ContentConflictError } from \"@/core/errors\";\n\n/**\n * Options for creating content\n */\nexport interface CreateContentOptions {\n /** Frontmatter data */\n frontmatter: Record<string, unknown>;\n /** Markdown body content */\n body: string;\n /** Custom slug (optional, generated from title if not provided) */\n slug?: string;\n /** File pattern for the collection (e.g., \"{slug}/index.md\", \"{date}-{slug}.md\") */\n filePattern?: string;\n /** Custom token values to override automatic resolution */\n customTokens?: Record<string, string>;\n}\n\n/**\n * Options for updating content\n */\nexport interface UpdateContentOptions {\n /** Updated frontmatter data */\n frontmatter?: Record<string, unknown>;\n /** Updated markdown body content */\n body?: string;\n /** Project root for version history (required for version creation) */\n projectRoot?: string;\n /** Collection name for version history */\n collection?: string;\n /** Version history configuration */\n versionHistoryConfig?: Required<VersionHistoryConfig>;\n /**\n * Expected modification time for conflict detection.\n * If provided and the file's mtime differs, the update will fail with a conflict error.\n */\n expectedMtime?: number;\n}\n\n/**\n * Result of a write operation\n */\nexport interface WriteResult {\n /** Whether the operation was successful */\n success: boolean;\n /** The content ID (slug) */\n id?: string;\n /** The file path */\n path?: string;\n /** Error message if failed */\n error?: string;\n /** New modification time after write (for conflict detection) */\n mtime?: number;\n /** Conflict error if update failed due to external modification */\n conflict?: ContentConflictError;\n}\n\n/**\n * Generate a URL-safe slug from a string\n *\n * @param text - Text to slugify\n * @returns URL-safe slug\n */\nexport function generateSlug(text: string): string {\n return slugify(text, {\n lower: true,\n strict: true,\n trim: true,\n });\n}\n\n/**\n * Check if a content file already exists for a given slug and pattern\n *\n * @param slug - The slug to check\n * @param collectionPath - Path to the collection directory\n * @param filePattern - File pattern (e.g., \"{slug}.md\", \"{slug}/index.md\")\n * @returns True if content already exists\n */\nfunction contentExists(\n slug: string,\n collectionPath: string,\n filePattern: string\n): boolean {\n const relativePath = generatePathFromPattern(filePattern, { slug });\n const fullPath = join(collectionPath, relativePath);\n\n // For folder-based patterns, check if the folder exists\n if (filePattern.includes(\"/index.\")) {\n const folderPath = join(collectionPath, slug);\n return existsSync(folderPath);\n }\n\n return existsSync(fullPath);\n}\n\n/**\n * Generate a unique slug that doesn't conflict with existing files\n *\n * @param baseSlug - The base slug to start with\n * @param collectionPath - Path to the collection directory\n * @param filePattern - File pattern (default: \"{slug}.md\")\n * @returns A unique slug\n */\nexport async function generateUniqueSlug(\n baseSlug: string,\n collectionPath: string,\n filePattern: string = \"{slug}.md\"\n): Promise<string> {\n let slug = baseSlug;\n let counter = 2;\n\n while (contentExists(slug, collectionPath, filePattern)) {\n slug = `${baseSlug}-${counter}`;\n counter++;\n }\n\n return slug;\n}\n\n/**\n * Convert frontmatter object to YAML string\n *\n * @param frontmatter - Frontmatter data\n * @returns YAML string\n */\nfunction frontmatterToYaml(frontmatter: Record<string, unknown>): string {\n const lines: string[] = [];\n\n for (const [key, value] of Object.entries(frontmatter)) {\n if (value === undefined || value === null) {\n continue;\n }\n\n if (typeof value === \"string\") {\n // Quote strings that contain special characters\n if (value.includes(\":\") || value.includes(\"#\") || value.includes(\"\\n\")) {\n lines.push(`${key}: \"${value.replace(/\"/g, '\\\\\"')}\"`);\n } else {\n lines.push(`${key}: ${value}`);\n }\n } else if (typeof value === \"number\" || typeof value === \"boolean\") {\n lines.push(`${key}: ${value}`);\n } else if (value instanceof Date) {\n lines.push(`${key}: ${value.toISOString().split(\"T\")[0]}`);\n } else if (Array.isArray(value)) {\n if (value.length === 0) {\n lines.push(`${key}: []`);\n } else {\n lines.push(`${key}:`);\n for (const item of value) {\n lines.push(` - ${item}`);\n }\n }\n } else if (typeof value === \"object\") {\n // Simple object serialization\n lines.push(`${key}:`);\n for (const [subKey, subValue] of Object.entries(value)) {\n lines.push(` ${subKey}: ${subValue}`);\n }\n }\n }\n\n return lines.join(\"\\n\");\n}\n\n/**\n * Create a content file with frontmatter and body\n *\n * @param frontmatter - Frontmatter data\n * @param body - Markdown body content\n * @returns Complete file content\n */\nfunction createFileContent(\n frontmatter: Record<string, unknown>,\n body: string\n): string {\n const yaml = frontmatterToYaml(frontmatter);\n return `---\\n${yaml}\\n---\\n\\n${body}`;\n}\n\n/**\n * Create a new content file in a collection\n *\n * Supports various file patterns with automatic token resolution:\n * - `{slug}.md` - Simple flat structure (default)\n * - `{slug}/index.md` - Folder-based content\n * - `{date}-{slug}.md` - Date-prefixed naming\n * - `{year}/{slug}.md` - Year folder structure\n * - `{year}/{month}/{slug}.md` - Year/month folder structure\n * - `{year}/{month}/{day}/{slug}.md` - Full date folder structure\n * - `{lang}/{slug}.md` - Language-prefixed (i18n)\n * - `{category}/{slug}.md` - Category folder structure\n * - `{author}/{slug}.md` - Author folder structure\n * - Any custom pattern with tokens from frontmatter\n *\n * Token resolution priority:\n * 1. Custom tokens (explicitly provided via customTokens)\n * 2. Known token resolvers (date, year, month, lang, category, etc.)\n * 3. Frontmatter values (for custom tokens)\n * 4. Default values\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param options - Content creation options\n * @returns WriteResult with success status and file info\n *\n * @example\n * ```typescript\n * // Flat structure\n * const result = await createContent('/project/src/content/blog', {\n * frontmatter: { title: 'My New Post', pubDate: new Date() },\n * body: '# Hello World',\n * });\n *\n * // Folder-based structure\n * const result = await createContent('/project/src/content/blog', {\n * frontmatter: { title: 'My New Post', pubDate: new Date() },\n * body: '# Hello World',\n * filePattern: '{slug}/index.md',\n * });\n *\n * // i18n structure with custom token\n * const result = await createContent('/project/src/content/blog', {\n * frontmatter: { title: 'My New Post', lang: 'id' },\n * body: '# Hello World',\n * filePattern: '{lang}/{slug}.md',\n * });\n *\n * // Custom pattern with explicit token\n * const result = await createContent('/project/src/content/blog', {\n * frontmatter: { title: 'My New Post' },\n * body: '# Hello World',\n * filePattern: '{category}/{slug}.md',\n * customTokens: { category: 'tutorials' },\n * });\n * ```\n */\nexport async function createContent(\n collectionPath: string,\n options: CreateContentOptions\n): Promise<WriteResult> {\n const {\n frontmatter,\n body,\n slug: customSlug,\n filePattern = \"{slug}.md\",\n customTokens = {},\n } = options;\n\n try {\n // Validate pattern\n const validation = isValidPattern(filePattern);\n if (!validation.valid) {\n return {\n success: false,\n error: `Invalid file pattern: ${validation.error}`,\n };\n }\n\n // Generate slug from title or use custom slug\n const title = frontmatter.title as string | undefined;\n const baseSlug = customSlug ?? (title ? generateSlug(title) : \"untitled\");\n\n // Ensure unique slug using the file pattern\n const slug = await generateUniqueSlug(\n baseSlug,\n collectionPath,\n filePattern\n );\n\n // Resolve all tokens using the flexible token resolver\n const tokens = resolvePatternTokens(filePattern, {\n slug,\n frontmatter,\n customTokens,\n });\n\n // Generate the relative file path from the pattern\n const relativePath = generatePathFromPattern(filePattern, tokens);\n const filePath = join(collectionPath, relativePath);\n\n // Ensure parent directory exists (important for folder-based patterns)\n const parentDir = dirname(filePath);\n if (!existsSync(parentDir)) {\n await mkdir(parentDir, { recursive: true });\n }\n\n // Create file content\n const content = createFileContent(frontmatter, body);\n\n // Write file\n await writeFile(filePath, content, \"utf-8\");\n\n return {\n success: true,\n id: slug,\n path: filePath,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n error: `Failed to create content: ${message}`,\n };\n }\n}\n\n/**\n * Update an existing content file\n *\n * Creates a version snapshot of the current content before updating\n * when version history is configured. Skips both version creation and\n * file write if the new content is identical to the current file content.\n * Version creation errors are logged but do not fail the save operation.\n *\n * @param filePath - Absolute path to the content file\n * @param collectionPath - Path to the collection directory\n * @param options - Update options including version history config\n * @returns WriteResult with success status\n *\n * @example\n * ```typescript\n * const result = await updateContent(\n * '/project/src/content/blog/my-post.md',\n * '/project/src/content/blog',\n * {\n * frontmatter: { title: 'Updated Title' },\n * body: '# Updated Content',\n * projectRoot: '/project',\n * collection: 'blog',\n * versionHistoryConfig: { enabled: true, maxVersions: 20, storagePath: '.writenex/versions' },\n * }\n * );\n * ```\n */\nexport async function updateContent(\n filePath: string,\n collectionPath: string,\n options: UpdateContentOptions\n): Promise<WriteResult> {\n const { projectRoot, collection, versionHistoryConfig, expectedMtime } =\n options;\n\n try {\n // Read existing content\n const existing = await readContentFile(filePath, collectionPath);\n\n if (!existing.success || !existing.content) {\n return {\n success: false,\n error: existing.error ?? \"Content not found\",\n };\n }\n\n // Conflict detection: check if file was modified externally\n if (expectedMtime !== undefined && existing.content.mtime !== undefined) {\n // Allow small tolerance (1ms) for filesystem precision differences\n const mtimeDiff = Math.abs(existing.content.mtime - expectedMtime);\n if (mtimeDiff > 1) {\n // File was modified externally - return conflict error\n const conflictError = new ContentConflictError(\n collection ?? \"unknown\",\n existing.content.id,\n existing.content.raw,\n existing.content.mtime,\n expectedMtime\n );\n\n return {\n success: false,\n error: conflictError.message,\n conflict: conflictError,\n };\n }\n }\n\n // Merge frontmatter\n const frontmatter = options.frontmatter\n ? { ...existing.content.frontmatter, ...options.frontmatter }\n : existing.content.frontmatter;\n\n // Use new body or existing\n const body = options.body ?? existing.content.body;\n\n // Create updated content\n const newContent = createFileContent(frontmatter, body);\n\n // Read current file content for comparison\n const currentContent = existsSync(filePath)\n ? await readFile(filePath, \"utf-8\")\n : \"\";\n\n // Skip if content is identical (no changes to save)\n if (newContent === currentContent) {\n return {\n success: true,\n id: existing.content.id,\n path: filePath,\n mtime: existing.content.mtime,\n };\n }\n\n // Create version snapshot before updating (if version history is configured)\n // Only create version if content actually changed\n if (\n projectRoot &&\n collection &&\n versionHistoryConfig &&\n versionHistoryConfig.enabled &&\n currentContent\n ) {\n try {\n // Extract content ID from file path\n const fileName = basename(filePath);\n const contentId =\n fileName === \"index.md\" || fileName === \"index.mdx\"\n ? basename(dirname(filePath))\n : fileName.replace(/\\.(md|mdx)$/, \"\");\n\n // Save version of the current content before overwriting\n // skipIfIdentical compares with last version in history\n const versionResult = await saveVersion(\n projectRoot,\n collection,\n contentId,\n currentContent,\n versionHistoryConfig,\n { skipIfIdentical: true }\n );\n\n if (!versionResult.success) {\n console.warn(\n `[writenex] Failed to create version snapshot: ${versionResult.error}`\n );\n }\n } catch (versionError) {\n // Log version creation error but continue with save\n console.warn(\n `[writenex] Version creation error (save will continue):`,\n versionError\n );\n }\n }\n\n // Write file with new content\n await writeFile(filePath, newContent, \"utf-8\");\n\n // Get new mtime after write\n const newStats = await stat(filePath);\n\n return {\n success: true,\n id: existing.content.id,\n path: filePath,\n mtime: newStats.mtimeMs,\n };\n } catch (error) {\n // Re-throw ContentConflictError as-is\n if (error instanceof ContentConflictError) {\n return {\n success: false,\n error: error.message,\n conflict: error,\n };\n }\n\n const message = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n error: `Failed to update content: ${message}`,\n };\n }\n}\n\n/**\n * Delete a content file\n *\n * @param filePath - Absolute path to the content file\n * @returns WriteResult with success status\n *\n * @example\n * ```typescript\n * const result = await deleteContent('/project/src/content/blog/my-post.md');\n * ```\n */\nexport async function deleteContent(filePath: string): Promise<WriteResult> {\n try {\n if (!existsSync(filePath)) {\n return {\n success: false,\n error: \"Content file not found\",\n };\n }\n\n await unlink(filePath);\n\n return {\n success: true,\n path: filePath,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n error: `Failed to delete content: ${message}`,\n };\n }\n}\n\n// Re-export getContentFilePath from reader for backward compatibility\nexport { getContentFilePath } from \"./reader\";\n","/**\n * @fileoverview Image handling for content collections\n *\n * This module provides functions for uploading and managing images\n * in content collections with support for different storage strategies.\n *\n * ## Strategies:\n * - colocated: Images stored alongside content files\n * - public: Images stored in public directory\n * - custom: User-defined storage paths\n *\n * @module @writenex/astro/filesystem/images\n */\n\nimport { writeFile, mkdir, readdir, stat } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, dirname, basename, extname, relative } from \"node:path\";\nimport type {\n ImageConfig,\n DiscoveredImage,\n ImageDiscoveryOptions,\n ImageDiscoveryResult,\n} from \"@/types\";\nimport { getContentFilePath } from \"./reader\";\n\n/**\n * Default image configuration\n */\nexport const DEFAULT_IMAGE_CONFIG: ImageConfig = {\n strategy: \"colocated\",\n publicPath: \"/images\",\n storagePath: \"public/images\",\n};\n\n/**\n * Supported image extensions\n */\nconst SUPPORTED_EXTENSIONS = new Set([\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".svg\",\n]);\n\n/**\n * Result of image upload operation\n */\nexport interface ImageUploadResult {\n success: boolean;\n /** Markdown-compatible path for the image */\n path?: string;\n /** Public URL for the image */\n url?: string;\n /** Error message if failed */\n error?: string;\n}\n\n/**\n * Options for image upload\n */\nexport interface ImageUploadOptions {\n /** Original filename */\n filename: string;\n /** Image binary data */\n data: Buffer;\n /** Collection name */\n collection: string;\n /** Content ID (slug) */\n contentId: string;\n /** Project root path */\n projectRoot: string;\n /** Image configuration */\n config?: ImageConfig;\n}\n\n/**\n * Validate image file\n *\n * @param filename - Original filename\n * @returns True if valid image file\n */\nexport function isValidImageFile(filename: string): boolean {\n const ext = extname(filename).toLowerCase();\n return SUPPORTED_EXTENSIONS.has(ext);\n}\n\n/**\n * Generate a unique filename for uploaded image\n *\n * @param originalName - Original filename\n * @param _contentId - Content ID for context (reserved for future use)\n * @returns Unique filename\n */\nfunction generateUniqueFilename(\n originalName: string,\n _contentId: string\n): string {\n const ext = extname(originalName).toLowerCase();\n const baseName = basename(originalName, ext)\n .toLowerCase()\n .replace(/[^a-z0-9]/g, \"-\")\n .replace(/-+/g, \"-\")\n .substring(0, 50);\n\n const timestamp = Date.now().toString(36);\n return `${baseName}-${timestamp}${ext}`;\n}\n\n/**\n * Get storage path for colocated strategy\n *\n * Images are stored in a folder named after the content file.\n * The markdown path is calculated based on the content structure:\n * - Folder-based (slug/index.md): ./filename (image in same folder as index.md)\n * - Flat file (slug.md): ./slug/filename (image in sibling folder)\n *\n * @param projectRoot - Project root path\n * @param collection - Collection name\n * @param contentId - Content ID\n * @param filename - Image filename\n * @returns Absolute path to store the image and markdown-compatible path\n */\nfunction getColocatedPath(\n projectRoot: string,\n collection: string,\n contentId: string,\n filename: string\n): { storagePath: string; markdownPath: string } {\n const collectionPath = join(projectRoot, \"src/content\", collection);\n const imageDir = join(collectionPath, contentId);\n const storagePath = join(imageDir, filename);\n\n // Detect content structure to determine correct markdown path\n // Check if content is folder-based (slug/index.md or slug/index.mdx)\n const indexMdPath = join(collectionPath, contentId, \"index.md\");\n const indexMdxPath = join(collectionPath, contentId, \"index.mdx\");\n const isFolderBased = existsSync(indexMdPath) || existsSync(indexMdxPath);\n\n // For folder-based: image is in same folder as index.md, so path is ./filename\n // For flat file: image is in sibling folder, so path is ./contentId/filename\n const markdownPath = isFolderBased\n ? `./${filename}`\n : `./${contentId}/${filename}`;\n\n return { storagePath, markdownPath };\n}\n\n/**\n * Get storage path for public strategy\n *\n * Images are stored in public/images/{collection}/{filename}\n *\n * @param projectRoot - Project root path\n * @param collection - Collection name\n * @param filename - Image filename\n * @param config - Image configuration\n * @returns Absolute path to store the image\n */\nfunction getPublicPath(\n projectRoot: string,\n collection: string,\n filename: string,\n config: ImageConfig\n): { storagePath: string; markdownPath: string; url: string } {\n const storagePath = join(\n projectRoot,\n config.storagePath ?? \"public/images\",\n collection,\n filename\n );\n const publicPath = config.publicPath ?? \"/images\";\n const url = `${publicPath}/${collection}/${filename}`;\n\n return { storagePath, markdownPath: url, url };\n}\n\n/**\n * Upload an image file\n *\n * @param options - Upload options\n * @returns Upload result with paths\n *\n * @example\n * ```typescript\n * const result = await uploadImage({\n * filename: \"hero.jpg\",\n * data: imageBuffer,\n * collection: \"blog\",\n * contentId: \"my-post\",\n * projectRoot: \"/path/to/project\",\n * });\n *\n * if (result.success) {\n * console.log(result.path); // \"./my-post/hero-abc123.jpg\"\n * }\n * ```\n */\nexport async function uploadImage(\n options: ImageUploadOptions\n): Promise<ImageUploadResult> {\n const {\n filename,\n data,\n collection,\n contentId,\n projectRoot,\n config = DEFAULT_IMAGE_CONFIG,\n } = options;\n\n // Validate file\n if (!isValidImageFile(filename)) {\n return {\n success: false,\n error: `Invalid image file type. Supported: ${[...SUPPORTED_EXTENSIONS].join(\", \")}`,\n };\n }\n\n // Generate unique filename\n const uniqueFilename = generateUniqueFilename(filename, contentId);\n\n try {\n let storagePath: string;\n let markdownPath: string;\n let url: string | undefined;\n\n switch (config.strategy) {\n case \"public\": {\n const paths = getPublicPath(\n projectRoot,\n collection,\n uniqueFilename,\n config\n );\n storagePath = paths.storagePath;\n markdownPath = paths.markdownPath;\n url = paths.url;\n break;\n }\n\n case \"colocated\":\n default: {\n const paths = getColocatedPath(\n projectRoot,\n collection,\n contentId,\n uniqueFilename\n );\n storagePath = paths.storagePath;\n markdownPath = paths.markdownPath;\n break;\n }\n }\n\n // Ensure directory exists\n const dir = dirname(storagePath);\n if (!existsSync(dir)) {\n await mkdir(dir, { recursive: true });\n }\n\n // Write file\n await writeFile(storagePath, data);\n\n return {\n success: true,\n path: markdownPath,\n url: url ?? markdownPath,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n return {\n success: false,\n error: `Failed to upload image: ${message}`,\n };\n }\n}\n\n/**\n * Parse multipart form data for image upload\n *\n * Simple parser for multipart/form-data with single file upload.\n * For production, consider using a proper multipart parser library.\n *\n * @param body - Raw request body\n * @param contentType - Content-Type header\n * @returns Parsed file data and fields\n */\nexport function parseMultipartFormData(\n body: Buffer,\n contentType: string\n): {\n file?: { filename: string; data: Buffer; contentType: string };\n fields: Record<string, string>;\n} {\n const result: {\n file?: { filename: string; data: Buffer; contentType: string };\n fields: Record<string, string>;\n } = { fields: {} };\n\n // Extract boundary from content-type\n const boundaryMatch = contentType.match(/boundary=(?:\"([^\"]+)\"|([^;]+))/);\n if (!boundaryMatch) {\n return result;\n }\n\n const boundary = boundaryMatch[1] ?? boundaryMatch[2];\n if (!boundary) {\n return result;\n }\n\n const boundaryBuffer = Buffer.from(`--${boundary}`);\n const parts = splitBuffer(body, boundaryBuffer);\n\n for (const part of parts) {\n // Skip empty parts and closing boundary\n if (part.length < 10) continue;\n\n // Find header/body separator (double CRLF)\n const separatorIndex = part.indexOf(\"\\r\\n\\r\\n\");\n if (separatorIndex === -1) continue;\n\n const headerSection = part.slice(0, separatorIndex).toString(\"utf-8\");\n const bodySection = part.slice(separatorIndex + 4);\n\n // Remove trailing CRLF from body\n const bodyEnd = bodySection.length - 2;\n const cleanBody = bodyEnd > 0 ? bodySection.slice(0, bodyEnd) : bodySection;\n\n // Parse headers\n const headers = parseHeaders(headerSection);\n const disposition = headers[\"content-disposition\"];\n\n if (!disposition) continue;\n\n // Extract name and filename from Content-Disposition\n const nameMatch = disposition.match(/name=\"([^\"]+)\"/);\n const filenameMatch = disposition.match(/filename=\"([^\"]+)\"/);\n\n if (filenameMatch) {\n // This is a file field\n result.file = {\n filename: filenameMatch[1] ?? \"unknown\",\n data: cleanBody,\n contentType: headers[\"content-type\"] ?? \"application/octet-stream\",\n };\n } else if (nameMatch) {\n // This is a regular field\n result.fields[nameMatch[1] ?? \"\"] = cleanBody.toString(\"utf-8\");\n }\n }\n\n return result;\n}\n\n/**\n * Split buffer by delimiter\n */\nfunction splitBuffer(buffer: Buffer, delimiter: Buffer): Buffer[] {\n const parts: Buffer[] = [];\n let start = 0;\n let index: number;\n\n while ((index = buffer.indexOf(delimiter, start)) !== -1) {\n if (index > start) {\n parts.push(buffer.slice(start, index));\n }\n start = index + delimiter.length;\n }\n\n if (start < buffer.length) {\n parts.push(buffer.slice(start));\n }\n\n return parts;\n}\n\n/**\n * Parse header section into key-value pairs\n */\nfunction parseHeaders(headerSection: string): Record<string, string> {\n const headers: Record<string, string> = {};\n const lines = headerSection.split(\"\\r\\n\");\n\n for (const line of lines) {\n const colonIndex = line.indexOf(\":\");\n if (colonIndex > 0) {\n const key = line.slice(0, colonIndex).trim().toLowerCase();\n const value = line.slice(colonIndex + 1).trim();\n headers[key] = value;\n }\n }\n\n return headers;\n}\n\n// =============================================================================\n// Image Discovery Functions\n// =============================================================================\n\n/**\n * Content structure type detected from file path\n */\nexport type ContentStructure = \"flat\" | \"folder-based\" | \"date-prefixed\";\n\n/**\n * Result of content structure detection\n */\nexport interface ContentStructureResult {\n /** Detected structure type */\n structure: ContentStructure;\n /** Path to the image folder (null if doesn't exist) */\n imageFolderPath: string | null;\n}\n\n/**\n * Date prefix pattern for content files (e.g., 2024-01-15-my-post.md)\n */\nconst DATE_PREFIX_PATTERN = /^\\d{4}-\\d{2}-\\d{2}-/;\n\n/**\n * Detect content structure and get the image folder path\n *\n * Handles three content structures:\n * - Flat file: `my-post.md` -> looks for `my-post/` sibling folder\n * - Folder-based: `slug/index.md` -> uses `slug/` parent folder\n * - Date-prefixed: `2024-01-15-my-post.md` -> looks for `2024-01-15-my-post/` sibling folder\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param contentId - Content ID (slug)\n * @param contentFilePath - Absolute path to the content file\n * @returns Path to the image folder, or null if no image folder exists\n *\n * @example\n * ```typescript\n * // Flat file structure\n * const folder = getContentImageFolder(\n * '/project/src/content/blog',\n * 'my-post',\n * '/project/src/content/blog/my-post.md'\n * );\n * // Returns: '/project/src/content/blog/my-post' (if exists)\n *\n * // Folder-based structure\n * const folder = getContentImageFolder(\n * '/project/src/content/blog',\n * 'my-post',\n * '/project/src/content/blog/my-post/index.md'\n * );\n * // Returns: '/project/src/content/blog/my-post'\n * ```\n */\nexport function getContentImageFolder(\n collectionPath: string,\n contentId: string,\n contentFilePath: string\n): string | null {\n const filename = basename(contentFilePath);\n const contentDir = dirname(contentFilePath);\n\n // Check for folder-based structure (index.md or index.mdx)\n if (filename === \"index.md\" || filename === \"index.mdx\") {\n // For folder-based content, the image folder is the parent folder itself\n // The folder already exists since it contains the index file\n return contentDir;\n }\n\n // For flat file and date-prefixed structures, look for sibling folder\n // Both use the contentId as the folder name\n const siblingFolderPath = join(collectionPath, contentId);\n\n if (existsSync(siblingFolderPath)) {\n return siblingFolderPath;\n }\n\n return null;\n}\n\n/**\n * Detect the content structure type from a content file path\n *\n * @param contentFilePath - Absolute path to the content file\n * @returns The detected content structure type\n */\nexport function detectContentStructure(\n contentFilePath: string\n): ContentStructure {\n const filename = basename(contentFilePath);\n\n // Check for folder-based structure\n if (filename === \"index.md\" || filename === \"index.mdx\") {\n return \"folder-based\";\n }\n\n // Check for date-prefixed structure\n const nameWithoutExt = filename.replace(/\\.(md|mdx)$/, \"\");\n if (DATE_PREFIX_PATTERN.test(nameWithoutExt)) {\n return \"date-prefixed\";\n }\n\n // Default to flat file structure\n return \"flat\";\n}\n\n// getContentFilePath is imported from ./reader\n\n// =============================================================================\n// Recursive Image Scanner\n// =============================================================================\n\n/**\n * Options for recursive directory scanning\n */\ninterface ScanOptions {\n /** Maximum recursion depth */\n maxDepth: number;\n /** Current recursion depth */\n currentDepth: number;\n /** Base path for calculating relative paths */\n basePath: string;\n}\n\n/**\n * Check if a directory should be skipped during scanning\n *\n * Skips hidden folders (starting with .) and special folders (starting with _)\n *\n * @param dirName - Directory name to check\n * @returns True if the directory should be skipped\n */\nfunction shouldSkipDirectory(dirName: string): boolean {\n return dirName.startsWith(\".\") || dirName.startsWith(\"_\");\n}\n\n/**\n * Scan a directory recursively for image files\n *\n * Recursively scans the given directory for image files with supported extensions.\n * Skips hidden folders (starting with .) and special folders (starting with _).\n * Limits recursion to the specified maxDepth.\n *\n * @param dirPath - Absolute path to the directory to scan\n * @param basePath - Base path for calculating relative paths\n * @param options - Scan options including maxDepth and currentDepth\n * @returns Array of discovered images\n *\n * @example\n * ```typescript\n * const images = await scanDirectoryForImages(\n * '/project/src/content/blog/my-post',\n * '/project/src/content/blog/my-post',\n * { maxDepth: 5, currentDepth: 0, basePath: '/project/src/content/blog/my-post' }\n * );\n * ```\n */\nexport async function scanDirectoryForImages(\n dirPath: string,\n basePath: string,\n options: ScanOptions\n): Promise<DiscoveredImage[]> {\n const { maxDepth, currentDepth } = options;\n\n // Stop if we've exceeded max depth\n if (currentDepth >= maxDepth) {\n return [];\n }\n\n // Check if directory exists\n if (!existsSync(dirPath)) {\n return [];\n }\n\n const images: DiscoveredImage[] = [];\n\n try {\n const entries = await readdir(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const entryPath = join(dirPath, entry.name);\n\n if (entry.isDirectory()) {\n // Skip hidden and special folders\n if (shouldSkipDirectory(entry.name)) {\n continue;\n }\n\n // Recursively scan subdirectory\n const subImages = await scanDirectoryForImages(entryPath, basePath, {\n maxDepth,\n currentDepth: currentDepth + 1,\n basePath,\n });\n images.push(...subImages);\n } else if (entry.isFile()) {\n // Check if it's a valid image file\n if (isValidImageFile(entry.name)) {\n const fileStat = await stat(entryPath);\n const extension = extname(entry.name).toLowerCase();\n\n // Calculate relative path from base path\n const relativePath = calculateRelativePathFromBase(\n basePath,\n entryPath\n );\n\n images.push({\n filename: entry.name,\n relativePath,\n absolutePath: entryPath,\n size: fileStat.size,\n extension,\n });\n }\n }\n }\n } catch {\n // Return empty array on error (e.g., permission denied)\n return [];\n }\n\n return images;\n}\n\n/**\n * Calculate relative path from base path to target path\n *\n * @param basePath - Base path (image folder root)\n * @param targetPath - Target path (image file)\n * @returns Relative path starting with ./\n */\nfunction calculateRelativePathFromBase(\n basePath: string,\n targetPath: string\n): string {\n const relPath = relative(basePath, targetPath);\n return `./${relPath}`;\n}\n\n/**\n * Calculate relative path from content file to image file\n *\n * Calculates the path that can be used in markdown to reference an image\n * relative to the content file's location. The path always starts with ./\n * to ensure it's treated as a relative reference.\n *\n * @param contentFilePath - Absolute path to the content file (e.g., /project/src/content/blog/my-post.md)\n * @param imagePath - Absolute path to the image file (e.g., /project/src/content/blog/my-post/images/hero.jpg)\n * @returns Relative path starting with ./ (e.g., ./my-post/images/hero.jpg)\n *\n * @example\n * ```typescript\n * // Flat file structure\n * const relPath = calculateRelativePath(\n * '/project/src/content/blog/my-post.md',\n * '/project/src/content/blog/my-post/hero.jpg'\n * );\n * // Returns: './my-post/hero.jpg'\n *\n * // Folder-based structure\n * const relPath = calculateRelativePath(\n * '/project/src/content/blog/my-post/index.md',\n * '/project/src/content/blog/my-post/images/hero.jpg'\n * );\n * // Returns: './images/hero.jpg'\n *\n * // Nested subfolder\n * const relPath = calculateRelativePath(\n * '/project/src/content/blog/my-post/index.md',\n * '/project/src/content/blog/my-post/assets/photos/hero.jpg'\n * );\n * // Returns: './assets/photos/hero.jpg'\n * ```\n */\nexport function calculateRelativePath(\n contentFilePath: string,\n imagePath: string\n): string {\n // Get the directory containing the content file\n const contentDir = dirname(contentFilePath);\n\n // Calculate relative path from content directory to image\n const relPath = relative(contentDir, imagePath);\n\n // Ensure path starts with ./ for relative references\n // The relative() function may return paths without ./ prefix\n if (relPath.startsWith(\"..\")) {\n // Path goes up directories - keep as is but ensure ./ prefix\n return relPath;\n }\n\n // For paths in same or child directories, ensure ./ prefix\n return `./${relPath}`;\n}\n\n// =============================================================================\n// Main Discovery Function\n// =============================================================================\n\n/**\n * Default maximum recursion depth for image discovery\n */\nconst DEFAULT_MAX_DEPTH = 5;\n\n/**\n * Discover all images associated with a content item\n *\n * This is the main entry point for image discovery. It:\n * 1. Locates the content file using getContentFilePath\n * 2. Determines the image folder using getContentImageFolder\n * 3. Scans the folder recursively using scanDirectoryForImages\n * 4. Calculates relative paths for all discovered images\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param contentId - Content ID (slug)\n * @param options - Optional discovery options\n * @returns ImageDiscoveryResult with discovered images or error\n *\n * @example\n * ```typescript\n * const result = await discoverContentImages(\n * '/project/src/content/blog',\n * 'my-post',\n * { maxDepth: 3 }\n * );\n *\n * if (result.success) {\n * console.log(`Found ${result.images.length} images`);\n * for (const img of result.images) {\n * console.log(`- ${img.relativePath}`);\n * }\n * }\n * ```\n */\nexport async function discoverContentImages(\n collectionPath: string,\n contentId: string,\n options?: ImageDiscoveryOptions\n): Promise<ImageDiscoveryResult> {\n const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;\n\n try {\n // Step 1: Get content file path\n const contentFilePath = getContentFilePath(collectionPath, contentId);\n\n if (!contentFilePath) {\n return {\n success: false,\n images: [],\n error: `Content '${contentId}' not found in collection`,\n };\n }\n\n // Step 2: Determine image folder\n const imageFolderPath = getContentImageFolder(\n collectionPath,\n contentId,\n contentFilePath\n );\n\n // If no image folder exists, return empty array (not an error per Requirement 1.4)\n if (!imageFolderPath) {\n return {\n success: true,\n images: [],\n };\n }\n\n // Step 3: Scan folder for images\n const scannedImages = await scanDirectoryForImages(\n imageFolderPath,\n imageFolderPath,\n {\n maxDepth,\n currentDepth: 0,\n basePath: imageFolderPath,\n }\n );\n\n // Step 4: Calculate relative paths from content file to each image\n const images: DiscoveredImage[] = scannedImages.map((img) => ({\n ...img,\n relativePath: calculateRelativePath(contentFilePath, img.absolutePath),\n }));\n\n return {\n success: true,\n images,\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n return {\n success: false,\n images: [],\n error: `Failed to discover images: ${message}`,\n };\n }\n}\n"],"mappings":"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","names":["WritenexErrorCode","writeFile","unlink","mkdir","readFile","stat","existsSync","join","basename","join","existsSync","mkdir","writeFile","readFile","basename","stat","unlink","writeFile","mkdir","readdir","stat","existsSync","join","dirname","basename","basename","join","existsSync","dirname","mkdir","writeFile","readdir","stat"]}
@@ -0,0 +1,52 @@
1
+ // src/config/defaults.ts
2
+ var DEFAULT_IMAGE_CONFIG = {
3
+ strategy: "colocated",
4
+ publicPath: "/images",
5
+ storagePath: "public/images"
6
+ };
7
+ var DEFAULT_EDITOR_CONFIG = {
8
+ autosave: true,
9
+ autosaveInterval: 3e3
10
+ };
11
+ var DEFAULT_DISCOVERY_CONFIG = {
12
+ enabled: true,
13
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"]
14
+ };
15
+ var DEFAULT_VERSION_HISTORY_CONFIG = {
16
+ enabled: true,
17
+ maxVersions: 20,
18
+ storagePath: ".writenex/versions"
19
+ };
20
+ var DEFAULT_FILE_PATTERN = "{slug}.md";
21
+ var DEFAULT_CONTENT_PATH = "src/content";
22
+ function applyCollectionDefaults(collection) {
23
+ return {
24
+ name: collection.name,
25
+ path: collection.path,
26
+ filePattern: collection.filePattern ?? DEFAULT_FILE_PATTERN,
27
+ previewUrl: collection.previewUrl ?? `/${collection.name}/{slug}`,
28
+ schema: collection.schema ?? {},
29
+ images: collection.images ? { ...DEFAULT_IMAGE_CONFIG, ...collection.images } : DEFAULT_IMAGE_CONFIG
30
+ };
31
+ }
32
+ function applyConfigDefaults(config = {}) {
33
+ return {
34
+ collections: (config.collections ?? []).map(applyCollectionDefaults),
35
+ images: config.images ? { ...DEFAULT_IMAGE_CONFIG, ...config.images } : DEFAULT_IMAGE_CONFIG,
36
+ editor: config.editor ? { ...DEFAULT_EDITOR_CONFIG, ...config.editor } : DEFAULT_EDITOR_CONFIG,
37
+ discovery: config.discovery ? { ...DEFAULT_DISCOVERY_CONFIG, ...config.discovery } : DEFAULT_DISCOVERY_CONFIG,
38
+ versionHistory: config.versionHistory ? { ...DEFAULT_VERSION_HISTORY_CONFIG, ...config.versionHistory } : DEFAULT_VERSION_HISTORY_CONFIG
39
+ };
40
+ }
41
+
42
+ export {
43
+ DEFAULT_IMAGE_CONFIG,
44
+ DEFAULT_EDITOR_CONFIG,
45
+ DEFAULT_DISCOVERY_CONFIG,
46
+ DEFAULT_VERSION_HISTORY_CONFIG,
47
+ DEFAULT_FILE_PATTERN,
48
+ DEFAULT_CONTENT_PATH,
49
+ applyCollectionDefaults,
50
+ applyConfigDefaults
51
+ };
52
+ //# sourceMappingURL=chunk-CRPZUUDU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config/defaults.ts"],"sourcesContent":["/**\n * @fileoverview Default configuration values for @writenex/astro\n *\n * This module provides default values for all configuration options.\n * These defaults are applied when loading configuration to ensure\n * all required values are present.\n *\n * @module @writenex/astro/config/defaults\n */\n\nimport type {\n WritenexConfig,\n ImageConfig,\n EditorConfig,\n DiscoveryConfig,\n CollectionConfig,\n VersionHistoryConfig,\n} from \"@/types\";\n\n/**\n * Default image configuration\n */\nexport const DEFAULT_IMAGE_CONFIG: Required<ImageConfig> = {\n strategy: \"colocated\",\n publicPath: \"/images\",\n storagePath: \"public/images\",\n};\n\n/**\n * Default editor configuration\n */\nexport const DEFAULT_EDITOR_CONFIG: Required<EditorConfig> = {\n autosave: true,\n autosaveInterval: 3000,\n};\n\n/**\n * Default discovery configuration\n */\nexport const DEFAULT_DISCOVERY_CONFIG: Required<DiscoveryConfig> = {\n enabled: true,\n ignore: [\"**/node_modules/**\", \"**/.git/**\", \"**/dist/**\"],\n};\n\n/**\n * Default version history configuration\n */\nexport const DEFAULT_VERSION_HISTORY_CONFIG: Required<VersionHistoryConfig> = {\n enabled: true,\n maxVersions: 20,\n storagePath: \".writenex/versions\",\n};\n\n/**\n * Default file pattern for content files\n */\nexport const DEFAULT_FILE_PATTERN = \"{slug}.md\";\n\n/**\n * Default content directory path\n */\nexport const DEFAULT_CONTENT_PATH = \"src/content\";\n\n/**\n * Apply defaults to a collection configuration\n *\n * @param collection - Partial collection configuration\n * @returns Collection configuration with defaults applied\n */\nexport function applyCollectionDefaults(\n collection: CollectionConfig\n): Required<CollectionConfig> {\n return {\n name: collection.name,\n path: collection.path,\n filePattern: collection.filePattern ?? DEFAULT_FILE_PATTERN,\n previewUrl: collection.previewUrl ?? `/${collection.name}/{slug}`,\n schema: collection.schema ?? {},\n images: collection.images\n ? { ...DEFAULT_IMAGE_CONFIG, ...collection.images }\n : DEFAULT_IMAGE_CONFIG,\n };\n}\n\n/**\n * Apply defaults to the main Writenex configuration\n *\n * @param config - Partial Writenex configuration\n * @returns Configuration with all defaults applied\n */\nexport function applyConfigDefaults(\n config: WritenexConfig = {}\n): Required<WritenexConfig> {\n return {\n collections: (config.collections ?? []).map(applyCollectionDefaults),\n images: config.images\n ? { ...DEFAULT_IMAGE_CONFIG, ...config.images }\n : DEFAULT_IMAGE_CONFIG,\n editor: config.editor\n ? { ...DEFAULT_EDITOR_CONFIG, ...config.editor }\n : DEFAULT_EDITOR_CONFIG,\n discovery: config.discovery\n ? { ...DEFAULT_DISCOVERY_CONFIG, ...config.discovery }\n : DEFAULT_DISCOVERY_CONFIG,\n versionHistory: config.versionHistory\n ? { ...DEFAULT_VERSION_HISTORY_CONFIG, ...config.versionHistory }\n : DEFAULT_VERSION_HISTORY_CONFIG,\n };\n}\n"],"mappings":";AAsBO,IAAM,uBAA8C;AAAA,EACzD,UAAU;AAAA,EACV,YAAY;AAAA,EACZ,aAAa;AACf;AAKO,IAAM,wBAAgD;AAAA,EAC3D,UAAU;AAAA,EACV,kBAAkB;AACpB;AAKO,IAAM,2BAAsD;AAAA,EACjE,SAAS;AAAA,EACT,QAAQ,CAAC,sBAAsB,cAAc,YAAY;AAC3D;AAKO,IAAM,iCAAiE;AAAA,EAC5E,SAAS;AAAA,EACT,aAAa;AAAA,EACb,aAAa;AACf;AAKO,IAAM,uBAAuB;AAK7B,IAAM,uBAAuB;AAQ7B,SAAS,wBACd,YAC4B;AAC5B,SAAO;AAAA,IACL,MAAM,WAAW;AAAA,IACjB,MAAM,WAAW;AAAA,IACjB,aAAa,WAAW,eAAe;AAAA,IACvC,YAAY,WAAW,cAAc,IAAI,WAAW,IAAI;AAAA,IACxD,QAAQ,WAAW,UAAU,CAAC;AAAA,IAC9B,QAAQ,WAAW,SACf,EAAE,GAAG,sBAAsB,GAAG,WAAW,OAAO,IAChD;AAAA,EACN;AACF;AAQO,SAAS,oBACd,SAAyB,CAAC,GACA;AAC1B,SAAO;AAAA,IACL,cAAc,OAAO,eAAe,CAAC,GAAG,IAAI,uBAAuB;AAAA,IACnE,QAAQ,OAAO,SACX,EAAE,GAAG,sBAAsB,GAAG,OAAO,OAAO,IAC5C;AAAA,IACJ,QAAQ,OAAO,SACX,EAAE,GAAG,uBAAuB,GAAG,OAAO,OAAO,IAC7C;AAAA,IACJ,WAAW,OAAO,YACd,EAAE,GAAG,0BAA0B,GAAG,OAAO,UAAU,IACnD;AAAA,IACJ,gBAAgB,OAAO,iBACnB,EAAE,GAAG,gCAAgC,GAAG,OAAO,eAAe,IAC9D;AAAA,EACN;AACF;","names":[]}