@imjp/writenex-astro 0.1.0 → 1.3.6
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.
- package/README.md +13 -13
- package/dist/{chunk-CF2XXJFF.js → chunk-4H63L4YO.js} +436 -436
- package/dist/chunk-4H63L4YO.js.map +1 -0
- package/dist/{chunk-AAOQHQPU.js → chunk-GYAFIVVI.js} +6 -6
- package/dist/chunk-GYAFIVVI.js.map +1 -0
- package/dist/{chunk-XNTQTTJU.js → chunk-JFQQJPDF.js} +2 -2
- package/dist/{chunk-XNTQTTJU.js.map → chunk-JFQQJPDF.js.map} +1 -1
- package/dist/{chunk-CYLDJ3HZ.js → chunk-JMNCPNQX.js} +4 -4
- package/dist/{chunk-CYLDJ3HZ.js.map → chunk-JMNCPNQX.js.map} +1 -1
- package/dist/{chunk-5PM6EQE5.js → chunk-N37EPLKG.js} +13 -5
- package/dist/chunk-N37EPLKG.js.map +1 -0
- package/dist/{chunk-7XU5X6CW.js → chunk-NSW7AIVF.js} +12 -12
- package/dist/chunk-NSW7AIVF.js.map +1 -0
- package/dist/{chunk-CRPZUUDU.js → chunk-YBCPOLMY.js} +1 -1
- package/dist/{chunk-CRPZUUDU.js.map → chunk-YBCPOLMY.js.map} +1 -1
- package/dist/client/index.css +1 -1
- package/dist/client/index.css.map +1 -1
- package/dist/client/index.d.ts +19 -0
- package/dist/client/index.js +159 -147
- package/dist/client/index.js.map +1 -1
- package/dist/client/styles.css +2 -8
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +2 -2
- package/dist/{config-BmEdBDo_.d.ts → config-CliL0CoN.d.ts} +1 -1
- package/dist/{content-BWR52vD-.d.ts → content-TuL3GT66.d.ts} +1 -1
- package/dist/discovery/index.d.ts +2 -2
- package/dist/discovery/index.js +3 -3
- package/dist/filesystem/index.d.ts +703 -703
- package/dist/filesystem/index.js +4 -4
- package/dist/filesystem/index.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +8 -8
- package/dist/index.js.map +1 -1
- package/dist/{loader-55LWCXHA.js → loader-53VVP2IN.js} +3 -3
- package/dist/schema-DDJyoVkj.d.ts +189 -0
- package/dist/server/index.d.ts +37 -37
- package/dist/server/index.js +5 -5
- package/package.json +17 -18
- package/src/client/App.tsx +18 -18
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +14 -13
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +1 -1
- package/src/client/components/Editor/Editor.tsx +27 -27
- package/src/client/components/Editor/ImageDialog.tsx +4 -3
- package/src/client/components/Editor/LinkDialog.tsx +7 -6
- package/src/client/components/Editor/index.ts +1 -1
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +1 -1
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +1 -1
- package/src/client/components/Header/Header.tsx +8 -8
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +1 -1
- package/src/client/components/LazyEditor.tsx +1 -1
- package/src/client/components/LiveRegion/index.ts +1 -1
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +5 -5
- package/src/client/components/SearchReplace/index.ts +1 -1
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +2 -2
- package/src/client/components/Sidebar/Sidebar.tsx +6 -6
- package/src/client/components/SkipLink/index.ts +1 -1
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +1 -1
- package/src/client/components/VersionHistory/DiffViewer.tsx +18 -11
- package/src/client/components/VersionHistory/VersionActions.tsx +6 -6
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +10 -10
- package/src/client/components/VersionHistory/index.ts +2 -2
- package/src/client/context/ApiContext.tsx +2 -2
- package/src/client/context/ThemeContext.tsx +2 -2
- package/src/client/hooks/useApi.ts +1 -1
- package/src/client/hooks/useFocusTrap.ts +1 -1
- package/src/client/hooks/useSearch.ts +1 -1
- package/src/client/hooks/useVersionHistory.ts +2 -2
- package/src/client/index.tsx +1 -1
- package/src/client/styles.css +2 -8
- package/src/config/defaults.ts +4 -4
- package/src/config/index.ts +14 -16
- package/src/config/loader.ts +24 -4
- package/src/config/schema.ts +8 -4
- package/src/core/index.ts +1 -1
- package/src/discovery/collections.ts +3 -3
- package/src/discovery/index.ts +9 -11
- package/src/discovery/patterns.ts +2 -2
- package/src/discovery/schema.ts +1 -1
- package/src/filesystem/images.ts +3 -3
- package/src/filesystem/index.ts +74 -79
- package/src/filesystem/reader.ts +5 -3
- package/src/filesystem/version-config.ts +10 -10
- package/src/filesystem/versions.ts +9 -9
- package/src/filesystem/watcher.ts +1 -1
- package/src/filesystem/writer.ts +6 -6
- package/src/global.d.ts +39 -0
- package/src/index.ts +10 -10
- package/src/integration.ts +6 -3
- package/src/server/assets.ts +3 -3
- package/src/server/cache.ts +1 -1
- package/src/server/index.ts +12 -15
- package/src/server/middleware.ts +3 -3
- package/src/server/routes.ts +28 -28
- package/src/types/index.ts +24 -28
- package/dist/chunk-5PM6EQE5.js.map +0 -1
- package/dist/chunk-7XU5X6CW.js.map +0 -1
- package/dist/chunk-AAOQHQPU.js.map +0 -1
- package/dist/chunk-CF2XXJFF.js.map +0 -1
- package/dist/loader-CrdnaAWR.d.ts +0 -327
- /package/dist/{loader-55LWCXHA.js.map → loader-53VVP2IN.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/errors.ts","../src/filesystem/images.ts","../src/filesystem/versions.ts","../src/filesystem/writer.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 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 { existsSync } from \"node:fs\";\nimport { mkdir, readdir, stat, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, extname, join, relative } from \"node:path\";\nimport type {\n DiscoveredImage,\n ImageConfig,\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","/**\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 { existsSync } from \"node:fs\";\nimport {\n mkdir,\n readdir,\n readFile,\n stat,\n unlink,\n writeFile,\n} from \"node:fs/promises\";\nimport { basename, join } from \"node:path\";\nimport matter from \"gray-matter\";\nimport type {\n RestoreResult,\n RestoreVersionOptions,\n SaveVersionOptions,\n Version,\n VersionEntry,\n VersionHistoryConfig,\n VersionManifest,\n VersionResult,\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 { existsSync } from \"node:fs\";\nimport { mkdir, readFile, stat, unlink, writeFile } from \"node:fs/promises\";\nimport { basename, dirname, join } from \"node:path\";\nimport slugify from \"slugify\";\nimport { ContentConflictError } from \"@/core/errors\";\nimport {\n generatePathFromPattern,\n isValidPattern,\n resolvePatternTokens,\n} from \"@/discovery/patterns\";\nimport type { VersionHistoryConfig } from \"@/types\";\nimport { readContentFile } from \"./reader\";\nimport { saveVersion } from \"./versions\";\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"],"mappings":"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","names":["WritenexErrorCode","existsSync","mkdir","readdir","stat","writeFile","basename","join","existsSync","mkdir","readFile","stat","unlink","writeFile","basename","dirname","join","join","existsSync","dirname","mkdir","writeFile","readFile","basename","stat","unlink"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/filesystem/reader.ts
|
|
2
|
-
import { readFile, readdir, stat } from "fs/promises";
|
|
3
2
|
import { existsSync } from "fs";
|
|
4
|
-
import {
|
|
3
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
4
|
+
import { basename, extname, join, relative } from "path";
|
|
5
5
|
import matter from "gray-matter";
|
|
6
6
|
var CONTENT_EXTENSIONS = [".md", ".mdx"];
|
|
7
7
|
var EXCERPT_LENGTH = 150;
|
|
@@ -14,7 +14,7 @@ function extractSlug(filePath, collectionPath) {
|
|
|
14
14
|
const filename = basename(relativePath);
|
|
15
15
|
const ext = extname(filename);
|
|
16
16
|
if (filename === "index.md" || filename === "index.mdx") {
|
|
17
|
-
const parts = relativePath.split("/");
|
|
17
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
18
18
|
if (parts.length >= 2) {
|
|
19
19
|
const slug = parts[parts.length - 2];
|
|
20
20
|
if (slug) return slug;
|
|
@@ -178,9 +178,9 @@ function getContentFilePath(collectionPath, contentId) {
|
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
// src/discovery/patterns.ts
|
|
181
|
-
import { readdir as readdir2 } from "fs/promises";
|
|
182
181
|
import { existsSync as existsSync2 } from "fs";
|
|
183
|
-
import {
|
|
182
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
183
|
+
import { extname as extname2, join as join2, relative as relative2 } from "path";
|
|
184
184
|
var PATTERN_DEFINITIONS = [
|
|
185
185
|
// {year}/{month}/{day}/{slug}.md - Full date folder structure
|
|
186
186
|
{
|
|
@@ -571,4 +571,4 @@ export {
|
|
|
571
571
|
isValidPattern,
|
|
572
572
|
getSupportedTokens
|
|
573
573
|
};
|
|
574
|
-
//# sourceMappingURL=chunk-
|
|
574
|
+
//# sourceMappingURL=chunk-GYAFIVVI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/filesystem/reader.ts","../src/discovery/patterns.ts"],"sourcesContent":["/**\n * @fileoverview Filesystem reader for content collections\n *\n * This module provides functions for reading content files from the filesystem,\n * parsing frontmatter, and extracting content metadata.\n *\n * ## Features:\n * - Read individual content files with frontmatter parsing\n * - List all content files in a collection\n * - Generate content summaries for listing\n * - Support for .md and .mdx files\n *\n * @module @writenex/astro/filesystem/reader\n */\n\nimport { existsSync } from \"node:fs\";\nimport { readdir, readFile, stat } from \"node:fs/promises\";\nimport { basename, extname, join, relative } from \"node:path\";\nimport matter from \"gray-matter\";\nimport type { ContentItem, ContentSummary } from \"@/types\";\n\n/**\n * Supported content file extensions\n */\nconst CONTENT_EXTENSIONS = [\".md\", \".mdx\"];\n\n/**\n * Maximum excerpt length in characters\n */\nconst EXCERPT_LENGTH = 150;\n\n/**\n * Options for reading content\n */\nexport interface ReadContentOptions {\n /** Include draft content in listings */\n includeDrafts?: boolean;\n /** Sort field for listings */\n sortBy?: string;\n /** Sort order */\n sortOrder?: \"asc\" | \"desc\";\n}\n\n/**\n * Result of reading a content file\n */\nexport interface ReadFileResult {\n /** Whether the read was successful */\n success: boolean;\n /** The content item (if successful) */\n content?: ContentItem;\n /** Error message (if failed) */\n error?: string;\n}\n\n/**\n * Check if a file is a content file based on extension\n *\n * @param filename - The filename to check\n * @returns True if the file is a content file\n */\nexport function isContentFile(filename: string): boolean {\n const ext = extname(filename).toLowerCase();\n return CONTENT_EXTENSIONS.includes(ext);\n}\n\n/**\n * Extract slug from a content file path\n *\n * Handles various file patterns:\n * - `my-post.md` -> `my-post`\n * - `2024-01-15-my-post.md` -> `2024-01-15-my-post`\n * - `my-post/index.md` -> `my-post`\n *\n * @param filePath - Path to the content file\n * @param collectionPath - Path to the collection directory\n * @returns The extracted slug\n */\nexport function extractSlug(filePath: string, collectionPath: string): string {\n const relativePath = relative(collectionPath, filePath);\n const filename = basename(relativePath);\n const ext = extname(filename);\n\n // Handle index files (folder-based content)\n // On Windows, path.relative() uses backslashes — normalise to forward slashes\n // so the split works on both platforms.\n if (filename === \"index.md\" || filename === \"index.mdx\") {\n const parts = relativePath.replace(/\\\\/g, \"/\").split(\"/\");\n if (parts.length >= 2) {\n const slug = parts[parts.length - 2];\n if (slug) return slug;\n }\n }\n\n // Remove extension to get slug\n return filename.slice(0, -ext.length);\n}\n\n/**\n * Generate an excerpt from markdown content\n *\n * @param body - The markdown body content\n * @param maxLength - Maximum excerpt length\n * @returns The generated excerpt\n */\nexport function generateExcerpt(\n body: string,\n maxLength: number = EXCERPT_LENGTH\n): string {\n // Remove markdown formatting for cleaner excerpt\n const cleaned = body\n // Remove headers\n .replace(/^#{1,6}\\s+/gm, \"\")\n // Remove bold/italic\n .replace(/\\*\\*([^*]+)\\*\\*/g, \"$1\")\n .replace(/\\*([^*]+)\\*/g, \"$1\")\n .replace(/__([^_]+)__/g, \"$1\")\n .replace(/_([^_]+)_/g, \"$1\")\n // Remove links but keep text\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, \"$1\")\n // Remove images\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, \"\")\n // Remove code blocks\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`([^`]+)`/g, \"$1\")\n // Remove blockquotes\n .replace(/^>\\s+/gm, \"\")\n // Remove horizontal rules\n .replace(/^[-*_]{3,}$/gm, \"\")\n // Collapse whitespace\n .replace(/\\s+/g, \" \")\n .trim();\n\n if (cleaned.length <= maxLength) {\n return cleaned;\n }\n\n // Truncate at word boundary\n const truncated = cleaned.slice(0, maxLength);\n const lastSpace = truncated.lastIndexOf(\" \");\n\n if (lastSpace > maxLength * 0.7) {\n return truncated.slice(0, lastSpace) + \"...\";\n }\n\n return truncated + \"...\";\n}\n\n/**\n * Read and parse a single content file\n *\n * @param filePath - Absolute path to the content file\n * @param collectionPath - Path to the collection directory\n * @returns ReadFileResult with the parsed content or error\n *\n * @example\n * ```typescript\n * const result = await readContentFile(\n * '/project/src/content/blog/my-post.md',\n * '/project/src/content/blog'\n * );\n *\n * if (result.success) {\n * console.log(result.content.frontmatter.title);\n * }\n * ```\n */\nexport async function readContentFile(\n filePath: string,\n collectionPath: string\n): Promise<ReadFileResult> {\n try {\n // Check if file exists\n if (!existsSync(filePath)) {\n return {\n success: false,\n error: `File not found: ${filePath}`,\n };\n }\n\n // Read file content and stats in parallel\n const [raw, stats] = await Promise.all([\n readFile(filePath, \"utf-8\"),\n stat(filePath),\n ]);\n\n // Parse frontmatter\n const { data: frontmatter, content: body } = matter(raw);\n\n // Extract slug\n const id = extractSlug(filePath, collectionPath);\n\n return {\n success: true,\n content: {\n id,\n path: filePath,\n frontmatter,\n body: body.trim(),\n raw,\n mtime: stats.mtimeMs,\n },\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n error: `Failed to read content file: ${message}`,\n };\n }\n}\n\n/**\n * List all content files in a directory recursively\n *\n * @param dirPath - Path to the directory to scan\n * @returns Array of absolute file paths\n */\nasync function listFilesRecursive(dirPath: string): Promise<string[]> {\n const files: string[] = [];\n\n if (!existsSync(dirPath)) {\n return files;\n }\n\n const entries = await readdir(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry.name);\n\n if (entry.isDirectory()) {\n // Recursively scan subdirectories\n const subFiles = await listFilesRecursive(fullPath);\n files.push(...subFiles);\n } else if (entry.isFile() && isContentFile(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\n/**\n * Read all content files in a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param options - Read options\n * @returns Array of content items\n *\n * @example\n * ```typescript\n * const items = await readCollection('/project/src/content/blog', {\n * includeDrafts: false,\n * sortBy: 'pubDate',\n * sortOrder: 'desc',\n * });\n * ```\n */\nexport async function readCollection(\n collectionPath: string,\n options: ReadContentOptions = {}\n): Promise<ContentItem[]> {\n const { includeDrafts = true, sortBy, sortOrder = \"desc\" } = options;\n\n // Get all content files\n const filePaths = await listFilesRecursive(collectionPath);\n\n // Read and parse all files\n const results = await Promise.all(\n filePaths.map((fp) => readContentFile(fp, collectionPath))\n );\n\n // Filter successful reads and optionally filter drafts\n let items = results\n .filter(\n (r): r is { success: true; content: ContentItem } =>\n r.success && !!r.content\n )\n .map((r) => r.content)\n .filter((item) => {\n if (!includeDrafts && item.frontmatter.draft === true) {\n return false;\n }\n return true;\n });\n\n // Sort if requested\n if (sortBy) {\n items = items.sort((a, b) => {\n const aVal = a.frontmatter[sortBy];\n const bVal = b.frontmatter[sortBy];\n\n // Handle undefined values\n if (aVal === undefined && bVal === undefined) return 0;\n if (aVal === undefined) return sortOrder === \"asc\" ? -1 : 1;\n if (bVal === undefined) return sortOrder === \"asc\" ? 1 : -1;\n\n // Compare values (convert to string for comparison)\n const aStr = String(aVal);\n const bStr = String(bVal);\n if (aStr < bStr) return sortOrder === \"asc\" ? -1 : 1;\n if (aStr > bStr) return sortOrder === \"asc\" ? 1 : -1;\n return 0;\n });\n }\n\n return items;\n}\n\n/**\n * Convert a content item to a summary for listing\n *\n * @param item - The full content item\n * @returns Content summary with essential fields\n */\nexport function toContentSummary(item: ContentItem): ContentSummary {\n const { id, path, frontmatter, body } = item;\n\n // Support both pubDate and publishDate naming conventions\n const dateValue =\n frontmatter.pubDate ?? frontmatter.publishDate ?? frontmatter.date;\n\n return {\n id,\n path,\n title: String(frontmatter.title ?? id),\n pubDate: dateValue ? String(dateValue) : undefined,\n draft: frontmatter.draft === true,\n excerpt: generateExcerpt(body),\n };\n}\n\n/**\n * Get content summaries for a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param options - Read options\n * @returns Array of content summaries\n */\nexport async function getCollectionSummaries(\n collectionPath: string,\n options: ReadContentOptions = {}\n): Promise<ContentSummary[]> {\n const items = await readCollection(collectionPath, options);\n return items.map(toContentSummary);\n}\n\n/**\n * Get the count of content files in a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Number of content files\n */\nexport async function getCollectionCount(\n collectionPath: string\n): Promise<number> {\n const filePaths = await listFilesRecursive(collectionPath);\n return filePaths.length;\n}\n\n/**\n * Check if a collection directory exists and contains content\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Object with exists and hasContent flags\n */\nexport async function checkCollection(collectionPath: string): Promise<{\n exists: boolean;\n hasContent: boolean;\n count: number;\n}> {\n if (!existsSync(collectionPath)) {\n return { exists: false, hasContent: false, count: 0 };\n }\n\n const count = await getCollectionCount(collectionPath);\n\n return {\n exists: true,\n hasContent: count > 0,\n count,\n };\n}\n\n/**\n * Get file stats for a content file\n *\n * @param filePath - Path to the content file\n * @returns File stats or null if file doesn't exist\n */\nexport async function getFileStats(filePath: string): Promise<{\n size: number;\n mtime: Date;\n ctime: Date;\n} | null> {\n try {\n const stats = await stat(filePath);\n return {\n size: stats.size,\n mtime: stats.mtime,\n ctime: stats.birthtime,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Get the file path for a content item by ID\n *\n * Searches for the content file in the collection directory,\n * handling different content structures:\n * - Folder-based: `slug/index.md` or `slug/index.mdx`\n * - Flat file: `slug.md` or `slug.mdx`\n *\n * @param collectionPath - Path to the collection directory\n * @param contentId - Content ID (slug)\n * @returns File path if found, null otherwise\n *\n * @example\n * ```typescript\n * const filePath = getContentFilePath('/project/src/content/blog', 'my-post');\n * // Returns: '/project/src/content/blog/my-post.md' or\n * // '/project/src/content/blog/my-post/index.md'\n * ```\n */\nexport function getContentFilePath(\n collectionPath: string,\n contentId: string\n): string | null {\n // Try folder-based structure first (slug/index.md or slug/index.mdx)\n const indexMdPath = join(collectionPath, contentId, \"index.md\");\n if (existsSync(indexMdPath)) {\n return indexMdPath;\n }\n\n const indexMdxPath = join(collectionPath, contentId, \"index.mdx\");\n if (existsSync(indexMdxPath)) {\n return indexMdxPath;\n }\n\n // Try flat file structure (slug.md or slug.mdx)\n const flatMdPath = join(collectionPath, `${contentId}.md`);\n if (existsSync(flatMdPath)) {\n return flatMdPath;\n }\n\n const flatMdxPath = join(collectionPath, `${contentId}.mdx`);\n if (existsSync(flatMdxPath)) {\n return flatMdxPath;\n }\n\n return null;\n}\n","/**\n * @fileoverview File pattern detection for content collections\n *\n * This module provides functions to detect and work with file naming patterns\n * in Astro content collections.\n *\n * ## Supported Patterns:\n * - `{slug}.md` - Simple slug-based naming\n * - `{date}-{slug}.md` - Date-prefixed naming (2024-01-15-my-post.md)\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 * - `{slug}/index.md` - Folder-based with index file\n * - `{category}/{slug}.md` - Category folder structure\n * - `{category}/{slug}/index.md` - Category with folder-based content\n * - `{lang}/{slug}.md` - Language-prefixed content (i18n)\n * - `{lang}/{slug}/index.md` - Language with folder-based content\n *\n * ## Custom Patterns:\n * Developers can configure custom patterns in their collection config.\n * Custom tokens are resolved from frontmatter data or use default values.\n *\n * ## Detection Process:\n * 1. Scan collection directory for all content files\n * 2. Analyze file paths and names for common patterns\n * 3. Score each pattern based on match frequency\n * 4. Return the best matching pattern\n *\n * @module @writenex/astro/discovery/patterns\n */\n\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { extname, join, relative } from \"node:path\";\nimport { isContentFile } from \"@/filesystem/reader\";\n\n/**\n * Pattern definition with regex and template\n */\ninterface PatternDefinition {\n /** Pattern name for identification */\n name: string;\n /** Template string with tokens */\n template: string;\n /** Regex to match against file paths */\n regex: RegExp;\n /** Function to extract tokens from a match */\n extract: (match: RegExpMatchArray, ext: string) => Record<string, string>;\n /** Priority when multiple patterns match (higher = preferred) */\n priority: number;\n}\n\n/**\n * Result of pattern detection\n */\nexport interface PatternDetectionResult {\n /** The detected pattern template */\n pattern: string;\n /** Confidence score (0-1) */\n confidence: number;\n /** Number of files that matched this pattern */\n matchCount: number;\n /** Total files analyzed */\n totalFiles: number;\n /** Sample matches for debugging */\n samples: Array<{\n filePath: string;\n extracted: Record<string, string>;\n }>;\n}\n\n/**\n * All supported pattern definitions\n *\n * Order matters - more specific patterns should come first.\n * Higher priority patterns are preferred when multiple patterns match.\n */\nconst PATTERN_DEFINITIONS: PatternDefinition[] = [\n // {year}/{month}/{day}/{slug}.md - Full date folder structure\n {\n name: \"year-month-day-slug\",\n template: \"{year}/{month}/{day}/{slug}.md\",\n regex: /^(\\d{4})\\/(\\d{2})\\/(\\d{2})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n month: match[2] ?? \"\",\n day: match[3] ?? \"\",\n slug: match[4] ?? \"\",\n extension: ext,\n }),\n priority: 95,\n },\n\n // {year}/{month}/{slug}.md - Year/month nested date structure\n {\n name: \"year-month-slug\",\n template: \"{year}/{month}/{slug}.md\",\n regex: /^(\\d{4})\\/(\\d{2})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n month: match[2] ?? \"\",\n slug: match[3] ?? \"\",\n extension: ext,\n }),\n priority: 90,\n },\n\n // {year}/{slug}.md - Year folder structure\n {\n name: \"year-slug\",\n template: \"{year}/{slug}.md\",\n regex: /^(\\d{4})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 85,\n },\n\n // {lang}/{slug}/index.md - Language with folder-based content (i18n)\n {\n name: \"lang-folder-index\",\n template: \"{lang}/{slug}/index.md\",\n regex: /^([a-z]{2}(?:-[A-Z]{2})?)\\/([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n lang: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 82,\n },\n\n // {category}/{slug}/index.md - Category with folder-based content\n {\n name: \"category-folder-index\",\n template: \"{category}/{slug}/index.md\",\n regex: /^([^/]+)\\/([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n category: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 80,\n },\n\n // {slug}/index.md - Folder-based content\n {\n name: \"folder-index\",\n template: \"{slug}/index.md\",\n regex: /^([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n slug: match[1] ?? \"\",\n extension: ext,\n }),\n priority: 75,\n },\n\n // {date}-{slug}.md - Date-prefixed (ISO format)\n {\n name: \"date-slug\",\n template: \"{date}-{slug}.md\",\n regex: /^(\\d{4}-\\d{2}-\\d{2})-(.+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n date: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 70,\n },\n\n // {lang}/{slug}.md - Language-prefixed content (i18n)\n // Matches: en/my-post.md, pt-BR/my-post.md\n {\n name: \"lang-slug\",\n template: \"{lang}/{slug}.md\",\n regex: /^([a-z]{2}(?:-[A-Z]{2})?)\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n lang: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 60,\n },\n\n // {category}/{slug}.md - Category folder (catch-all for non-date/non-lang folders)\n {\n name: \"category-slug\",\n template: \"{category}/{slug}.md\",\n regex: /^([^/]+)\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n category: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 50,\n },\n\n // {slug}.md - Simple flat structure (default fallback)\n {\n name: \"simple-slug\",\n template: \"{slug}.md\",\n regex: /^([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n slug: match[1] ?? \"\",\n extension: ext,\n }),\n priority: 10,\n },\n];\n\n/**\n * List all content files in a directory recursively\n *\n * @param dirPath - Directory to scan\n * @returns Array of relative file paths\n */\nasync function listContentFiles(dirPath: string): Promise<string[]> {\n const files: string[] = [];\n\n if (!existsSync(dirPath)) {\n return files;\n }\n\n async function scan(currentPath: string, relativeTo: string): Promise<void> {\n const entries = await readdir(currentPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(currentPath, entry.name);\n const relativePath = relative(relativeTo, fullPath);\n\n if (entry.isDirectory()) {\n // Skip hidden and special directories\n if (!entry.name.startsWith(\".\") && !entry.name.startsWith(\"_\")) {\n await scan(fullPath, relativeTo);\n }\n } else if (entry.isFile() && isContentFile(entry.name)) {\n files.push(relativePath);\n }\n }\n }\n\n await scan(dirPath, dirPath);\n return files;\n}\n\n/**\n * Try to match a file path against all pattern definitions\n *\n * @param relativePath - Relative path to the content file\n * @returns Matched pattern and extracted tokens, or null\n */\nfunction matchPattern(\n relativePath: string\n): { pattern: PatternDefinition; match: RegExpMatchArray } | null {\n // Normalize path separators\n const normalizedPath = relativePath.replace(/\\\\/g, \"/\");\n\n for (const pattern of PATTERN_DEFINITIONS) {\n const match = normalizedPath.match(pattern.regex);\n if (match) {\n return { pattern, match };\n }\n }\n\n return null;\n}\n\n/**\n * Detect the file naming pattern used in a collection\n *\n * Analyzes all content files in the collection directory and determines\n * the most likely pattern based on file names and structure.\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Pattern detection result with confidence score\n *\n * @example\n * ```typescript\n * const result = await detectFilePattern('/project/src/content/blog');\n * console.log(result.pattern); // \"{date}-{slug}.md\"\n * console.log(result.confidence); // 0.95\n * ```\n */\nexport async function detectFilePattern(\n collectionPath: string\n): Promise<PatternDetectionResult> {\n const files = await listContentFiles(collectionPath);\n\n if (files.length === 0) {\n return {\n pattern: \"{slug}.md\",\n confidence: 0,\n matchCount: 0,\n totalFiles: 0,\n samples: [],\n };\n }\n\n // Count matches for each pattern\n const patternCounts = new Map<\n string,\n {\n pattern: PatternDefinition;\n count: number;\n samples: Array<{ filePath: string; extracted: Record<string, string> }>;\n extension: string;\n }\n >();\n\n for (const pattern of PATTERN_DEFINITIONS) {\n patternCounts.set(pattern.name, {\n pattern,\n count: 0,\n samples: [],\n extension: \".md\",\n });\n }\n\n // Analyze each file\n for (const filePath of files) {\n const result = matchPattern(filePath);\n\n if (result) {\n const { pattern, match } = result;\n const entry = patternCounts.get(pattern.name);\n\n if (entry) {\n const ext = extname(filePath);\n const extracted = pattern.extract(match, ext);\n\n entry.count++;\n entry.extension = ext;\n\n // Keep up to 3 samples\n if (entry.samples.length < 3) {\n entry.samples.push({ filePath, extracted });\n }\n }\n }\n }\n\n // Find the best matching pattern\n // Consider both match count and pattern priority\n let bestPattern: PatternDetectionResult | null = null;\n let bestScore = -1;\n\n for (const [, entry] of patternCounts) {\n if (entry.count === 0) continue;\n\n // Score = (match ratio * 100) + priority\n // This ensures high match ratio wins, but priority breaks ties\n const matchRatio = entry.count / files.length;\n const score = matchRatio * 100 + entry.pattern.priority;\n\n if (score > bestScore) {\n bestScore = score;\n\n // Adjust template for actual extension used\n let template = entry.pattern.template;\n if (entry.extension === \".mdx\") {\n template = template.replace(\".md\", \".mdx\");\n }\n\n bestPattern = {\n pattern: template,\n confidence: matchRatio,\n matchCount: entry.count,\n totalFiles: files.length,\n samples: entry.samples,\n };\n }\n }\n\n // Return best pattern or default\n return (\n bestPattern ?? {\n pattern: \"{slug}.md\",\n confidence: 0,\n matchCount: 0,\n totalFiles: files.length,\n samples: [],\n }\n );\n}\n\n/**\n * Generate a file path from a pattern and tokens\n *\n * @param pattern - Pattern template (e.g., \"{date}-{slug}.md\")\n * @param tokens - Token values to substitute\n * @returns Generated file path\n *\n * @example\n * ```typescript\n * const path = generatePathFromPattern(\n * \"{date}-{slug}.md\",\n * { date: \"2024-01-15\", slug: \"my-post\" }\n * );\n * // Returns: \"2024-01-15-my-post.md\"\n * ```\n */\nexport function generatePathFromPattern(\n pattern: string,\n tokens: Record<string, string>\n): string {\n let result = pattern;\n\n for (const [key, value] of Object.entries(tokens)) {\n result = result.replace(`{${key}}`, value);\n }\n\n return result;\n}\n\n/**\n * Parse a pattern template to extract token names\n *\n * @param pattern - Pattern template\n * @returns Array of token names\n *\n * @example\n * ```typescript\n * const tokens = parsePatternTokens(\"{year}/{month}/{slug}.md\");\n * // Returns: [\"year\", \"month\", \"slug\"]\n * ```\n */\nexport function parsePatternTokens(pattern: string): string[] {\n const tokenRegex = /\\{([^}]+)\\}/g;\n const tokens: string[] = [];\n let match;\n\n while ((match = tokenRegex.exec(pattern)) !== null) {\n if (match[1]) {\n tokens.push(match[1]);\n }\n }\n\n return tokens;\n}\n\n/**\n * Validate that a pattern has all required tokens\n *\n * @param pattern - Pattern template\n * @param requiredTokens - Required token names\n * @returns True if all required tokens are present\n */\nexport function validatePattern(\n pattern: string,\n requiredTokens: string[] = [\"slug\"]\n): boolean {\n const tokens = parsePatternTokens(pattern);\n return requiredTokens.every((req) => tokens.includes(req));\n}\n\n/**\n * Get the default extension for a pattern\n *\n * @param pattern - Pattern template\n * @returns The file extension (.md or .mdx)\n */\nexport function getPatternExtension(pattern: string): string {\n if (pattern.endsWith(\".mdx\")) {\n return \".mdx\";\n }\n return \".md\";\n}\n\n/**\n * Known token types and their default value generators\n */\ntype TokenResolver = (\n frontmatter: Record<string, unknown>,\n slug: string\n) => string;\n\nconst TOKEN_RESOLVERS: Record<string, TokenResolver> = {\n // Core tokens\n slug: (_fm, slug) => slug,\n\n // Date tokens - from pubDate or current date\n date: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.toISOString().split(\"T\")[0] ?? \"\";\n },\n year: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.getFullYear().toString();\n },\n month: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return (pubDate.getMonth() + 1).toString().padStart(2, \"0\");\n },\n day: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.getDate().toString().padStart(2, \"0\");\n },\n\n // i18n tokens\n lang: (fm) => {\n if (typeof fm.lang === \"string\") return fm.lang;\n if (typeof fm.language === \"string\") return fm.language;\n if (typeof fm.locale === \"string\") return fm.locale;\n return \"en\"; // Default to English\n },\n\n // Organization tokens\n category: (fm) => {\n if (typeof fm.category === \"string\") return fm.category;\n if (Array.isArray(fm.categories) && typeof fm.categories[0] === \"string\") {\n return fm.categories[0];\n }\n return \"uncategorized\";\n },\n author: (fm) => {\n if (typeof fm.author === \"string\") return slugifyValue(fm.author);\n if (\n typeof fm.author === \"object\" &&\n fm.author !== null &&\n \"name\" in fm.author\n ) {\n return slugifyValue(String(fm.author.name));\n }\n return \"anonymous\";\n },\n type: (fm) => {\n if (typeof fm.type === \"string\") return fm.type;\n if (typeof fm.contentType === \"string\") return fm.contentType;\n return \"post\";\n },\n status: (fm) => {\n if (typeof fm.status === \"string\") return fm.status;\n if (fm.draft === true) return \"draft\";\n return \"published\";\n },\n series: (fm) => {\n if (typeof fm.series === \"string\") return slugifyValue(fm.series);\n return \"\";\n },\n collection: (fm) => {\n if (typeof fm.collection === \"string\") return fm.collection;\n return \"\";\n },\n};\n\n/**\n * Resolve a date from frontmatter\n *\n * Checks common date field names: pubDate, date, publishDate, createdAt\n *\n * @param frontmatter - Frontmatter data\n * @returns Resolved Date object\n */\nfunction resolveDateFromFrontmatter(\n frontmatter: Record<string, unknown>\n): Date {\n const dateFields = [\"pubDate\", \"date\", \"publishDate\", \"createdAt\", \"created\"];\n\n for (const field of dateFields) {\n const value = frontmatter[field];\n if (value instanceof Date) return value;\n if (typeof value === \"string\") {\n const parsed = new Date(value);\n if (!isNaN(parsed.getTime())) return parsed;\n }\n }\n\n return new Date();\n}\n\n/**\n * Convert a string to a URL-safe slug\n *\n * @param value - String to slugify\n * @returns URL-safe slug\n */\nfunction slugifyValue(value: string): string {\n return value\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/[\\s_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/**\n * Options for resolving pattern tokens\n */\nexport interface ResolveTokensOptions {\n /** The content slug */\n slug: string;\n /** Frontmatter data for resolving dynamic tokens */\n frontmatter?: Record<string, unknown>;\n /** Custom token values (override automatic resolution) */\n customTokens?: Record<string, string>;\n}\n\n/**\n * Resolve all tokens in a pattern to their values\n *\n * Token resolution priority:\n * 1. Custom tokens (explicitly provided)\n * 2. Known token resolvers (date, year, month, etc.)\n * 3. Frontmatter values (for custom tokens)\n * 4. Empty string (fallback)\n *\n * @param pattern - Pattern template with tokens\n * @param options - Resolution options\n * @returns Record of token names to resolved values\n *\n * @example\n * ```typescript\n * const tokens = resolvePatternTokens(\"{year}/{month}/{slug}.md\", {\n * slug: \"my-post\",\n * frontmatter: { pubDate: new Date(\"2024-06-15\") }\n * });\n * // Returns: { year: \"2024\", month: \"06\", slug: \"my-post\" }\n * ```\n */\nexport function resolvePatternTokens(\n pattern: string,\n options: ResolveTokensOptions\n): Record<string, string> {\n const { slug, frontmatter = {}, customTokens = {} } = options;\n const tokenNames = parsePatternTokens(pattern);\n const resolved: Record<string, string> = {};\n\n for (const tokenName of tokenNames) {\n // Priority 1: Custom tokens\n if (tokenName in customTokens) {\n resolved[tokenName] = customTokens[tokenName] ?? \"\";\n continue;\n }\n\n // Priority 2: Known token resolvers\n const resolver = TOKEN_RESOLVERS[tokenName];\n if (resolver) {\n resolved[tokenName] = resolver(frontmatter, slug);\n continue;\n }\n\n // Priority 3: Direct frontmatter value\n const fmValue = frontmatter[tokenName];\n if (typeof fmValue === \"string\") {\n resolved[tokenName] = slugifyValue(fmValue);\n continue;\n }\n if (typeof fmValue === \"number\") {\n resolved[tokenName] = fmValue.toString();\n continue;\n }\n\n // Priority 4: Fallback to empty string\n resolved[tokenName] = \"\";\n }\n\n return resolved;\n}\n\n/**\n * Check if a pattern is valid for content creation\n *\n * A pattern is valid if:\n * - It contains the {slug} token (required)\n * - It ends with .md or .mdx\n * - All tokens can be resolved\n *\n * @param pattern - Pattern template to validate\n * @returns Validation result with error message if invalid\n */\nexport function isValidPattern(pattern: string): {\n valid: boolean;\n error?: string;\n} {\n // Must contain slug token\n if (!pattern.includes(\"{slug}\")) {\n return { valid: false, error: \"Pattern must contain {slug} token\" };\n }\n\n // Must end with .md or .mdx\n if (!pattern.endsWith(\".md\") && !pattern.endsWith(\".mdx\")) {\n return { valid: false, error: \"Pattern must end with .md or .mdx\" };\n }\n\n // Check for unclosed tokens\n const unclosed = pattern.match(/\\{[^}]*$/);\n if (unclosed) {\n return { valid: false, error: \"Pattern contains unclosed token\" };\n }\n\n return { valid: true };\n}\n\n/**\n * Get list of all supported token names\n *\n * @returns Array of supported token names\n */\nexport function getSupportedTokens(): string[] {\n return Object.keys(TOKEN_RESOLVERS);\n}\n"],"mappings":"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","names":["existsSync","readdir","extname","join","relative","existsSync","readdir","join","relative","extname"]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/filesystem/watcher.ts
|
|
2
|
-
import { watch } from "chokidar";
|
|
3
2
|
import { stat } from "fs/promises";
|
|
4
3
|
import { join } from "path";
|
|
4
|
+
import { watch } from "chokidar";
|
|
5
5
|
var ContentWatcher = class {
|
|
6
6
|
watcher = null;
|
|
7
7
|
projectRoot;
|
|
@@ -142,4 +142,4 @@ export {
|
|
|
142
142
|
FileModificationTracker,
|
|
143
143
|
createContentWatcher
|
|
144
144
|
};
|
|
145
|
-
//# sourceMappingURL=chunk-
|
|
145
|
+
//# sourceMappingURL=chunk-JFQQJPDF.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/filesystem/watcher.ts"],"sourcesContent":["/**\n * @fileoverview File watcher for detecting external changes\n *\n * This module provides file watching capabilities to detect when\n * content files are modified outside of the Writenex editor\n * (e.g., in VS Code or another editor).\n *\n * @module @writenex/astro/filesystem/watcher\n */\n\nimport {
|
|
1
|
+
{"version":3,"sources":["../src/filesystem/watcher.ts"],"sourcesContent":["/**\n * @fileoverview File watcher for detecting external changes\n *\n * This module provides file watching capabilities to detect when\n * content files are modified outside of the Writenex editor\n * (e.g., in VS Code or another editor).\n *\n * @module @writenex/astro/filesystem/watcher\n */\n\nimport { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { type FSWatcher, watch } from \"chokidar\";\n\n/**\n * File change event types\n */\nexport type FileChangeType = \"add\" | \"change\" | \"unlink\";\n\n/**\n * File change event\n */\nexport interface FileChangeEvent {\n type: FileChangeType;\n path: string;\n collection: string;\n}\n\n/**\n * Watcher options\n */\nexport interface WatcherOptions {\n /** Callback when a file changes */\n onChange?: (event: FileChangeEvent) => void;\n /** Debounce delay in milliseconds */\n debounceMs?: number;\n /** Patterns to ignore */\n ignored?: string[];\n}\n\n/**\n * Content file watcher\n *\n * Watches the src/content directory for changes and emits events\n * when files are added, modified, or deleted.\n */\nexport class ContentWatcher {\n private watcher: FSWatcher | null = null;\n private projectRoot: string;\n private contentDir: string;\n private options: WatcherOptions;\n private debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n\n constructor(\n projectRoot: string,\n contentDir: string = \"src/content\",\n options: WatcherOptions = {}\n ) {\n this.projectRoot = projectRoot;\n this.contentDir = contentDir;\n this.options = {\n debounceMs: 100,\n ignored: [\"**/node_modules/**\", \"**/.git/**\"],\n ...options,\n };\n }\n\n /**\n * Start watching for file changes\n */\n start(): void {\n if (this.watcher) {\n return; // Already watching\n }\n\n const watchPath = join(this.projectRoot, this.contentDir);\n\n this.watcher = watch(watchPath, {\n ignored: this.options.ignored,\n persistent: true,\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n });\n\n this.watcher\n .on(\"add\", (path) => this.handleChange(\"add\", path))\n .on(\"change\", (path) => this.handleChange(\"change\", path))\n .on(\"unlink\", (path) => this.handleChange(\"unlink\", path))\n .on(\"error\", (error) => {\n console.error(\"[writenex] Watcher error:\", error);\n });\n }\n\n /**\n * Stop watching for file changes\n */\n async stop(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n\n // Clear all debounce timers\n for (const timer of this.debounceTimers.values()) {\n clearTimeout(timer);\n }\n this.debounceTimers.clear();\n }\n\n /**\n * Handle a file change event\n */\n private handleChange(type: FileChangeType, filePath: string): void {\n // Only handle markdown files\n if (!filePath.endsWith(\".md\") && !filePath.endsWith(\".mdx\")) {\n return;\n }\n\n // Debounce rapid changes\n const existingTimer = this.debounceTimers.get(filePath);\n if (existingTimer) {\n clearTimeout(existingTimer);\n }\n\n const timer = setTimeout(() => {\n this.debounceTimers.delete(filePath);\n this.emitChange(type, filePath);\n }, this.options.debounceMs);\n\n this.debounceTimers.set(filePath, timer);\n }\n\n /**\n * Emit a file change event\n */\n private emitChange(type: FileChangeType, filePath: string): void {\n if (!this.options.onChange) {\n return;\n }\n\n // Extract collection name from path\n const contentPath = join(this.projectRoot, this.contentDir);\n const relativePath = filePath\n .replace(contentPath, \"\")\n .replace(/^[/\\\\]/, \"\");\n const parts = relativePath.split(/[/\\\\]/);\n const collection = parts[0] ?? \"\";\n\n this.options.onChange({\n type,\n path: filePath,\n collection,\n });\n }\n\n /**\n * Check if the watcher is running\n */\n isWatching(): boolean {\n return this.watcher !== null;\n }\n}\n\n/**\n * Track file modification times for conflict detection\n */\nexport class FileModificationTracker {\n private mtimes: Map<string, number> = new Map();\n\n /**\n * Record the current modification time of a file\n */\n async track(filePath: string): Promise<void> {\n try {\n const stats = await stat(filePath);\n this.mtimes.set(filePath, stats.mtimeMs);\n } catch {\n // File might not exist yet\n this.mtimes.delete(filePath);\n }\n }\n\n /**\n * Check if a file has been modified externally\n */\n async hasExternalChanges(filePath: string): Promise<boolean> {\n const lastKnown = this.mtimes.get(filePath);\n if (lastKnown === undefined) {\n return false; // Not tracked, assume no changes\n }\n\n try {\n const stats = await stat(filePath);\n return stats.mtimeMs > lastKnown;\n } catch {\n return true; // File might have been deleted\n }\n }\n\n /**\n * Clear tracking for a file\n */\n untrack(filePath: string): void {\n this.mtimes.delete(filePath);\n }\n\n /**\n * Clear all tracking\n */\n clear(): void {\n this.mtimes.clear();\n }\n}\n\n/**\n * Create a content watcher instance\n */\nexport function createContentWatcher(\n projectRoot: string,\n options?: WatcherOptions\n): ContentWatcher {\n return new ContentWatcher(projectRoot, \"src/content\", options);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
detectFilePattern,
|
|
3
3
|
getCollectionCount,
|
|
4
4
|
readCollection
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-GYAFIVVI.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_FILE_PATTERN
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-YBCPOLMY.js";
|
|
9
9
|
|
|
10
10
|
// src/discovery/schema.ts
|
|
11
11
|
var MAX_SAMPLE_FILES = 20;
|
|
@@ -218,8 +218,8 @@ function describeSchema(schema) {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
// src/discovery/collections.ts
|
|
221
|
-
import { readdir, stat } from "fs/promises";
|
|
222
221
|
import { existsSync } from "fs";
|
|
222
|
+
import { readdir, stat } from "fs/promises";
|
|
223
223
|
import { join } from "path";
|
|
224
224
|
var DEFAULT_CONTENT_DIR = "src/content";
|
|
225
225
|
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", ".git", "_", "."]);
|
|
@@ -307,4 +307,4 @@ export {
|
|
|
307
307
|
getCollection,
|
|
308
308
|
collectionExists
|
|
309
309
|
};
|
|
310
|
-
//# sourceMappingURL=chunk-
|
|
310
|
+
//# sourceMappingURL=chunk-JMNCPNQX.js.map
|