@knpkv/confluence-to-markdown 0.2.0 → 0.4.1

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 +60 -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 +356 -0
  7. package/dist/ConfluenceAuth.js.map +1 -0
  8. package/dist/ConfluenceClient.d.ts +26 -2
  9. package/dist/ConfluenceClient.d.ts.map +1 -1
  10. package/dist/ConfluenceClient.js +98 -92
  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 +89 -6
  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 +27 -16
  255. package/src/ConfluenceAuth.ts +571 -0
  256. package/src/ConfluenceClient.ts +188 -156
  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,26 @@
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
8
  import * as HttpClient from "@effect/platform/HttpClient"
7
- import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
9
+ import {
10
+ ConfluenceApiClient,
11
+ ConfluenceApiConfig,
12
+ type V1ApiError,
13
+ type V2ApiError
14
+ } from "@knpkv/confluence-api-client"
8
15
  import * as Context from "effect/Context"
9
16
  import * as Effect from "effect/Effect"
10
17
  import * as Layer from "effect/Layer"
18
+ import * as Redacted from "effect/Redacted"
11
19
  import * as Schedule from "effect/Schedule"
12
- import * as Schema from "effect/Schema"
13
20
  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"
21
+ import type { RateLimitError } from "./ConfluenceError.js"
22
+ import { ApiError } from "./ConfluenceError.js"
23
+ import type { AtlassianUser, PageChildrenResponse, PageListItem, PageResponse, PageVersion } from "./Schemas.js"
17
24
 
18
25
  /**
19
26
  * Request to create a new page.
@@ -100,6 +107,30 @@ export class ConfluenceClient extends Context.Tag(
100
107
  * Delete a page.
101
108
  */
102
109
  readonly deletePage: (id: PageId) => Effect.Effect<void, ApiError | RateLimitError>
110
+
111
+ /**
112
+ * Get version history for a page.
113
+ */
114
+ readonly getPageVersions: (
115
+ id: PageId,
116
+ options?: { since?: number; includeBody?: boolean }
117
+ ) => Effect.Effect<ReadonlyArray<PageVersion>, ApiError | RateLimitError>
118
+
119
+ /**
120
+ * Get user info by account ID.
121
+ */
122
+ readonly getUser: (accountId: string) => Effect.Effect<AtlassianUser, ApiError | RateLimitError>
123
+
124
+ /**
125
+ * Get space ID for a page.
126
+ */
127
+ readonly getSpaceId: (pageId: PageId) => Effect.Effect<string, ApiError | RateLimitError>
128
+
129
+ /**
130
+ * Set editor version for a page (v1 or v2).
131
+ * Uses V1 API to set page property.
132
+ */
133
+ readonly setEditorVersion: (pageId: PageId, version: "v1" | "v2") => Effect.Effect<void, ApiError | RateLimitError>
103
134
  }
104
135
  >() {}
105
136
 
@@ -117,9 +148,16 @@ export interface ConfluenceClientConfig {
117
148
  } | {
118
149
  readonly type: "oauth2"
119
150
  readonly accessToken: string
151
+ readonly cloudId: string
120
152
  }
121
153
  }
122
154
 
155
+ /** Maximum pagination iterations to prevent infinite loops */
156
+ const MAX_PAGINATION_ITERATIONS = 100
157
+
158
+ /** Default page size for version fetching */
159
+ const VERSIONS_PAGE_SIZE = 50
160
+
123
161
  /**
124
162
  * Rate limit retry schedule with exponential backoff.
125
163
  */
@@ -129,6 +167,17 @@ const rateLimitSchedule = Schedule.exponential("1 second").pipe(
129
167
  Schedule.intersect(Schedule.recurs(3))
130
168
  )
131
169
 
170
+ /**
171
+ * Map API client errors to domain errors.
172
+ */
173
+ const mapApiError = (error: V1ApiError | V2ApiError, endpoint: string, pageId?: string): ApiError =>
174
+ new ApiError({
175
+ status: error.status,
176
+ message: error.message,
177
+ endpoint,
178
+ ...(pageId !== undefined && { pageId })
179
+ })
180
+
132
181
  /**
133
182
  * Create the Confluence client service.
134
183
  */
@@ -136,168 +185,128 @@ const make = (
136
185
  config: ConfluenceClientConfig
137
186
  ): Effect.Effect<Context.Tag.Service<typeof ConfluenceClient>, never, HttpClient.HttpClient> =>
138
187
  Effect.gen(function*() {
139
- const httpClient = yield* HttpClient.HttpClient
188
+ // Create underlying API client
189
+ const apiConfigLayer = Layer.succeed(ConfluenceApiConfig, {
190
+ baseUrl: config.baseUrl,
191
+ auth: config.auth.type === "token"
192
+ ? { type: "basic", email: config.auth.email, apiToken: Redacted.make(config.auth.token) }
193
+ : { type: "oauth2", accessToken: Redacted.make(config.auth.accessToken), cloudId: config.auth.cloudId }
194
+ })
140
195
 
141
- const authHeader = config.auth.type === "token"
142
- ? `Basic ${Buffer.from(`${config.auth.email}:${config.auth.token}`).toString("base64")}`
143
- : `Bearer ${config.auth.accessToken}`
196
+ const httpClient = yield* HttpClient.HttpClient
144
197
 
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")
198
+ const apiClient = yield* ConfluenceApiClient.pipe(
199
+ Effect.provide(ConfluenceApiClient.layer),
200
+ Effect.provide(apiConfigLayer),
201
+ Effect.provide(Layer.succeed(HttpClient.HttpClient, httpClient))
149
202
  )
150
203
 
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
204
  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
- })
205
+ apiClient.v2.getPageById(id, { bodyFormat: "storage" }).pipe(
206
+ Effect.mapError((e) => mapApiError(e, `/pages/${id}`, id)),
207
+ Effect.retry(rateLimitSchedule)
208
+ ) as Effect.Effect<PageResponse, ApiError | RateLimitError>
242
209
 
243
210
  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
- })
211
+ apiClient.v2.getPageChildren(id, { bodyFormat: "storage" }).pipe(
212
+ Effect.mapError((e) => mapApiError(e, `/pages/${id}/children`, id)),
213
+ Effect.retry(rateLimitSchedule)
214
+ ) as Effect.Effect<PageChildrenResponse, ApiError | RateLimitError>
260
215
 
261
216
  const getAllChildren = (id: PageId): Effect.Effect<ReadonlyArray<PageListItem>, ApiError | RateLimitError> =>
262
217
  Effect.gen(function*() {
263
218
  const allChildren: Array<PageListItem> = []
264
219
  let cursor: string | undefined
265
220
  let iterations = 0
266
- const maxIterations = 100 // Prevent unbounded pagination
267
221
 
268
222
  do {
269
- if (iterations >= maxIterations) {
223
+ if (iterations >= MAX_PAGINATION_ITERATIONS) {
270
224
  return yield* Effect.fail(
271
225
  new ApiError({
272
226
  status: 0,
273
- message: `Pagination limit exceeded: more than ${maxIterations} pages of children`,
227
+ message: `Pagination limit exceeded: more than ${MAX_PAGINATION_ITERATIONS} pages of children`,
274
228
  endpoint: `/pages/${id}/children`,
275
229
  pageId: id
276
230
  })
277
231
  )
278
232
  }
279
233
 
280
- const path = cursor
281
- ? `/pages/${id}/children?body-format=storage&cursor=${cursor}`
282
- : `/pages/${id}/children?body-format=storage`
234
+ const response = yield* apiClient.v2.getPageChildren(id, {
235
+ bodyFormat: "storage",
236
+ cursor
237
+ }).pipe(
238
+ Effect.mapError((e) => mapApiError(e, `/pages/${id}/children`, id)),
239
+ Effect.retry(rateLimitSchedule)
240
+ )
241
+
242
+ for (const child of response.results) {
243
+ allChildren.push(child as PageListItem)
244
+ }
245
+
246
+ cursor = response._links?.next
247
+ ? new URL(response._links.next, config.baseUrl).searchParams.get("cursor") ?? undefined
248
+ : undefined
249
+
250
+ iterations++
251
+ } while (cursor)
252
+
253
+ return allChildren
254
+ })
255
+
256
+ const createPage = (req: CreatePageRequest): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
257
+ apiClient.v2.createPage(req).pipe(
258
+ Effect.mapError((e) => mapApiError(e, "/pages")),
259
+ Effect.retry(rateLimitSchedule)
260
+ ) as Effect.Effect<PageResponse, ApiError | RateLimitError>
283
261
 
284
- const raw = yield* request("GET", path)
285
- const response = yield* Schema.decodeUnknown(PageChildrenResponseSchema)(raw).pipe(
286
- Effect.mapError((error) =>
262
+ const updatePage = (req: UpdatePageRequest): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
263
+ apiClient.v2.updatePage(req.id, req).pipe(
264
+ Effect.mapError((e) => mapApiError(e, `/pages/${req.id}`, req.id)),
265
+ Effect.retry(rateLimitSchedule)
266
+ ) as Effect.Effect<PageResponse, ApiError | RateLimitError>
267
+
268
+ const deletePage = (id: PageId): Effect.Effect<void, ApiError | RateLimitError> =>
269
+ apiClient.v2.deletePage(id).pipe(
270
+ Effect.mapError((e) => mapApiError(e, `/pages/${id}`, id)),
271
+ Effect.retry(rateLimitSchedule)
272
+ )
273
+
274
+ const getPageVersions = (
275
+ id: PageId,
276
+ options?: { since?: number; includeBody?: boolean }
277
+ ): Effect.Effect<ReadonlyArray<PageVersion>, ApiError | RateLimitError> =>
278
+ Effect.gen(function*() {
279
+ const allVersions: Array<PageVersion> = []
280
+ let cursor: string | undefined
281
+ let iterations = 0
282
+
283
+ do {
284
+ if (iterations >= MAX_PAGINATION_ITERATIONS) {
285
+ return yield* Effect.fail(
287
286
  new ApiError({
288
287
  status: 0,
289
- message: `Invalid response schema: ${error.message}`,
290
- endpoint: path,
288
+ message: `Pagination limit exceeded: more than ${MAX_PAGINATION_ITERATIONS} pages of versions`,
289
+ endpoint: `/pages/${id}/versions`,
291
290
  pageId: id
292
291
  })
293
292
  )
293
+ }
294
+
295
+ const response = yield* apiClient.v2.getPageVersions(id, {
296
+ bodyFormat: options?.includeBody ? "storage" : undefined,
297
+ cursor,
298
+ limit: VERSIONS_PAGE_SIZE
299
+ }).pipe(
300
+ Effect.mapError((e) => mapApiError(e, `/pages/${id}/versions`, id)),
301
+ Effect.retry(rateLimitSchedule)
294
302
  )
295
303
 
296
- for (const child of response.results) {
297
- allChildren.push(child)
304
+ for (const version of response.results) {
305
+ if (options?.since === undefined || (version.number ?? 0) > options.since) {
306
+ allVersions.push(version as PageVersion)
307
+ }
298
308
  }
299
309
 
300
- // Extract cursor from next link if present
301
310
  cursor = response._links?.next
302
311
  ? new URL(response._links.next, config.baseUrl).searchParams.get("cursor") ?? undefined
303
312
  : undefined
@@ -305,40 +314,59 @@ const make = (
305
314
  iterations++
306
315
  } while (cursor)
307
316
 
308
- return allChildren
317
+ return allVersions
309
318
  })
310
319
 
311
- const createPage = (req: CreatePageRequest): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
320
+ const getUser = (accountId: string): Effect.Effect<AtlassianUser, ApiError | RateLimitError> =>
321
+ apiClient.v1.getUser({ accountId }).pipe(
322
+ Effect.mapError((e) => mapApiError(e, `/user?accountId=${accountId}`)),
323
+ Effect.retry(rateLimitSchedule)
324
+ ) as Effect.Effect<AtlassianUser, ApiError | RateLimitError>
325
+
326
+ const getSpaceId = (pageId: PageId): Effect.Effect<string, ApiError | RateLimitError> =>
312
327
  Effect.gen(function*() {
313
- const raw = yield* request("POST", "/pages", req)
314
- return yield* Schema.decodeUnknown(PageResponseSchema)(raw).pipe(
315
- Effect.mapError((error) =>
328
+ const page = yield* getPage(pageId)
329
+ if (!page.spaceId) {
330
+ return yield* Effect.fail(
316
331
  new ApiError({
317
332
  status: 0,
318
- message: `Invalid response schema: ${error.message}`,
319
- endpoint: "/pages"
333
+ message: `Page ${pageId} does not have spaceId`,
334
+ endpoint: `/pages/${pageId}`,
335
+ pageId
320
336
  })
321
337
  )
322
- )
338
+ }
339
+ return page.spaceId
323
340
  })
324
341
 
325
- const updatePage = (req: UpdatePageRequest): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
342
+ const setEditorVersion = (pageId: PageId, version: "v1" | "v2"): Effect.Effect<void, ApiError | RateLimitError> =>
326
343
  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
- )
344
+ // Try to get current property version, only treat 404 as "not exists"
345
+ const propertyVersion = yield* apiClient.v1.getContentProperty(pageId, "editor").pipe(
346
+ Effect.map((prop) => prop.version.number + 1),
347
+ Effect.catchIf(
348
+ (e) => e.status === 404,
349
+ () => Effect.succeed(1)
350
+ ),
351
+ Effect.mapError((e) => mapApiError(e, `/content/${pageId}/property/editor`, pageId))
337
352
  )
338
- })
339
353
 
340
- const deletePage = (id: PageId): Effect.Effect<void, ApiError | RateLimitError> =>
341
- request("DELETE", `/pages/${id}`).pipe(Effect.asVoid)
354
+ const payload = {
355
+ key: "editor",
356
+ value: version,
357
+ version: { number: propertyVersion }
358
+ }
359
+
360
+ if (propertyVersion === 1) {
361
+ yield* apiClient.v1.createContentProperty(pageId, { payload }).pipe(
362
+ Effect.mapError((e) => mapApiError(e, `/content/${pageId}/property/editor`, pageId))
363
+ )
364
+ } else {
365
+ yield* apiClient.v1.updateContentProperty(pageId, "editor", { payload }).pipe(
366
+ Effect.mapError((e) => mapApiError(e, `/content/${pageId}/property/editor`, pageId))
367
+ )
368
+ }
369
+ }).pipe(Effect.retry(rateLimitSchedule))
342
370
 
343
371
  return ConfluenceClient.of({
344
372
  getPage,
@@ -346,7 +374,11 @@ const make = (
346
374
  getAllChildren,
347
375
  createPage,
348
376
  updatePage,
349
- deletePage
377
+ deletePage,
378
+ getPageVersions,
379
+ getUser,
380
+ getSpaceId,
381
+ setEditorVersion
350
382
  })
351
383
  })
352
384
 
@@ -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 }))