@knpkv/confluence-to-markdown 0.2.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (336) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/LICENSE +21 -0
  3. package/README.md +282 -14
  4. package/dist/ConfluenceAuth.d.ts +76 -0
  5. package/dist/ConfluenceAuth.d.ts.map +1 -0
  6. package/dist/ConfluenceAuth.js +356 -0
  7. package/dist/ConfluenceAuth.js.map +1 -0
  8. package/dist/ConfluenceClient.d.ts +26 -2
  9. package/dist/ConfluenceClient.d.ts.map +1 -1
  10. package/dist/ConfluenceClient.js +98 -92
  11. package/dist/ConfluenceClient.js.map +1 -1
  12. package/dist/ConfluenceConfig.d.ts +4 -24
  13. package/dist/ConfluenceConfig.d.ts.map +1 -1
  14. package/dist/ConfluenceConfig.js +45 -7
  15. package/dist/ConfluenceConfig.js.map +1 -1
  16. package/dist/ConfluenceError.d.ts +89 -6
  17. package/dist/ConfluenceError.d.ts.map +1 -1
  18. package/dist/ConfluenceError.js +88 -5
  19. package/dist/ConfluenceError.js.map +1 -1
  20. package/dist/GitError.d.ts +103 -0
  21. package/dist/GitError.d.ts.map +1 -0
  22. package/dist/GitError.js +85 -0
  23. package/dist/GitError.js.map +1 -0
  24. package/dist/GitService.d.ts +175 -0
  25. package/dist/GitService.d.ts.map +1 -0
  26. package/dist/GitService.js +431 -0
  27. package/dist/GitService.js.map +1 -0
  28. package/dist/LocalFileSystem.d.ts +29 -4
  29. package/dist/LocalFileSystem.d.ts.map +1 -1
  30. package/dist/LocalFileSystem.js +80 -6
  31. package/dist/LocalFileSystem.js.map +1 -1
  32. package/dist/MarkdownConverter.d.ts +49 -2
  33. package/dist/MarkdownConverter.d.ts.map +1 -1
  34. package/dist/MarkdownConverter.js +73 -111
  35. package/dist/MarkdownConverter.js.map +1 -1
  36. package/dist/SchemaConverterError.d.ts +108 -0
  37. package/dist/SchemaConverterError.d.ts.map +1 -0
  38. package/dist/SchemaConverterError.js +84 -0
  39. package/dist/SchemaConverterError.js.map +1 -0
  40. package/dist/Schemas.d.ts +225 -1
  41. package/dist/Schemas.d.ts.map +1 -1
  42. package/dist/Schemas.js +155 -6
  43. package/dist/Schemas.js.map +1 -1
  44. package/dist/SyncEngine.d.ts +30 -20
  45. package/dist/SyncEngine.d.ts.map +1 -1
  46. package/dist/SyncEngine.js +566 -117
  47. package/dist/SyncEngine.js.map +1 -1
  48. package/dist/ast/BlockNode.d.ts +468 -0
  49. package/dist/ast/BlockNode.d.ts.map +1 -0
  50. package/dist/ast/BlockNode.js +319 -0
  51. package/dist/ast/BlockNode.js.map +1 -0
  52. package/dist/ast/Document.d.ts +244 -0
  53. package/dist/ast/Document.d.ts.map +1 -0
  54. package/dist/ast/Document.js +69 -0
  55. package/dist/ast/Document.js.map +1 -0
  56. package/dist/ast/InlineNode.d.ts +477 -0
  57. package/dist/ast/InlineNode.d.ts.map +1 -0
  58. package/dist/ast/InlineNode.js +263 -0
  59. package/dist/ast/InlineNode.js.map +1 -0
  60. package/dist/ast/MacroNode.d.ts +267 -0
  61. package/dist/ast/MacroNode.d.ts.map +1 -0
  62. package/dist/ast/MacroNode.js +164 -0
  63. package/dist/ast/MacroNode.js.map +1 -0
  64. package/dist/ast/index.d.ts +10 -0
  65. package/dist/ast/index.d.ts.map +1 -0
  66. package/dist/ast/index.js +14 -0
  67. package/dist/ast/index.js.map +1 -0
  68. package/dist/bin.js +33 -149
  69. package/dist/bin.js.map +1 -1
  70. package/dist/commands/auth.d.ts +15 -0
  71. package/dist/commands/auth.d.ts.map +1 -0
  72. package/dist/commands/auth.js +86 -0
  73. package/dist/commands/auth.js.map +1 -0
  74. package/dist/commands/clone.d.ts +12 -0
  75. package/dist/commands/clone.d.ts.map +1 -0
  76. package/dist/commands/clone.js +93 -0
  77. package/dist/commands/clone.js.map +1 -0
  78. package/dist/commands/delete.d.ts +13 -0
  79. package/dist/commands/delete.d.ts.map +1 -0
  80. package/dist/commands/delete.js +48 -0
  81. package/dist/commands/delete.js.map +1 -0
  82. package/dist/commands/errorHandler.d.ts +14 -0
  83. package/dist/commands/errorHandler.d.ts.map +1 -0
  84. package/dist/commands/errorHandler.js +33 -0
  85. package/dist/commands/errorHandler.js.map +1 -0
  86. package/dist/commands/git.d.ts +22 -0
  87. package/dist/commands/git.d.ts.map +1 -0
  88. package/dist/commands/git.js +72 -0
  89. package/dist/commands/git.js.map +1 -0
  90. package/dist/commands/index.d.ts +11 -0
  91. package/dist/commands/index.d.ts.map +1 -0
  92. package/dist/commands/index.js +11 -0
  93. package/dist/commands/index.js.map +1 -0
  94. package/dist/commands/layers.d.ts +31 -0
  95. package/dist/commands/layers.d.ts.map +1 -0
  96. package/dist/commands/layers.js +137 -0
  97. package/dist/commands/layers.js.map +1 -0
  98. package/dist/commands/new.d.ts +9 -0
  99. package/dist/commands/new.d.ts.map +1 -0
  100. package/dist/commands/new.js +80 -0
  101. package/dist/commands/new.js.map +1 -0
  102. package/dist/commands/pageTree.d.ts +18 -0
  103. package/dist/commands/pageTree.d.ts.map +1 -0
  104. package/dist/commands/pageTree.js +20 -0
  105. package/dist/commands/pageTree.js.map +1 -0
  106. package/dist/commands/shared.d.ts +15 -0
  107. package/dist/commands/shared.d.ts.map +1 -0
  108. package/dist/commands/shared.js +27 -0
  109. package/dist/commands/shared.js.map +1 -0
  110. package/dist/commands/sync.d.ts +15 -0
  111. package/dist/commands/sync.d.ts.map +1 -0
  112. package/dist/commands/sync.js +101 -0
  113. package/dist/commands/sync.js.map +1 -0
  114. package/dist/index.d.ts +10 -1
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/index.js +14 -0
  117. package/dist/index.js.map +1 -1
  118. package/dist/internal/NodeLayers.d.ts +7 -0
  119. package/dist/internal/NodeLayers.d.ts.map +1 -0
  120. package/dist/internal/NodeLayers.js +19 -0
  121. package/dist/internal/NodeLayers.js.map +1 -0
  122. package/dist/internal/frontmatter.d.ts +10 -0
  123. package/dist/internal/frontmatter.d.ts.map +1 -1
  124. package/dist/internal/frontmatter.js +16 -0
  125. package/dist/internal/frontmatter.js.map +1 -1
  126. package/dist/internal/gitCommands.d.ts +78 -0
  127. package/dist/internal/gitCommands.d.ts.map +1 -0
  128. package/dist/internal/gitCommands.js +156 -0
  129. package/dist/internal/gitCommands.js.map +1 -0
  130. package/dist/internal/hashUtils.d.ts +42 -1
  131. package/dist/internal/hashUtils.d.ts.map +1 -1
  132. package/dist/internal/hashUtils.js +38 -2
  133. package/dist/internal/hashUtils.js.map +1 -1
  134. package/dist/internal/oauthServer.d.ts +55 -0
  135. package/dist/internal/oauthServer.d.ts.map +1 -0
  136. package/dist/internal/oauthServer.js +110 -0
  137. package/dist/internal/oauthServer.js.map +1 -0
  138. package/dist/internal/pathUtils.d.ts +21 -4
  139. package/dist/internal/pathUtils.d.ts.map +1 -1
  140. package/dist/internal/pathUtils.js +24 -13
  141. package/dist/internal/pathUtils.js.map +1 -1
  142. package/dist/internal/tokenStorage.d.ts +75 -0
  143. package/dist/internal/tokenStorage.d.ts.map +1 -0
  144. package/dist/internal/tokenStorage.js +149 -0
  145. package/dist/internal/tokenStorage.js.map +1 -0
  146. package/dist/internal/userCache.d.ts +42 -0
  147. package/dist/internal/userCache.d.ts.map +1 -0
  148. package/dist/internal/userCache.js +51 -0
  149. package/dist/internal/userCache.js.map +1 -0
  150. package/dist/parsers/ConfluenceParser.d.ts +26 -0
  151. package/dist/parsers/ConfluenceParser.d.ts.map +1 -0
  152. package/dist/parsers/ConfluenceParser.js +792 -0
  153. package/dist/parsers/ConfluenceParser.js.map +1 -0
  154. package/dist/parsers/MarkdownParser.d.ts +26 -0
  155. package/dist/parsers/MarkdownParser.d.ts.map +1 -0
  156. package/dist/parsers/MarkdownParser.js +873 -0
  157. package/dist/parsers/MarkdownParser.js.map +1 -0
  158. package/dist/parsers/index.d.ts +8 -0
  159. package/dist/parsers/index.d.ts.map +1 -0
  160. package/dist/parsers/index.js +8 -0
  161. package/dist/parsers/index.js.map +1 -0
  162. package/dist/schemas/ConfluenceSchema.d.ts +21 -0
  163. package/dist/schemas/ConfluenceSchema.d.ts.map +1 -0
  164. package/dist/schemas/ConfluenceSchema.js +38 -0
  165. package/dist/schemas/ConfluenceSchema.js.map +1 -0
  166. package/dist/schemas/ConversionSchema.d.ts +35 -0
  167. package/dist/schemas/ConversionSchema.d.ts.map +1 -0
  168. package/dist/schemas/ConversionSchema.js +208 -0
  169. package/dist/schemas/ConversionSchema.js.map +1 -0
  170. package/dist/schemas/MarkdownSchema.d.ts +21 -0
  171. package/dist/schemas/MarkdownSchema.d.ts.map +1 -0
  172. package/dist/schemas/MarkdownSchema.js +38 -0
  173. package/dist/schemas/MarkdownSchema.js.map +1 -0
  174. package/dist/schemas/hast/HastFromHtml.d.ts +27 -0
  175. package/dist/schemas/hast/HastFromHtml.d.ts.map +1 -0
  176. package/dist/schemas/hast/HastFromHtml.js +107 -0
  177. package/dist/schemas/hast/HastFromHtml.js.map +1 -0
  178. package/dist/schemas/hast/HastSchema.d.ts +195 -0
  179. package/dist/schemas/hast/HastSchema.d.ts.map +1 -0
  180. package/dist/schemas/hast/HastSchema.js +183 -0
  181. package/dist/schemas/hast/HastSchema.js.map +1 -0
  182. package/dist/schemas/hast/index.d.ts +9 -0
  183. package/dist/schemas/hast/index.d.ts.map +1 -0
  184. package/dist/schemas/hast/index.js +3 -0
  185. package/dist/schemas/hast/index.js.map +1 -0
  186. package/dist/schemas/index.d.ts +14 -0
  187. package/dist/schemas/index.d.ts.map +1 -0
  188. package/dist/schemas/index.js +16 -0
  189. package/dist/schemas/index.js.map +1 -0
  190. package/dist/schemas/mdast/MdastFromMarkdown.d.ts +30 -0
  191. package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +1 -0
  192. package/dist/schemas/mdast/MdastFromMarkdown.js +79 -0
  193. package/dist/schemas/mdast/MdastFromMarkdown.js.map +1 -0
  194. package/dist/schemas/mdast/MdastSchema.d.ts +385 -0
  195. package/dist/schemas/mdast/MdastSchema.d.ts.map +1 -0
  196. package/dist/schemas/mdast/MdastSchema.js +266 -0
  197. package/dist/schemas/mdast/MdastSchema.js.map +1 -0
  198. package/dist/schemas/mdast/index.d.ts +10 -0
  199. package/dist/schemas/mdast/index.d.ts.map +1 -0
  200. package/dist/schemas/mdast/index.js +4 -0
  201. package/dist/schemas/mdast/index.js.map +1 -0
  202. package/dist/schemas/mdast/mdastToString.d.ts +13 -0
  203. package/dist/schemas/mdast/mdastToString.d.ts.map +1 -0
  204. package/dist/schemas/mdast/mdastToString.js +85 -0
  205. package/dist/schemas/mdast/mdastToString.js.map +1 -0
  206. package/dist/schemas/nodes/block/BlockSchema.d.ts +43 -0
  207. package/dist/schemas/nodes/block/BlockSchema.d.ts.map +1 -0
  208. package/dist/schemas/nodes/block/BlockSchema.js +634 -0
  209. package/dist/schemas/nodes/block/BlockSchema.js.map +1 -0
  210. package/dist/schemas/nodes/block/index.d.ts +7 -0
  211. package/dist/schemas/nodes/block/index.d.ts.map +1 -0
  212. package/dist/schemas/nodes/block/index.js +7 -0
  213. package/dist/schemas/nodes/block/index.js.map +1 -0
  214. package/dist/schemas/nodes/index.d.ts +9 -0
  215. package/dist/schemas/nodes/index.d.ts.map +1 -0
  216. package/dist/schemas/nodes/index.js +12 -0
  217. package/dist/schemas/nodes/index.js.map +1 -0
  218. package/dist/schemas/nodes/inline/InlineSchema.d.ts +48 -0
  219. package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +1 -0
  220. package/dist/schemas/nodes/inline/InlineSchema.js +436 -0
  221. package/dist/schemas/nodes/inline/InlineSchema.js.map +1 -0
  222. package/dist/schemas/nodes/inline/index.d.ts +7 -0
  223. package/dist/schemas/nodes/inline/index.d.ts.map +1 -0
  224. package/dist/schemas/nodes/inline/index.js +7 -0
  225. package/dist/schemas/nodes/inline/index.js.map +1 -0
  226. package/dist/schemas/nodes/macro/MacroSchema.d.ts +27 -0
  227. package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +1 -0
  228. package/dist/schemas/nodes/macro/MacroSchema.js +162 -0
  229. package/dist/schemas/nodes/macro/MacroSchema.js.map +1 -0
  230. package/dist/schemas/nodes/macro/index.d.ts +7 -0
  231. package/dist/schemas/nodes/macro/index.d.ts.map +1 -0
  232. package/dist/schemas/nodes/macro/index.js +7 -0
  233. package/dist/schemas/nodes/macro/index.js.map +1 -0
  234. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +24 -0
  235. package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +1 -0
  236. package/dist/schemas/preprocessing/ConfluencePreprocessor.js +351 -0
  237. package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +1 -0
  238. package/dist/schemas/preprocessing/index.d.ts +8 -0
  239. package/dist/schemas/preprocessing/index.d.ts.map +1 -0
  240. package/dist/schemas/preprocessing/index.js +2 -0
  241. package/dist/schemas/preprocessing/index.js.map +1 -0
  242. package/dist/serializers/ConfluenceSerializer.d.ts +30 -0
  243. package/dist/serializers/ConfluenceSerializer.d.ts.map +1 -0
  244. package/dist/serializers/ConfluenceSerializer.js +551 -0
  245. package/dist/serializers/ConfluenceSerializer.js.map +1 -0
  246. package/dist/serializers/MarkdownSerializer.d.ts +34 -0
  247. package/dist/serializers/MarkdownSerializer.d.ts.map +1 -0
  248. package/dist/serializers/MarkdownSerializer.js +355 -0
  249. package/dist/serializers/MarkdownSerializer.js.map +1 -0
  250. package/dist/serializers/index.d.ts +8 -0
  251. package/dist/serializers/index.d.ts.map +1 -0
  252. package/dist/serializers/index.js +8 -0
  253. package/dist/serializers/index.js.map +1 -0
  254. package/package.json +27 -16
  255. package/src/ConfluenceAuth.ts +571 -0
  256. package/src/ConfluenceClient.ts +188 -156
  257. package/src/ConfluenceConfig.ts +63 -7
  258. package/src/ConfluenceError.ts +110 -14
  259. package/src/GitError.ts +92 -0
  260. package/src/GitService.ts +859 -0
  261. package/src/LocalFileSystem.ts +179 -9
  262. package/src/MarkdownConverter.ts +126 -122
  263. package/src/SchemaConverterError.ts +108 -0
  264. package/src/Schemas.ts +223 -6
  265. package/src/SyncEngine.ts +745 -162
  266. package/src/ast/BlockNode.ts +425 -0
  267. package/src/ast/Document.ts +90 -0
  268. package/src/ast/InlineNode.ts +323 -0
  269. package/src/ast/MacroNode.ts +245 -0
  270. package/src/ast/index.ts +83 -0
  271. package/src/bin.ts +50 -249
  272. package/src/commands/auth.ts +117 -0
  273. package/src/commands/clone.ts +145 -0
  274. package/src/commands/delete.ts +57 -0
  275. package/src/commands/errorHandler.ts +32 -0
  276. package/src/commands/git.ts +114 -0
  277. package/src/commands/index.ts +10 -0
  278. package/src/commands/layers.ts +211 -0
  279. package/src/commands/new.ts +99 -0
  280. package/src/commands/pageTree.ts +40 -0
  281. package/src/commands/shared.ts +35 -0
  282. package/src/commands/sync.ts +129 -0
  283. package/src/index.ts +21 -1
  284. package/src/internal/NodeLayers.ts +21 -0
  285. package/src/internal/frontmatter.ts +21 -0
  286. package/src/internal/gitCommands.ts +229 -0
  287. package/src/internal/hashUtils.ts +65 -3
  288. package/src/internal/oauthServer.ts +199 -0
  289. package/src/internal/pathUtils.ts +34 -17
  290. package/src/internal/tokenStorage.ts +240 -0
  291. package/src/internal/userCache.ts +90 -0
  292. package/src/parsers/ConfluenceParser.ts +950 -0
  293. package/src/parsers/MarkdownParser.ts +1198 -0
  294. package/src/parsers/index.ts +8 -0
  295. package/src/schemas/ConfluenceSchema.ts +56 -0
  296. package/src/schemas/ConversionSchema.ts +318 -0
  297. package/src/schemas/MarkdownSchema.ts +56 -0
  298. package/src/schemas/hast/HastFromHtml.ts +153 -0
  299. package/src/schemas/hast/HastSchema.ts +274 -0
  300. package/src/schemas/hast/index.ts +35 -0
  301. package/src/schemas/index.ts +20 -0
  302. package/src/schemas/mdast/MdastFromMarkdown.ts +118 -0
  303. package/src/schemas/mdast/MdastSchema.ts +566 -0
  304. package/src/schemas/mdast/index.ts +59 -0
  305. package/src/schemas/mdast/mdastToString.ts +102 -0
  306. package/src/schemas/nodes/block/BlockSchema.ts +773 -0
  307. package/src/schemas/nodes/block/index.ts +13 -0
  308. package/src/schemas/nodes/index.ts +20 -0
  309. package/src/schemas/nodes/inline/InlineSchema.ts +523 -0
  310. package/src/schemas/nodes/inline/index.ts +14 -0
  311. package/src/schemas/nodes/macro/MacroSchema.ts +226 -0
  312. package/src/schemas/nodes/macro/index.ts +6 -0
  313. package/src/schemas/preprocessing/ConfluencePreprocessor.ts +446 -0
  314. package/src/schemas/preprocessing/index.ts +8 -0
  315. package/src/serializers/ConfluenceSerializer.ts +717 -0
  316. package/src/serializers/MarkdownSerializer.ts +493 -0
  317. package/src/serializers/index.ts +8 -0
  318. package/test/GitService.test.ts +209 -0
  319. package/test/MarkdownConverter.test.ts +37 -3
  320. package/test/Schemas.test.ts +97 -2
  321. package/test/ast/BlockNode.test.ts +265 -0
  322. package/test/ast/Document.test.ts +126 -0
  323. package/test/ast/InlineNode.test.ts +161 -0
  324. package/test/fixtures/integration-test.html.fixture +103 -0
  325. package/test/fixtures/integration-test.md.expected +257 -0
  326. package/test/integration.test.ts +269 -0
  327. package/test/oauthServer.test.ts +50 -0
  328. package/test/parsers/ConfluenceParser.test.ts +283 -0
  329. package/test/schemas/ConfluencePreprocessor.test.ts +180 -0
  330. package/test/schemas/ConversionSchema.test.ts +159 -0
  331. package/test/schemas/HastSchema.test.ts +138 -0
  332. package/test/schemas/MdastSchema.test.ts +145 -0
  333. package/test/schemas/nodes/block/BlockSchema.test.ts +173 -0
  334. package/test/schemas/nodes/inline/InlineSchema.test.ts +198 -0
  335. package/test/schemas/nodes/macro/MacroSchema.test.ts +142 -0
  336. package/test/tokenStorage.test.ts +99 -0
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Node.js-specific layer implementations.
3
+ *
4
+ * This is the ONLY file that should import directly from node:* modules.
5
+ * All other code should use Effect platform abstractions.
6
+ *
7
+ * @module
8
+ * @internal
9
+ */
10
+ import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"
11
+ import { createServer } from "node:http"
12
+ import { makeHttpServerFactory } from "./oauthServer.js"
13
+
14
+ /**
15
+ * HTTP Server factory layer using Node.js http module.
16
+ *
17
+ * @category Layers
18
+ */
19
+ export const HttpServerFactoryLive = makeHttpServerFactory(
20
+ (port) => NodeHttpServer.layerServer(createServer, { port })
21
+ )
@@ -96,3 +96,24 @@ export const serializeMarkdown = (
96
96
 
97
97
  return matter.stringify(content, fm)
98
98
  }
99
+
100
+ /**
101
+ * Serialize a new page markdown with minimal front-matter.
102
+ *
103
+ * @param frontMatter - The new page front-matter (title only)
104
+ * @param content - The markdown content
105
+ * @returns The serialized markdown file content
106
+ *
107
+ * @internal
108
+ */
109
+ export const serializeNewPageMarkdown = (
110
+ frontMatter: NewPageFrontMatter,
111
+ content: string
112
+ ): string => {
113
+ const fm = {
114
+ title: frontMatter.title,
115
+ ...(frontMatter.parentId !== undefined ? { parentId: frontMatter.parentId } : {})
116
+ }
117
+
118
+ return matter.stringify(content, fm)
119
+ }
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Git shell command helpers.
3
+ *
4
+ * @module
5
+ * @internal
6
+ */
7
+ import * as Command from "@effect/platform/Command"
8
+ import type * as CommandExecutor from "@effect/platform/CommandExecutor"
9
+ import type * as PlatformError from "@effect/platform/Error"
10
+ import * as Effect from "effect/Effect"
11
+ import * as String from "effect/String"
12
+ import { GitError, GitNotInstalledError } from "../GitError.js"
13
+
14
+ /**
15
+ * Git file status codes from `git status --porcelain`.
16
+ */
17
+ export type GitFileStatus =
18
+ | "modified"
19
+ | "added"
20
+ | "deleted"
21
+ | "renamed"
22
+ | "copied"
23
+ | "untracked"
24
+ | "ignored"
25
+ | "unmerged"
26
+
27
+ /**
28
+ * Parsed git status entry.
29
+ */
30
+ export interface GitStatusEntry {
31
+ readonly status: GitFileStatus
32
+ readonly path: string
33
+ readonly staged: boolean
34
+ }
35
+
36
+ /**
37
+ * Parsed git log entry.
38
+ */
39
+ export interface GitLogEntry {
40
+ readonly hash: string
41
+ readonly author: string
42
+ readonly email: string
43
+ readonly date: Date
44
+ readonly message: string
45
+ }
46
+
47
+ /**
48
+ * Convert PlatformError to GitError or GitNotInstalledError.
49
+ */
50
+ const mapPlatformError = (
51
+ error: PlatformError.PlatformError,
52
+ commandStr: string
53
+ ): GitError | GitNotInstalledError => {
54
+ if (error._tag === "SystemError" && error.reason === "NotFound") {
55
+ return new GitNotInstalledError({
56
+ message: `Git not found: ${error.message}. Please install git.`
57
+ })
58
+ }
59
+ return new GitError({
60
+ command: commandStr,
61
+ message: error.message
62
+ })
63
+ }
64
+
65
+ /**
66
+ * Run a git command in the specified working directory.
67
+ *
68
+ * @param args - Git command arguments
69
+ * @param cwd - Working directory
70
+ * @returns Command output
71
+ *
72
+ * @internal
73
+ */
74
+ export const runGit = (
75
+ args: ReadonlyArray<string>,
76
+ cwd: string
77
+ ): Effect.Effect<string, GitError | GitNotInstalledError, CommandExecutor.CommandExecutor> =>
78
+ Effect.gen(function*() {
79
+ const command = Command.make("git", ...args).pipe(
80
+ Command.workingDirectory(cwd)
81
+ )
82
+
83
+ const commandStr = `git ${args.join(" ")}`
84
+
85
+ const result = yield* Command.string(command).pipe(
86
+ Effect.mapError((error) => mapPlatformError(error, commandStr))
87
+ )
88
+
89
+ return result
90
+ })
91
+
92
+ /**
93
+ * Run a git command that may fail with exit code 1 (e.g., diff with no changes).
94
+ * Returns empty string on exit code 1.
95
+ *
96
+ * @internal
97
+ */
98
+ export const runGitAllowEmpty = (
99
+ args: ReadonlyArray<string>,
100
+ cwd: string
101
+ ): Effect.Effect<string, GitError | GitNotInstalledError, CommandExecutor.CommandExecutor> =>
102
+ runGit(args, cwd).pipe(
103
+ Effect.catchIf(
104
+ (e): e is GitError => e._tag === "GitError",
105
+ () => Effect.succeed("")
106
+ )
107
+ )
108
+
109
+ /**
110
+ * Parse git status --porcelain output.
111
+ *
112
+ * @param output - Raw output from `git status --porcelain`
113
+ * @returns Parsed status entries
114
+ *
115
+ * @internal
116
+ */
117
+ export const parseGitStatus = (output: string): ReadonlyArray<GitStatusEntry> => {
118
+ if (String.isEmpty(output.trim())) {
119
+ return []
120
+ }
121
+
122
+ return output
123
+ .split("\n")
124
+ .filter((line) => line.length >= 3) // Valid lines have XY + space + path
125
+ .map((line) => {
126
+ const indexStatus = line[0] ?? " "
127
+ const workTreeStatus = line[1] ?? " "
128
+ const path = line.slice(3)
129
+
130
+ const staged = indexStatus !== " " && indexStatus !== "?"
131
+ const status = parseStatusCode(staged ? indexStatus : workTreeStatus)
132
+
133
+ return { status, path, staged }
134
+ })
135
+ }
136
+
137
+ /**
138
+ * Parse a single status code character.
139
+ */
140
+ const parseStatusCode = (code: string): GitFileStatus => {
141
+ switch (code) {
142
+ case "M":
143
+ return "modified"
144
+ case "A":
145
+ return "added"
146
+ case "D":
147
+ return "deleted"
148
+ case "R":
149
+ return "renamed"
150
+ case "C":
151
+ return "copied"
152
+ case "?":
153
+ return "untracked"
154
+ case "!":
155
+ return "ignored"
156
+ case "U":
157
+ return "unmerged"
158
+ default:
159
+ return "modified"
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Parse git log output with custom format.
165
+ *
166
+ * Uses format: hash<|>author<|>email<|>date<|>message
167
+ *
168
+ * @param output - Raw output from git log
169
+ * @returns Parsed log entries
170
+ *
171
+ * @internal
172
+ */
173
+ export const parseGitLog = (output: string): ReadonlyArray<GitLogEntry> => {
174
+ if (String.isEmpty(output.trim())) {
175
+ return []
176
+ }
177
+
178
+ return output
179
+ .trim()
180
+ .split("\n")
181
+ .flatMap((line) => {
182
+ const parts = line.split("<|>")
183
+ if (parts.length < 5) {
184
+ return []
185
+ }
186
+
187
+ const hash = parts[0]
188
+ const author = parts[1]
189
+ const email = parts[2]
190
+ const dateStr = parts[3]
191
+ const messageParts = parts.slice(4)
192
+
193
+ if (hash === undefined || author === undefined || email === undefined || dateStr === undefined) {
194
+ return []
195
+ }
196
+
197
+ const message = messageParts.join("<|>")
198
+
199
+ return [{
200
+ hash: hash.trim(),
201
+ author: author.trim(),
202
+ email: email.trim(),
203
+ date: new Date(dateStr.trim()),
204
+ message: message.trim()
205
+ }]
206
+ })
207
+ }
208
+
209
+ /**
210
+ * Git log format string for parseGitLog.
211
+ *
212
+ * @internal
213
+ */
214
+ export const GIT_LOG_FORMAT = "%H<|>%an<|>%ae<|>%aI<|>%s"
215
+
216
+ /**
217
+ * Check if output indicates merge conflicts.
218
+ *
219
+ * @param statusOutput - Output from `git status --porcelain`
220
+ * @returns List of conflicted files
221
+ *
222
+ * @internal
223
+ */
224
+ export const getConflictedFiles = (statusOutput: string): ReadonlyArray<string> => {
225
+ const entries = parseGitStatus(statusOutput)
226
+ return entries
227
+ .filter((e) => e.status === "unmerged")
228
+ .map((e) => e.path)
229
+ }
@@ -4,9 +4,68 @@
4
4
  * @module
5
5
  * @internal
6
6
  */
7
- import * as crypto from "node:crypto"
7
+ import * as Context from "effect/Context"
8
+ import * as Effect from "effect/Effect"
9
+ import * as Layer from "effect/Layer"
8
10
  import type { ContentHash } from "../Brand.js"
9
11
 
12
+ /**
13
+ * Hash computation service.
14
+ * This allows mocking in tests.
15
+ *
16
+ * @category Services
17
+ */
18
+ export interface HashService {
19
+ readonly computeSha256: (content: string) => Effect.Effect<ContentHash>
20
+ }
21
+
22
+ /**
23
+ * Tag for the HashService.
24
+ *
25
+ * @category Services
26
+ */
27
+ export class HashServiceTag extends Context.Tag("@knpkv/confluence-to-markdown/HashService")<
28
+ HashServiceTag,
29
+ HashService
30
+ >() {}
31
+
32
+ /**
33
+ * Create a HashService layer from a hash function.
34
+ * This allows injecting platform-specific implementations.
35
+ *
36
+ * @param hashFn - Function that computes SHA256 hash
37
+ * @category Layers
38
+ */
39
+ export const makeHashServiceLive = (
40
+ hashFn: (content: string) => Promise<string>
41
+ ): Layer.Layer<HashServiceTag> =>
42
+ Layer.succeed(
43
+ HashServiceTag,
44
+ {
45
+ computeSha256: (content: string) =>
46
+ Effect.promise(() => hashFn(content)).pipe(
47
+ Effect.map((hash) => hash as ContentHash)
48
+ )
49
+ }
50
+ )
51
+
52
+ /**
53
+ * Default implementation using Web Crypto API (available in all modern runtimes).
54
+ *
55
+ * @category Layers
56
+ */
57
+ export const HashServiceLive: Layer.Layer<HashServiceTag> = makeHashServiceLive(
58
+ async (content: string) => {
59
+ const encoder = new TextEncoder()
60
+ const data = encoder.encode(content)
61
+ const hashBuffer = await globalThis.crypto.subtle.digest("SHA-256", data)
62
+ const hashArray = new Uint8Array(hashBuffer)
63
+ return Array.from(hashArray)
64
+ .map((b) => b.toString(16).padStart(2, "0"))
65
+ .join("")
66
+ }
67
+ )
68
+
10
69
  /**
11
70
  * Compute SHA256 hash of content.
12
71
  *
@@ -15,5 +74,8 @@ import type { ContentHash } from "../Brand.js"
15
74
  *
16
75
  * @internal
17
76
  */
18
- export const computeHash = (content: string): ContentHash =>
19
- crypto.createHash("sha256").update(content, "utf8").digest("hex") as ContentHash
77
+ export const computeHash = (content: string): Effect.Effect<ContentHash, never, HashServiceTag> =>
78
+ Effect.gen(function*() {
79
+ const hashService = yield* HashServiceTag
80
+ return yield* hashService.computeSha256(content)
81
+ })
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Local HTTP server for OAuth callback.
3
+ *
4
+ * @module
5
+ */
6
+ import * as HttpRouter from "@effect/platform/HttpRouter"
7
+ import * as HttpServer from "@effect/platform/HttpServer"
8
+ import type { ServeError } from "@effect/platform/HttpServerError"
9
+ import * as HttpServerRequest from "@effect/platform/HttpServerRequest"
10
+ import * as HttpServerResponse from "@effect/platform/HttpServerResponse"
11
+ import * as Context from "effect/Context"
12
+ import * as Deferred from "effect/Deferred"
13
+ import * as Effect from "effect/Effect"
14
+ import * as Fiber from "effect/Fiber"
15
+ import * as Layer from "effect/Layer"
16
+ import { OAuthError } from "../ConfluenceError.js"
17
+
18
+ const DEFAULT_PORT = 8585
19
+ const MAX_PORT_ATTEMPTS = 10
20
+
21
+ /**
22
+ * Factory service for creating HTTP servers.
23
+ * This allows mocking the server creation in tests.
24
+ *
25
+ * @category Services
26
+ */
27
+ export interface HttpServerFactory {
28
+ readonly createServerLayer: (port: number) => Layer.Layer<
29
+ HttpServer.HttpServer,
30
+ ServeError,
31
+ never
32
+ >
33
+ }
34
+
35
+ /**
36
+ * Tag for the HttpServerFactory service.
37
+ *
38
+ * @category Services
39
+ */
40
+ export class HttpServerFactoryTag extends Context.Tag("@knpkv/confluence-to-markdown/HttpServerFactory")<
41
+ HttpServerFactoryTag,
42
+ HttpServerFactory
43
+ >() {}
44
+
45
+ /**
46
+ * Create a HttpServerFactory layer from a layer factory function.
47
+ * This allows injecting platform-specific implementations.
48
+ *
49
+ * @param createLayerFn - Function that creates HttpServer layer for a given port
50
+ * @returns Layer providing HttpServerFactory
51
+ *
52
+ * @category Layers
53
+ */
54
+ export const makeHttpServerFactory = (
55
+ createLayerFn: (port: number) => Layer.Layer<HttpServer.HttpServer, ServeError, never>
56
+ ): Layer.Layer<HttpServerFactoryTag> =>
57
+ Layer.succeed(HttpServerFactoryTag, {
58
+ createServerLayer: createLayerFn
59
+ })
60
+
61
+ /**
62
+ * Check if a port is available by attempting to start a server.
63
+ */
64
+ const isPortAvailable = (port: number): Effect.Effect<boolean, never, HttpServerFactoryTag> =>
65
+ Effect.gen(function*() {
66
+ const factory = yield* HttpServerFactoryTag
67
+ const serverLayer = factory.createServerLayer(port)
68
+
69
+ // Try to acquire and immediately release
70
+ const result = yield* Layer.build(serverLayer).pipe(
71
+ Effect.scoped,
72
+ Effect.as(true),
73
+ Effect.catchAll(() => Effect.succeed(false))
74
+ )
75
+ return result
76
+ })
77
+
78
+ /**
79
+ * Find an available port starting from the default.
80
+ */
81
+ const findAvailablePort = (): Effect.Effect<number, OAuthError, HttpServerFactoryTag> =>
82
+ Effect.gen(function*() {
83
+ for (let attempt = 0; attempt < MAX_PORT_ATTEMPTS; attempt++) {
84
+ const port = DEFAULT_PORT + attempt
85
+ const available = yield* isPortAvailable(port)
86
+ if (available) {
87
+ return port
88
+ }
89
+ }
90
+ return yield* Effect.fail(
91
+ new OAuthError({
92
+ step: "authorize",
93
+ cause: `Could not find available port (tried ${DEFAULT_PORT}-${
94
+ DEFAULT_PORT + MAX_PORT_ATTEMPTS - 1
95
+ }). Close other applications using these ports.`
96
+ })
97
+ )
98
+ })
99
+
100
+ /**
101
+ * Result from the OAuth callback server.
102
+ */
103
+ export interface CallbackServerResult {
104
+ /** Promise that resolves with the authorization code */
105
+ readonly codePromise: Effect.Effect<string, OAuthError>
106
+ /** Shutdown the callback server */
107
+ readonly shutdown: Effect.Effect<void, never>
108
+ /** The port the server is listening on */
109
+ readonly port: number
110
+ }
111
+
112
+ /**
113
+ * Start a local HTTP server to receive OAuth callback.
114
+ *
115
+ * @param expectedState - The state parameter to verify against CSRF
116
+ * @returns Server control interface with code promise, shutdown, and port
117
+ *
118
+ * @category OAuth
119
+ */
120
+ export const startCallbackServer = (
121
+ expectedState: string
122
+ ): Effect.Effect<CallbackServerResult, OAuthError, HttpServerFactoryTag> =>
123
+ Effect.gen(function*() {
124
+ const factory = yield* HttpServerFactoryTag
125
+ const port = yield* findAvailablePort()
126
+ const deferred = yield* Deferred.make<string, OAuthError>()
127
+ const readyDeferred = yield* Deferred.make<void, OAuthError>()
128
+
129
+ const app = HttpRouter.empty.pipe(
130
+ HttpRouter.get(
131
+ "/callback",
132
+ Effect.gen(function*() {
133
+ const req = yield* HttpServerRequest.HttpServerRequest
134
+ const url = new URL(req.url, `http://localhost:${port}`)
135
+ const code = url.searchParams.get("code")
136
+ const state = url.searchParams.get("state")
137
+ const error = url.searchParams.get("error")
138
+ const errorDescription = url.searchParams.get("error_description")
139
+
140
+ if (error) {
141
+ yield* Deferred.fail(
142
+ deferred,
143
+ new OAuthError({ step: "authorize", cause: errorDescription || error })
144
+ )
145
+ return HttpServerResponse.html(
146
+ "<html><body><h1>Authorization Failed</h1><p>You can close this window.</p></body></html>"
147
+ )
148
+ }
149
+
150
+ if (state !== expectedState) {
151
+ yield* Deferred.fail(
152
+ deferred,
153
+ new OAuthError({ step: "authorize", cause: "State mismatch - possible CSRF attack" })
154
+ )
155
+ return HttpServerResponse.html(
156
+ "<html><body><h1>Security Error</h1><p>State verification failed.</p></body></html>"
157
+ )
158
+ }
159
+
160
+ if (!code) {
161
+ yield* Deferred.fail(
162
+ deferred,
163
+ new OAuthError({ step: "authorize", cause: "No authorization code received" })
164
+ )
165
+ return HttpServerResponse.html(
166
+ "<html><body><h1>Error</h1><p>No authorization code received.</p></body></html>"
167
+ )
168
+ }
169
+
170
+ yield* Deferred.succeed(deferred, code)
171
+ return HttpServerResponse.html(
172
+ "<html><body><h1>Success!</h1><p>You can close this window and return to the terminal.</p></body></html>"
173
+ )
174
+ })
175
+ )
176
+ )
177
+
178
+ const serverLayer = factory.createServerLayer(port)
179
+
180
+ const serverFiber = yield* HttpServer.serve(app).pipe(
181
+ Layer.provide(serverLayer),
182
+ Layer.build,
183
+ Effect.tap(() => Deferred.succeed(readyDeferred, undefined)),
184
+ Effect.tapError((err) => Deferred.fail(readyDeferred, new OAuthError({ step: "authorize", cause: err }))),
185
+ // Keep the layer alive until fiber is interrupted
186
+ Effect.flatMap(() => Effect.never),
187
+ Effect.scoped,
188
+ Effect.fork
189
+ )
190
+
191
+ // Wait for server to be ready (or fail)
192
+ yield* Deferred.await(readyDeferred)
193
+
194
+ return {
195
+ codePromise: Deferred.await(deferred),
196
+ shutdown: Fiber.interrupt(serverFiber).pipe(Effect.asVoid),
197
+ port
198
+ }
199
+ })
@@ -4,7 +4,8 @@
4
4
  * @module
5
5
  * @internal
6
6
  */
7
- import * as path from "node:path"
7
+ import * as Path from "@effect/platform/Path"
8
+ import * as Effect from "effect/Effect"
8
9
 
9
10
  /**
10
11
  * Convert a page title to a URL-safe slug.
@@ -31,8 +32,17 @@ export const slugify = (title: string): string => {
31
32
  /**
32
33
  * Convert a page to a local file path.
33
34
  *
35
+ * Pages are always stored as `slug.md`. If a page has children,
36
+ * the children are stored in a `slug/` directory alongside the parent file.
37
+ *
38
+ * Example structure:
39
+ * - guide.md (page with children)
40
+ * - guide/ (directory for children)
41
+ * - getting-started.md
42
+ * - advanced.md
43
+ *
34
44
  * @param title - The page title
35
- * @param hasChildren - Whether the page has child pages
45
+ * @param _hasChildren - Whether the page has child pages (unused, kept for API compat)
36
46
  * @param parentPath - The parent directory path
37
47
  * @returns The local file path for the page
38
48
  *
@@ -40,14 +50,15 @@ export const slugify = (title: string): string => {
40
50
  */
41
51
  export const pageToPath = (
42
52
  title: string,
43
- hasChildren: boolean,
53
+ _hasChildren: boolean,
44
54
  parentPath: string
45
- ): string => {
46
- const slug = slugify(title)
47
- return hasChildren
48
- ? path.join(parentPath, slug, "index.md")
49
- : path.join(parentPath, `${slug}.md`)
50
- }
55
+ ): Effect.Effect<string, never, Path.Path> =>
56
+ Effect.gen(function*() {
57
+ const path = yield* Path.Path
58
+ const slug = slugify(title)
59
+ // Always use slug.md, children go in slug/ directory
60
+ return path.join(parentPath, `${slug}.md`)
61
+ })
51
62
 
52
63
  /**
53
64
  * Get the directory path for a page (used when creating children).
@@ -58,10 +69,15 @@ export const pageToPath = (
58
69
  *
59
70
  * @internal
60
71
  */
61
- export const pageToDir = (title: string, parentPath: string): string => {
62
- const slug = slugify(title)
63
- return path.join(parentPath, slug)
64
- }
72
+ export const pageToDir = (
73
+ title: string,
74
+ parentPath: string
75
+ ): Effect.Effect<string, never, Path.Path> =>
76
+ Effect.gen(function*() {
77
+ const path = yield* Path.Path
78
+ const slug = slugify(title)
79
+ return path.join(parentPath, slug)
80
+ })
65
81
 
66
82
  /**
67
83
  * Extract page slug from a file path.
@@ -71,7 +87,8 @@ export const pageToDir = (title: string, parentPath: string): string => {
71
87
  *
72
88
  * @internal
73
89
  */
74
- export const pathToSlug = (filePath: string): string => {
75
- const basename = path.basename(filePath, ".md")
76
- return basename === "index" ? path.basename(path.dirname(filePath)) : basename
77
- }
90
+ export const pathToSlug = (filePath: string): Effect.Effect<string, never, Path.Path> =>
91
+ Effect.gen(function*() {
92
+ const path = yield* Path.Path
93
+ return path.basename(filePath, ".md")
94
+ })