@knpkv/confluence-to-markdown 0.2.0 → 0.4.2

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 (336) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/LICENSE +21 -0
  3. package/README.md +282 -14
  4. package/dist/ConfluenceAuth.d.ts +76 -0
  5. package/dist/ConfluenceAuth.d.ts.map +1 -0
  6. package/dist/ConfluenceAuth.js +366 -0
  7. package/dist/ConfluenceAuth.js.map +1 -0
  8. package/dist/ConfluenceClient.d.ts +26 -12
  9. package/dist/ConfluenceClient.d.ts.map +1 -1
  10. package/dist/ConfluenceClient.js +139 -97
  11. package/dist/ConfluenceClient.js.map +1 -1
  12. package/dist/ConfluenceConfig.d.ts +4 -24
  13. package/dist/ConfluenceConfig.d.ts.map +1 -1
  14. package/dist/ConfluenceConfig.js +45 -7
  15. package/dist/ConfluenceConfig.js.map +1 -1
  16. package/dist/ConfluenceError.d.ts +99 -16
  17. package/dist/ConfluenceError.d.ts.map +1 -1
  18. package/dist/ConfluenceError.js +88 -5
  19. package/dist/ConfluenceError.js.map +1 -1
  20. package/dist/GitError.d.ts +103 -0
  21. package/dist/GitError.d.ts.map +1 -0
  22. package/dist/GitError.js +85 -0
  23. package/dist/GitError.js.map +1 -0
  24. package/dist/GitService.d.ts +175 -0
  25. package/dist/GitService.d.ts.map +1 -0
  26. package/dist/GitService.js +431 -0
  27. package/dist/GitService.js.map +1 -0
  28. package/dist/LocalFileSystem.d.ts +29 -4
  29. package/dist/LocalFileSystem.d.ts.map +1 -1
  30. package/dist/LocalFileSystem.js +80 -6
  31. package/dist/LocalFileSystem.js.map +1 -1
  32. package/dist/MarkdownConverter.d.ts +49 -2
  33. package/dist/MarkdownConverter.d.ts.map +1 -1
  34. package/dist/MarkdownConverter.js +73 -111
  35. package/dist/MarkdownConverter.js.map +1 -1
  36. package/dist/SchemaConverterError.d.ts +108 -0
  37. package/dist/SchemaConverterError.d.ts.map +1 -0
  38. package/dist/SchemaConverterError.js +84 -0
  39. package/dist/SchemaConverterError.js.map +1 -0
  40. package/dist/Schemas.d.ts +225 -1
  41. package/dist/Schemas.d.ts.map +1 -1
  42. package/dist/Schemas.js +155 -6
  43. package/dist/Schemas.js.map +1 -1
  44. package/dist/SyncEngine.d.ts +30 -20
  45. package/dist/SyncEngine.d.ts.map +1 -1
  46. package/dist/SyncEngine.js +566 -117
  47. package/dist/SyncEngine.js.map +1 -1
  48. package/dist/ast/BlockNode.d.ts +468 -0
  49. package/dist/ast/BlockNode.d.ts.map +1 -0
  50. package/dist/ast/BlockNode.js +319 -0
  51. package/dist/ast/BlockNode.js.map +1 -0
  52. package/dist/ast/Document.d.ts +244 -0
  53. package/dist/ast/Document.d.ts.map +1 -0
  54. package/dist/ast/Document.js +69 -0
  55. package/dist/ast/Document.js.map +1 -0
  56. package/dist/ast/InlineNode.d.ts +477 -0
  57. package/dist/ast/InlineNode.d.ts.map +1 -0
  58. package/dist/ast/InlineNode.js +263 -0
  59. package/dist/ast/InlineNode.js.map +1 -0
  60. package/dist/ast/MacroNode.d.ts +267 -0
  61. package/dist/ast/MacroNode.d.ts.map +1 -0
  62. package/dist/ast/MacroNode.js +164 -0
  63. package/dist/ast/MacroNode.js.map +1 -0
  64. package/dist/ast/index.d.ts +10 -0
  65. package/dist/ast/index.d.ts.map +1 -0
  66. package/dist/ast/index.js +14 -0
  67. package/dist/ast/index.js.map +1 -0
  68. package/dist/bin.js +33 -149
  69. package/dist/bin.js.map +1 -1
  70. package/dist/commands/auth.d.ts +15 -0
  71. package/dist/commands/auth.d.ts.map +1 -0
  72. package/dist/commands/auth.js +86 -0
  73. package/dist/commands/auth.js.map +1 -0
  74. package/dist/commands/clone.d.ts +12 -0
  75. package/dist/commands/clone.d.ts.map +1 -0
  76. package/dist/commands/clone.js +93 -0
  77. package/dist/commands/clone.js.map +1 -0
  78. package/dist/commands/delete.d.ts +13 -0
  79. package/dist/commands/delete.d.ts.map +1 -0
  80. package/dist/commands/delete.js +48 -0
  81. package/dist/commands/delete.js.map +1 -0
  82. package/dist/commands/errorHandler.d.ts +14 -0
  83. package/dist/commands/errorHandler.d.ts.map +1 -0
  84. package/dist/commands/errorHandler.js +33 -0
  85. package/dist/commands/errorHandler.js.map +1 -0
  86. package/dist/commands/git.d.ts +22 -0
  87. package/dist/commands/git.d.ts.map +1 -0
  88. package/dist/commands/git.js +72 -0
  89. package/dist/commands/git.js.map +1 -0
  90. package/dist/commands/index.d.ts +11 -0
  91. package/dist/commands/index.d.ts.map +1 -0
  92. package/dist/commands/index.js +11 -0
  93. package/dist/commands/index.js.map +1 -0
  94. package/dist/commands/layers.d.ts +31 -0
  95. package/dist/commands/layers.d.ts.map +1 -0
  96. package/dist/commands/layers.js +137 -0
  97. package/dist/commands/layers.js.map +1 -0
  98. package/dist/commands/new.d.ts +9 -0
  99. package/dist/commands/new.d.ts.map +1 -0
  100. package/dist/commands/new.js +80 -0
  101. package/dist/commands/new.js.map +1 -0
  102. package/dist/commands/pageTree.d.ts +18 -0
  103. package/dist/commands/pageTree.d.ts.map +1 -0
  104. package/dist/commands/pageTree.js +20 -0
  105. package/dist/commands/pageTree.js.map +1 -0
  106. package/dist/commands/shared.d.ts +15 -0
  107. package/dist/commands/shared.d.ts.map +1 -0
  108. package/dist/commands/shared.js +27 -0
  109. package/dist/commands/shared.js.map +1 -0
  110. package/dist/commands/sync.d.ts +15 -0
  111. package/dist/commands/sync.d.ts.map +1 -0
  112. package/dist/commands/sync.js +101 -0
  113. package/dist/commands/sync.js.map +1 -0
  114. package/dist/index.d.ts +10 -1
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/index.js +14 -0
  117. package/dist/index.js.map +1 -1
  118. package/dist/internal/NodeLayers.d.ts +7 -0
  119. package/dist/internal/NodeLayers.d.ts.map +1 -0
  120. package/dist/internal/NodeLayers.js +19 -0
  121. package/dist/internal/NodeLayers.js.map +1 -0
  122. package/dist/internal/frontmatter.d.ts +10 -0
  123. package/dist/internal/frontmatter.d.ts.map +1 -1
  124. package/dist/internal/frontmatter.js +16 -0
  125. package/dist/internal/frontmatter.js.map +1 -1
  126. package/dist/internal/gitCommands.d.ts +78 -0
  127. package/dist/internal/gitCommands.d.ts.map +1 -0
  128. package/dist/internal/gitCommands.js +156 -0
  129. package/dist/internal/gitCommands.js.map +1 -0
  130. package/dist/internal/hashUtils.d.ts +42 -1
  131. package/dist/internal/hashUtils.d.ts.map +1 -1
  132. package/dist/internal/hashUtils.js +38 -2
  133. package/dist/internal/hashUtils.js.map +1 -1
  134. package/dist/internal/oauthServer.d.ts +55 -0
  135. package/dist/internal/oauthServer.d.ts.map +1 -0
  136. package/dist/internal/oauthServer.js +110 -0
  137. package/dist/internal/oauthServer.js.map +1 -0
  138. package/dist/internal/pathUtils.d.ts +21 -4
  139. package/dist/internal/pathUtils.d.ts.map +1 -1
  140. package/dist/internal/pathUtils.js +24 -13
  141. package/dist/internal/pathUtils.js.map +1 -1
  142. package/dist/internal/tokenStorage.d.ts +75 -0
  143. package/dist/internal/tokenStorage.d.ts.map +1 -0
  144. package/dist/internal/tokenStorage.js +149 -0
  145. package/dist/internal/tokenStorage.js.map +1 -0
  146. package/dist/internal/userCache.d.ts +42 -0
  147. package/dist/internal/userCache.d.ts.map +1 -0
  148. package/dist/internal/userCache.js +51 -0
  149. package/dist/internal/userCache.js.map +1 -0
  150. package/dist/parsers/ConfluenceParser.d.ts +26 -0
  151. package/dist/parsers/ConfluenceParser.d.ts.map +1 -0
  152. package/dist/parsers/ConfluenceParser.js +792 -0
  153. package/dist/parsers/ConfluenceParser.js.map +1 -0
  154. package/dist/parsers/MarkdownParser.d.ts +26 -0
  155. package/dist/parsers/MarkdownParser.d.ts.map +1 -0
  156. package/dist/parsers/MarkdownParser.js +873 -0
  157. package/dist/parsers/MarkdownParser.js.map +1 -0
  158. package/dist/parsers/index.d.ts +8 -0
  159. package/dist/parsers/index.d.ts.map +1 -0
  160. package/dist/parsers/index.js +8 -0
  161. package/dist/parsers/index.js.map +1 -0
  162. package/dist/schemas/ConfluenceSchema.d.ts +21 -0
  163. package/dist/schemas/ConfluenceSchema.d.ts.map +1 -0
  164. package/dist/schemas/ConfluenceSchema.js +38 -0
  165. package/dist/schemas/ConfluenceSchema.js.map +1 -0
  166. package/dist/schemas/ConversionSchema.d.ts +35 -0
  167. package/dist/schemas/ConversionSchema.d.ts.map +1 -0
  168. package/dist/schemas/ConversionSchema.js +208 -0
  169. package/dist/schemas/ConversionSchema.js.map +1 -0
  170. package/dist/schemas/MarkdownSchema.d.ts +21 -0
  171. package/dist/schemas/MarkdownSchema.d.ts.map +1 -0
  172. package/dist/schemas/MarkdownSchema.js +38 -0
  173. package/dist/schemas/MarkdownSchema.js.map +1 -0
  174. package/dist/schemas/hast/HastFromHtml.d.ts +27 -0
  175. package/dist/schemas/hast/HastFromHtml.d.ts.map +1 -0
  176. package/dist/schemas/hast/HastFromHtml.js +107 -0
  177. package/dist/schemas/hast/HastFromHtml.js.map +1 -0
  178. package/dist/schemas/hast/HastSchema.d.ts +195 -0
  179. package/dist/schemas/hast/HastSchema.d.ts.map +1 -0
  180. package/dist/schemas/hast/HastSchema.js +183 -0
  181. package/dist/schemas/hast/HastSchema.js.map +1 -0
  182. package/dist/schemas/hast/index.d.ts +9 -0
  183. package/dist/schemas/hast/index.d.ts.map +1 -0
  184. package/dist/schemas/hast/index.js +3 -0
  185. package/dist/schemas/hast/index.js.map +1 -0
  186. package/dist/schemas/index.d.ts +14 -0
  187. package/dist/schemas/index.d.ts.map +1 -0
  188. package/dist/schemas/index.js +16 -0
  189. package/dist/schemas/index.js.map +1 -0
  190. package/dist/schemas/mdast/MdastFromMarkdown.d.ts +30 -0
  191. package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +1 -0
  192. package/dist/schemas/mdast/MdastFromMarkdown.js +79 -0
  193. package/dist/schemas/mdast/MdastFromMarkdown.js.map +1 -0
  194. package/dist/schemas/mdast/MdastSchema.d.ts +385 -0
  195. package/dist/schemas/mdast/MdastSchema.d.ts.map +1 -0
  196. package/dist/schemas/mdast/MdastSchema.js +266 -0
  197. package/dist/schemas/mdast/MdastSchema.js.map +1 -0
  198. package/dist/schemas/mdast/index.d.ts +10 -0
  199. package/dist/schemas/mdast/index.d.ts.map +1 -0
  200. package/dist/schemas/mdast/index.js +4 -0
  201. package/dist/schemas/mdast/index.js.map +1 -0
  202. package/dist/schemas/mdast/mdastToString.d.ts +13 -0
  203. package/dist/schemas/mdast/mdastToString.d.ts.map +1 -0
  204. package/dist/schemas/mdast/mdastToString.js +85 -0
  205. package/dist/schemas/mdast/mdastToString.js.map +1 -0
  206. package/dist/schemas/nodes/block/BlockSchema.d.ts +43 -0
  207. package/dist/schemas/nodes/block/BlockSchema.d.ts.map +1 -0
  208. package/dist/schemas/nodes/block/BlockSchema.js +634 -0
  209. package/dist/schemas/nodes/block/BlockSchema.js.map +1 -0
  210. package/dist/schemas/nodes/block/index.d.ts +7 -0
  211. package/dist/schemas/nodes/block/index.d.ts.map +1 -0
  212. package/dist/schemas/nodes/block/index.js +7 -0
  213. package/dist/schemas/nodes/block/index.js.map +1 -0
  214. package/dist/schemas/nodes/index.d.ts +9 -0
  215. package/dist/schemas/nodes/index.d.ts.map +1 -0
  216. package/dist/schemas/nodes/index.js +12 -0
  217. package/dist/schemas/nodes/index.js.map +1 -0
  218. package/dist/schemas/nodes/inline/InlineSchema.d.ts +48 -0
  219. package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +1 -0
  220. package/dist/schemas/nodes/inline/InlineSchema.js +436 -0
  221. package/dist/schemas/nodes/inline/InlineSchema.js.map +1 -0
  222. package/dist/schemas/nodes/inline/index.d.ts +7 -0
  223. package/dist/schemas/nodes/inline/index.d.ts.map +1 -0
  224. package/dist/schemas/nodes/inline/index.js +7 -0
  225. package/dist/schemas/nodes/inline/index.js.map +1 -0
  226. package/dist/schemas/nodes/macro/MacroSchema.d.ts +27 -0
  227. package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +1 -0
  228. package/dist/schemas/nodes/macro/MacroSchema.js +162 -0
  229. package/dist/schemas/nodes/macro/MacroSchema.js.map +1 -0
  230. package/dist/schemas/nodes/macro/index.d.ts +7 -0
  231. package/dist/schemas/nodes/macro/index.d.ts.map +1 -0
  232. package/dist/schemas/nodes/macro/index.js +7 -0
  233. package/dist/schemas/nodes/macro/index.js.map +1 -0
  234. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +24 -0
  235. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +1 -0
  236. package/dist/schemas/preprocessing/ConfluencePreprocessor.js +351 -0
  237. package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +1 -0
  238. package/dist/schemas/preprocessing/index.d.ts +8 -0
  239. package/dist/schemas/preprocessing/index.d.ts.map +1 -0
  240. package/dist/schemas/preprocessing/index.js +2 -0
  241. package/dist/schemas/preprocessing/index.js.map +1 -0
  242. package/dist/serializers/ConfluenceSerializer.d.ts +30 -0
  243. package/dist/serializers/ConfluenceSerializer.d.ts.map +1 -0
  244. package/dist/serializers/ConfluenceSerializer.js +551 -0
  245. package/dist/serializers/ConfluenceSerializer.js.map +1 -0
  246. package/dist/serializers/MarkdownSerializer.d.ts +34 -0
  247. package/dist/serializers/MarkdownSerializer.d.ts.map +1 -0
  248. package/dist/serializers/MarkdownSerializer.js +355 -0
  249. package/dist/serializers/MarkdownSerializer.js.map +1 -0
  250. package/dist/serializers/index.d.ts +8 -0
  251. package/dist/serializers/index.d.ts.map +1 -0
  252. package/dist/serializers/index.js +8 -0
  253. package/dist/serializers/index.js.map +1 -0
  254. package/package.json +32 -21
  255. package/src/ConfluenceAuth.ts +581 -0
  256. package/src/ConfluenceClient.ts +230 -165
  257. package/src/ConfluenceConfig.ts +63 -7
  258. package/src/ConfluenceError.ts +110 -14
  259. package/src/GitError.ts +92 -0
  260. package/src/GitService.ts +859 -0
  261. package/src/LocalFileSystem.ts +179 -9
  262. package/src/MarkdownConverter.ts +126 -122
  263. package/src/SchemaConverterError.ts +108 -0
  264. package/src/Schemas.ts +223 -6
  265. package/src/SyncEngine.ts +745 -162
  266. package/src/ast/BlockNode.ts +425 -0
  267. package/src/ast/Document.ts +90 -0
  268. package/src/ast/InlineNode.ts +323 -0
  269. package/src/ast/MacroNode.ts +245 -0
  270. package/src/ast/index.ts +83 -0
  271. package/src/bin.ts +50 -249
  272. package/src/commands/auth.ts +117 -0
  273. package/src/commands/clone.ts +145 -0
  274. package/src/commands/delete.ts +57 -0
  275. package/src/commands/errorHandler.ts +32 -0
  276. package/src/commands/git.ts +114 -0
  277. package/src/commands/index.ts +10 -0
  278. package/src/commands/layers.ts +211 -0
  279. package/src/commands/new.ts +99 -0
  280. package/src/commands/pageTree.ts +40 -0
  281. package/src/commands/shared.ts +35 -0
  282. package/src/commands/sync.ts +129 -0
  283. package/src/index.ts +21 -1
  284. package/src/internal/NodeLayers.ts +21 -0
  285. package/src/internal/frontmatter.ts +21 -0
  286. package/src/internal/gitCommands.ts +229 -0
  287. package/src/internal/hashUtils.ts +65 -3
  288. package/src/internal/oauthServer.ts +199 -0
  289. package/src/internal/pathUtils.ts +34 -17
  290. package/src/internal/tokenStorage.ts +240 -0
  291. package/src/internal/userCache.ts +90 -0
  292. package/src/parsers/ConfluenceParser.ts +950 -0
  293. package/src/parsers/MarkdownParser.ts +1198 -0
  294. package/src/parsers/index.ts +8 -0
  295. package/src/schemas/ConfluenceSchema.ts +56 -0
  296. package/src/schemas/ConversionSchema.ts +318 -0
  297. package/src/schemas/MarkdownSchema.ts +56 -0
  298. package/src/schemas/hast/HastFromHtml.ts +153 -0
  299. package/src/schemas/hast/HastSchema.ts +274 -0
  300. package/src/schemas/hast/index.ts +35 -0
  301. package/src/schemas/index.ts +20 -0
  302. package/src/schemas/mdast/MdastFromMarkdown.ts +118 -0
  303. package/src/schemas/mdast/MdastSchema.ts +566 -0
  304. package/src/schemas/mdast/index.ts +59 -0
  305. package/src/schemas/mdast/mdastToString.ts +102 -0
  306. package/src/schemas/nodes/block/BlockSchema.ts +773 -0
  307. package/src/schemas/nodes/block/index.ts +13 -0
  308. package/src/schemas/nodes/index.ts +20 -0
  309. package/src/schemas/nodes/inline/InlineSchema.ts +523 -0
  310. package/src/schemas/nodes/inline/index.ts +14 -0
  311. package/src/schemas/nodes/macro/MacroSchema.ts +226 -0
  312. package/src/schemas/nodes/macro/index.ts +6 -0
  313. package/src/schemas/preprocessing/ConfluencePreprocessor.ts +446 -0
  314. package/src/schemas/preprocessing/index.ts +8 -0
  315. package/src/serializers/ConfluenceSerializer.ts +717 -0
  316. package/src/serializers/MarkdownSerializer.ts +493 -0
  317. package/src/serializers/index.ts +8 -0
  318. package/test/GitService.test.ts +209 -0
  319. package/test/MarkdownConverter.test.ts +37 -3
  320. package/test/Schemas.test.ts +97 -2
  321. package/test/ast/BlockNode.test.ts +265 -0
  322. package/test/ast/Document.test.ts +126 -0
  323. package/test/ast/InlineNode.test.ts +161 -0
  324. package/test/fixtures/integration-test.html.fixture +103 -0
  325. package/test/fixtures/integration-test.md.expected +257 -0
  326. package/test/integration.test.ts +269 -0
  327. package/test/oauthServer.test.ts +50 -0
  328. package/test/parsers/ConfluenceParser.test.ts +283 -0
  329. package/test/schemas/ConfluencePreprocessor.test.ts +180 -0
  330. package/test/schemas/ConversionSchema.test.ts +159 -0
  331. package/test/schemas/HastSchema.test.ts +138 -0
  332. package/test/schemas/MdastSchema.test.ts +145 -0
  333. package/test/schemas/nodes/block/BlockSchema.test.ts +173 -0
  334. package/test/schemas/nodes/inline/InlineSchema.test.ts +198 -0
  335. package/test/schemas/nodes/macro/MacroSchema.test.ts +142 -0
  336. package/test/tokenStorage.test.ts +99 -0
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * Confluence REST API v2 client service.
3
3
  *
4
+ * Wraps @knpkv/confluence-api-client with rate limit retry logic and pagination helpers.
5
+ *
4
6
  * @module
5
7
  */
6
- import * as HttpClient from "@effect/platform/HttpClient"
7
- import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
8
+ import { ConfluenceApiClient, ConfluenceApiConfig, type FetchClientError, toEffect } from "@knpkv/confluence-api-client"
8
9
  import * as Context from "effect/Context"
9
10
  import * as Effect from "effect/Effect"
10
11
  import * as Layer from "effect/Layer"
12
+ import * as Redacted from "effect/Redacted"
11
13
  import * as Schedule from "effect/Schedule"
12
- import * as Schema from "effect/Schema"
13
14
  import type { PageId } from "./Brand.js"
14
- import { ApiError, RateLimitError } from "./ConfluenceError.js"
15
- import type { PageChildrenResponse, PageListItem, PageResponse } from "./Schemas.js"
16
- import { PageChildrenResponseSchema, PageResponseSchema } from "./Schemas.js"
15
+ import type { RateLimitError } from "./ConfluenceError.js"
16
+ import { ApiError } from "./ConfluenceError.js"
17
+ import type { AtlassianUser, PageChildrenResponse, PageListItem, PageResponse, PageVersion } from "./Schemas.js"
17
18
 
18
19
  /**
19
20
  * Request to create a new page.
@@ -100,6 +101,30 @@ export class ConfluenceClient extends Context.Tag(
100
101
  * Delete a page.
101
102
  */
102
103
  readonly deletePage: (id: PageId) => Effect.Effect<void, ApiError | RateLimitError>
104
+
105
+ /**
106
+ * Get version history for a page.
107
+ */
108
+ readonly getPageVersions: (
109
+ id: PageId,
110
+ options?: { since?: number; includeBody?: boolean }
111
+ ) => Effect.Effect<ReadonlyArray<PageVersion>, ApiError | RateLimitError>
112
+
113
+ /**
114
+ * Get user info by account ID.
115
+ */
116
+ readonly getUser: (accountId: string) => Effect.Effect<AtlassianUser, ApiError | RateLimitError>
117
+
118
+ /**
119
+ * Get space ID for a page.
120
+ */
121
+ readonly getSpaceId: (pageId: PageId) => Effect.Effect<string, ApiError | RateLimitError>
122
+
123
+ /**
124
+ * Set editor version for a page (v1 or v2).
125
+ * Uses V2 page properties API.
126
+ */
127
+ readonly setEditorVersion: (pageId: PageId, version: "v1" | "v2") => Effect.Effect<void, ApiError | RateLimitError>
103
128
  }
104
129
  >() {}
105
130
 
@@ -117,9 +142,16 @@ export interface ConfluenceClientConfig {
117
142
  } | {
118
143
  readonly type: "oauth2"
119
144
  readonly accessToken: string
145
+ readonly cloudId: string
120
146
  }
121
147
  }
122
148
 
149
+ /** Maximum pagination iterations to prevent infinite loops */
150
+ const MAX_PAGINATION_ITERATIONS = 100
151
+
152
+ /** Default page size for version fetching */
153
+ const VERSIONS_PAGE_SIZE = 50
154
+
123
155
  /**
124
156
  * Rate limit retry schedule with exponential backoff.
125
157
  */
@@ -129,216 +161,247 @@ const rateLimitSchedule = Schedule.exponential("1 second").pipe(
129
161
  Schedule.intersect(Schedule.recurs(3))
130
162
  )
131
163
 
164
+ /**
165
+ * Map API client errors to domain errors.
166
+ */
167
+ const mapApiError = (error: FetchClientError, endpoint: string, pageId?: string): ApiError =>
168
+ new ApiError({
169
+ status: error.status,
170
+ message: error.message,
171
+ endpoint,
172
+ ...(pageId !== undefined && { pageId })
173
+ })
174
+
132
175
  /**
133
176
  * Create the Confluence client service.
134
177
  */
135
178
  const make = (
136
179
  config: ConfluenceClientConfig
137
- ): Effect.Effect<Context.Tag.Service<typeof ConfluenceClient>, never, HttpClient.HttpClient> =>
180
+ ): Effect.Effect<Context.Tag.Service<typeof ConfluenceClient>> =>
138
181
  Effect.gen(function*() {
139
- const httpClient = yield* HttpClient.HttpClient
140
-
141
- const authHeader = config.auth.type === "token"
142
- ? `Basic ${Buffer.from(`${config.auth.email}:${config.auth.token}`).toString("base64")}`
143
- : `Bearer ${config.auth.accessToken}`
182
+ // Create underlying API client
183
+ const apiConfigLayer = Layer.succeed(ConfluenceApiConfig, {
184
+ baseUrl: config.baseUrl,
185
+ auth: config.auth.type === "token"
186
+ ? { type: "basic", email: config.auth.email, apiToken: Redacted.make(config.auth.token) }
187
+ : { type: "oauth2", accessToken: Redacted.make(config.auth.accessToken), cloudId: config.auth.cloudId }
188
+ })
144
189
 
145
- const baseRequest = HttpClientRequest.get(`${config.baseUrl}/wiki/api/v2`).pipe(
146
- HttpClientRequest.setHeader("Authorization", authHeader),
147
- HttpClientRequest.setHeader("Accept", "application/json"),
148
- HttpClientRequest.setHeader("Content-Type", "application/json")
190
+ const apiClient = yield* ConfluenceApiClient.pipe(
191
+ Effect.provide(ConfluenceApiClient.layer),
192
+ Effect.provide(apiConfigLayer)
149
193
  )
150
194
 
151
- /**
152
- * Make an HTTP request to the Confluence API.
153
- * Returns raw JSON - callers must validate with Schema.decodeUnknown.
154
- */
155
- const request = (
156
- method: "GET" | "POST" | "PUT" | "DELETE",
157
- path: string,
158
- body?: unknown
159
- ): Effect.Effect<unknown, ApiError | RateLimitError, never> =>
160
- Effect.gen(function*() {
161
- let req = baseRequest.pipe(
162
- HttpClientRequest.setMethod(method),
163
- HttpClientRequest.setUrl(`${config.baseUrl}/wiki/api/v2${path}`)
164
- )
165
-
166
- if (body !== undefined) {
167
- req = HttpClientRequest.bodyJson(req, body).pipe(
168
- Effect.catchAll(() => Effect.succeed(req)),
169
- Effect.runSync
170
- )
171
- }
172
-
173
- const response = yield* httpClient.execute(req).pipe(
174
- Effect.mapError((error) =>
175
- new ApiError({
176
- status: 0,
177
- message: `Request failed: ${error.message}`,
178
- endpoint: path
179
- })
180
- )
181
- )
182
-
183
- if (response.status === 429) {
184
- const retryAfterHeader = response.headers["retry-after"]
185
- const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : undefined
186
- return yield* Effect.fail(
187
- retryAfter !== undefined
188
- ? new RateLimitError({ retryAfter })
189
- : new RateLimitError({})
190
- )
191
- }
192
-
193
- if (response.status >= 400) {
194
- const text = yield* response.text.pipe(
195
- Effect.catchAll(() => Effect.succeed(""))
196
- )
197
- return yield* Effect.fail(
198
- new ApiError({
199
- status: response.status,
200
- message: text || `HTTP ${response.status}`,
201
- endpoint: path
202
- })
203
- )
204
- }
205
-
206
- if (method === "DELETE" && response.status === 204) {
207
- return undefined
208
- }
209
-
210
- const json = yield* response.json.pipe(
211
- Effect.mapError((error) =>
212
- new ApiError({
213
- status: response.status,
214
- message: `Failed to parse response: ${error}`,
215
- endpoint: path
216
- })
217
- )
218
- )
219
-
220
- return json
221
- }).pipe(
222
- Effect.retry(rateLimitSchedule)
223
- )
224
-
225
195
  const getPage = (id: PageId): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
226
- Effect.gen(function*() {
227
- const raw = yield* request(
228
- "GET",
229
- `/pages/${id}?body-format=storage`
230
- )
231
- return yield* Schema.decodeUnknown(PageResponseSchema)(raw).pipe(
232
- Effect.mapError((error) =>
233
- new ApiError({
234
- status: 0,
235
- message: `Invalid response schema: ${error.message}`,
236
- endpoint: `/pages/${id}`,
237
- pageId: id
238
- })
239
- )
240
- )
241
- })
196
+ toEffect(apiClient.v2.client.GET("/pages/{id}", {
197
+ params: { path: { id: Number(id) }, query: { "body-format": "storage" } }
198
+ })).pipe(
199
+ Effect.mapError((e) => mapApiError(e, `/pages/${id}`, id)),
200
+ Effect.retry(rateLimitSchedule)
201
+ ) as Effect.Effect<PageResponse, ApiError | RateLimitError>
242
202
 
243
203
  const getChildren = (id: PageId): Effect.Effect<PageChildrenResponse, ApiError | RateLimitError> =>
244
- Effect.gen(function*() {
245
- const raw = yield* request(
246
- "GET",
247
- `/pages/${id}/children?body-format=storage`
248
- )
249
- return yield* Schema.decodeUnknown(PageChildrenResponseSchema)(raw).pipe(
250
- Effect.mapError((error) =>
251
- new ApiError({
252
- status: 0,
253
- message: `Invalid response schema: ${error.message}`,
254
- endpoint: `/pages/${id}/children`,
255
- pageId: id
256
- })
257
- )
258
- )
259
- })
204
+ toEffect(apiClient.v2.client.GET("/pages/{id}/children", {
205
+ params: { path: { id: Number(id) } }
206
+ })).pipe(
207
+ Effect.mapError((e) => mapApiError(e, `/pages/${id}/children`, id)),
208
+ Effect.retry(rateLimitSchedule)
209
+ ) as Effect.Effect<PageChildrenResponse, ApiError | RateLimitError>
260
210
 
261
211
  const getAllChildren = (id: PageId): Effect.Effect<ReadonlyArray<PageListItem>, ApiError | RateLimitError> =>
262
212
  Effect.gen(function*() {
263
213
  const allChildren: Array<PageListItem> = []
264
214
  let cursor: string | undefined
265
215
  let iterations = 0
266
- const maxIterations = 100 // Prevent unbounded pagination
267
216
 
268
217
  do {
269
- if (iterations >= maxIterations) {
218
+ if (iterations >= MAX_PAGINATION_ITERATIONS) {
270
219
  return yield* Effect.fail(
271
220
  new ApiError({
272
221
  status: 0,
273
- message: `Pagination limit exceeded: more than ${maxIterations} pages of children`,
222
+ message: `Pagination limit exceeded: more than ${MAX_PAGINATION_ITERATIONS} pages of children`,
274
223
  endpoint: `/pages/${id}/children`,
275
224
  pageId: id
276
225
  })
277
226
  )
278
227
  }
279
228
 
280
- const path = cursor
281
- ? `/pages/${id}/children?body-format=storage&cursor=${cursor}`
282
- : `/pages/${id}/children?body-format=storage`
229
+ const response = yield* toEffect(apiClient.v2.client.GET("/pages/{id}/children", {
230
+ params: { path: { id: Number(id) }, query: { ...(cursor ? { cursor } : {}) } }
231
+ })).pipe(
232
+ Effect.mapError((e) => mapApiError(e, `/pages/${id}/children`, id)),
233
+ Effect.retry(rateLimitSchedule)
234
+ )
235
+
236
+ for (const child of (response as { results?: Array<PageListItem> }).results ?? []) {
237
+ allChildren.push(child)
238
+ }
239
+
240
+ cursor = (response as { _links?: { next?: string } })._links?.next
241
+ ? new URL((response as { _links: { next: string } })._links.next, config.baseUrl).searchParams.get(
242
+ "cursor"
243
+ ) ?? undefined
244
+ : undefined
245
+
246
+ iterations++
247
+ } while (cursor)
248
+
249
+ return allChildren
250
+ })
251
+
252
+ const createPage = (req: CreatePageRequest): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
253
+ toEffect(apiClient.v2.client.POST("/pages", {
254
+ body: {
255
+ spaceId: req.spaceId,
256
+ title: req.title,
257
+ ...(req.parentId ? { parentId: req.parentId } : {}),
258
+ body: { representation: req.body.representation, value: req.body.value },
259
+ status: "current"
260
+ }
261
+ })).pipe(
262
+ Effect.mapError((e) => mapApiError(e, "/pages")),
263
+ Effect.retry(rateLimitSchedule)
264
+ ) as Effect.Effect<PageResponse, ApiError | RateLimitError>
265
+
266
+ const updatePage = (req: UpdatePageRequest): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
267
+ toEffect(apiClient.v2.client.PUT("/pages/{id}", {
268
+ params: { path: { id: Number(req.id) } },
269
+ body: {
270
+ id: req.id,
271
+ title: req.title,
272
+ status: req.status ?? "current",
273
+ body: { representation: req.body.representation, value: req.body.value },
274
+ version: { number: req.version.number, ...(req.version.message ? { message: req.version.message } : {}) }
275
+ }
276
+ })).pipe(
277
+ Effect.mapError((e) => mapApiError(e, `/pages/${req.id}`, req.id)),
278
+ Effect.retry(rateLimitSchedule)
279
+ ) as Effect.Effect<PageResponse, ApiError | RateLimitError>
280
+
281
+ const deletePage = (id: PageId): Effect.Effect<void, ApiError | RateLimitError> =>
282
+ toEffect(apiClient.v2.client.DELETE("/pages/{id}", {
283
+ params: { path: { id: Number(id) } }
284
+ })).pipe(
285
+ Effect.map(() => void 0),
286
+ Effect.mapError((e) => mapApiError(e, `/pages/${id}`, id)),
287
+ Effect.retry(rateLimitSchedule)
288
+ )
289
+
290
+ const getPageVersions = (
291
+ id: PageId,
292
+ options?: { since?: number; includeBody?: boolean }
293
+ ): Effect.Effect<ReadonlyArray<PageVersion>, ApiError | RateLimitError> =>
294
+ Effect.gen(function*() {
295
+ const allVersions: Array<PageVersion> = []
296
+ let cursor: string | undefined
297
+ let iterations = 0
283
298
 
284
- const raw = yield* request("GET", path)
285
- const response = yield* Schema.decodeUnknown(PageChildrenResponseSchema)(raw).pipe(
286
- Effect.mapError((error) =>
299
+ do {
300
+ if (iterations >= MAX_PAGINATION_ITERATIONS) {
301
+ return yield* Effect.fail(
287
302
  new ApiError({
288
303
  status: 0,
289
- message: `Invalid response schema: ${error.message}`,
290
- endpoint: path,
304
+ message: `Pagination limit exceeded: more than ${MAX_PAGINATION_ITERATIONS} pages of versions`,
305
+ endpoint: `/pages/${id}/versions`,
291
306
  pageId: id
292
307
  })
293
308
  )
309
+ }
310
+
311
+ const response = yield* toEffect(apiClient.v2.client.GET("/pages/{id}/versions", {
312
+ params: {
313
+ path: { id: Number(id) },
314
+ query: {
315
+ ...(options?.includeBody ? { "body-format": "storage" as const } : {}),
316
+ ...(cursor ? { cursor } : {}),
317
+ limit: VERSIONS_PAGE_SIZE
318
+ }
319
+ }
320
+ })).pipe(
321
+ Effect.mapError((e) => mapApiError(e, `/pages/${id}/versions`, id)),
322
+ Effect.retry(rateLimitSchedule)
294
323
  )
295
324
 
296
- for (const child of response.results) {
297
- allChildren.push(child)
325
+ for (const version of (response as { results?: Array<PageVersion> }).results ?? []) {
326
+ if (options?.since === undefined || (version.number ?? 0) > options.since) {
327
+ allVersions.push(version)
328
+ }
298
329
  }
299
330
 
300
- // Extract cursor from next link if present
301
- cursor = response._links?.next
302
- ? new URL(response._links.next, config.baseUrl).searchParams.get("cursor") ?? undefined
331
+ cursor = (response as { _links?: { next?: string } })._links?.next
332
+ ? new URL((response as { _links: { next: string } })._links.next, config.baseUrl).searchParams.get(
333
+ "cursor"
334
+ ) ?? undefined
303
335
  : undefined
304
336
 
305
337
  iterations++
306
338
  } while (cursor)
307
339
 
308
- return allChildren
340
+ return allVersions
309
341
  })
310
342
 
311
- const createPage = (req: CreatePageRequest): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
343
+ const getUser = (accountId: string): Effect.Effect<AtlassianUser, ApiError | RateLimitError> =>
344
+ toEffect(apiClient.v1.client.GET("/wiki/rest/api/user", {
345
+ params: { query: { accountId } }
346
+ })).pipe(
347
+ Effect.mapError((e) => mapApiError(e, `/user?accountId=${accountId}`)),
348
+ Effect.retry(rateLimitSchedule)
349
+ ) as Effect.Effect<AtlassianUser, ApiError | RateLimitError>
350
+
351
+ const getSpaceId = (pageId: PageId): Effect.Effect<string, ApiError | RateLimitError> =>
312
352
  Effect.gen(function*() {
313
- const raw = yield* request("POST", "/pages", req)
314
- return yield* Schema.decodeUnknown(PageResponseSchema)(raw).pipe(
315
- Effect.mapError((error) =>
353
+ const page = yield* getPage(pageId)
354
+ if (!page.spaceId) {
355
+ return yield* Effect.fail(
316
356
  new ApiError({
317
357
  status: 0,
318
- message: `Invalid response schema: ${error.message}`,
319
- endpoint: "/pages"
358
+ message: `Page ${pageId} does not have spaceId`,
359
+ endpoint: `/pages/${pageId}`,
360
+ pageId
320
361
  })
321
362
  )
322
- )
363
+ }
364
+ return page.spaceId
323
365
  })
324
366
 
325
- const updatePage = (req: UpdatePageRequest): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
367
+ const setEditorVersion = (pageId: PageId, version: "v1" | "v2"): Effect.Effect<void, ApiError | RateLimitError> =>
326
368
  Effect.gen(function*() {
327
- const raw = yield* request("PUT", `/pages/${req.id}`, req)
328
- return yield* Schema.decodeUnknown(PageResponseSchema)(raw).pipe(
329
- Effect.mapError((error) =>
330
- new ApiError({
331
- status: 0,
332
- message: `Invalid response schema: ${error.message}`,
333
- endpoint: `/pages/${req.id}`,
334
- pageId: req.id
335
- })
336
- )
369
+ // Try to get existing property by key, treat 404 as "not exists"
370
+ const existing = yield* toEffect(apiClient.v2.client.GET("/pages/{page-id}/properties", {
371
+ params: { path: { "page-id": Number(pageId) }, query: { key: "editor" } }
372
+ })).pipe(
373
+ Effect.map((resp) => {
374
+ const results = (resp as { results?: Array<{ id?: string; version?: { number?: number } }> }).results
375
+ return results?.[0]
376
+ }),
377
+ Effect.catchIf(
378
+ (e: FetchClientError) => e.status === 404,
379
+ () => Effect.succeed(undefined)
380
+ ),
381
+ Effect.mapError((e) => mapApiError(e, `/pages/${pageId}/properties?key=editor`, pageId))
337
382
  )
338
- })
339
383
 
340
- const deletePage = (id: PageId): Effect.Effect<void, ApiError | RateLimitError> =>
341
- request("DELETE", `/pages/${id}`).pipe(Effect.asVoid)
384
+ if (existing?.id) {
385
+ // Update existing property
386
+ const nextVersion = (existing.version?.number ?? 0) + 1
387
+ yield* toEffect(apiClient.v2.client.PUT("/pages/{page-id}/properties/{property-id}", {
388
+ params: {
389
+ path: { "page-id": Number(pageId), "property-id": Number(existing.id) }
390
+ },
391
+ body: { key: "editor", value: version, version: { number: nextVersion } }
392
+ })).pipe(
393
+ Effect.mapError((e) => mapApiError(e, `/pages/${pageId}/properties/editor`, pageId))
394
+ )
395
+ } else {
396
+ // Create new property
397
+ yield* toEffect(apiClient.v2.client.POST("/pages/{page-id}/properties", {
398
+ params: { path: { "page-id": Number(pageId) } },
399
+ body: { key: "editor", value: version }
400
+ })).pipe(
401
+ Effect.mapError((e) => mapApiError(e, `/pages/${pageId}/properties/editor`, pageId))
402
+ )
403
+ }
404
+ }).pipe(Effect.retry(rateLimitSchedule))
342
405
 
343
406
  return ConfluenceClient.of({
344
407
  getPage,
@@ -346,7 +409,11 @@ const make = (
346
409
  getAllChildren,
347
410
  createPage,
348
411
  updatePage,
349
- deletePage
412
+ deletePage,
413
+ getPageVersions,
414
+ getUser,
415
+ getSpaceId,
416
+ setEditorVersion
350
417
  })
351
418
  })
352
419
 
@@ -356,7 +423,6 @@ const make = (
356
423
  * @example
357
424
  * ```typescript
358
425
  * import { ConfluenceClient } from "@knpkv/confluence-to-markdown/ConfluenceClient"
359
- * import { NodeHttpClient } from "@effect/platform-node"
360
426
  * import { Effect } from "effect"
361
427
  *
362
428
  * const program = Effect.gen(function* () {
@@ -374,8 +440,7 @@ const make = (
374
440
  * email: "you@example.com",
375
441
  * token: process.env.CONFLUENCE_API_KEY
376
442
  * }
377
- * })),
378
- * Effect.provide(NodeHttpClient.layer)
443
+ * }))
379
444
  * )
380
445
  * )
381
446
  * ```
@@ -384,4 +449,4 @@ const make = (
384
449
  */
385
450
  export const layer = (
386
451
  config: ConfluenceClientConfig
387
- ): Layer.Layer<ConfluenceClient, never, HttpClient.HttpClient> => Layer.effect(ConfluenceClient, make(config))
452
+ ): Layer.Layer<ConfluenceClient> => Layer.effect(ConfluenceClient, make(config))
@@ -46,13 +46,22 @@ export class ConfluenceConfig extends Context.Tag(
46
46
  readonly docsPath: string
47
47
  /** Glob patterns to exclude */
48
48
  readonly excludePatterns: ReadonlyArray<string>
49
+ /** Save original Confluence HTML alongside markdown */
50
+ readonly saveSource: boolean
51
+ /** Glob patterns for files to track in git */
52
+ readonly trackedPaths: ReadonlyArray<string>
49
53
  }
50
54
  >() {}
51
55
 
56
+ /**
57
+ * Default config directory.
58
+ */
59
+ const CONFIG_DIR = ".confluence"
60
+
52
61
  /**
53
62
  * Default config file name.
54
63
  */
55
- const CONFIG_FILE_NAME = ".confluence.json"
64
+ const CONFIG_FILE_NAME = "config.json"
56
65
 
57
66
  /**
58
67
  * Load configuration from a file.
@@ -108,6 +117,35 @@ const loadConfig = (
108
117
  *
109
118
  * @category Layers
110
119
  */
120
+ /**
121
+ * Validate docsPath configuration.
122
+ * docsPath must be either:
123
+ * - ".confluence/docs" (default, files in git repo)
124
+ * - A path outside ".confluence/" (external docs directory)
125
+ * NOT: ".confluence/other" which would cause confusion
126
+ */
127
+ const validateDocsPath = (
128
+ docsPath: string,
129
+ configPath: string
130
+ ): Effect.Effect<void, ConfigParseError> => {
131
+ const isDefault = docsPath === ".confluence/docs"
132
+ const isInsideConfluence = docsPath.startsWith(".confluence/") || docsPath.startsWith(".confluence\\")
133
+ const isOutside = !isInsideConfluence
134
+
135
+ if (isDefault || isOutside) {
136
+ return Effect.void
137
+ }
138
+
139
+ // docsPath is inside .confluence/ but not the default - this is invalid
140
+ return Effect.fail(
141
+ new ConfigParseError({
142
+ path: configPath,
143
+ cause:
144
+ `Invalid docsPath "${docsPath}". Must be either ".confluence/docs" (default) or a path outside ".confluence/" (e.g., "docs" for external docs).`
145
+ })
146
+ )
147
+ }
148
+
111
149
  export const layer = (
112
150
  configPath?: string
113
151
  ): Layer.Layer<ConfluenceConfig, ConfigNotFoundError | ConfigParseError, FileSystem.FileSystem | Path.Path> =>
@@ -115,15 +153,20 @@ export const layer = (
115
153
  ConfluenceConfig,
116
154
  Effect.gen(function*() {
117
155
  const path = yield* Path.Path
118
- const resolvedPath = configPath ?? path.join(process.cwd(), CONFIG_FILE_NAME)
156
+ const resolvedPath = configPath ?? path.join(process.cwd(), CONFIG_DIR, CONFIG_FILE_NAME)
119
157
  const config = yield* loadConfig(resolvedPath)
120
158
 
159
+ // Validate docsPath
160
+ yield* validateDocsPath(config.docsPath, resolvedPath)
161
+
121
162
  return ConfluenceConfig.of({
122
163
  rootPageId: config.rootPageId,
123
164
  baseUrl: config.baseUrl,
124
165
  ...(config.spaceKey !== undefined ? { spaceKey: config.spaceKey } : {}),
125
166
  docsPath: config.docsPath,
126
- excludePatterns: config.excludePatterns
167
+ excludePatterns: config.excludePatterns,
168
+ saveSource: config.saveSource,
169
+ trackedPaths: config.trackedPaths
127
170
  })
128
171
  })
129
172
  )
@@ -143,7 +186,9 @@ export const layerFromValues = (
143
186
  baseUrl: config.baseUrl,
144
187
  ...(config.spaceKey !== undefined ? { spaceKey: config.spaceKey } : {}),
145
188
  docsPath: config.docsPath,
146
- excludePatterns: config.excludePatterns
189
+ excludePatterns: config.excludePatterns,
190
+ saveSource: config.saveSource,
191
+ trackedPaths: config.trackedPaths
147
192
  })
148
193
  )
149
194
 
@@ -161,13 +206,16 @@ export const createConfigFile = (
161
206
  const fs = yield* FileSystem.FileSystem
162
207
  const pathService = yield* Path.Path
163
208
 
164
- const resolvedPath = configPath ?? pathService.join(process.cwd(), CONFIG_FILE_NAME)
209
+ const configDir = pathService.join(process.cwd(), CONFIG_DIR)
210
+ const resolvedPath = configPath ?? pathService.join(configDir, CONFIG_FILE_NAME)
165
211
 
166
212
  const config: ConfluenceConfigFile = {
167
213
  rootPageId: rootPageId as PageId,
168
214
  baseUrl,
169
- docsPath: ".docs/confluence",
170
- excludePatterns: []
215
+ docsPath: ".confluence/docs",
216
+ excludePatterns: [],
217
+ saveSource: false,
218
+ trackedPaths: ["**/*.md"]
171
219
  }
172
220
 
173
221
  // Validate the config
@@ -175,6 +223,14 @@ export const createConfigFile = (
175
223
  Effect.mapError((cause) => new ConfigParseError({ path: resolvedPath, cause }))
176
224
  )
177
225
 
226
+ // Create .confluence directory if it doesn't exist
227
+ const dirExists = yield* fs.exists(configDir).pipe(Effect.catchAll(() => Effect.succeed(false)))
228
+ if (!dirExists) {
229
+ yield* fs.makeDirectory(configDir, { recursive: true }).pipe(
230
+ Effect.mapError((cause) => new ConfigParseError({ path: configDir, cause }))
231
+ )
232
+ }
233
+
178
234
  const content = JSON.stringify(config, null, 2)
179
235
  yield* fs.writeFileString(resolvedPath, content).pipe(
180
236
  Effect.mapError((cause) => new ConfigParseError({ path: resolvedPath, cause }))