@imjp/writenex-astro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +539 -0
  2. package/dist/chunk-5PM6EQE5.js +151 -0
  3. package/dist/chunk-5PM6EQE5.js.map +1 -0
  4. package/dist/chunk-7XU5X6CW.js +1331 -0
  5. package/dist/chunk-7XU5X6CW.js.map +1 -0
  6. package/dist/chunk-AAOQHQPU.js +574 -0
  7. package/dist/chunk-AAOQHQPU.js.map +1 -0
  8. package/dist/chunk-CF2XXJFF.js +1410 -0
  9. package/dist/chunk-CF2XXJFF.js.map +1 -0
  10. package/dist/chunk-CRPZUUDU.js +52 -0
  11. package/dist/chunk-CRPZUUDU.js.map +1 -0
  12. package/dist/chunk-CYLDJ3HZ.js +310 -0
  13. package/dist/chunk-CYLDJ3HZ.js.map +1 -0
  14. package/dist/chunk-KIKIPIFA.js +1 -0
  15. package/dist/chunk-KIKIPIFA.js.map +1 -0
  16. package/dist/chunk-XNTQTTJU.js +145 -0
  17. package/dist/chunk-XNTQTTJU.js.map +1 -0
  18. package/dist/client/index.css +2 -0
  19. package/dist/client/index.css.map +1 -0
  20. package/dist/client/index.js +375 -0
  21. package/dist/client/index.js.map +1 -0
  22. package/dist/client/styles.css +584 -0
  23. package/dist/client/variables.css +304 -0
  24. package/dist/config/index.d.ts +54 -0
  25. package/dist/config/index.js +38 -0
  26. package/dist/config/index.js.map +1 -0
  27. package/dist/config-BmEdBDo_.d.ts +220 -0
  28. package/dist/content-BWR52vD-.d.ts +64 -0
  29. package/dist/discovery/index.d.ts +310 -0
  30. package/dist/discovery/index.js +38 -0
  31. package/dist/discovery/index.js.map +1 -0
  32. package/dist/errors-C0iYiDTv.d.ts +107 -0
  33. package/dist/filesystem/index.d.ts +1292 -0
  34. package/dist/filesystem/index.js +203 -0
  35. package/dist/filesystem/index.js.map +1 -0
  36. package/dist/image-FP7w5ZIs.d.ts +47 -0
  37. package/dist/index.d.ts +64 -0
  38. package/dist/index.js +151 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/loader-55LWCXHA.js +12 -0
  41. package/dist/loader-55LWCXHA.js.map +1 -0
  42. package/dist/loader-CrdnaAWR.d.ts +327 -0
  43. package/dist/server/index.d.ts +357 -0
  44. package/dist/server/index.js +37 -0
  45. package/dist/server/index.js.map +1 -0
  46. package/package.json +94 -0
  47. package/src/client/App.tsx +900 -0
  48. package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
  49. package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
  50. package/src/client/components/ConfigPanel/index.ts +6 -0
  51. package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
  52. package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
  53. package/src/client/components/CreateContentModal/index.ts +7 -0
  54. package/src/client/components/Editor/Editor.css +885 -0
  55. package/src/client/components/Editor/Editor.tsx +484 -0
  56. package/src/client/components/Editor/ImageDialog.css +344 -0
  57. package/src/client/components/Editor/ImageDialog.tsx +367 -0
  58. package/src/client/components/Editor/LinkDialog.css +326 -0
  59. package/src/client/components/Editor/LinkDialog.tsx +332 -0
  60. package/src/client/components/Editor/index.ts +6 -0
  61. package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
  62. package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
  63. package/src/client/components/FrontmatterForm/index.ts +7 -0
  64. package/src/client/components/Header/Header.css +300 -0
  65. package/src/client/components/Header/Header.tsx +300 -0
  66. package/src/client/components/Header/index.ts +7 -0
  67. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
  68. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
  69. package/src/client/components/KeyboardShortcuts/index.ts +6 -0
  70. package/src/client/components/LazyEditor.tsx +75 -0
  71. package/src/client/components/LiveRegion/LiveRegion.css +19 -0
  72. package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
  73. package/src/client/components/LiveRegion/index.ts +7 -0
  74. package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
  75. package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
  76. package/src/client/components/SearchReplace/index.ts +7 -0
  77. package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
  78. package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
  79. package/src/client/components/SelectCollectionModal/index.ts +7 -0
  80. package/src/client/components/Sidebar/Sidebar.css +570 -0
  81. package/src/client/components/Sidebar/Sidebar.tsx +617 -0
  82. package/src/client/components/Sidebar/index.ts +7 -0
  83. package/src/client/components/SkipLink/SkipLink.css +51 -0
  84. package/src/client/components/SkipLink/SkipLink.tsx +67 -0
  85. package/src/client/components/SkipLink/index.ts +7 -0
  86. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
  87. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
  88. package/src/client/components/UnsavedChangesModal/index.ts +1 -0
  89. package/src/client/components/VersionHistory/DiffViewer.css +430 -0
  90. package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
  91. package/src/client/components/VersionHistory/VersionActions.css +318 -0
  92. package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
  93. package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
  94. package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
  95. package/src/client/components/VersionHistory/index.ts +9 -0
  96. package/src/client/context/ApiContext.tsx +154 -0
  97. package/src/client/context/ThemeContext.tsx +172 -0
  98. package/src/client/hooks/useAnnounce.ts +201 -0
  99. package/src/client/hooks/useApi.ts +374 -0
  100. package/src/client/hooks/useArrowNavigation.ts +286 -0
  101. package/src/client/hooks/useAutosave.ts +241 -0
  102. package/src/client/hooks/useFocusTrap.ts +178 -0
  103. package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
  104. package/src/client/hooks/useSearch.ts +206 -0
  105. package/src/client/hooks/useVersionHistory.ts +451 -0
  106. package/src/client/index.tsx +70 -0
  107. package/src/client/styles.css +584 -0
  108. package/src/client/utils/focus.ts +57 -0
  109. package/src/client/utils/openInEditor.ts +130 -0
  110. package/src/client/variables.css +304 -0
  111. package/src/config/defaults.ts +109 -0
  112. package/src/config/index.ts +32 -0
  113. package/src/config/loader.ts +174 -0
  114. package/src/config/schema.ts +161 -0
  115. package/src/core/constants.ts +39 -0
  116. package/src/core/errors.ts +739 -0
  117. package/src/core/index.ts +11 -0
  118. package/src/discovery/collections.ts +216 -0
  119. package/src/discovery/index.ts +33 -0
  120. package/src/discovery/patterns.ts +702 -0
  121. package/src/discovery/schema.ts +453 -0
  122. package/src/filesystem/images.ts +798 -0
  123. package/src/filesystem/index.ts +107 -0
  124. package/src/filesystem/reader.ts +452 -0
  125. package/src/filesystem/version-config.ts +390 -0
  126. package/src/filesystem/versions.ts +1339 -0
  127. package/src/filesystem/watcher.ts +226 -0
  128. package/src/filesystem/writer.ts +540 -0
  129. package/src/index.ts +61 -0
  130. package/src/integration.ts +228 -0
  131. package/src/server/assets.ts +254 -0
  132. package/src/server/cache.ts +355 -0
  133. package/src/server/index.ts +33 -0
  134. package/src/server/middleware.ts +209 -0
  135. package/src/server/routes.ts +1428 -0
  136. package/src/types/api.ts +61 -0
  137. package/src/types/config.ts +134 -0
  138. package/src/types/content.ts +64 -0
  139. package/src/types/image.ts +48 -0
  140. package/src/types/index.ts +58 -0
  141. package/src/types/version.ts +117 -0
@@ -0,0 +1,1410 @@
1
+ import {
2
+ generatePathFromPattern,
3
+ getContentFilePath,
4
+ isValidPattern,
5
+ readContentFile,
6
+ resolvePatternTokens
7
+ } from "./chunk-AAOQHQPU.js";
8
+
9
+ // src/core/errors.ts
10
+ var WritenexErrorCode = /* @__PURE__ */ ((WritenexErrorCode2) => {
11
+ WritenexErrorCode2["CONFIG_NOT_FOUND"] = "CONFIG_NOT_FOUND";
12
+ WritenexErrorCode2["CONFIG_INVALID"] = "CONFIG_INVALID";
13
+ WritenexErrorCode2["CONFIG_PARSE_ERROR"] = "CONFIG_PARSE_ERROR";
14
+ WritenexErrorCode2["FS_READ_ERROR"] = "FS_READ_ERROR";
15
+ WritenexErrorCode2["FS_WRITE_ERROR"] = "FS_WRITE_ERROR";
16
+ WritenexErrorCode2["FS_DELETE_ERROR"] = "FS_DELETE_ERROR";
17
+ WritenexErrorCode2["FS_PERMISSION_DENIED"] = "FS_PERMISSION_DENIED";
18
+ WritenexErrorCode2["FS_PATH_NOT_FOUND"] = "FS_PATH_NOT_FOUND";
19
+ WritenexErrorCode2["FS_PATH_TRAVERSAL"] = "FS_PATH_TRAVERSAL";
20
+ WritenexErrorCode2["CONTENT_NOT_FOUND"] = "CONTENT_NOT_FOUND";
21
+ WritenexErrorCode2["CONTENT_PARSE_ERROR"] = "CONTENT_PARSE_ERROR";
22
+ WritenexErrorCode2["CONTENT_VALIDATION_ERROR"] = "CONTENT_VALIDATION_ERROR";
23
+ WritenexErrorCode2["CONTENT_ALREADY_EXISTS"] = "CONTENT_ALREADY_EXISTS";
24
+ WritenexErrorCode2["CONTENT_INVALID_SLUG"] = "CONTENT_INVALID_SLUG";
25
+ WritenexErrorCode2["CONTENT_CONFLICT"] = "CONTENT_CONFLICT";
26
+ WritenexErrorCode2["COLLECTION_NOT_FOUND"] = "COLLECTION_NOT_FOUND";
27
+ WritenexErrorCode2["COLLECTION_EMPTY"] = "COLLECTION_EMPTY";
28
+ WritenexErrorCode2["COLLECTION_DISCOVERY_ERROR"] = "COLLECTION_DISCOVERY_ERROR";
29
+ WritenexErrorCode2["API_BAD_REQUEST"] = "API_BAD_REQUEST";
30
+ WritenexErrorCode2["API_METHOD_NOT_ALLOWED"] = "API_METHOD_NOT_ALLOWED";
31
+ WritenexErrorCode2["API_INTERNAL_ERROR"] = "API_INTERNAL_ERROR";
32
+ WritenexErrorCode2["API_TIMEOUT"] = "API_TIMEOUT";
33
+ WritenexErrorCode2["IMAGE_INVALID_TYPE"] = "IMAGE_INVALID_TYPE";
34
+ WritenexErrorCode2["IMAGE_TOO_LARGE"] = "IMAGE_TOO_LARGE";
35
+ WritenexErrorCode2["IMAGE_UPLOAD_ERROR"] = "IMAGE_UPLOAD_ERROR";
36
+ WritenexErrorCode2["IMAGE_NOT_FOUND"] = "IMAGE_NOT_FOUND";
37
+ WritenexErrorCode2["VERSION_NOT_FOUND"] = "VERSION_NOT_FOUND";
38
+ WritenexErrorCode2["VERSION_MANIFEST_CORRUPT"] = "VERSION_MANIFEST_CORRUPT";
39
+ WritenexErrorCode2["VERSION_LOCK_TIMEOUT"] = "VERSION_LOCK_TIMEOUT";
40
+ WritenexErrorCode2["VERSION_SAVE_ERROR"] = "VERSION_SAVE_ERROR";
41
+ WritenexErrorCode2["VERSION_RESTORE_ERROR"] = "VERSION_RESTORE_ERROR";
42
+ WritenexErrorCode2["PATTERN_INVALID"] = "PATTERN_INVALID";
43
+ WritenexErrorCode2["PATTERN_MISSING_TOKEN"] = "PATTERN_MISSING_TOKEN";
44
+ WritenexErrorCode2["UNKNOWN_ERROR"] = "UNKNOWN_ERROR";
45
+ return WritenexErrorCode2;
46
+ })(WritenexErrorCode || {});
47
+ var ERROR_HTTP_STATUS = {
48
+ // Configuration errors - 500 (server misconfiguration)
49
+ ["CONFIG_NOT_FOUND" /* CONFIG_NOT_FOUND */]: 500,
50
+ ["CONFIG_INVALID" /* CONFIG_INVALID */]: 500,
51
+ ["CONFIG_PARSE_ERROR" /* CONFIG_PARSE_ERROR */]: 500,
52
+ // Filesystem errors
53
+ ["FS_READ_ERROR" /* FS_READ_ERROR */]: 500,
54
+ ["FS_WRITE_ERROR" /* FS_WRITE_ERROR */]: 500,
55
+ ["FS_DELETE_ERROR" /* FS_DELETE_ERROR */]: 500,
56
+ ["FS_PERMISSION_DENIED" /* FS_PERMISSION_DENIED */]: 403,
57
+ ["FS_PATH_NOT_FOUND" /* FS_PATH_NOT_FOUND */]: 404,
58
+ ["FS_PATH_TRAVERSAL" /* FS_PATH_TRAVERSAL */]: 400,
59
+ // Content errors
60
+ ["CONTENT_NOT_FOUND" /* CONTENT_NOT_FOUND */]: 404,
61
+ ["CONTENT_PARSE_ERROR" /* CONTENT_PARSE_ERROR */]: 500,
62
+ ["CONTENT_VALIDATION_ERROR" /* CONTENT_VALIDATION_ERROR */]: 400,
63
+ ["CONTENT_ALREADY_EXISTS" /* CONTENT_ALREADY_EXISTS */]: 409,
64
+ ["CONTENT_INVALID_SLUG" /* CONTENT_INVALID_SLUG */]: 400,
65
+ ["CONTENT_CONFLICT" /* CONTENT_CONFLICT */]: 409,
66
+ // Collection errors
67
+ ["COLLECTION_NOT_FOUND" /* COLLECTION_NOT_FOUND */]: 404,
68
+ ["COLLECTION_EMPTY" /* COLLECTION_EMPTY */]: 404,
69
+ ["COLLECTION_DISCOVERY_ERROR" /* COLLECTION_DISCOVERY_ERROR */]: 500,
70
+ // API errors
71
+ ["API_BAD_REQUEST" /* API_BAD_REQUEST */]: 400,
72
+ ["API_METHOD_NOT_ALLOWED" /* API_METHOD_NOT_ALLOWED */]: 405,
73
+ ["API_INTERNAL_ERROR" /* API_INTERNAL_ERROR */]: 500,
74
+ ["API_TIMEOUT" /* API_TIMEOUT */]: 504,
75
+ // Image errors
76
+ ["IMAGE_INVALID_TYPE" /* IMAGE_INVALID_TYPE */]: 400,
77
+ ["IMAGE_TOO_LARGE" /* IMAGE_TOO_LARGE */]: 413,
78
+ ["IMAGE_UPLOAD_ERROR" /* IMAGE_UPLOAD_ERROR */]: 500,
79
+ ["IMAGE_NOT_FOUND" /* IMAGE_NOT_FOUND */]: 404,
80
+ // Version history errors
81
+ ["VERSION_NOT_FOUND" /* VERSION_NOT_FOUND */]: 404,
82
+ ["VERSION_MANIFEST_CORRUPT" /* VERSION_MANIFEST_CORRUPT */]: 500,
83
+ ["VERSION_LOCK_TIMEOUT" /* VERSION_LOCK_TIMEOUT */]: 503,
84
+ ["VERSION_SAVE_ERROR" /* VERSION_SAVE_ERROR */]: 500,
85
+ ["VERSION_RESTORE_ERROR" /* VERSION_RESTORE_ERROR */]: 500,
86
+ // Pattern errors
87
+ ["PATTERN_INVALID" /* PATTERN_INVALID */]: 400,
88
+ ["PATTERN_MISSING_TOKEN" /* PATTERN_MISSING_TOKEN */]: 400,
89
+ // Unknown
90
+ ["UNKNOWN_ERROR" /* UNKNOWN_ERROR */]: 500
91
+ };
92
+ var WritenexError = class _WritenexError extends Error {
93
+ /** Error code for categorization */
94
+ code;
95
+ /** HTTP status code for API responses */
96
+ httpStatus;
97
+ /** Additional context data for debugging */
98
+ context;
99
+ /** Original error if this wraps another error */
100
+ cause;
101
+ constructor(code, message, options) {
102
+ super(message);
103
+ this.name = "WritenexError";
104
+ this.code = code;
105
+ this.httpStatus = ERROR_HTTP_STATUS[code];
106
+ this.context = options?.context;
107
+ this.cause = options?.cause;
108
+ if (Error.captureStackTrace) {
109
+ Error.captureStackTrace(this, _WritenexError);
110
+ }
111
+ }
112
+ /**
113
+ * Convert error to JSON for API responses
114
+ */
115
+ toJSON() {
116
+ return {
117
+ error: this.message,
118
+ code: this.code,
119
+ ...this.context ? { context: this.context } : {}
120
+ };
121
+ }
122
+ /**
123
+ * Create a user-friendly error message
124
+ */
125
+ toUserMessage() {
126
+ return this.message;
127
+ }
128
+ };
129
+ var PathTraversalError = class extends WritenexError {
130
+ constructor(requestedPath, basePath) {
131
+ super(
132
+ "FS_PATH_TRAVERSAL" /* FS_PATH_TRAVERSAL */,
133
+ "Invalid path: attempted path traversal detected",
134
+ { context: { requestedPath, basePath } }
135
+ );
136
+ this.name = "PathTraversalError";
137
+ }
138
+ };
139
+ var ContentNotFoundError = class extends WritenexError {
140
+ constructor(collection, contentId) {
141
+ super(
142
+ "CONTENT_NOT_FOUND" /* CONTENT_NOT_FOUND */,
143
+ `Content '${contentId}' not found in collection '${collection}'`,
144
+ { context: { collection, contentId } }
145
+ );
146
+ this.name = "ContentNotFoundError";
147
+ }
148
+ };
149
+ var ContentConflictError = class extends WritenexError {
150
+ /** Current content on disk */
151
+ serverContent;
152
+ /** Server's current mtime */
153
+ serverMtime;
154
+ /** Client's expected mtime */
155
+ clientMtime;
156
+ constructor(collection, contentId, serverContent, serverMtime, clientMtime) {
157
+ super(
158
+ "CONTENT_CONFLICT" /* CONTENT_CONFLICT */,
159
+ `Content '${contentId}' in '${collection}' was modified externally. Expected mtime: ${clientMtime}, actual: ${serverMtime}`,
160
+ {
161
+ context: {
162
+ collection,
163
+ contentId,
164
+ serverMtime,
165
+ clientMtime,
166
+ timeDiff: serverMtime - clientMtime
167
+ }
168
+ }
169
+ );
170
+ this.name = "ContentConflictError";
171
+ this.serverContent = serverContent;
172
+ this.serverMtime = serverMtime;
173
+ this.clientMtime = clientMtime;
174
+ }
175
+ /**
176
+ * Override toJSON to include conflict-specific data
177
+ */
178
+ toJSON() {
179
+ return {
180
+ ...super.toJSON(),
181
+ serverContent: this.serverContent,
182
+ serverMtime: this.serverMtime,
183
+ clientMtime: this.clientMtime
184
+ };
185
+ }
186
+ };
187
+ var CollectionNotFoundError = class extends WritenexError {
188
+ constructor(collectionName) {
189
+ super(
190
+ "COLLECTION_NOT_FOUND" /* COLLECTION_NOT_FOUND */,
191
+ `Collection '${collectionName}' not found`,
192
+ { context: { collectionName } }
193
+ );
194
+ this.name = "CollectionNotFoundError";
195
+ }
196
+ };
197
+ var CollectionDiscoveryError = class extends WritenexError {
198
+ constructor(contentPath, cause) {
199
+ super(
200
+ "COLLECTION_DISCOVERY_ERROR" /* COLLECTION_DISCOVERY_ERROR */,
201
+ `Failed to discover collections in: ${contentPath}`,
202
+ { context: { contentPath }, cause }
203
+ );
204
+ this.name = "CollectionDiscoveryError";
205
+ }
206
+ };
207
+ var ApiBadRequestError = class extends WritenexError {
208
+ constructor(message, details) {
209
+ super("API_BAD_REQUEST" /* API_BAD_REQUEST */, message, { context: details });
210
+ this.name = "ApiBadRequestError";
211
+ }
212
+ };
213
+ var ApiMethodNotAllowedError = class extends WritenexError {
214
+ constructor(method, allowedMethods) {
215
+ super(
216
+ "API_METHOD_NOT_ALLOWED" /* API_METHOD_NOT_ALLOWED */,
217
+ `Method ${method} not allowed. Allowed: ${allowedMethods.join(", ")}`,
218
+ { context: { method, allowedMethods } }
219
+ );
220
+ this.name = "ApiMethodNotAllowedError";
221
+ }
222
+ };
223
+ var ImageInvalidTypeError = class extends WritenexError {
224
+ constructor(filename, supportedTypes) {
225
+ super(
226
+ "IMAGE_INVALID_TYPE" /* IMAGE_INVALID_TYPE */,
227
+ `Invalid image type for '${filename}'. Supported: ${supportedTypes.join(", ")}`,
228
+ { context: { filename, supportedTypes } }
229
+ );
230
+ this.name = "ImageInvalidTypeError";
231
+ }
232
+ };
233
+ var ImageNotFoundError = class extends WritenexError {
234
+ constructor(imagePath) {
235
+ super("IMAGE_NOT_FOUND" /* IMAGE_NOT_FOUND */, `Image not found: ${imagePath}`, {
236
+ context: { imagePath }
237
+ });
238
+ this.name = "ImageNotFoundError";
239
+ }
240
+ };
241
+ var VersionNotFoundError = class extends WritenexError {
242
+ constructor(collection, contentId, versionId) {
243
+ super(
244
+ "VERSION_NOT_FOUND" /* VERSION_NOT_FOUND */,
245
+ `Version '${versionId}' not found for content '${contentId}' in '${collection}'`,
246
+ { context: { collection, contentId, versionId } }
247
+ );
248
+ this.name = "VersionNotFoundError";
249
+ }
250
+ };
251
+ function isWritenexError(error) {
252
+ return error instanceof WritenexError;
253
+ }
254
+ function wrapError(error, defaultCode = "UNKNOWN_ERROR" /* UNKNOWN_ERROR */) {
255
+ if (isWritenexError(error)) {
256
+ return error;
257
+ }
258
+ const message = error instanceof Error ? error.message : String(error);
259
+ const cause = error instanceof Error ? error : void 0;
260
+ return new WritenexError(defaultCode, message, { cause });
261
+ }
262
+
263
+ // src/filesystem/versions.ts
264
+ import {
265
+ readFile,
266
+ writeFile,
267
+ mkdir,
268
+ readdir,
269
+ stat,
270
+ unlink
271
+ } from "fs/promises";
272
+ import { existsSync } from "fs";
273
+ import { join, basename } from "path";
274
+ import matter from "gray-matter";
275
+ var PREVIEW_MAX_LENGTH = 100;
276
+ var GITIGNORE_CONTENT = "*\n";
277
+ var LABEL_FRONTMATTER_KEY = "_writenex_label";
278
+ var LOCK_TIMEOUT_MS = 3e4;
279
+ var LOCK_RETRY_INTERVAL_MS = 50;
280
+ var locks = /* @__PURE__ */ new Map();
281
+ async function acquireLock(storagePath, timeoutMs = LOCK_TIMEOUT_MS) {
282
+ const startTime = Date.now();
283
+ while (locks.has(storagePath)) {
284
+ const existingLock = locks.get(storagePath);
285
+ if (Date.now() - existingLock.acquiredAt > timeoutMs) {
286
+ console.warn(
287
+ `[writenex] Releasing stale lock for ${storagePath} (held for ${Date.now() - existingLock.acquiredAt}ms)`
288
+ );
289
+ existingLock.release();
290
+ locks.delete(storagePath);
291
+ break;
292
+ }
293
+ if (Date.now() - startTime > timeoutMs) {
294
+ throw new Error(
295
+ `[writenex] Timeout waiting for lock on ${storagePath} after ${timeoutMs}ms`
296
+ );
297
+ }
298
+ await Promise.race([
299
+ existingLock.promise,
300
+ new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS))
301
+ ]);
302
+ }
303
+ let releaseFunc;
304
+ const lockPromise = new Promise((resolve) => {
305
+ releaseFunc = resolve;
306
+ });
307
+ const lockEntry = {
308
+ promise: lockPromise,
309
+ release: releaseFunc,
310
+ acquiredAt: Date.now()
311
+ };
312
+ locks.set(storagePath, lockEntry);
313
+ return () => {
314
+ lockEntry.release();
315
+ locks.delete(storagePath);
316
+ };
317
+ }
318
+ async function withLock(storagePath, fn) {
319
+ const release = await acquireLock(storagePath);
320
+ try {
321
+ return await fn();
322
+ } finally {
323
+ release();
324
+ }
325
+ }
326
+ function generateVersionId() {
327
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-");
328
+ const randomSuffix = Math.random().toString(36).substring(2, 6);
329
+ return `${timestamp}-${randomSuffix}`;
330
+ }
331
+ function parseVersionId(versionId) {
332
+ const withoutSuffix = versionId.replace(/-[a-z0-9]{4}$/, "");
333
+ const isoString = withoutSuffix.replace(
334
+ /T(\d{2})-(\d{2})-(\d{2})\.(\d{3})Z/,
335
+ "T$1:$2:$3.$4Z"
336
+ );
337
+ const date = new Date(isoString);
338
+ return isNaN(date.getTime()) ? null : date;
339
+ }
340
+ function getVersionStoragePath(projectRoot, collection, contentId, config) {
341
+ return join(projectRoot, config.storagePath, collection, contentId);
342
+ }
343
+ function getVersionFilePath(storagePath, versionId) {
344
+ return join(storagePath, `${versionId}.md`);
345
+ }
346
+ function getManifestPath(storagePath) {
347
+ return join(storagePath, "manifest.json");
348
+ }
349
+ function generatePreview(content) {
350
+ try {
351
+ const { content: body } = matter(content);
352
+ const trimmed = body.trim();
353
+ if (trimmed.length <= PREVIEW_MAX_LENGTH) {
354
+ return trimmed;
355
+ }
356
+ return trimmed.substring(0, PREVIEW_MAX_LENGTH);
357
+ } catch {
358
+ const trimmed = content.trim();
359
+ return trimmed.length <= PREVIEW_MAX_LENGTH ? trimmed : trimmed.substring(0, PREVIEW_MAX_LENGTH);
360
+ }
361
+ }
362
+ function extractLabelFromContent(content) {
363
+ try {
364
+ const { data } = matter(content);
365
+ const label = data[LABEL_FRONTMATTER_KEY];
366
+ return typeof label === "string" ? label : void 0;
367
+ } catch {
368
+ return void 0;
369
+ }
370
+ }
371
+ function injectLabelIntoContent(content, label) {
372
+ try {
373
+ const { data, content: body } = matter(content);
374
+ const newData = { ...data, [LABEL_FRONTMATTER_KEY]: label };
375
+ return matter.stringify(body, newData);
376
+ } catch {
377
+ return `---
378
+ ${LABEL_FRONTMATTER_KEY}: "${label}"
379
+ ---
380
+
381
+ ${content}`;
382
+ }
383
+ }
384
+ function stripLabelFromContent(content) {
385
+ try {
386
+ const { data, content: body } = matter(content);
387
+ if (LABEL_FRONTMATTER_KEY in data) {
388
+ const { [LABEL_FRONTMATTER_KEY]: _, ...cleanData } = data;
389
+ if (Object.keys(cleanData).length === 0) {
390
+ return body.startsWith("\n") ? body.slice(1) : body;
391
+ }
392
+ return matter.stringify(body, cleanData);
393
+ }
394
+ return content;
395
+ } catch {
396
+ return content;
397
+ }
398
+ }
399
+ async function ensureGitignore(projectRoot, config) {
400
+ const storageRoot = join(projectRoot, config.storagePath);
401
+ const gitignorePath = join(storageRoot, ".gitignore");
402
+ if (!existsSync(storageRoot)) {
403
+ await mkdir(storageRoot, { recursive: true });
404
+ }
405
+ if (!existsSync(gitignorePath)) {
406
+ await writeFile(gitignorePath, GITIGNORE_CONTENT, "utf-8");
407
+ }
408
+ }
409
+ async function ensureStorageDirectory(storagePath) {
410
+ if (!existsSync(storagePath)) {
411
+ await mkdir(storagePath, { recursive: true });
412
+ }
413
+ }
414
+ async function readManifest(storagePath) {
415
+ const manifestPath = getManifestPath(storagePath);
416
+ if (!existsSync(manifestPath)) {
417
+ return null;
418
+ }
419
+ try {
420
+ const content = await readFile(manifestPath, "utf-8");
421
+ const data = JSON.parse(content);
422
+ if (!data.contentId || !data.collection || !Array.isArray(data.versions)) {
423
+ console.warn(`[writenex] Corrupted manifest at ${manifestPath}`);
424
+ return null;
425
+ }
426
+ return data;
427
+ } catch (error) {
428
+ console.warn(
429
+ `[writenex] Failed to read manifest at ${manifestPath}:`,
430
+ error
431
+ );
432
+ return null;
433
+ }
434
+ }
435
+ async function writeManifest(storagePath, manifest) {
436
+ await ensureStorageDirectory(storagePath);
437
+ const manifestPath = getManifestPath(storagePath);
438
+ const content = JSON.stringify(manifest, null, 2);
439
+ await writeFile(manifestPath, content, "utf-8");
440
+ }
441
+ function createEmptyManifest(collection, contentId) {
442
+ return {
443
+ contentId,
444
+ collection,
445
+ versions: [],
446
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
447
+ };
448
+ }
449
+ async function recoverManifest(storagePath, collection, contentId) {
450
+ const manifest = createEmptyManifest(collection, contentId);
451
+ if (!existsSync(storagePath)) {
452
+ return manifest;
453
+ }
454
+ try {
455
+ const files = await readdir(storagePath);
456
+ const versionFiles = files.filter(
457
+ (f) => f.endsWith(".md") && f !== "manifest.json"
458
+ );
459
+ for (const file of versionFiles) {
460
+ const versionId = basename(file, ".md");
461
+ const filePath = join(storagePath, file);
462
+ try {
463
+ const content = await readFile(filePath, "utf-8");
464
+ const stats = await stat(filePath);
465
+ const timestamp = parseVersionId(versionId);
466
+ if (timestamp) {
467
+ const label = extractLabelFromContent(content);
468
+ const entry = {
469
+ id: versionId,
470
+ timestamp: timestamp.toISOString(),
471
+ preview: generatePreview(content),
472
+ size: stats.size,
473
+ ...label ? { label } : {}
474
+ };
475
+ manifest.versions.push(entry);
476
+ }
477
+ } catch {
478
+ console.warn(`[writenex] Skipping unreadable version file: ${file}`);
479
+ }
480
+ }
481
+ manifest.versions.sort(
482
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
483
+ );
484
+ manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
485
+ await writeManifest(storagePath, manifest);
486
+ return manifest;
487
+ } catch (error) {
488
+ console.warn(`[writenex] Failed to recover manifest:`, error);
489
+ return manifest;
490
+ }
491
+ }
492
+ async function getOrRecoverManifest(storagePath, collection, contentId) {
493
+ const manifest = await readManifest(storagePath);
494
+ if (manifest) {
495
+ return manifest;
496
+ }
497
+ return recoverManifest(storagePath, collection, contentId);
498
+ }
499
+ async function saveVersion(projectRoot, collection, contentId, content, config, options = {}) {
500
+ const { label, skipIfIdentical = false } = options;
501
+ if (!config.enabled) {
502
+ return { success: true };
503
+ }
504
+ const storagePath = getVersionStoragePath(
505
+ projectRoot,
506
+ collection,
507
+ contentId,
508
+ config
509
+ );
510
+ return withLock(storagePath, async () => {
511
+ try {
512
+ await ensureGitignore(projectRoot, config);
513
+ await ensureStorageDirectory(storagePath);
514
+ const manifest = await getOrRecoverManifest(
515
+ storagePath,
516
+ collection,
517
+ contentId
518
+ );
519
+ if (skipIfIdentical && manifest.versions.length > 0) {
520
+ const lastVersion = manifest.versions[0];
521
+ if (lastVersion) {
522
+ const lastVersionPath = getVersionFilePath(
523
+ storagePath,
524
+ lastVersion.id
525
+ );
526
+ if (existsSync(lastVersionPath)) {
527
+ try {
528
+ const lastContent = await readFile(lastVersionPath, "utf-8");
529
+ if (lastContent === content) {
530
+ return { success: true, version: lastVersion };
531
+ }
532
+ } catch {
533
+ }
534
+ }
535
+ }
536
+ }
537
+ const now = /* @__PURE__ */ new Date();
538
+ const versionId = now.toISOString().replace(/:/g, "-");
539
+ const versionPath = getVersionFilePath(storagePath, versionId);
540
+ const contentToSave = label ? injectLabelIntoContent(content, label) : content;
541
+ await writeFile(versionPath, contentToSave, "utf-8");
542
+ const stats = await stat(versionPath);
543
+ const entry = {
544
+ id: versionId,
545
+ timestamp: now.toISOString(),
546
+ preview: generatePreview(content),
547
+ size: stats.size,
548
+ ...label ? { label } : {}
549
+ };
550
+ manifest.versions.unshift(entry);
551
+ manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
552
+ await writeManifest(storagePath, manifest);
553
+ await pruneVersionsInternal(storagePath, config);
554
+ return { success: true, version: entry };
555
+ } catch (error) {
556
+ const message = error instanceof Error ? error.message : String(error);
557
+ console.error(`[writenex] Failed to save version:`, error);
558
+ return { success: false, error: `Failed to save version: ${message}` };
559
+ }
560
+ });
561
+ }
562
+ async function getVersions(projectRoot, collection, contentId, config) {
563
+ if (!config.enabled) {
564
+ return [];
565
+ }
566
+ try {
567
+ const storagePath = getVersionStoragePath(
568
+ projectRoot,
569
+ collection,
570
+ contentId,
571
+ config
572
+ );
573
+ if (!existsSync(storagePath)) {
574
+ return [];
575
+ }
576
+ const manifest = await getOrRecoverManifest(
577
+ storagePath,
578
+ collection,
579
+ contentId
580
+ );
581
+ return [...manifest.versions].sort(
582
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
583
+ );
584
+ } catch (error) {
585
+ console.warn(`[writenex] Failed to get versions:`, error);
586
+ return [];
587
+ }
588
+ }
589
+ async function getVersion(projectRoot, collection, contentId, versionId, config) {
590
+ if (!config.enabled) {
591
+ return null;
592
+ }
593
+ try {
594
+ const storagePath = getVersionStoragePath(
595
+ projectRoot,
596
+ collection,
597
+ contentId,
598
+ config
599
+ );
600
+ const versionPath = getVersionFilePath(storagePath, versionId);
601
+ if (!existsSync(versionPath)) {
602
+ return null;
603
+ }
604
+ const rawContent = await readFile(versionPath, "utf-8");
605
+ const stats = await stat(versionPath);
606
+ const labelFromContent = extractLabelFromContent(rawContent);
607
+ const content = stripLabelFromContent(rawContent);
608
+ const { data: frontmatter, content: body } = matter(content);
609
+ const timestamp = parseVersionId(versionId);
610
+ if (!timestamp) {
611
+ return null;
612
+ }
613
+ const manifest = await readManifest(storagePath);
614
+ const manifestEntry = manifest?.versions.find((v) => v.id === versionId);
615
+ const label = manifestEntry?.label ?? labelFromContent;
616
+ return {
617
+ id: versionId,
618
+ timestamp: timestamp.toISOString(),
619
+ preview: generatePreview(content),
620
+ size: stats.size,
621
+ content,
622
+ frontmatter,
623
+ body: body.trim(),
624
+ ...label ? { label } : {}
625
+ };
626
+ } catch (error) {
627
+ console.warn(`[writenex] Failed to get version ${versionId}:`, error);
628
+ return null;
629
+ }
630
+ }
631
+ async function deleteVersion(projectRoot, collection, contentId, versionId, config) {
632
+ const storagePath = getVersionStoragePath(
633
+ projectRoot,
634
+ collection,
635
+ contentId,
636
+ config
637
+ );
638
+ return withLock(storagePath, async () => {
639
+ try {
640
+ const versionPath = getVersionFilePath(storagePath, versionId);
641
+ if (!existsSync(versionPath)) {
642
+ return { success: false, error: `Version not found: ${versionId}` };
643
+ }
644
+ const manifest = await getOrRecoverManifest(
645
+ storagePath,
646
+ collection,
647
+ contentId
648
+ );
649
+ const entryIndex = manifest.versions.findIndex((v) => v.id === versionId);
650
+ const entry = entryIndex >= 0 ? manifest.versions[entryIndex] : void 0;
651
+ await unlink(versionPath);
652
+ if (entryIndex >= 0) {
653
+ manifest.versions.splice(entryIndex, 1);
654
+ manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
655
+ await writeManifest(storagePath, manifest);
656
+ }
657
+ return { success: true, version: entry };
658
+ } catch (error) {
659
+ const message = error instanceof Error ? error.message : String(error);
660
+ console.error(`[writenex] Failed to delete version:`, error);
661
+ return { success: false, error: `Failed to delete version: ${message}` };
662
+ }
663
+ });
664
+ }
665
+ async function clearVersions(projectRoot, collection, contentId, config) {
666
+ const storagePath = getVersionStoragePath(
667
+ projectRoot,
668
+ collection,
669
+ contentId,
670
+ config
671
+ );
672
+ if (!existsSync(storagePath)) {
673
+ return { success: true };
674
+ }
675
+ return withLock(storagePath, async () => {
676
+ try {
677
+ const files = await readdir(storagePath);
678
+ const versionFiles = files.filter(
679
+ (f) => f.endsWith(".md") && f !== "manifest.json"
680
+ );
681
+ for (const file of versionFiles) {
682
+ const filePath = join(storagePath, file);
683
+ try {
684
+ await unlink(filePath);
685
+ } catch {
686
+ }
687
+ }
688
+ const manifest = createEmptyManifest(collection, contentId);
689
+ await writeManifest(storagePath, manifest);
690
+ return { success: true };
691
+ } catch (error) {
692
+ const message = error instanceof Error ? error.message : String(error);
693
+ console.error(`[writenex] Failed to clear versions:`, error);
694
+ return { success: false, error: `Failed to clear versions: ${message}` };
695
+ }
696
+ });
697
+ }
698
+ async function pruneVersionsInternal(storagePath, config) {
699
+ try {
700
+ if (!existsSync(storagePath)) {
701
+ return { success: true };
702
+ }
703
+ const manifest = await readManifest(storagePath);
704
+ if (!manifest) {
705
+ return { success: true };
706
+ }
707
+ const labeledVersions = manifest.versions.filter((v) => v.label);
708
+ const unlabeledVersions = manifest.versions.filter((v) => !v.label);
709
+ if (unlabeledVersions.length <= config.maxVersions) {
710
+ return { success: true };
711
+ }
712
+ const sortedUnlabeled = [...unlabeledVersions].sort(
713
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
714
+ );
715
+ const toDelete = sortedUnlabeled.slice(
716
+ 0,
717
+ unlabeledVersions.length - config.maxVersions
718
+ );
719
+ for (const version of toDelete) {
720
+ const versionPath = getVersionFilePath(storagePath, version.id);
721
+ try {
722
+ if (existsSync(versionPath)) {
723
+ await unlink(versionPath);
724
+ }
725
+ } catch {
726
+ }
727
+ }
728
+ const remainingUnlabeled = sortedUnlabeled.slice(
729
+ unlabeledVersions.length - config.maxVersions
730
+ );
731
+ manifest.versions = [...labeledVersions, ...remainingUnlabeled].sort(
732
+ (a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
733
+ );
734
+ manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
735
+ await writeManifest(storagePath, manifest);
736
+ return { success: true };
737
+ } catch (error) {
738
+ const message = error instanceof Error ? error.message : String(error);
739
+ console.error(`[writenex] Failed to prune versions:`, error);
740
+ return { success: false, error: `Failed to prune versions: ${message}` };
741
+ }
742
+ }
743
+ async function pruneVersions(projectRoot, collection, contentId, config) {
744
+ const storagePath = getVersionStoragePath(
745
+ projectRoot,
746
+ collection,
747
+ contentId,
748
+ config
749
+ );
750
+ if (!existsSync(storagePath)) {
751
+ return { success: true };
752
+ }
753
+ return withLock(
754
+ storagePath,
755
+ () => pruneVersionsInternal(storagePath, config)
756
+ );
757
+ }
758
+ async function restoreVersion(projectRoot, collection, contentId, versionId, contentFilePath, config, options = {}) {
759
+ const { safetySnapshotLabel = "Before restore", skipSafetySnapshot = false } = options;
760
+ try {
761
+ const versionToRestore = await getVersion(
762
+ projectRoot,
763
+ collection,
764
+ contentId,
765
+ versionId,
766
+ config
767
+ );
768
+ if (!versionToRestore) {
769
+ return {
770
+ success: false,
771
+ error: `Version not found: ${versionId}`
772
+ };
773
+ }
774
+ let safetySnapshot;
775
+ if (!skipSafetySnapshot && existsSync(contentFilePath)) {
776
+ try {
777
+ const currentContent = await readFile(contentFilePath, "utf-8");
778
+ const snapshotResult = await saveVersion(
779
+ projectRoot,
780
+ collection,
781
+ contentId,
782
+ currentContent,
783
+ config,
784
+ { label: safetySnapshotLabel }
785
+ );
786
+ if (snapshotResult.success && snapshotResult.version) {
787
+ safetySnapshot = snapshotResult.version;
788
+ }
789
+ } catch (error) {
790
+ console.warn(
791
+ `[writenex] Failed to create safety snapshot before restore:`,
792
+ error
793
+ );
794
+ }
795
+ }
796
+ await writeFile(contentFilePath, versionToRestore.content, "utf-8");
797
+ return {
798
+ success: true,
799
+ version: {
800
+ id: versionToRestore.id,
801
+ timestamp: versionToRestore.timestamp,
802
+ preview: versionToRestore.preview,
803
+ size: versionToRestore.size,
804
+ ...versionToRestore.label ? { label: versionToRestore.label } : {}
805
+ },
806
+ content: versionToRestore.content,
807
+ safetySnapshot
808
+ };
809
+ } catch (error) {
810
+ const message = error instanceof Error ? error.message : String(error);
811
+ console.error(`[writenex] Failed to restore version:`, error);
812
+ return {
813
+ success: false,
814
+ error: `Failed to restore version: ${message}`
815
+ };
816
+ }
817
+ }
818
+
819
+ // src/filesystem/writer.ts
820
+ import { writeFile as writeFile2, unlink as unlink2, mkdir as mkdir2, readFile as readFile2, stat as stat2 } from "fs/promises";
821
+ import { existsSync as existsSync2 } from "fs";
822
+ import { join as join2, dirname, basename as basename2 } from "path";
823
+ import slugify from "slugify";
824
+ function generateSlug(text) {
825
+ return slugify(text, {
826
+ lower: true,
827
+ strict: true,
828
+ trim: true
829
+ });
830
+ }
831
+ function contentExists(slug, collectionPath, filePattern) {
832
+ const relativePath = generatePathFromPattern(filePattern, { slug });
833
+ const fullPath = join2(collectionPath, relativePath);
834
+ if (filePattern.includes("/index.")) {
835
+ const folderPath = join2(collectionPath, slug);
836
+ return existsSync2(folderPath);
837
+ }
838
+ return existsSync2(fullPath);
839
+ }
840
+ async function generateUniqueSlug(baseSlug, collectionPath, filePattern = "{slug}.md") {
841
+ let slug = baseSlug;
842
+ let counter = 2;
843
+ while (contentExists(slug, collectionPath, filePattern)) {
844
+ slug = `${baseSlug}-${counter}`;
845
+ counter++;
846
+ }
847
+ return slug;
848
+ }
849
+ function frontmatterToYaml(frontmatter) {
850
+ const lines = [];
851
+ for (const [key, value] of Object.entries(frontmatter)) {
852
+ if (value === void 0 || value === null) {
853
+ continue;
854
+ }
855
+ if (typeof value === "string") {
856
+ if (value.includes(":") || value.includes("#") || value.includes("\n")) {
857
+ lines.push(`${key}: "${value.replace(/"/g, '\\"')}"`);
858
+ } else {
859
+ lines.push(`${key}: ${value}`);
860
+ }
861
+ } else if (typeof value === "number" || typeof value === "boolean") {
862
+ lines.push(`${key}: ${value}`);
863
+ } else if (value instanceof Date) {
864
+ lines.push(`${key}: ${value.toISOString().split("T")[0]}`);
865
+ } else if (Array.isArray(value)) {
866
+ if (value.length === 0) {
867
+ lines.push(`${key}: []`);
868
+ } else {
869
+ lines.push(`${key}:`);
870
+ for (const item of value) {
871
+ lines.push(` - ${item}`);
872
+ }
873
+ }
874
+ } else if (typeof value === "object") {
875
+ lines.push(`${key}:`);
876
+ for (const [subKey, subValue] of Object.entries(value)) {
877
+ lines.push(` ${subKey}: ${subValue}`);
878
+ }
879
+ }
880
+ }
881
+ return lines.join("\n");
882
+ }
883
+ function createFileContent(frontmatter, body) {
884
+ const yaml = frontmatterToYaml(frontmatter);
885
+ return `---
886
+ ${yaml}
887
+ ---
888
+
889
+ ${body}`;
890
+ }
891
+ async function createContent(collectionPath, options) {
892
+ const {
893
+ frontmatter,
894
+ body,
895
+ slug: customSlug,
896
+ filePattern = "{slug}.md",
897
+ customTokens = {}
898
+ } = options;
899
+ try {
900
+ const validation = isValidPattern(filePattern);
901
+ if (!validation.valid) {
902
+ return {
903
+ success: false,
904
+ error: `Invalid file pattern: ${validation.error}`
905
+ };
906
+ }
907
+ const title = frontmatter.title;
908
+ const baseSlug = customSlug ?? (title ? generateSlug(title) : "untitled");
909
+ const slug = await generateUniqueSlug(
910
+ baseSlug,
911
+ collectionPath,
912
+ filePattern
913
+ );
914
+ const tokens = resolvePatternTokens(filePattern, {
915
+ slug,
916
+ frontmatter,
917
+ customTokens
918
+ });
919
+ const relativePath = generatePathFromPattern(filePattern, tokens);
920
+ const filePath = join2(collectionPath, relativePath);
921
+ const parentDir = dirname(filePath);
922
+ if (!existsSync2(parentDir)) {
923
+ await mkdir2(parentDir, { recursive: true });
924
+ }
925
+ const content = createFileContent(frontmatter, body);
926
+ await writeFile2(filePath, content, "utf-8");
927
+ return {
928
+ success: true,
929
+ id: slug,
930
+ path: filePath
931
+ };
932
+ } catch (error) {
933
+ const message = error instanceof Error ? error.message : String(error);
934
+ return {
935
+ success: false,
936
+ error: `Failed to create content: ${message}`
937
+ };
938
+ }
939
+ }
940
+ async function updateContent(filePath, collectionPath, options) {
941
+ const { projectRoot, collection, versionHistoryConfig, expectedMtime } = options;
942
+ try {
943
+ const existing = await readContentFile(filePath, collectionPath);
944
+ if (!existing.success || !existing.content) {
945
+ return {
946
+ success: false,
947
+ error: existing.error ?? "Content not found"
948
+ };
949
+ }
950
+ if (expectedMtime !== void 0 && existing.content.mtime !== void 0) {
951
+ const mtimeDiff = Math.abs(existing.content.mtime - expectedMtime);
952
+ if (mtimeDiff > 1) {
953
+ const conflictError = new ContentConflictError(
954
+ collection ?? "unknown",
955
+ existing.content.id,
956
+ existing.content.raw,
957
+ existing.content.mtime,
958
+ expectedMtime
959
+ );
960
+ return {
961
+ success: false,
962
+ error: conflictError.message,
963
+ conflict: conflictError
964
+ };
965
+ }
966
+ }
967
+ const frontmatter = options.frontmatter ? { ...existing.content.frontmatter, ...options.frontmatter } : existing.content.frontmatter;
968
+ const body = options.body ?? existing.content.body;
969
+ const newContent = createFileContent(frontmatter, body);
970
+ const currentContent = existsSync2(filePath) ? await readFile2(filePath, "utf-8") : "";
971
+ if (newContent === currentContent) {
972
+ return {
973
+ success: true,
974
+ id: existing.content.id,
975
+ path: filePath,
976
+ mtime: existing.content.mtime
977
+ };
978
+ }
979
+ if (projectRoot && collection && versionHistoryConfig && versionHistoryConfig.enabled && currentContent) {
980
+ try {
981
+ const fileName = basename2(filePath);
982
+ const contentId = fileName === "index.md" || fileName === "index.mdx" ? basename2(dirname(filePath)) : fileName.replace(/\.(md|mdx)$/, "");
983
+ const versionResult = await saveVersion(
984
+ projectRoot,
985
+ collection,
986
+ contentId,
987
+ currentContent,
988
+ versionHistoryConfig,
989
+ { skipIfIdentical: true }
990
+ );
991
+ if (!versionResult.success) {
992
+ console.warn(
993
+ `[writenex] Failed to create version snapshot: ${versionResult.error}`
994
+ );
995
+ }
996
+ } catch (versionError) {
997
+ console.warn(
998
+ `[writenex] Version creation error (save will continue):`,
999
+ versionError
1000
+ );
1001
+ }
1002
+ }
1003
+ await writeFile2(filePath, newContent, "utf-8");
1004
+ const newStats = await stat2(filePath);
1005
+ return {
1006
+ success: true,
1007
+ id: existing.content.id,
1008
+ path: filePath,
1009
+ mtime: newStats.mtimeMs
1010
+ };
1011
+ } catch (error) {
1012
+ if (error instanceof ContentConflictError) {
1013
+ return {
1014
+ success: false,
1015
+ error: error.message,
1016
+ conflict: error
1017
+ };
1018
+ }
1019
+ const message = error instanceof Error ? error.message : String(error);
1020
+ return {
1021
+ success: false,
1022
+ error: `Failed to update content: ${message}`
1023
+ };
1024
+ }
1025
+ }
1026
+ async function deleteContent(filePath) {
1027
+ try {
1028
+ if (!existsSync2(filePath)) {
1029
+ return {
1030
+ success: false,
1031
+ error: "Content file not found"
1032
+ };
1033
+ }
1034
+ await unlink2(filePath);
1035
+ return {
1036
+ success: true,
1037
+ path: filePath
1038
+ };
1039
+ } catch (error) {
1040
+ const message = error instanceof Error ? error.message : String(error);
1041
+ return {
1042
+ success: false,
1043
+ error: `Failed to delete content: ${message}`
1044
+ };
1045
+ }
1046
+ }
1047
+
1048
+ // src/filesystem/images.ts
1049
+ import { writeFile as writeFile3, mkdir as mkdir3, readdir as readdir2, stat as stat3 } from "fs/promises";
1050
+ import { existsSync as existsSync3 } from "fs";
1051
+ import { join as join3, dirname as dirname2, basename as basename3, extname, relative } from "path";
1052
+ var DEFAULT_IMAGE_CONFIG = {
1053
+ strategy: "colocated",
1054
+ publicPath: "/images",
1055
+ storagePath: "public/images"
1056
+ };
1057
+ var SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([
1058
+ ".jpg",
1059
+ ".jpeg",
1060
+ ".png",
1061
+ ".gif",
1062
+ ".webp",
1063
+ ".avif",
1064
+ ".svg"
1065
+ ]);
1066
+ function isValidImageFile(filename) {
1067
+ const ext = extname(filename).toLowerCase();
1068
+ return SUPPORTED_EXTENSIONS.has(ext);
1069
+ }
1070
+ function generateUniqueFilename(originalName, _contentId) {
1071
+ const ext = extname(originalName).toLowerCase();
1072
+ const baseName = basename3(originalName, ext).toLowerCase().replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").substring(0, 50);
1073
+ const timestamp = Date.now().toString(36);
1074
+ return `${baseName}-${timestamp}${ext}`;
1075
+ }
1076
+ function getColocatedPath(projectRoot, collection, contentId, filename) {
1077
+ const collectionPath = join3(projectRoot, "src/content", collection);
1078
+ const imageDir = join3(collectionPath, contentId);
1079
+ const storagePath = join3(imageDir, filename);
1080
+ const indexMdPath = join3(collectionPath, contentId, "index.md");
1081
+ const indexMdxPath = join3(collectionPath, contentId, "index.mdx");
1082
+ const isFolderBased = existsSync3(indexMdPath) || existsSync3(indexMdxPath);
1083
+ const markdownPath = isFolderBased ? `./${filename}` : `./${contentId}/${filename}`;
1084
+ return { storagePath, markdownPath };
1085
+ }
1086
+ function getPublicPath(projectRoot, collection, filename, config) {
1087
+ const storagePath = join3(
1088
+ projectRoot,
1089
+ config.storagePath ?? "public/images",
1090
+ collection,
1091
+ filename
1092
+ );
1093
+ const publicPath = config.publicPath ?? "/images";
1094
+ const url = `${publicPath}/${collection}/${filename}`;
1095
+ return { storagePath, markdownPath: url, url };
1096
+ }
1097
+ async function uploadImage(options) {
1098
+ const {
1099
+ filename,
1100
+ data,
1101
+ collection,
1102
+ contentId,
1103
+ projectRoot,
1104
+ config = DEFAULT_IMAGE_CONFIG
1105
+ } = options;
1106
+ if (!isValidImageFile(filename)) {
1107
+ return {
1108
+ success: false,
1109
+ error: `Invalid image file type. Supported: ${[...SUPPORTED_EXTENSIONS].join(", ")}`
1110
+ };
1111
+ }
1112
+ const uniqueFilename = generateUniqueFilename(filename, contentId);
1113
+ try {
1114
+ let storagePath;
1115
+ let markdownPath;
1116
+ let url;
1117
+ switch (config.strategy) {
1118
+ case "public": {
1119
+ const paths = getPublicPath(
1120
+ projectRoot,
1121
+ collection,
1122
+ uniqueFilename,
1123
+ config
1124
+ );
1125
+ storagePath = paths.storagePath;
1126
+ markdownPath = paths.markdownPath;
1127
+ url = paths.url;
1128
+ break;
1129
+ }
1130
+ case "colocated":
1131
+ default: {
1132
+ const paths = getColocatedPath(
1133
+ projectRoot,
1134
+ collection,
1135
+ contentId,
1136
+ uniqueFilename
1137
+ );
1138
+ storagePath = paths.storagePath;
1139
+ markdownPath = paths.markdownPath;
1140
+ break;
1141
+ }
1142
+ }
1143
+ const dir = dirname2(storagePath);
1144
+ if (!existsSync3(dir)) {
1145
+ await mkdir3(dir, { recursive: true });
1146
+ }
1147
+ await writeFile3(storagePath, data);
1148
+ return {
1149
+ success: true,
1150
+ path: markdownPath,
1151
+ url: url ?? markdownPath
1152
+ };
1153
+ } catch (error) {
1154
+ const message = error instanceof Error ? error.message : "Unknown error";
1155
+ return {
1156
+ success: false,
1157
+ error: `Failed to upload image: ${message}`
1158
+ };
1159
+ }
1160
+ }
1161
+ function parseMultipartFormData(body, contentType) {
1162
+ const result = { fields: {} };
1163
+ const boundaryMatch = contentType.match(/boundary=(?:"([^"]+)"|([^;]+))/);
1164
+ if (!boundaryMatch) {
1165
+ return result;
1166
+ }
1167
+ const boundary = boundaryMatch[1] ?? boundaryMatch[2];
1168
+ if (!boundary) {
1169
+ return result;
1170
+ }
1171
+ const boundaryBuffer = Buffer.from(`--${boundary}`);
1172
+ const parts = splitBuffer(body, boundaryBuffer);
1173
+ for (const part of parts) {
1174
+ if (part.length < 10) continue;
1175
+ const separatorIndex = part.indexOf("\r\n\r\n");
1176
+ if (separatorIndex === -1) continue;
1177
+ const headerSection = part.slice(0, separatorIndex).toString("utf-8");
1178
+ const bodySection = part.slice(separatorIndex + 4);
1179
+ const bodyEnd = bodySection.length - 2;
1180
+ const cleanBody = bodyEnd > 0 ? bodySection.slice(0, bodyEnd) : bodySection;
1181
+ const headers = parseHeaders(headerSection);
1182
+ const disposition = headers["content-disposition"];
1183
+ if (!disposition) continue;
1184
+ const nameMatch = disposition.match(/name="([^"]+)"/);
1185
+ const filenameMatch = disposition.match(/filename="([^"]+)"/);
1186
+ if (filenameMatch) {
1187
+ result.file = {
1188
+ filename: filenameMatch[1] ?? "unknown",
1189
+ data: cleanBody,
1190
+ contentType: headers["content-type"] ?? "application/octet-stream"
1191
+ };
1192
+ } else if (nameMatch) {
1193
+ result.fields[nameMatch[1] ?? ""] = cleanBody.toString("utf-8");
1194
+ }
1195
+ }
1196
+ return result;
1197
+ }
1198
+ function splitBuffer(buffer, delimiter) {
1199
+ const parts = [];
1200
+ let start = 0;
1201
+ let index;
1202
+ while ((index = buffer.indexOf(delimiter, start)) !== -1) {
1203
+ if (index > start) {
1204
+ parts.push(buffer.slice(start, index));
1205
+ }
1206
+ start = index + delimiter.length;
1207
+ }
1208
+ if (start < buffer.length) {
1209
+ parts.push(buffer.slice(start));
1210
+ }
1211
+ return parts;
1212
+ }
1213
+ function parseHeaders(headerSection) {
1214
+ const headers = {};
1215
+ const lines = headerSection.split("\r\n");
1216
+ for (const line of lines) {
1217
+ const colonIndex = line.indexOf(":");
1218
+ if (colonIndex > 0) {
1219
+ const key = line.slice(0, colonIndex).trim().toLowerCase();
1220
+ const value = line.slice(colonIndex + 1).trim();
1221
+ headers[key] = value;
1222
+ }
1223
+ }
1224
+ return headers;
1225
+ }
1226
+ var DATE_PREFIX_PATTERN = /^\d{4}-\d{2}-\d{2}-/;
1227
+ function getContentImageFolder(collectionPath, contentId, contentFilePath) {
1228
+ const filename = basename3(contentFilePath);
1229
+ const contentDir = dirname2(contentFilePath);
1230
+ if (filename === "index.md" || filename === "index.mdx") {
1231
+ return contentDir;
1232
+ }
1233
+ const siblingFolderPath = join3(collectionPath, contentId);
1234
+ if (existsSync3(siblingFolderPath)) {
1235
+ return siblingFolderPath;
1236
+ }
1237
+ return null;
1238
+ }
1239
+ function detectContentStructure(contentFilePath) {
1240
+ const filename = basename3(contentFilePath);
1241
+ if (filename === "index.md" || filename === "index.mdx") {
1242
+ return "folder-based";
1243
+ }
1244
+ const nameWithoutExt = filename.replace(/\.(md|mdx)$/, "");
1245
+ if (DATE_PREFIX_PATTERN.test(nameWithoutExt)) {
1246
+ return "date-prefixed";
1247
+ }
1248
+ return "flat";
1249
+ }
1250
+ function shouldSkipDirectory(dirName) {
1251
+ return dirName.startsWith(".") || dirName.startsWith("_");
1252
+ }
1253
+ async function scanDirectoryForImages(dirPath, basePath, options) {
1254
+ const { maxDepth, currentDepth } = options;
1255
+ if (currentDepth >= maxDepth) {
1256
+ return [];
1257
+ }
1258
+ if (!existsSync3(dirPath)) {
1259
+ return [];
1260
+ }
1261
+ const images = [];
1262
+ try {
1263
+ const entries = await readdir2(dirPath, { withFileTypes: true });
1264
+ for (const entry of entries) {
1265
+ const entryPath = join3(dirPath, entry.name);
1266
+ if (entry.isDirectory()) {
1267
+ if (shouldSkipDirectory(entry.name)) {
1268
+ continue;
1269
+ }
1270
+ const subImages = await scanDirectoryForImages(entryPath, basePath, {
1271
+ maxDepth,
1272
+ currentDepth: currentDepth + 1,
1273
+ basePath
1274
+ });
1275
+ images.push(...subImages);
1276
+ } else if (entry.isFile()) {
1277
+ if (isValidImageFile(entry.name)) {
1278
+ const fileStat = await stat3(entryPath);
1279
+ const extension = extname(entry.name).toLowerCase();
1280
+ const relativePath = calculateRelativePathFromBase(
1281
+ basePath,
1282
+ entryPath
1283
+ );
1284
+ images.push({
1285
+ filename: entry.name,
1286
+ relativePath,
1287
+ absolutePath: entryPath,
1288
+ size: fileStat.size,
1289
+ extension
1290
+ });
1291
+ }
1292
+ }
1293
+ }
1294
+ } catch {
1295
+ return [];
1296
+ }
1297
+ return images;
1298
+ }
1299
+ function calculateRelativePathFromBase(basePath, targetPath) {
1300
+ const relPath = relative(basePath, targetPath);
1301
+ return `./${relPath}`;
1302
+ }
1303
+ function calculateRelativePath(contentFilePath, imagePath) {
1304
+ const contentDir = dirname2(contentFilePath);
1305
+ const relPath = relative(contentDir, imagePath);
1306
+ if (relPath.startsWith("..")) {
1307
+ return relPath;
1308
+ }
1309
+ return `./${relPath}`;
1310
+ }
1311
+ var DEFAULT_MAX_DEPTH = 5;
1312
+ async function discoverContentImages(collectionPath, contentId, options) {
1313
+ const maxDepth = options?.maxDepth ?? DEFAULT_MAX_DEPTH;
1314
+ try {
1315
+ const contentFilePath = getContentFilePath(collectionPath, contentId);
1316
+ if (!contentFilePath) {
1317
+ return {
1318
+ success: false,
1319
+ images: [],
1320
+ error: `Content '${contentId}' not found in collection`
1321
+ };
1322
+ }
1323
+ const imageFolderPath = getContentImageFolder(
1324
+ collectionPath,
1325
+ contentId,
1326
+ contentFilePath
1327
+ );
1328
+ if (!imageFolderPath) {
1329
+ return {
1330
+ success: true,
1331
+ images: []
1332
+ };
1333
+ }
1334
+ const scannedImages = await scanDirectoryForImages(
1335
+ imageFolderPath,
1336
+ imageFolderPath,
1337
+ {
1338
+ maxDepth,
1339
+ currentDepth: 0,
1340
+ basePath: imageFolderPath
1341
+ }
1342
+ );
1343
+ const images = scannedImages.map((img) => ({
1344
+ ...img,
1345
+ relativePath: calculateRelativePath(contentFilePath, img.absolutePath)
1346
+ }));
1347
+ return {
1348
+ success: true,
1349
+ images
1350
+ };
1351
+ } catch (error) {
1352
+ const message = error instanceof Error ? error.message : "Unknown error";
1353
+ return {
1354
+ success: false,
1355
+ images: [],
1356
+ error: `Failed to discover images: ${message}`
1357
+ };
1358
+ }
1359
+ }
1360
+
1361
+ export {
1362
+ WritenexErrorCode,
1363
+ WritenexError,
1364
+ PathTraversalError,
1365
+ ContentNotFoundError,
1366
+ CollectionNotFoundError,
1367
+ CollectionDiscoveryError,
1368
+ ApiBadRequestError,
1369
+ ApiMethodNotAllowedError,
1370
+ ImageInvalidTypeError,
1371
+ ImageNotFoundError,
1372
+ VersionNotFoundError,
1373
+ isWritenexError,
1374
+ wrapError,
1375
+ generateVersionId,
1376
+ parseVersionId,
1377
+ getVersionStoragePath,
1378
+ getVersionFilePath,
1379
+ getManifestPath,
1380
+ generatePreview,
1381
+ ensureGitignore,
1382
+ ensureStorageDirectory,
1383
+ readManifest,
1384
+ writeManifest,
1385
+ createEmptyManifest,
1386
+ recoverManifest,
1387
+ getOrRecoverManifest,
1388
+ saveVersion,
1389
+ getVersions,
1390
+ getVersion,
1391
+ deleteVersion,
1392
+ clearVersions,
1393
+ pruneVersions,
1394
+ restoreVersion,
1395
+ generateSlug,
1396
+ generateUniqueSlug,
1397
+ createContent,
1398
+ updateContent,
1399
+ deleteContent,
1400
+ DEFAULT_IMAGE_CONFIG,
1401
+ isValidImageFile,
1402
+ uploadImage,
1403
+ parseMultipartFormData,
1404
+ getContentImageFolder,
1405
+ detectContentStructure,
1406
+ scanDirectoryForImages,
1407
+ calculateRelativePath,
1408
+ discoverContentImages
1409
+ };
1410
+ //# sourceMappingURL=chunk-CF2XXJFF.js.map