@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
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Integration tests for confluence-to-markdown CLI.
3
+ *
4
+ * Tests full cycle: clone -> create page -> push -> pull -> modify -> push -> re-clone -> delete -> verify
5
+ *
6
+ * Requires:
7
+ * - CONFLUENCE_BASE_URL: Confluence base URL
8
+ * - CONFLUENCE_ROOT_PAGE_ID: Test page ID
9
+ * - OAuth tokens in ~/.confluence/ or CONFLUENCE_API_KEY + CONFLUENCE_EMAIL env vars
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"
15
+ import { afterAll, beforeAll, describe, expect, it } from "vitest"
16
+
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
20
+
21
+ // Test state
22
+ interface TestState {
23
+ testDir: string
24
+ pageFile: string | null
25
+ pageSlug: string | null
26
+ pageId: string | null
27
+ }
28
+
29
+ const state: TestState = {
30
+ testDir: "",
31
+ pageFile: null,
32
+ pageSlug: null,
33
+ pageId: null
34
+ }
35
+
36
+ // === Helper Functions ===
37
+
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
+ })
44
+ }
45
+
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
+ }
57
+ }
58
+ return null
59
+ }
60
+
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
+ }
65
+
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
+ }
70
+
71
+ // === Step Functions ===
72
+
73
+ /**
74
+ * Clone pages from Confluence with full history.
75
+ */
76
+ const clonePages = (): string => {
77
+ const output = runCli(["clone", "--root-page-id", ROOT_PAGE_ID, "--base-url", BASE_URL], { timeout: 120000 })
78
+
79
+ expect(output).toContain("Cloning pages from Confluence")
80
+ 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)
84
+
85
+ return output
86
+ }
87
+
88
+ /**
89
+ * Remove .confluence directory for fresh clone.
90
+ */
91
+ const removeConfluenceDir = (): void => {
92
+ fs.rmSync(path.join(state.testDir, ".confluence"), { recursive: true, force: true })
93
+ }
94
+
95
+ /**
96
+ * Create a new page by copying template content.
97
+ */
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()
108
+ const slug = `integration-test-${timestamp}`
109
+ const file = path.join(templateDir, `${slug}.md`)
110
+
111
+ const newPageContent = `---
112
+ title: "Integration Test ${timestamp}"
113
+ ---
114
+
115
+ ${bodyContent}
116
+
117
+ ---
118
+ Created by integration test at ${new Date().toISOString()}
119
+ `
120
+ fs.writeFileSync(file, newPageContent)
121
+ expect(fs.existsSync(file)).toBe(true)
122
+
123
+ state.pageFile = file
124
+ state.pageSlug = slug
125
+
126
+ return { file, slug }
127
+ }
128
+
129
+ /**
130
+ * Commit current changes.
131
+ */
132
+ const commitChanges = (message: string): string => {
133
+ const output = runCli(["commit", "-m", message])
134
+ expect(output).toContain("Committed:")
135
+ return output
136
+ }
137
+
138
+ /**
139
+ * Push changes to Confluence.
140
+ */
141
+ const pushChanges = (): { pushed: number; created: number; deleted: number } => {
142
+ const output = runCli(["push"], { timeout: 90000 })
143
+
144
+ const pushedMatch = output.match(/Pushed:\s*(\d+)/)
145
+ const createdMatch = output.match(/Created:\s*(\d+)/)
146
+ const deletedMatch = output.match(/Deleted:\s*(\d+)/)
147
+
148
+ return {
149
+ pushed: pushedMatch ? parseInt(pushedMatch[1]!, 10) : 0,
150
+ created: createdMatch ? parseInt(createdMatch[1]!, 10) : 0,
151
+ deleted: deletedMatch ? parseInt(deletedMatch[1]!, 10) : 0
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Pull changes from Confluence.
157
+ */
158
+ const pullChanges = (): string => {
159
+ return runCli(["pull"])
160
+ }
161
+
162
+ /**
163
+ * Extract pageId from file front-matter.
164
+ */
165
+ const extractPageId = (filePath: string): string | null => {
166
+ const content = fs.readFileSync(filePath, "utf-8")
167
+ const match = content.match(/pageId:\s*["']?(\d+)/)
168
+ return match ? match[1]! : null
169
+ }
170
+
171
+ /**
172
+ * Modify page content.
173
+ */
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
+ }
178
+
179
+ /**
180
+ * Delete a local file.
181
+ */
182
+ const deleteLocalFile = (filePath: string): void => {
183
+ fs.unlinkSync(filePath)
184
+ expect(fs.existsSync(filePath)).toBe(false)
185
+ }
186
+
187
+ // === Tests ===
188
+
189
+ describe("CLI Integration - Page Creation Flow", () => {
190
+ beforeAll(() => {
191
+ state.testDir = fs.mkdtempSync(path.join(os.tmpdir(), "confluence-test-"))
192
+ })
193
+
194
+ afterAll(() => {
195
+ if (state.testDir && fs.existsSync(state.testDir)) {
196
+ fs.rmSync(state.testDir, { recursive: true, force: true })
197
+ }
198
+ })
199
+
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)
254
+
255
+ // 7. Delete page via git workflow
256
+ deleteLocalFile(reclonedFile!)
257
+ commitChanges("Delete integration test page")
258
+
259
+ const pushResult3 = pushChanges()
260
+ expect(pushResult3.deleted).toBe(1)
261
+
262
+ // 8. Verify deletion - re-clone should not include the page
263
+ removeConfluenceDir()
264
+ clonePages()
265
+
266
+ const deletedFile = findPageBySlug(slug)
267
+ expect(deletedFile).toBeNull()
268
+ })
269
+ })
@@ -0,0 +1,50 @@
1
+ import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
2
+ import * as HttpClient from "@effect/platform/HttpClient"
3
+ import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
4
+ import { describe, expect, it } from "@effect/vitest"
5
+ import * as Effect from "effect/Effect"
6
+ import * as Fiber from "effect/Fiber"
7
+ import * as Layer from "effect/Layer"
8
+ import { HttpServerFactoryLive } from "../src/internal/NodeLayers.js"
9
+ import { startCallbackServer } from "../src/internal/oauthServer.js"
10
+
11
+ const HttpClientLive = NodeHttpClient.layer
12
+
13
+ describe("oauthServer", () => {
14
+ describe("startCallbackServer", () => {
15
+ it.effect("starts server and returns port", () =>
16
+ Effect.gen(function*() {
17
+ const expectedState = "test-state-123"
18
+ const result = yield* startCallbackServer(expectedState)
19
+
20
+ expect(result.port).toBeGreaterThanOrEqual(8585)
21
+ expect(typeof result.codePromise).toBe("object")
22
+ expect(typeof result.shutdown).toBe("object")
23
+
24
+ // Clean up
25
+ yield* result.shutdown
26
+ }).pipe(Effect.provide(HttpServerFactoryLive)))
27
+
28
+ it.effect("handles successful callback with code", () =>
29
+ Effect.gen(function*() {
30
+ const expectedState = "test-state-456"
31
+ const { codePromise, port, shutdown } = yield* startCallbackServer(expectedState)
32
+
33
+ // Make callback request in background
34
+ const codeReceiver = yield* Effect.fork(codePromise)
35
+
36
+ // Simulate OAuth callback using Effect HttpClient
37
+ const client = yield* HttpClient.HttpClient
38
+ const request = HttpClientRequest.get(`http://localhost:${port}/callback`).pipe(
39
+ HttpClientRequest.setUrlParam("code", "auth_code_123"),
40
+ HttpClientRequest.setUrlParam("state", expectedState)
41
+ )
42
+ yield* client.execute(request)
43
+
44
+ const code = yield* Fiber.join(codeReceiver)
45
+ expect(code).toBe("auth_code_123")
46
+
47
+ yield* shutdown
48
+ }).pipe(Effect.provide(Layer.mergeAll(HttpServerFactoryLive, HttpClientLive))))
49
+ })
50
+ })
@@ -0,0 +1,283 @@
1
+ import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem"
2
+ import * as NodePath from "@effect/platform-node/NodePath"
3
+ import * as FileSystem from "@effect/platform/FileSystem"
4
+ import * as Path from "@effect/platform/Path"
5
+ import { describe, expect, it } from "@effect/vitest"
6
+ import * as Effect from "effect/Effect"
7
+ import * as Layer from "effect/Layer"
8
+ import { parseConfluenceHtml } from "../../src/parsers/ConfluenceParser.js"
9
+ import { parseMarkdown } from "../../src/parsers/MarkdownParser.js"
10
+ import { serializeToConfluence } from "../../src/serializers/ConfluenceSerializer.js"
11
+ import { serializeToMarkdown } from "../../src/serializers/MarkdownSerializer.js"
12
+
13
+ const PlatformLive = Layer.mergeAll(NodeFileSystem.layer, NodePath.layer)
14
+
15
+ const getFixturesDir = Effect.gen(function*() {
16
+ const path = yield* Path.Path
17
+ return path.join(import.meta.dirname, "../fixtures")
18
+ })
19
+
20
+ const readFixture = (filename: string) =>
21
+ Effect.gen(function*() {
22
+ const fs = yield* FileSystem.FileSystem
23
+ const path = yield* Path.Path
24
+ const fixturesDir = yield* getFixturesDir
25
+ return yield* fs.readFileString(path.join(fixturesDir, filename))
26
+ }).pipe(Effect.provide(PlatformLive))
27
+
28
+ describe("ConfluenceParser", () => {
29
+ describe("parseConfluenceHtml", () => {
30
+ it.effect("parses basic headings", () =>
31
+ Effect.gen(function*() {
32
+ const html = "<h1>Title</h1><h2>Subtitle</h2>"
33
+ const doc = yield* parseConfluenceHtml(html)
34
+ expect(doc.children.length).toBe(2)
35
+ expect(doc.children[0]?._tag).toBe("Heading")
36
+ }))
37
+
38
+ it.effect("parses paragraphs", () =>
39
+ Effect.gen(function*() {
40
+ const html = "<p>Hello world</p>"
41
+ const doc = yield* parseConfluenceHtml(html)
42
+ expect(doc.children.length).toBe(1)
43
+ expect(doc.children[0]?._tag).toBe("Paragraph")
44
+ }))
45
+
46
+ it.effect("parses task lists", () =>
47
+ Effect.gen(function*() {
48
+ const html = `<ac:task-list ac:task-list-id="test">
49
+ <ac:task>
50
+ <ac:task-id>1</ac:task-id>
51
+ <ac:task-uuid>uuid-1</ac:task-uuid>
52
+ <ac:task-status>incomplete</ac:task-status>
53
+ <ac:task-body><span>Task 1</span></ac:task-body>
54
+ </ac:task>
55
+ <ac:task>
56
+ <ac:task-id>2</ac:task-id>
57
+ <ac:task-uuid>uuid-2</ac:task-uuid>
58
+ <ac:task-status>complete</ac:task-status>
59
+ <ac:task-body><span>Task 2</span></ac:task-body>
60
+ </ac:task>
61
+ </ac:task-list>`
62
+ const doc = yield* parseConfluenceHtml(html)
63
+ expect(doc.children.length).toBe(1)
64
+ const taskList = doc.children[0]
65
+ expect(taskList?._tag).toBe("TaskList")
66
+ if (taskList?._tag === "TaskList") {
67
+ expect(taskList.children.length).toBe(2)
68
+ expect(taskList.children[0]?.status).toBe("incomplete")
69
+ expect(taskList.children[0]?.body[0]?._tag).toBe("Text")
70
+ expect(taskList.children[1]?.status).toBe("complete")
71
+ }
72
+ }))
73
+
74
+ it.effect("parses images with attachments", () =>
75
+ Effect.gen(function*() {
76
+ const html = `<ac:image ac:align="center" ac:width="250" ac:alt="logo">
77
+ <ri:attachment ri:filename="logo.svg"/>
78
+ </ac:image>`
79
+ const doc = yield* parseConfluenceHtml(html)
80
+ expect(doc.children.length).toBe(1)
81
+ const image = doc.children[0]
82
+ expect(image?._tag).toBe("Image")
83
+ if (image?._tag === "Image") {
84
+ expect(image.attachment?.filename).toBe("logo.svg")
85
+ expect(image.align).toBe("center")
86
+ expect(image.width).toBe(250)
87
+ expect(image.alt).toBe("logo")
88
+ }
89
+ }))
90
+
91
+ it.effect("parses emoticons", () =>
92
+ Effect.gen(function*() {
93
+ const html =
94
+ `<p><ac:emoticon ac:emoji-shortname=":grinning:" ac:emoji-id="1f600" ac:emoji-fallback="smile"/></p>`
95
+ const doc = yield* parseConfluenceHtml(html)
96
+ expect(doc.children.length).toBe(1)
97
+ const para = doc.children[0]
98
+ expect(para?._tag).toBe("Paragraph")
99
+ if (para?._tag === "Paragraph") {
100
+ const emoticon = para.children[0]
101
+ expect(emoticon?._tag).toBe("Emoticon")
102
+ if (emoticon?._tag === "Emoticon") {
103
+ expect(emoticon.shortname).toBe(":grinning:")
104
+ expect(emoticon.emojiId).toBe("1f600")
105
+ expect(emoticon.fallback).toBe("smile")
106
+ }
107
+ }
108
+ }))
109
+
110
+ it.effect("parses user mentions", () =>
111
+ Effect.gen(function*() {
112
+ const html = `<p><ac:link><ri:user ri:account-id="557058:abc123"/></ac:link></p>`
113
+ const doc = yield* parseConfluenceHtml(html)
114
+ expect(doc.children.length).toBe(1)
115
+ const para = doc.children[0]
116
+ if (para?._tag === "Paragraph") {
117
+ const mention = para.children[0]
118
+ expect(mention?._tag).toBe("UserMention")
119
+ if (mention?._tag === "UserMention") {
120
+ expect(mention.accountId).toBe("557058:abc123")
121
+ }
122
+ }
123
+ }))
124
+
125
+ it.effect("parses colored text", () =>
126
+ Effect.gen(function*() {
127
+ const html = `<p><span style="color: rgb(255,0,0);">Red text</span></p>`
128
+ const doc = yield* parseConfluenceHtml(html)
129
+ const para = doc.children[0]
130
+ if (para?._tag === "Paragraph") {
131
+ const colored = para.children[0]
132
+ expect(colored?._tag).toBe("ColoredText")
133
+ if (colored?._tag === "ColoredText") {
134
+ expect(colored.color).toBe("rgb(255,0,0)")
135
+ }
136
+ }
137
+ }))
138
+
139
+ it.effect("parses highlighted text", () =>
140
+ Effect.gen(function*() {
141
+ const html = `<p><span style="background-color: rgb(255,255,0);">Highlighted</span></p>`
142
+ const doc = yield* parseConfluenceHtml(html)
143
+ const para = doc.children[0]
144
+ if (para?._tag === "Paragraph") {
145
+ const highlight = para.children[0]
146
+ expect(highlight?._tag).toBe("Highlight")
147
+ if (highlight?._tag === "Highlight") {
148
+ expect(highlight.backgroundColor).toBe("rgb(255,255,0)")
149
+ }
150
+ }
151
+ }))
152
+
153
+ it.effect("parses paragraph with alignment", () =>
154
+ Effect.gen(function*() {
155
+ const html = `<p style="text-align: center;">Centered</p>`
156
+ const doc = yield* parseConfluenceHtml(html)
157
+ const para = doc.children[0]
158
+ expect(para?._tag).toBe("Paragraph")
159
+ if (para?._tag === "Paragraph") {
160
+ expect(para.alignment).toBe("center")
161
+ }
162
+ }))
163
+
164
+ it.effect("parses paragraph with indent", () =>
165
+ Effect.gen(function*() {
166
+ const html = `<p style="margin-left: 30px;">Indented</p>`
167
+ const doc = yield* parseConfluenceHtml(html)
168
+ const para = doc.children[0]
169
+ expect(para?._tag === "Paragraph").toBe(true)
170
+ if (para?._tag === "Paragraph") {
171
+ expect(para.indent).toBe(30)
172
+ }
173
+ }))
174
+
175
+ it.effect("parses underline, subscript, superscript", () =>
176
+ Effect.gen(function*() {
177
+ const html = `<p><u>Underline</u> <sub>Sub</sub> <sup>Sup</sup></p>`
178
+ const doc = yield* parseConfluenceHtml(html)
179
+ const para = doc.children[0]
180
+ if (para?._tag === "Paragraph") {
181
+ const [u, , sub, , sup] = para.children
182
+ expect(u?._tag).toBe("Underline")
183
+ expect(sub?._tag).toBe("Subscript")
184
+ expect(sup?._tag).toBe("Superscript")
185
+ }
186
+ }))
187
+
188
+ it.effect("parses tables with proper cell content", () =>
189
+ Effect.gen(function*() {
190
+ const html = `<table>
191
+ <thead><tr><th><p>Header 1</p></th><th><p>Header 2</p></th></tr></thead>
192
+ <tbody><tr><td><p>Cell 1</p></td><td><p>Cell 2</p></td></tr></tbody>
193
+ </table>`
194
+ const doc = yield* parseConfluenceHtml(html)
195
+ const table = doc.children[0]
196
+ expect(table?._tag).toBe("Table")
197
+ if (table?._tag === "Table") {
198
+ // Header should have 2 cells
199
+ expect(table.header?.cells.length).toBe(2)
200
+ // First header cell should have text "Header 1"
201
+ const headerCell = table.header?.cells[0]
202
+ if (headerCell) {
203
+ expect(headerCell.children[0]?._tag).toBe("Text")
204
+ if (headerCell.children[0]?._tag === "Text") {
205
+ expect(headerCell.children[0].value).toBe("Header 1")
206
+ }
207
+ }
208
+ // Body rows
209
+ expect(table.rows.length).toBe(1)
210
+ const bodyCell = table.rows[0]?.cells[0]
211
+ if (bodyCell) {
212
+ expect(bodyCell.children[0]?._tag).toBe("Text")
213
+ if (bodyCell.children[0]?._tag === "Text") {
214
+ expect(bodyCell.children[0].value).toBe("Cell 1")
215
+ }
216
+ }
217
+ }
218
+ }))
219
+ })
220
+
221
+ describe("integration test fixture", () => {
222
+ it.effect("parses and serializes integration test fixture", () =>
223
+ Effect.gen(function*() {
224
+ const html = yield* readFixture("integration-test.html.fixture")
225
+ const doc = yield* parseConfluenceHtml(html)
226
+
227
+ // Doc should have children - layout markers plus actual content
228
+ expect(doc.children.length).toBeGreaterThan(0)
229
+
230
+ // Serialize to markdown - content should be readable (not URL-encoded)
231
+ const markdown = yield* serializeToMarkdown(doc)
232
+ // Layout markers should be present as comments
233
+ expect(markdown).toContain("cf:layout-start")
234
+ expect(markdown).toContain("cf:section:")
235
+ // Content should be readable markdown, not URL-encoded
236
+ expect(markdown).toContain("# Heading 1")
237
+ expect(markdown).toContain("## Heading 2")
238
+
239
+ // Serialize to confluence - should reconstruct layout structure
240
+ const confluenceHtml = yield* serializeToConfluence(doc)
241
+ expect(confluenceHtml).toContain("<ac:layout>")
242
+ expect(confluenceHtml).toContain("<ac:layout-section")
243
+ expect(confluenceHtml).toContain("<ac:layout-cell>")
244
+ // Note: ac:task-list may have attributes, so search for the tag name
245
+ expect(confluenceHtml).toContain("ac:task-list")
246
+ expect(confluenceHtml).toContain("<ac:task-status>incomplete</ac:task-status>")
247
+ expect(confluenceHtml).toContain("<ac:task-status>complete</ac:task-status>")
248
+ expect(confluenceHtml).toContain("ac:emoticon")
249
+ expect(confluenceHtml).toContain("ri:account-id")
250
+ }))
251
+ })
252
+
253
+ describe("roundtrip HTML -> MD -> HTML", () => {
254
+ it.effect("roundtrips integration-test.html with 1-to-1 preservation", () =>
255
+ Effect.gen(function*() {
256
+ const originalHtml = yield* readFixture("integration-test.html.fixture")
257
+ const doc1 = yield* parseConfluenceHtml(originalHtml)
258
+ const md = yield* serializeToMarkdown(doc1)
259
+ const doc2 = yield* parseMarkdown(md)
260
+ const finalHtml = yield* serializeToConfluence(doc2)
261
+
262
+ // 1-to-1 roundtrip: finalHtml should EXACTLY match originalHtml
263
+ expect(finalHtml).toBe(originalHtml)
264
+ }))
265
+
266
+ it.effect("roundtrips TOC macro", () =>
267
+ Effect.gen(function*() {
268
+ const html =
269
+ `<ac:structured-macro ac:name="toc"><ac:parameter ac:name="minLevel">2</ac:parameter></ac:structured-macro>`
270
+ const doc1 = yield* parseConfluenceHtml(html)
271
+ expect(doc1.children[0]?._tag).toBe("TocMacro")
272
+
273
+ const md = yield* serializeToMarkdown(doc1)
274
+ expect(md).toContain("[[toc]]")
275
+
276
+ const doc2 = yield* parseMarkdown(md)
277
+ expect(doc2.children[0]?._tag).toBe("TocMacro")
278
+
279
+ const html2 = yield* serializeToConfluence(doc2)
280
+ expect(html2).toContain("<ac:structured-macro ac:name=\"toc\">")
281
+ }))
282
+ })
283
+ })