@knpkv/confluence-to-markdown 0.5.0 → 0.7.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 (395) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +58 -14
  3. package/dist/AdfPlaceholders.d.ts +42 -0
  4. package/dist/AdfPlaceholders.d.ts.map +1 -0
  5. package/dist/AdfPlaceholders.js +547 -0
  6. package/dist/AdfPlaceholders.js.map +1 -0
  7. package/dist/AdfSchemaValidator.d.ts +37 -0
  8. package/dist/AdfSchemaValidator.d.ts.map +1 -0
  9. package/dist/AdfSchemaValidator.js +37 -0
  10. package/dist/AdfSchemaValidator.js.map +1 -0
  11. package/dist/AdfWalker.d.ts +39 -0
  12. package/dist/AdfWalker.d.ts.map +1 -0
  13. package/dist/AdfWalker.js +527 -0
  14. package/dist/AdfWalker.js.map +1 -0
  15. package/dist/AtlaskitTransformers.d.ts +35 -0
  16. package/dist/AtlaskitTransformers.d.ts.map +1 -0
  17. package/dist/AtlaskitTransformers.js +48 -0
  18. package/dist/AtlaskitTransformers.js.map +1 -0
  19. package/dist/Brand.d.ts +6 -6
  20. package/dist/Brand.d.ts.map +1 -1
  21. package/dist/Brand.js +8 -6
  22. package/dist/Brand.js.map +1 -1
  23. package/dist/ConfluenceAuth.d.ts +4 -4
  24. package/dist/ConfluenceAuth.d.ts.map +1 -1
  25. package/dist/ConfluenceAuth.js +15 -27
  26. package/dist/ConfluenceAuth.js.map +1 -1
  27. package/dist/ConfluenceClient.d.ts +4 -4
  28. package/dist/ConfluenceClient.d.ts.map +1 -1
  29. package/dist/ConfluenceClient.js +21 -14
  30. package/dist/ConfluenceClient.js.map +1 -1
  31. package/dist/ConfluenceConfig.d.ts +3 -3
  32. package/dist/ConfluenceConfig.d.ts.map +1 -1
  33. package/dist/ConfluenceConfig.js +13 -11
  34. package/dist/ConfluenceConfig.js.map +1 -1
  35. package/dist/ConfluenceError.d.ts +56 -4
  36. package/dist/ConfluenceError.d.ts.map +1 -1
  37. package/dist/ConfluenceError.js +30 -1
  38. package/dist/ConfluenceError.js.map +1 -1
  39. package/dist/GitService.d.ts +11 -3
  40. package/dist/GitService.d.ts.map +1 -1
  41. package/dist/GitService.js +19 -27
  42. package/dist/GitService.js.map +1 -1
  43. package/dist/LocalFileSystem.d.ts +3 -3
  44. package/dist/LocalFileSystem.d.ts.map +1 -1
  45. package/dist/LocalFileSystem.js +6 -6
  46. package/dist/LocalFileSystem.js.map +1 -1
  47. package/dist/MarkdownConverter.d.ts +16 -65
  48. package/dist/MarkdownConverter.d.ts.map +1 -1
  49. package/dist/MarkdownConverter.js +64 -85
  50. package/dist/MarkdownConverter.js.map +1 -1
  51. package/dist/Schemas.d.ts +128 -141
  52. package/dist/Schemas.d.ts.map +1 -1
  53. package/dist/Schemas.js +21 -23
  54. package/dist/Schemas.js.map +1 -1
  55. package/dist/SyncEngine.d.ts +8 -5
  56. package/dist/SyncEngine.d.ts.map +1 -1
  57. package/dist/SyncEngine.js +189 -113
  58. package/dist/SyncEngine.js.map +1 -1
  59. package/dist/bin.js +23 -35
  60. package/dist/bin.js.map +1 -1
  61. package/dist/commands/auth.d.ts +2 -14
  62. package/dist/commands/auth.d.ts.map +1 -1
  63. package/dist/commands/auth.js +11 -16
  64. package/dist/commands/auth.js.map +1 -1
  65. package/dist/commands/clone.d.ts +4 -6
  66. package/dist/commands/clone.d.ts.map +1 -1
  67. package/dist/commands/clone.js +34 -32
  68. package/dist/commands/clone.js.map +1 -1
  69. package/dist/commands/delete.d.ts +2 -10
  70. package/dist/commands/delete.d.ts.map +1 -1
  71. package/dist/commands/delete.js +5 -4
  72. package/dist/commands/delete.js.map +1 -1
  73. package/dist/commands/errorHandler.d.ts +2 -1
  74. package/dist/commands/errorHandler.d.ts.map +1 -1
  75. package/dist/commands/errorHandler.js +22 -15
  76. package/dist/commands/errorHandler.js.map +1 -1
  77. package/dist/commands/fetch.d.ts +27 -0
  78. package/dist/commands/fetch.d.ts.map +1 -0
  79. package/dist/commands/fetch.js +48 -0
  80. package/dist/commands/fetch.js.map +1 -0
  81. package/dist/commands/git.d.ts +7 -10
  82. package/dist/commands/git.d.ts.map +1 -1
  83. package/dist/commands/git.js +6 -6
  84. package/dist/commands/git.js.map +1 -1
  85. package/dist/commands/index.d.ts +1 -0
  86. package/dist/commands/index.d.ts.map +1 -1
  87. package/dist/commands/index.js +1 -0
  88. package/dist/commands/index.js.map +1 -1
  89. package/dist/commands/layers.d.ts +10 -9
  90. package/dist/commands/layers.d.ts.map +1 -1
  91. package/dist/commands/layers.js +41 -30
  92. package/dist/commands/layers.js.map +1 -1
  93. package/dist/commands/new.d.ts +2 -6
  94. package/dist/commands/new.d.ts.map +1 -1
  95. package/dist/commands/new.js +5 -4
  96. package/dist/commands/new.js.map +1 -1
  97. package/dist/commands/pageInput.d.ts +19 -0
  98. package/dist/commands/pageInput.d.ts.map +1 -0
  99. package/dist/commands/pageInput.js +68 -0
  100. package/dist/commands/pageInput.js.map +1 -0
  101. package/dist/commands/root.d.ts +8 -0
  102. package/dist/commands/root.d.ts.map +1 -0
  103. package/dist/commands/root.js +29 -0
  104. package/dist/commands/root.js.map +1 -0
  105. package/dist/commands/sync.d.ts +6 -9
  106. package/dist/commands/sync.d.ts.map +1 -1
  107. package/dist/commands/sync.js +5 -6
  108. package/dist/commands/sync.js.map +1 -1
  109. package/dist/index.d.ts +3 -8
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +2 -13
  112. package/dist/index.js.map +1 -1
  113. package/dist/internal/NodeLayers.d.ts.map +1 -1
  114. package/dist/internal/NodeLayers.js +1 -2
  115. package/dist/internal/NodeLayers.js.map +1 -1
  116. package/dist/internal/adfMetadata.d.ts +30 -0
  117. package/dist/internal/adfMetadata.d.ts.map +1 -0
  118. package/dist/internal/adfMetadata.js +126 -0
  119. package/dist/internal/adfMetadata.js.map +1 -0
  120. package/dist/internal/cleanMarkdown.d.ts +5 -0
  121. package/dist/internal/cleanMarkdown.d.ts.map +1 -0
  122. package/dist/internal/cleanMarkdown.js +13 -0
  123. package/dist/internal/cleanMarkdown.js.map +1 -0
  124. package/dist/internal/frontmatter.d.ts.map +1 -1
  125. package/dist/internal/frontmatter.js +41 -8
  126. package/dist/internal/frontmatter.js.map +1 -1
  127. package/dist/internal/gitCommands.d.ts +9 -3
  128. package/dist/internal/gitCommands.d.ts.map +1 -1
  129. package/dist/internal/gitCommands.js +18 -9
  130. package/dist/internal/gitCommands.js.map +1 -1
  131. package/dist/internal/hashUtils.d.ts +1 -1
  132. package/dist/internal/hashUtils.d.ts.map +1 -1
  133. package/dist/internal/hashUtils.js +1 -1
  134. package/dist/internal/hashUtils.js.map +1 -1
  135. package/dist/internal/oauthServer.d.ts +10 -5
  136. package/dist/internal/oauthServer.d.ts.map +1 -1
  137. package/dist/internal/oauthServer.js +19 -40
  138. package/dist/internal/oauthServer.js.map +1 -1
  139. package/dist/internal/pathUtils.d.ts +1 -1
  140. package/dist/internal/pathUtils.d.ts.map +1 -1
  141. package/dist/internal/pathUtils.js +1 -1
  142. package/dist/internal/pathUtils.js.map +1 -1
  143. package/dist/internal/process.d.ts +15 -0
  144. package/dist/internal/process.d.ts.map +1 -0
  145. package/dist/internal/process.js +10 -0
  146. package/dist/internal/process.js.map +1 -0
  147. package/dist/internal/stdio.d.ts +6 -0
  148. package/dist/internal/stdio.d.ts.map +1 -0
  149. package/dist/internal/stdio.js +15 -0
  150. package/dist/internal/stdio.js.map +1 -0
  151. package/dist/internal/tokenStorage.d.ts +3 -13
  152. package/dist/internal/tokenStorage.d.ts.map +1 -1
  153. package/dist/internal/tokenStorage.js +26 -24
  154. package/dist/internal/tokenStorage.js.map +1 -1
  155. package/dist/internal/userCache.d.ts +1 -1
  156. package/dist/internal/userCache.d.ts.map +1 -1
  157. package/dist/internal/userCache.js +1 -1
  158. package/dist/internal/userCache.js.map +1 -1
  159. package/package.json +29 -20
  160. package/skills/confluence/SKILL.md +143 -0
  161. package/skills/confluence/agents/openai.yaml +4 -0
  162. package/src/AdfPlaceholders.ts +563 -0
  163. package/src/AdfSchemaValidator.ts +65 -0
  164. package/src/AdfWalker.ts +591 -0
  165. package/src/AtlaskitTransformers.ts +70 -0
  166. package/src/Brand.ts +11 -16
  167. package/src/ConfluenceAuth.ts +22 -30
  168. package/src/ConfluenceClient.ts +28 -24
  169. package/src/ConfluenceConfig.ts +14 -14
  170. package/src/ConfluenceError.ts +65 -3
  171. package/src/GitService.ts +39 -49
  172. package/src/LocalFileSystem.ts +7 -9
  173. package/src/MarkdownConverter.ts +108 -143
  174. package/src/Schemas.ts +17 -16
  175. package/src/SyncEngine.ts +272 -127
  176. package/src/atlaskit-adf-schema.d.ts +3 -0
  177. package/src/bin.ts +30 -56
  178. package/src/commands/auth.ts +21 -18
  179. package/src/commands/clone.ts +46 -38
  180. package/src/commands/delete.ts +5 -4
  181. package/src/commands/errorHandler.ts +25 -18
  182. package/src/commands/fetch.ts +90 -0
  183. package/src/commands/git.ts +6 -6
  184. package/src/commands/index.ts +1 -0
  185. package/src/commands/layers.ts +64 -37
  186. package/src/commands/new.ts +5 -4
  187. package/src/commands/pageInput.ts +103 -0
  188. package/src/commands/root.ts +59 -0
  189. package/src/commands/sync.ts +7 -6
  190. package/src/index.ts +3 -18
  191. package/src/internal/NodeLayers.ts +1 -2
  192. package/src/internal/adfMetadata.ts +145 -0
  193. package/src/internal/cleanMarkdown.ts +15 -0
  194. package/src/internal/frontmatter.ts +45 -8
  195. package/src/internal/gitCommands.ts +23 -17
  196. package/src/internal/hashUtils.ts +2 -2
  197. package/src/internal/oauthServer.ts +84 -105
  198. package/src/internal/pathUtils.ts +1 -1
  199. package/src/internal/process.ts +15 -0
  200. package/src/internal/stdio.ts +22 -0
  201. package/src/internal/tokenStorage.ts +39 -29
  202. package/src/internal/userCache.ts +2 -2
  203. package/test/AdfPlaceholders.test.ts +508 -0
  204. package/test/AdfSchemaValidator.test.ts +34 -0
  205. package/test/AdfWalker.test.ts +676 -0
  206. package/test/AtlaskitTransformers.test.ts +25 -0
  207. package/test/Brand.test.ts +11 -11
  208. package/test/GitService.test.ts +6 -2
  209. package/test/MarkdownConverter.test.ts +121 -105
  210. package/test/RoundTrip.test.ts +521 -0
  211. package/test/Schemas.test.ts +40 -40
  212. package/test/adfMetadata.test.ts +110 -0
  213. package/test/cleanMarkdown.test.ts +36 -0
  214. package/test/commandHarness.test.ts +79 -0
  215. package/test/commandHarness.ts +147 -0
  216. package/test/fetch.test.ts +61 -0
  217. package/test/frontmatter.test.ts +41 -0
  218. package/test/integration.test.ts +569 -156
  219. package/test/layers.test.ts +12 -0
  220. package/test/oauthServer.test.ts +4 -5
  221. package/test/pageInput.test.ts +83 -0
  222. package/test/tokenStorage.test.ts +17 -17
  223. package/dist/SchemaConverterError.d.ts +0 -108
  224. package/dist/SchemaConverterError.d.ts.map +0 -1
  225. package/dist/SchemaConverterError.js +0 -84
  226. package/dist/SchemaConverterError.js.map +0 -1
  227. package/dist/ast/BlockNode.d.ts +0 -453
  228. package/dist/ast/BlockNode.d.ts.map +0 -1
  229. package/dist/ast/BlockNode.js +0 -310
  230. package/dist/ast/BlockNode.js.map +0 -1
  231. package/dist/ast/Document.d.ts +0 -216
  232. package/dist/ast/Document.d.ts.map +0 -1
  233. package/dist/ast/Document.js +0 -69
  234. package/dist/ast/Document.js.map +0 -1
  235. package/dist/ast/InlineNode.d.ts +0 -477
  236. package/dist/ast/InlineNode.d.ts.map +0 -1
  237. package/dist/ast/InlineNode.js +0 -263
  238. package/dist/ast/InlineNode.js.map +0 -1
  239. package/dist/ast/MacroNode.d.ts +0 -267
  240. package/dist/ast/MacroNode.d.ts.map +0 -1
  241. package/dist/ast/MacroNode.js +0 -164
  242. package/dist/ast/MacroNode.js.map +0 -1
  243. package/dist/ast/index.d.ts +0 -10
  244. package/dist/ast/index.d.ts.map +0 -1
  245. package/dist/ast/index.js +0 -14
  246. package/dist/ast/index.js.map +0 -1
  247. package/dist/parsers/ConfluenceParser.d.ts +0 -26
  248. package/dist/parsers/ConfluenceParser.d.ts.map +0 -1
  249. package/dist/parsers/ConfluenceParser.js +0 -797
  250. package/dist/parsers/ConfluenceParser.js.map +0 -1
  251. package/dist/parsers/MarkdownParser.d.ts +0 -26
  252. package/dist/parsers/MarkdownParser.d.ts.map +0 -1
  253. package/dist/parsers/MarkdownParser.js +0 -982
  254. package/dist/parsers/MarkdownParser.js.map +0 -1
  255. package/dist/parsers/index.d.ts +0 -8
  256. package/dist/parsers/index.d.ts.map +0 -1
  257. package/dist/parsers/index.js +0 -8
  258. package/dist/parsers/index.js.map +0 -1
  259. package/dist/schemas/ConfluenceSchema.d.ts +0 -21
  260. package/dist/schemas/ConfluenceSchema.d.ts.map +0 -1
  261. package/dist/schemas/ConfluenceSchema.js +0 -38
  262. package/dist/schemas/ConfluenceSchema.js.map +0 -1
  263. package/dist/schemas/ConversionSchema.d.ts +0 -35
  264. package/dist/schemas/ConversionSchema.d.ts.map +0 -1
  265. package/dist/schemas/ConversionSchema.js +0 -208
  266. package/dist/schemas/ConversionSchema.js.map +0 -1
  267. package/dist/schemas/MarkdownSchema.d.ts +0 -21
  268. package/dist/schemas/MarkdownSchema.d.ts.map +0 -1
  269. package/dist/schemas/MarkdownSchema.js +0 -38
  270. package/dist/schemas/MarkdownSchema.js.map +0 -1
  271. package/dist/schemas/hast/HastFromHtml.d.ts +0 -27
  272. package/dist/schemas/hast/HastFromHtml.d.ts.map +0 -1
  273. package/dist/schemas/hast/HastFromHtml.js +0 -107
  274. package/dist/schemas/hast/HastFromHtml.js.map +0 -1
  275. package/dist/schemas/hast/HastSchema.d.ts +0 -195
  276. package/dist/schemas/hast/HastSchema.d.ts.map +0 -1
  277. package/dist/schemas/hast/HastSchema.js +0 -183
  278. package/dist/schemas/hast/HastSchema.js.map +0 -1
  279. package/dist/schemas/hast/index.d.ts +0 -9
  280. package/dist/schemas/hast/index.d.ts.map +0 -1
  281. package/dist/schemas/hast/index.js +0 -3
  282. package/dist/schemas/hast/index.js.map +0 -1
  283. package/dist/schemas/index.d.ts +0 -14
  284. package/dist/schemas/index.d.ts.map +0 -1
  285. package/dist/schemas/index.js +0 -16
  286. package/dist/schemas/index.js.map +0 -1
  287. package/dist/schemas/mdast/MdastFromMarkdown.d.ts +0 -30
  288. package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +0 -1
  289. package/dist/schemas/mdast/MdastFromMarkdown.js +0 -79
  290. package/dist/schemas/mdast/MdastFromMarkdown.js.map +0 -1
  291. package/dist/schemas/mdast/MdastSchema.d.ts +0 -385
  292. package/dist/schemas/mdast/MdastSchema.d.ts.map +0 -1
  293. package/dist/schemas/mdast/MdastSchema.js +0 -266
  294. package/dist/schemas/mdast/MdastSchema.js.map +0 -1
  295. package/dist/schemas/mdast/index.d.ts +0 -10
  296. package/dist/schemas/mdast/index.d.ts.map +0 -1
  297. package/dist/schemas/mdast/index.js +0 -4
  298. package/dist/schemas/mdast/index.js.map +0 -1
  299. package/dist/schemas/mdast/mdastToString.d.ts +0 -13
  300. package/dist/schemas/mdast/mdastToString.d.ts.map +0 -1
  301. package/dist/schemas/mdast/mdastToString.js +0 -85
  302. package/dist/schemas/mdast/mdastToString.js.map +0 -1
  303. package/dist/schemas/nodes/block/BlockSchema.d.ts +0 -43
  304. package/dist/schemas/nodes/block/BlockSchema.d.ts.map +0 -1
  305. package/dist/schemas/nodes/block/BlockSchema.js +0 -634
  306. package/dist/schemas/nodes/block/BlockSchema.js.map +0 -1
  307. package/dist/schemas/nodes/block/index.d.ts +0 -7
  308. package/dist/schemas/nodes/block/index.d.ts.map +0 -1
  309. package/dist/schemas/nodes/block/index.js +0 -7
  310. package/dist/schemas/nodes/block/index.js.map +0 -1
  311. package/dist/schemas/nodes/index.d.ts +0 -9
  312. package/dist/schemas/nodes/index.d.ts.map +0 -1
  313. package/dist/schemas/nodes/index.js +0 -12
  314. package/dist/schemas/nodes/index.js.map +0 -1
  315. package/dist/schemas/nodes/inline/InlineSchema.d.ts +0 -48
  316. package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +0 -1
  317. package/dist/schemas/nodes/inline/InlineSchema.js +0 -436
  318. package/dist/schemas/nodes/inline/InlineSchema.js.map +0 -1
  319. package/dist/schemas/nodes/inline/index.d.ts +0 -7
  320. package/dist/schemas/nodes/inline/index.d.ts.map +0 -1
  321. package/dist/schemas/nodes/inline/index.js +0 -7
  322. package/dist/schemas/nodes/inline/index.js.map +0 -1
  323. package/dist/schemas/nodes/macro/MacroSchema.d.ts +0 -27
  324. package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +0 -1
  325. package/dist/schemas/nodes/macro/MacroSchema.js +0 -162
  326. package/dist/schemas/nodes/macro/MacroSchema.js.map +0 -1
  327. package/dist/schemas/nodes/macro/index.d.ts +0 -7
  328. package/dist/schemas/nodes/macro/index.d.ts.map +0 -1
  329. package/dist/schemas/nodes/macro/index.js +0 -7
  330. package/dist/schemas/nodes/macro/index.js.map +0 -1
  331. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +0 -24
  332. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +0 -1
  333. package/dist/schemas/preprocessing/ConfluencePreprocessor.js +0 -359
  334. package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +0 -1
  335. package/dist/schemas/preprocessing/index.d.ts +0 -8
  336. package/dist/schemas/preprocessing/index.d.ts.map +0 -1
  337. package/dist/schemas/preprocessing/index.js +0 -2
  338. package/dist/schemas/preprocessing/index.js.map +0 -1
  339. package/dist/serializers/ConfluenceSerializer.d.ts +0 -30
  340. package/dist/serializers/ConfluenceSerializer.d.ts.map +0 -1
  341. package/dist/serializers/ConfluenceSerializer.js +0 -560
  342. package/dist/serializers/ConfluenceSerializer.js.map +0 -1
  343. package/dist/serializers/MarkdownSerializer.d.ts +0 -34
  344. package/dist/serializers/MarkdownSerializer.d.ts.map +0 -1
  345. package/dist/serializers/MarkdownSerializer.js +0 -395
  346. package/dist/serializers/MarkdownSerializer.js.map +0 -1
  347. package/dist/serializers/index.d.ts +0 -8
  348. package/dist/serializers/index.d.ts.map +0 -1
  349. package/dist/serializers/index.js +0 -8
  350. package/dist/serializers/index.js.map +0 -1
  351. package/src/SchemaConverterError.ts +0 -108
  352. package/src/ast/BlockNode.ts +0 -469
  353. package/src/ast/Document.ts +0 -90
  354. package/src/ast/InlineNode.ts +0 -323
  355. package/src/ast/MacroNode.ts +0 -245
  356. package/src/ast/index.ts +0 -83
  357. package/src/parsers/ConfluenceParser.ts +0 -956
  358. package/src/parsers/MarkdownParser.ts +0 -1338
  359. package/src/parsers/index.ts +0 -8
  360. package/src/schemas/ConfluenceSchema.ts +0 -56
  361. package/src/schemas/ConversionSchema.ts +0 -318
  362. package/src/schemas/MarkdownSchema.ts +0 -56
  363. package/src/schemas/hast/HastFromHtml.ts +0 -153
  364. package/src/schemas/hast/HastSchema.ts +0 -274
  365. package/src/schemas/hast/index.ts +0 -35
  366. package/src/schemas/index.ts +0 -20
  367. package/src/schemas/mdast/MdastFromMarkdown.ts +0 -118
  368. package/src/schemas/mdast/MdastSchema.ts +0 -566
  369. package/src/schemas/mdast/index.ts +0 -59
  370. package/src/schemas/mdast/mdastToString.ts +0 -102
  371. package/src/schemas/nodes/block/BlockSchema.ts +0 -773
  372. package/src/schemas/nodes/block/index.ts +0 -13
  373. package/src/schemas/nodes/index.ts +0 -20
  374. package/src/schemas/nodes/inline/InlineSchema.ts +0 -523
  375. package/src/schemas/nodes/inline/index.ts +0 -14
  376. package/src/schemas/nodes/macro/MacroSchema.ts +0 -226
  377. package/src/schemas/nodes/macro/index.ts +0 -6
  378. package/src/schemas/preprocessing/ConfluencePreprocessor.ts +0 -455
  379. package/src/schemas/preprocessing/index.ts +0 -8
  380. package/src/serializers/ConfluenceSerializer.ts +0 -737
  381. package/src/serializers/MarkdownSerializer.ts +0 -543
  382. package/src/serializers/index.ts +0 -8
  383. package/test/ast/BlockNode.test.ts +0 -265
  384. package/test/ast/Document.test.ts +0 -126
  385. package/test/ast/InlineNode.test.ts +0 -161
  386. package/test/fixtures/integration-test.html.fixture +0 -103
  387. package/test/fixtures/integration-test.md.expected +0 -257
  388. package/test/parsers/ConfluenceParser.test.ts +0 -452
  389. package/test/schemas/ConfluencePreprocessor.test.ts +0 -180
  390. package/test/schemas/ConversionSchema.test.ts +0 -159
  391. package/test/schemas/HastSchema.test.ts +0 -138
  392. package/test/schemas/MdastSchema.test.ts +0 -145
  393. package/test/schemas/nodes/block/BlockSchema.test.ts +0 -173
  394. package/test/schemas/nodes/inline/InlineSchema.test.ts +0 -198
  395. package/test/schemas/nodes/macro/MacroSchema.test.ts +0 -142
@@ -6,107 +6,269 @@
6
6
  * Requires:
7
7
  * - CONFLUENCE_BASE_URL: Confluence base URL
8
8
  * - CONFLUENCE_ROOT_PAGE_ID: Test page ID
9
- * - OAuth tokens in ~/.confluence/ or CONFLUENCE_API_KEY + CONFLUENCE_EMAIL env vars
9
+ * - CONFLUENCE_API_KEY + CONFLUENCE_EMAIL env vars for raw ADF verification
10
10
  */
11
- import { execFileSync } from "node:child_process"
12
- import * as fs from "node:fs"
13
- import * as os from "node:os"
14
- import * as path from "node:path"
11
+ import * as NodeServices from "@effect/platform-node/NodeServices"
12
+ import { Config, Effect, Option } from "effect"
13
+ import * as FileSystem from "effect/FileSystem"
14
+ import * as Path from "effect/Path"
15
+ import * as ChildProcess from "effect/unstable/process/ChildProcess"
16
+ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner"
15
17
  import { afterAll, beforeAll, describe, expect, it } from "vitest"
16
18
 
17
- const CLI_PATH = path.resolve(__dirname, "../dist/bin.js")
18
- const BASE_URL = process.env.CONFLUENCE_BASE_URL
19
- const ROOT_PAGE_ID = process.env.CONFLUENCE_ROOT_PAGE_ID
19
+ let CLI_PATH = ""
20
+ let BASE_URL = ""
21
+ let ROOT_PAGE_ID = ""
22
+ let CONFLUENCE_EMAIL = ""
23
+ let CONFLUENCE_API_KEY = ""
24
+ let HAS_INTEGRATION_CONFIG = false
25
+ let HAS_API_AUTH_CONFIG = false
26
+
27
+ const SHOULD_RUN_INTEGRATION = Effect.runSync(
28
+ Effect.all([
29
+ Config.option(Config.string("CONFLUENCE_BASE_URL")),
30
+ Config.option(Config.string("CONFLUENCE_ROOT_PAGE_ID")),
31
+ Config.option(Config.string("CONFLUENCE_EMAIL")),
32
+ Config.option(Config.string("CONFLUENCE_API_KEY"))
33
+ ]).pipe(
34
+ Effect.map(([baseUrl, rootPageId, email, apiKey]) =>
35
+ Option.isSome(baseUrl) && Option.isSome(rootPageId) && Option.isSome(email) && Option.isSome(apiKey)
36
+ )
37
+ )
38
+ )
20
39
 
21
40
  // Test state
22
41
  interface TestState {
23
42
  testDir: string
24
43
  pageFile: string | null
25
- pageSlug: string | null
26
44
  pageId: string | null
27
45
  }
28
46
 
29
47
  const state: TestState = {
30
48
  testDir: "",
31
49
  pageFile: null,
32
- pageSlug: null,
33
50
  pageId: null
34
51
  }
35
52
 
36
- // === Helper Functions ===
53
+ const timestampForTitle = (date: Date): string => date.toISOString().replace(/[:.]/g, "-")
54
+
55
+ const timestampLine = (label: string, date: Date): string => `${label} at ${date.toISOString()}`
56
+
57
+ const RAW_ROUND_TRIP_NODE_TYPES = [
58
+ "blockCard",
59
+ "bodiedExtension",
60
+ "date",
61
+ "decisionItem",
62
+ "decisionList",
63
+ "embedCard",
64
+ "emoji",
65
+ "expand",
66
+ "extension",
67
+ "inlineCard",
68
+ "inlineExtension",
69
+ "layoutColumn",
70
+ "layoutSection",
71
+ "nestedExpand",
72
+ "panel",
73
+ "status",
74
+ "table",
75
+ "tableCell",
76
+ "tableHeader",
77
+ "tableRow",
78
+ "taskItem",
79
+ "taskList"
80
+ ] as const
81
+
82
+ const RAW_ROUND_TRIP_MARK_TYPES = [
83
+ "alignment",
84
+ "backgroundColor",
85
+ "indentation",
86
+ "subsup",
87
+ "textColor",
88
+ "underline"
89
+ ] as const
90
+
91
+ interface RawAdfEvidence {
92
+ readonly types: Set<string>
93
+ readonly attrSignatures: Set<string>
94
+ readonly markTypes: Set<string>
95
+ readonly markSignatures: Set<string>
96
+ readonly paragraphMarkSignatures: Set<string>
97
+ readonly inlineCardUrls: Set<string>
98
+ }
37
99
 
38
- const runCli = (args: ReadonlyArray<string>, options?: { timeout?: number }): string => {
39
- return execFileSync("node", [CLI_PATH, ...args], {
40
- cwd: state.testDir,
41
- encoding: "utf-8",
42
- timeout: options?.timeout ?? 60000
43
- })
100
+ interface RawAdfSnapshot {
101
+ readonly value: string
102
+ readonly evidence: RawAdfEvidence
44
103
  }
45
104
 
46
- const findFile = (dir: string, predicate: (name: string) => boolean): string | null => {
47
- if (!fs.existsSync(dir)) return null
48
- const entries = fs.readdirSync(dir, { withFileTypes: true })
49
- for (const entry of entries) {
50
- const fullPath = path.join(dir, entry.name)
51
- if (entry.isDirectory()) {
52
- const found = findFile(fullPath, predicate)
53
- if (found) return found
54
- } else if (predicate(entry.name)) {
55
- return fullPath
56
- }
105
+ // === Helper Functions ===
106
+
107
+ const runPlatform = <A, E>(effect: Effect.Effect<A, E, NodeServices.NodeServices>): Promise<A> =>
108
+ Effect.runPromise(effect.pipe(Effect.provide(NodeServices.layer)))
109
+
110
+ const runCli = (args: ReadonlyArray<string>, options?: { timeout?: number }) =>
111
+ Effect.gen(function*() {
112
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
113
+ const command = ChildProcess.make("node", [CLI_PATH, ...args], {
114
+ cwd: state.testDir,
115
+ stderr: "inherit"
116
+ })
117
+ return yield* spawner.string(command)
118
+ }).pipe(Effect.timeout(`${options?.timeout ?? 60000} millis`))
119
+
120
+ const pathExists = (filePath: string) =>
121
+ FileSystem.FileSystem.pipe(
122
+ Effect.flatMap((fs) => fs.exists(filePath))
123
+ )
124
+
125
+ const readText = (filePath: string) =>
126
+ FileSystem.FileSystem.pipe(
127
+ Effect.flatMap((fs) => fs.readFileString(filePath))
128
+ )
129
+
130
+ const writeText = (filePath: string, content: string) =>
131
+ FileSystem.FileSystem.pipe(
132
+ Effect.flatMap((fs) => fs.writeFileString(filePath, content))
133
+ )
134
+
135
+ const removePath = (filePath: string, options?: { readonly recursive?: boolean; readonly force?: boolean }) =>
136
+ FileSystem.FileSystem.pipe(
137
+ Effect.flatMap((fs) => fs.remove(filePath, options))
138
+ )
139
+
140
+ const joinPath = (...parts: ReadonlyArray<string>) =>
141
+ Path.Path.pipe(
142
+ Effect.map((path) => path.join(...parts))
143
+ )
144
+
145
+ const dirname = (filePath: string) =>
146
+ Path.Path.pipe(
147
+ Effect.map((path) => path.dirname(filePath))
148
+ )
149
+
150
+ const initializeTestEnvironment = Effect.gen(function*() {
151
+ const fs = yield* FileSystem.FileSystem
152
+ const path = yield* Path.Path
153
+ CLI_PATH = yield* path.fromFileUrl(new URL("../dist/bin.js", import.meta.url))
154
+ const baseUrl = yield* Config.option(Config.string("CONFLUENCE_BASE_URL"))
155
+ const rootPageId = yield* Config.option(Config.string("CONFLUENCE_ROOT_PAGE_ID"))
156
+ const email = yield* Config.option(Config.string("CONFLUENCE_EMAIL"))
157
+ const apiKey = yield* Config.option(Config.string("CONFLUENCE_API_KEY"))
158
+ if (Option.isNone(baseUrl) || Option.isNone(rootPageId)) {
159
+ HAS_INTEGRATION_CONFIG = false
160
+ return
57
161
  }
58
- return null
59
- }
162
+ BASE_URL = baseUrl.value
163
+ ROOT_PAGE_ID = rootPageId.value
164
+ if (Option.isSome(email) && Option.isSome(apiKey)) {
165
+ CONFLUENCE_EMAIL = email.value
166
+ CONFLUENCE_API_KEY = apiKey.value
167
+ HAS_API_AUTH_CONFIG = true
168
+ }
169
+ HAS_INTEGRATION_CONFIG = true
170
+ state.testDir = yield* fs.makeTempDirectory({ prefix: "confluence-test-" })
171
+ })
60
172
 
61
- const findTemplate = (): string | null => {
62
- const docsDir = path.join(state.testDir, ".confluence/docs")
63
- return findFile(docsDir, (name) => name.toLowerCase().includes("template") && name.endsWith(".md"))
64
- }
173
+ const cleanupTestEnvironment = Effect.gen(function*() {
174
+ if (state.pageId !== null && HAS_API_AUTH_CONFIG) {
175
+ yield* deleteRemotePageIfPresent(state.pageId).pipe(Effect.ignore)
176
+ }
177
+ if (state.testDir === "") {
178
+ return
179
+ }
180
+ const exists = yield* pathExists(state.testDir)
181
+ if (exists) {
182
+ yield* removePath(state.testDir, { recursive: true, force: true })
183
+ }
184
+ })
65
185
 
66
- const findPageBySlug = (slug: string): string | null => {
67
- const docsDir = path.join(state.testDir, ".confluence/docs")
68
- return findFile(docsDir, (name) => name === `${slug}.md`)
69
- }
186
+ const findSeedMarkdownFile = Effect.gen(function*() {
187
+ const fs = yield* FileSystem.FileSystem
188
+ const path = yield* Path.Path
189
+ const docsDir = yield* joinPath(state.testDir, ".confluence/docs")
190
+ const exists = yield* fs.exists(docsDir)
191
+ if (!exists) return null
192
+
193
+ const entries = yield* fs.readDirectory(docsDir, { recursive: true })
194
+ const markdownFiles = entries
195
+ .filter((entry) => path.basename(entry).endsWith(".md"))
196
+ .sort((a, b) => {
197
+ const aName = path.basename(a).toLowerCase()
198
+ const bName = path.basename(b).toLowerCase()
199
+ const aIsTemplate = aName.includes("template")
200
+ const bIsTemplate = bName.includes("template")
201
+ if (aIsTemplate !== bIsTemplate) return aIsTemplate ? -1 : 1
202
+ return b.split(/[\\/]/).length - a.split(/[\\/]/).length
203
+ })
204
+
205
+ const entry = markdownFiles[0]
206
+ if (!entry) return null
207
+ return path.isAbsolute(entry) ? entry : path.join(docsDir, entry)
208
+ })
209
+
210
+ const findPageByPageId = (pageId: string) =>
211
+ Effect.gen(function*() {
212
+ const fs = yield* FileSystem.FileSystem
213
+ const path = yield* Path.Path
214
+ const docsDir = yield* joinPath(state.testDir, ".confluence/docs")
215
+ const exists = yield* fs.exists(docsDir)
216
+ if (!exists) return null
217
+
218
+ const entries = yield* fs.readDirectory(docsDir, { recursive: true })
219
+ for (const entry of entries) {
220
+ if (!path.basename(entry).endsWith(".md")) continue
221
+ const filePath = path.isAbsolute(entry) ? entry : path.join(docsDir, entry)
222
+ const content = yield* readText(filePath)
223
+ if (content.match(new RegExp(`pageId:\\s*["']?${pageId}["']?`))) {
224
+ return filePath
225
+ }
226
+ }
227
+ return null
228
+ })
70
229
 
71
230
  // === Step Functions ===
72
231
 
73
232
  /**
74
233
  * Clone pages from Confluence with full history.
75
234
  */
76
- const clonePages = (): string => {
77
- const output = runCli(["clone", "--root-page-id", ROOT_PAGE_ID, "--base-url", BASE_URL], { timeout: 120000 })
235
+ const clonePages = Effect.gen(function*() {
236
+ const output = yield* runCli(["clone", "--root-page-id", ROOT_PAGE_ID, "--base-url", BASE_URL], { timeout: 120000 })
78
237
 
79
238
  expect(output).toContain("Cloning pages from Confluence")
80
239
  expect(output).toMatch(/Cloned \d+ pages with \d+ commits/)
81
- expect(fs.existsSync(path.join(state.testDir, ".confluence"))).toBe(true)
82
- expect(fs.existsSync(path.join(state.testDir, ".confluence/config.json"))).toBe(true)
83
- expect(fs.existsSync(path.join(state.testDir, ".confluence/.git"))).toBe(true)
240
+ expect(yield* pathExists(yield* joinPath(state.testDir, ".confluence"))).toBe(true)
241
+ expect(yield* pathExists(yield* joinPath(state.testDir, ".confluence/config.json"))).toBe(true)
242
+ expect(yield* pathExists(yield* joinPath(state.testDir, ".confluence/.git"))).toBe(true)
84
243
 
85
244
  return output
86
- }
245
+ })
87
246
 
88
247
  /**
89
248
  * Remove .confluence directory for fresh clone.
90
249
  */
91
- const removeConfluenceDir = (): void => {
92
- fs.rmSync(path.join(state.testDir, ".confluence"), { recursive: true, force: true })
93
- }
250
+ const removeConfluenceDir = Effect.gen(function*() {
251
+ yield* removePath(yield* joinPath(state.testDir, ".confluence"), { recursive: true, force: true })
252
+ })
94
253
 
95
254
  /**
96
- * Create a new page by copying template content.
255
+ * Create a new page by copying content from a cloned seed page.
97
256
  */
98
- const createPageFromTemplate = (): { file: string; slug: string } => {
99
- const templatePath = findTemplate()
100
- expect(templatePath).not.toBeNull()
101
-
102
- const templateContent = fs.readFileSync(templatePath!, "utf-8")
103
- const contentMatch = templateContent.match(/^---[\s\S]*?---\s*([\s\S]*)$/)
104
- const bodyContent = contentMatch ? contentMatch[1]!.trim() : templateContent
105
-
106
- const templateDir = path.dirname(templatePath!)
107
- const timestamp = Date.now()
257
+ const createPageFromSeed = Effect.gen(function*() {
258
+ const seedPath = yield* findSeedMarkdownFile
259
+ expect(seedPath).not.toBeNull()
260
+
261
+ const seedContent = yield* readText(seedPath!)
262
+ const contentMatch = seedContent.match(/^---[\s\S]*?---\s*([\s\S]*)$/)
263
+ const bodyContent = contentMatch ? contentMatch[1]!.trim() : seedContent
264
+ const seedPageId = extractPageIdFromMarkdown(seedContent)
265
+
266
+ const templateDir = yield* dirname(seedPath!)
267
+ const createdAt = new Date()
268
+ const timestamp = timestampForTitle(createdAt)
108
269
  const slug = `integration-test-${timestamp}`
109
- const file = path.join(templateDir, `${slug}.md`)
270
+ const file = yield* joinPath(templateDir, `${slug}.md`)
271
+ const createMarker = timestampLine("Created by integration test", createdAt)
110
272
 
111
273
  const newPageContent = `---
112
274
  title: "Integration Test ${timestamp}"
@@ -115,31 +277,31 @@ title: "Integration Test ${timestamp}"
115
277
  ${bodyContent}
116
278
 
117
279
  ---
118
- Created by integration test at ${new Date().toISOString()}
280
+ ${createMarker}
119
281
  `
120
- fs.writeFileSync(file, newPageContent)
121
- expect(fs.existsSync(file)).toBe(true)
282
+ yield* writeText(file, newPageContent)
283
+ expect(yield* pathExists(file)).toBe(true)
122
284
 
123
285
  state.pageFile = file
124
- state.pageSlug = slug
125
286
 
126
- return { file, slug }
127
- }
287
+ return { file, createMarker, seedPageId, timestamp }
288
+ })
128
289
 
129
290
  /**
130
291
  * Commit current changes.
131
292
  */
132
- const commitChanges = (message: string): string => {
133
- const output = runCli(["commit", "-m", message])
134
- expect(output).toContain("Committed:")
135
- return output
136
- }
293
+ const commitChanges = (message: string) =>
294
+ Effect.gen(function*() {
295
+ const output = yield* runCli(["commit", "-m", message])
296
+ expect(output).toContain("Committed:")
297
+ return output
298
+ })
137
299
 
138
300
  /**
139
301
  * Push changes to Confluence.
140
302
  */
141
- const pushChanges = (): { pushed: number; created: number; deleted: number } => {
142
- const output = runCli(["push"], { timeout: 90000 })
303
+ const pushChanges = Effect.gen(function*() {
304
+ const output = yield* runCli(["push"], { timeout: 90000 })
143
305
 
144
306
  const pushedMatch = output.match(/Pushed:\s*(\d+)/)
145
307
  const createdMatch = output.match(/Created:\s*(\d+)/)
@@ -150,20 +312,23 @@ const pushChanges = (): { pushed: number; created: number; deleted: number } =>
150
312
  created: createdMatch ? parseInt(createdMatch[1]!, 10) : 0,
151
313
  deleted: deletedMatch ? parseInt(deletedMatch[1]!, 10) : 0
152
314
  }
153
- }
315
+ })
154
316
 
155
317
  /**
156
318
  * Pull changes from Confluence.
157
319
  */
158
- const pullChanges = (): string => {
159
- return runCli(["pull"])
160
- }
320
+ const pullChanges = runCli(["pull"])
161
321
 
162
322
  /**
163
323
  * Extract pageId from file front-matter.
164
324
  */
165
- const extractPageId = (filePath: string): string | null => {
166
- const content = fs.readFileSync(filePath, "utf-8")
325
+ const extractPageId = (filePath: string) =>
326
+ Effect.gen(function*() {
327
+ const content = yield* readText(filePath)
328
+ return extractPageIdFromMarkdown(content)
329
+ })
330
+
331
+ const extractPageIdFromMarkdown = (content: string): string | null => {
167
332
  const match = content.match(/pageId:\s*["']?(\d+)/)
168
333
  return match ? match[1]! : null
169
334
  }
@@ -171,99 +336,347 @@ const extractPageId = (filePath: string): string | null => {
171
336
  /**
172
337
  * Modify page content.
173
338
  */
174
- const modifyPage = (filePath: string, marker: string): void => {
175
- const content = fs.readFileSync(filePath, "utf-8")
176
- fs.writeFileSync(filePath, content + `\n\n${marker}\n`)
177
- }
339
+ const modifyPage = (filePath: string, marker: string) =>
340
+ Effect.gen(function*() {
341
+ const content = yield* readText(filePath)
342
+ yield* writeText(filePath, content + `\n\n${marker}\n`)
343
+ })
178
344
 
179
345
  /**
180
346
  * Delete a local file.
181
347
  */
182
- const deleteLocalFile = (filePath: string): void => {
183
- fs.unlinkSync(filePath)
184
- expect(fs.existsSync(filePath)).toBe(false)
348
+ const deleteLocalFile = (filePath: string) =>
349
+ Effect.gen(function*() {
350
+ yield* removePath(filePath)
351
+ expect(yield* pathExists(filePath)).toBe(false)
352
+ })
353
+
354
+ const adfEvidence = (adf: unknown): RawAdfEvidence => {
355
+ const evidence: RawAdfEvidence = {
356
+ types: new Set<string>(),
357
+ attrSignatures: new Set<string>(),
358
+ markTypes: new Set<string>(),
359
+ markSignatures: new Set<string>(),
360
+ paragraphMarkSignatures: new Set<string>(),
361
+ inlineCardUrls: new Set<string>()
362
+ }
363
+ const selectedMarkTypes = new Set<string>(RAW_ROUND_TRIP_MARK_TYPES)
364
+ const selectedNodeTypes = new Set<string>(RAW_ROUND_TRIP_NODE_TYPES)
365
+ const normalizeAttrs = (value: unknown): unknown => {
366
+ if (Array.isArray(value)) return value.map(normalizeAttrs)
367
+ if (value !== null && typeof value === "object") {
368
+ const entries = Object.entries(value as Record<string, unknown>)
369
+ .map(([key, v]) => [key, normalizeAttrs(v)] as const)
370
+ .filter(([key, v]) => {
371
+ if (key === "localId" || key === "macroMetadata") return false
372
+ if (key === "layout" && v === "default") return false
373
+ if (key === "macroId" && v !== null && typeof v === "object") return false
374
+ if (
375
+ (key === "macroParams" || key === "parameters") &&
376
+ v !== null &&
377
+ typeof v === "object" &&
378
+ !Array.isArray(v) &&
379
+ Object.keys(v).length === 0
380
+ ) {
381
+ return false
382
+ }
383
+ return true
384
+ })
385
+ return Object.fromEntries(entries)
386
+ }
387
+ return value
388
+ }
389
+ const stableJson = (value: unknown): string => {
390
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`
391
+ if (value !== null && typeof value === "object") {
392
+ return `{${
393
+ Object.entries(value as Record<string, unknown>)
394
+ .filter(([, v]) => v !== undefined)
395
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
396
+ .map(([k, v]) => `${JSON.stringify(k)}:${stableJson(v)}`)
397
+ .join(",")
398
+ }}`
399
+ }
400
+ return JSON.stringify(value) ?? "null"
401
+ }
402
+ const markSignature = (mark: Record<string, unknown>): string =>
403
+ `${String(mark["type"])}:${stableJson(mark["attrs"] ?? {})}`
404
+ const cardUrl = (attrs: Record<string, unknown>): string | null => {
405
+ const url = attrs["url"]
406
+ if (typeof url === "string") return url
407
+ const data = attrs["data"]
408
+ if (data !== null && typeof data === "object" && !Array.isArray(data)) {
409
+ const dataUrl = (data as Record<string, unknown>)["url"]
410
+ if (typeof dataUrl === "string") return dataUrl
411
+ }
412
+ return null
413
+ }
414
+ const walk = (node: unknown): void => {
415
+ if (node === null || typeof node !== "object") return
416
+ const record = node as Record<string, unknown>
417
+ const type = record["type"]
418
+ if (typeof type === "string") evidence.types.add(type)
419
+ const attrs = record["attrs"]
420
+ if (
421
+ typeof type === "string" &&
422
+ selectedNodeTypes.has(type) &&
423
+ attrs !== null &&
424
+ typeof attrs === "object" &&
425
+ !Array.isArray(attrs)
426
+ ) {
427
+ const normalized = normalizeAttrs(attrs)
428
+ const extensionKey = (normalized as Record<string, unknown>)["extensionKey"]
429
+ if (!(type === "extension" && extensionKey === "toc")) {
430
+ evidence.attrSignatures.add(`${type}:${stableJson(normalized)}`)
431
+ }
432
+ }
433
+ if (type === "inlineCard") {
434
+ if (attrs !== null && typeof attrs === "object" && !Array.isArray(attrs)) {
435
+ const url = cardUrl(attrs as Record<string, unknown>)
436
+ if (url !== null) evidence.inlineCardUrls.add(url)
437
+ }
438
+ }
439
+ const marks = record["marks"]
440
+ if (Array.isArray(marks)) {
441
+ for (const mark of marks) {
442
+ if (mark === null || typeof mark !== "object" || Array.isArray(mark)) continue
443
+ const markRecord = mark as Record<string, unknown>
444
+ const markType = markRecord["type"]
445
+ if (typeof markType !== "string") continue
446
+ evidence.markTypes.add(markType)
447
+ if (selectedMarkTypes.has(markType)) {
448
+ const signature = markSignature(markRecord)
449
+ evidence.markSignatures.add(signature)
450
+ if (type === "paragraph") evidence.paragraphMarkSignatures.add(signature)
451
+ }
452
+ }
453
+ }
454
+ for (const child of Object.values(record)) {
455
+ if (Array.isArray(child)) {
456
+ for (const item of child) walk(item)
457
+ } else {
458
+ walk(child)
459
+ }
460
+ }
461
+ }
462
+ walk(adf)
463
+ return evidence
185
464
  }
186
465
 
187
- // === Tests ===
466
+ const getRemoteAdfSnapshot = (pageId: string) =>
467
+ Effect.tryPromise({
468
+ try: async () => {
469
+ const response = await fetch(`${BASE_URL}/wiki/api/v2/pages/${pageId}?body-format=atlas_doc_format`, {
470
+ headers: {
471
+ Authorization: `Basic ${btoa(`${CONFLUENCE_EMAIL}:${CONFLUENCE_API_KEY}`)}`
472
+ }
473
+ })
474
+ if (!response.ok) {
475
+ throw new Error(`Confluence returned ${response.status} for page ${pageId}`)
476
+ }
477
+ const page = await response.json() as { body?: { atlas_doc_format?: { value?: string } } }
478
+ const value = page.body?.atlas_doc_format?.value ?? "{}"
479
+ const adf = JSON.parse(value) as unknown
480
+ return { value, evidence: adfEvidence(adf) }
481
+ },
482
+ catch: (cause) => cause
483
+ })
188
484
 
189
- describe("CLI Integration - Page Creation Flow", () => {
190
- beforeAll(() => {
191
- state.testDir = fs.mkdtempSync(path.join(os.tmpdir(), "confluence-test-"))
485
+ const deleteRemotePageIfPresent = (pageId: string) =>
486
+ Effect.tryPromise({
487
+ try: async () => {
488
+ const response = await fetch(`${BASE_URL}/wiki/api/v2/pages/${pageId}`, {
489
+ method: "DELETE",
490
+ headers: {
491
+ Authorization: `Basic ${btoa(`${CONFLUENCE_EMAIL}:${CONFLUENCE_API_KEY}`)}`
492
+ }
493
+ })
494
+ if (!response.ok && response.status !== 404) {
495
+ throw new Error(`Confluence returned ${response.status} while cleaning up page ${pageId}`)
496
+ }
497
+ },
498
+ catch: (cause) => cause
192
499
  })
193
500
 
194
- afterAll(() => {
195
- if (state.testDir && fs.existsSync(state.testDir)) {
196
- fs.rmSync(state.testDir, { recursive: true, force: true })
501
+ const expectNativePanelsIfPresent = (pageId: string, markdown: string) =>
502
+ Effect.gen(function*() {
503
+ if (!HAS_API_AUTH_CONFIG || !markdown.includes("adf:panel")) {
504
+ return
197
505
  }
506
+ const snapshot = yield* getRemoteAdfSnapshot(pageId)
507
+ expect(snapshot.evidence.types.has("panel")).toBe(true)
198
508
  })
199
509
 
200
- it("full cycle: clone -> create -> push -> pull -> modify -> push -> re-clone -> delete -> verify", () => {
201
- // 1. Clone pages from Confluence
202
- clonePages()
203
-
204
- // 2. Create new page from template
205
- const { file, slug } = createPageFromTemplate()
206
-
207
- // 3. Commit and push new page
208
- commitChanges("Add integration test page")
209
-
210
- const statusBefore = runCli(["status"])
211
- expect(statusBefore).toContain("Local Only:")
212
-
213
- const pushResult1 = pushChanges()
214
- expect(pushResult1.created).toBe(1)
215
-
216
- // Verify file has pageId after push
217
- const contentAfterPush = fs.readFileSync(file, "utf-8")
218
- expect(contentAfterPush).toMatch(/pageId:\s*["']?\d+/)
219
- expect(contentAfterPush).toMatch(/version:\s*\d+/)
220
- expect(contentAfterPush).toMatch(/contentHash:/)
221
-
222
- const pageId = extractPageId(file)
223
- expect(pageId).not.toBeNull()
224
- state.pageId = pageId
225
-
226
- // 4. Pull should be no-op (already in sync)
227
- const contentBeforePull = fs.readFileSync(file, "utf-8")
228
- pullChanges()
229
- const contentAfterPull = fs.readFileSync(file, "utf-8")
230
- expect(contentAfterPull).toBe(contentBeforePull)
231
-
232
- // 5. Modify page, commit, and push
233
- const modifyMarker = `Modified at ${Date.now()}`
234
- modifyPage(file, modifyMarker)
235
- commitChanges("Modify integration test page")
236
-
237
- const pushResult2 = pushChanges()
238
- expect(pushResult2.pushed).toBe(1)
239
-
240
- const contentAfterModify = fs.readFileSync(file, "utf-8")
241
- expect(contentAfterModify).toContain(modifyMarker)
242
-
243
- // 6. Remove and re-clone - verify idempotency
244
- const contentBeforeReclone = fs.readFileSync(file, "utf-8")
245
- removeConfluenceDir()
246
- clonePages()
247
-
248
- const reclonedFile = findPageBySlug(slug)
249
- expect(reclonedFile).not.toBeNull()
250
- expect(fs.existsSync(reclonedFile!)).toBe(true)
251
-
252
- const contentAfterReclone = fs.readFileSync(reclonedFile!, "utf-8")
253
- expect(contentAfterReclone).toBe(contentBeforeReclone)
510
+ const expectRawRoundTripTypes = (
511
+ before: RawAdfSnapshot,
512
+ after: RawAdfSnapshot
513
+ ) =>
514
+ Effect.sync(() => {
515
+ for (const type of RAW_ROUND_TRIP_NODE_TYPES) {
516
+ if (before.evidence.types.has(type)) {
517
+ expect(after.evidence.types.has(type), `expected raw ADF after push to preserve ${type}`).toBe(true)
518
+ }
519
+ }
520
+ for (const signature of before.evidence.attrSignatures) {
521
+ expect(after.evidence.attrSignatures.has(signature), `expected raw ADF after push to preserve attrs ${signature}`)
522
+ .toBe(true)
523
+ }
524
+ for (const mark of RAW_ROUND_TRIP_MARK_TYPES) {
525
+ if (before.evidence.markTypes.has(mark)) {
526
+ expect(after.evidence.markTypes.has(mark), `expected raw ADF after push to preserve ${mark} marks`).toBe(true)
527
+ }
528
+ }
529
+ for (const signature of before.evidence.markSignatures) {
530
+ expect(after.evidence.markSignatures.has(signature), `expected raw ADF after push to preserve mark ${signature}`)
531
+ .toBe(true)
532
+ }
533
+ for (const signature of before.evidence.paragraphMarkSignatures) {
534
+ expect(
535
+ after.evidence.paragraphMarkSignatures.has(signature),
536
+ `expected raw ADF after push to preserve paragraph mark ${signature}`
537
+ ).toBe(true)
538
+ }
539
+ for (const url of before.evidence.inlineCardUrls) {
540
+ expect(after.evidence.inlineCardUrls.has(url), `expected raw ADF after push to preserve inlineCard ${url}`).toBe(
541
+ true
542
+ )
543
+ }
544
+ })
254
545
 
255
- // 7. Delete page via git workflow
256
- deleteLocalFile(reclonedFile!)
257
- commitChanges("Delete integration test page")
546
+ const expectSidecarMetadata = (filePath: string, pageId: string, markdown: string) =>
547
+ Effect.gen(function*() {
548
+ const dir = yield* dirname(filePath)
549
+ const sidecarPath = yield* joinPath(dir, `${pageId}.adf.json`)
550
+ expect(markdown).toMatch(new RegExp(`ref=\\./${pageId}\\.adf\\.json#[A-Za-z0-9-]+`))
551
+ expect(markdown).not.toMatch(/<!--\s*adf:[^>]+(?:attrs|node|marks)=/)
552
+ expect(yield* pathExists(sidecarPath)).toBe(true)
553
+
554
+ const sidecar = JSON.parse(yield* readText(sidecarPath)) as unknown
555
+ expect(sidecar).toMatchObject({ version: 1 })
556
+
557
+ const record = sidecar !== null && typeof sidecar === "object" && !Array.isArray(sidecar)
558
+ ? sidecar as Record<string, unknown>
559
+ : {}
560
+ const entries = record["entries"] !== null && typeof record["entries"] === "object" &&
561
+ !Array.isArray(record["entries"])
562
+ ? record["entries"] as Record<string, unknown>
563
+ : {}
564
+ expect(Object.keys(entries).length).toBeGreaterThan(0)
565
+ expect(
566
+ Object.values(entries).some((entry) => {
567
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false
568
+ const value = (entry as Record<string, unknown>)["value"]
569
+ return value !== null && typeof value === "object"
570
+ })
571
+ ).toBe(true)
572
+ })
258
573
 
259
- const pushResult3 = pushChanges()
260
- expect(pushResult3.deleted).toBe(1)
574
+ // === Tests ===
261
575
 
262
- // 8. Verify deletion - re-clone should not include the page
263
- removeConfluenceDir()
264
- clonePages()
576
+ describe("CLI Integration - Page Creation Flow", () => {
577
+ beforeAll(async () => {
578
+ await runPlatform(initializeTestEnvironment)
579
+ })
265
580
 
266
- const deletedFile = findPageBySlug(slug)
267
- expect(deletedFile).toBeNull()
581
+ afterAll(async () => {
582
+ await runPlatform(cleanupTestEnvironment)
268
583
  })
584
+
585
+ it.skipIf(!SHOULD_RUN_INTEGRATION)(
586
+ "full cycle: clone -> create -> push -> pull -> modify -> push -> re-clone -> delete -> verify",
587
+ async () => {
588
+ if (!HAS_INTEGRATION_CONFIG) {
589
+ throw new Error("Confluence integration config was available at test definition but missing at setup")
590
+ }
591
+
592
+ await runPlatform(Effect.gen(function*() {
593
+ // 1. Clone pages from Confluence
594
+ yield* clonePages
595
+
596
+ // 2. Create new page from template
597
+ const { createMarker, file, seedPageId, timestamp } = yield* createPageFromSeed
598
+ const seedRawAdf = seedPageId && HAS_API_AUTH_CONFIG ? yield* getRemoteAdfSnapshot(seedPageId) : null
599
+
600
+ // 3. Commit and push new page
601
+ yield* commitChanges(`Add integration test page ${timestamp}`)
602
+
603
+ const statusBefore = yield* runCli(["status"])
604
+ expect(statusBefore).toContain("Local Only:")
605
+
606
+ const pushResult1 = yield* pushChanges
607
+ expect(pushResult1.created).toBe(1)
608
+
609
+ // Verify file has pageId after push
610
+ const contentAfterPush = yield* readText(file)
611
+ expect(contentAfterPush).toMatch(/pageId:\s*["']?\d+/)
612
+ expect(contentAfterPush).toMatch(/version:\s*\d+/)
613
+ expect(contentAfterPush).toMatch(/contentHash:/)
614
+ expect(contentAfterPush).toContain(createMarker)
615
+
616
+ const pageId = yield* extractPageId(file)
617
+ expect(pageId).not.toBeNull()
618
+ state.pageId = pageId
619
+ yield* expectSidecarMetadata(file, pageId!, contentAfterPush)
620
+ yield* expectNativePanelsIfPresent(pageId!, contentAfterPush)
621
+ const createdRawAdf = HAS_API_AUTH_CONFIG ? yield* getRemoteAdfSnapshot(pageId!) : null
622
+ if (seedRawAdf !== null && createdRawAdf !== null) {
623
+ yield* expectRawRoundTripTypes(seedRawAdf, createdRawAdf)
624
+ }
625
+
626
+ // 4. Pull should be no-op (already in sync)
627
+ const contentBeforePull = yield* readText(file)
628
+ yield* pullChanges
629
+ const contentAfterPull = yield* readText(file)
630
+ expect(contentAfterPull).toBe(contentBeforePull)
631
+
632
+ // 5. Modify page, commit, and push
633
+ const modifiedAt = new Date()
634
+ const modifyMarker = timestampLine("Modified by integration test", modifiedAt)
635
+ yield* modifyPage(file, modifyMarker)
636
+ yield* commitChanges(`Modify integration test page ${timestampForTitle(modifiedAt)}`)
637
+
638
+ const pushResult2 = yield* pushChanges
639
+ expect(pushResult2.pushed).toBe(1)
640
+
641
+ const contentAfterModify = yield* readText(file)
642
+ expect(contentAfterModify).toContain(modifyMarker)
643
+ yield* expectSidecarMetadata(file, pageId!, contentAfterModify)
644
+ yield* expectNativePanelsIfPresent(pageId!, contentAfterModify)
645
+ const modifiedRawAdf = HAS_API_AUTH_CONFIG ? yield* getRemoteAdfSnapshot(pageId!) : null
646
+ if (createdRawAdf !== null && modifiedRawAdf !== null) {
647
+ yield* expectRawRoundTripTypes(createdRawAdf, modifiedRawAdf)
648
+ }
649
+
650
+ // 6. Remove and re-clone - verify idempotency
651
+ const contentBeforeReclone = yield* readText(file)
652
+ yield* removeConfluenceDir
653
+ yield* clonePages
654
+
655
+ const reclonedFile = yield* findPageByPageId(pageId!)
656
+ expect(reclonedFile).not.toBeNull()
657
+ expect(yield* pathExists(reclonedFile!)).toBe(true)
658
+
659
+ const contentAfterReclone = yield* readText(reclonedFile!)
660
+ expect(contentAfterReclone).toBe(contentBeforeReclone)
661
+ expect(contentAfterReclone).toContain(createMarker)
662
+ expect(contentAfterReclone).toContain(modifyMarker)
663
+ yield* expectSidecarMetadata(reclonedFile!, pageId!, contentAfterReclone)
664
+
665
+ // 7. Delete page via git workflow
666
+ yield* deleteLocalFile(reclonedFile!)
667
+ yield* commitChanges(`Delete integration test page ${timestampForTitle(new Date())}`)
668
+
669
+ const pushResult3 = yield* pushChanges
670
+ expect(pushResult3.deleted).toBe(1)
671
+
672
+ // 8. Verify deletion - re-clone should not include the page
673
+ yield* removeConfluenceDir
674
+ yield* clonePages
675
+
676
+ const deletedFile = yield* findPageByPageId(pageId!)
677
+ expect(deletedFile).toBeNull()
678
+ state.pageId = null
679
+ }))
680
+ }
681
+ )
269
682
  })