@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
package/src/bin.ts CHANGED
@@ -2,237 +2,41 @@
2
2
  /**
3
3
  * CLI entry point for confluence-to-markdown.
4
4
  */
5
- import { Command, Options, Prompt } from "@effect/cli"
6
- import * as NodeContext from "@effect/platform-node/NodeContext"
7
- import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
8
- import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
9
- import * as NodeTerminal from "@effect/platform-node/NodeTerminal"
10
- import * as Config from "effect/Config"
11
- import * as Console from "effect/Console"
5
+ import { Command } from "@effect/cli"
12
6
  import * as Effect from "effect/Effect"
13
- import * as Layer from "effect/Layer"
14
- import * as Option from "effect/Option"
7
+ import * as Logger from "effect/Logger"
8
+ import * as LogLevel from "effect/LogLevel"
15
9
  import pkg from "../package.json" with { type: "json" }
16
- import type { PageId } from "./Brand.js"
17
- import { ConfluenceClient, type ConfluenceClientConfig, layer as ConfluenceClientLayer } from "./ConfluenceClient.js"
10
+ import { handleError } from "./commands/errorHandler.js"
18
11
  import {
19
- ConfluenceConfig,
20
- createConfigFile,
21
- layer as ConfluenceConfigLayer,
22
- layerFromValues as ConfluenceConfigLayerFromValues
23
- } from "./ConfluenceConfig.js"
24
- import { AuthMissingError, ConfigError } from "./ConfluenceError.js"
25
- import { layer as LocalFileSystemLayer } from "./LocalFileSystem.js"
26
- import { layer as MarkdownConverterLayer } from "./MarkdownConverter.js"
27
- import { layer as SyncEngineLayer, SyncEngine } from "./SyncEngine.js"
28
-
29
- // === Auth config ===
30
- const AuthConfig = Config.all({
31
- apiKey: Config.string("CONFLUENCE_API_KEY"),
32
- email: Config.string("CONFLUENCE_EMAIL")
33
- })
34
-
35
- const getAuth = (): Effect.Effect<ConfluenceClientConfig["auth"], AuthMissingError> =>
36
- AuthConfig.pipe(
37
- Effect.map(({ apiKey, email }) => ({ type: "token" as const, email, token: apiKey })),
38
- Effect.mapError(() => new AuthMissingError())
39
- )
40
-
41
- // === Init command ===
42
- const rootPageIdOption = Options.text("root-page-id").pipe(
43
- Options.withDescription("Confluence root page ID to sync from"),
44
- Options.optional
45
- )
46
- const baseUrlOption = Options.text("base-url").pipe(
47
- Options.withDescription("Confluence Cloud base URL (e.g., https://yoursite.atlassian.net)"),
48
- Options.optional
49
- )
50
-
51
- /** Validate page ID format */
52
- const validatePageId = (input: string): Effect.Effect<string, ConfigError> =>
53
- input.trim().length > 0
54
- ? Effect.succeed(input.trim())
55
- : Effect.fail(new ConfigError({ message: "Page ID cannot be empty" }))
56
-
57
- /** Validate base URL format */
58
- const validateBaseUrl = (input: string): Effect.Effect<string, ConfigError> => {
59
- const pattern = /^https:\/\/[a-z0-9-]+\.atlassian\.net$/
60
- return pattern.test(input)
61
- ? Effect.succeed(input)
62
- : Effect.fail(
63
- new ConfigError({
64
- message: `Invalid Confluence URL: ${input}. Expected format: https://yoursite.atlassian.net`
65
- })
66
- )
67
- }
68
-
69
- const initCommand = Command.make(
70
- "init",
71
- { rootPageId: rootPageIdOption, baseUrl: baseUrlOption },
72
- ({ baseUrl, rootPageId }) =>
73
- Effect.gen(function*() {
74
- const rawPageId = Option.isSome(rootPageId)
75
- ? rootPageId.value
76
- : yield* Prompt.text({ message: "Enter Confluence root page ID:" })
77
- const rawUrl = Option.isSome(baseUrl)
78
- ? baseUrl.value
79
- : yield* Prompt.text({ message: "Enter Confluence base URL (e.g., https://yoursite.atlassian.net):" })
80
-
81
- const pageId = yield* validatePageId(rawPageId)
82
- const url = yield* validateBaseUrl(rawUrl)
83
-
84
- const path = yield* createConfigFile(pageId, url)
85
- yield* Console.log(`Created configuration file: ${path}`)
86
- })
87
- ).pipe(Command.withDescription("Initialize .confluence.json configuration"))
88
-
89
- // === Pull command ===
90
- const forceOption = Options.boolean("force").pipe(
91
- Options.withAlias("f"),
92
- Options.withDescription("Overwrite local changes")
93
- )
94
-
95
- const pullCommand = Command.make("pull", { force: forceOption }, ({ force }) =>
96
- Effect.gen(function*() {
97
- const engine = yield* SyncEngine
98
- yield* Console.log("Pulling pages from Confluence...")
99
- const result = yield* engine.pull({ force })
100
- yield* Console.log(`Pulled ${result.pulled} pages`)
101
- if (result.errors.length > 0) {
102
- yield* Console.error("Errors:", result.errors.join("\n"))
103
- }
104
- })).pipe(Command.withDescription("Download pages from Confluence to local markdown"))
105
-
106
- // === Push command ===
107
- const dryRunOption = Options.boolean("dry-run").pipe(
108
- Options.withAlias("n"),
109
- Options.withDescription("Show changes without applying")
110
- )
111
-
112
- const pushCommand = Command.make("push", { dryRun: dryRunOption }, ({ dryRun }) =>
113
- Effect.gen(function*() {
114
- const engine = yield* SyncEngine
115
- yield* Console.log(dryRun ? "Dry run - showing changes..." : "Pushing changes to Confluence...")
116
- const result = yield* engine.push({ dryRun })
117
- yield* Console.log(`Pushed: ${result.pushed}, Created: ${result.created}, Skipped: ${result.skipped}`)
118
- if (result.errors.length > 0) {
119
- yield* Console.error("Errors:", result.errors.join("\n"))
120
- }
121
- })).pipe(Command.withDescription("Upload local markdown changes to Confluence"))
122
-
123
- // === Sync command ===
124
- const syncCommand = Command.make("sync", {}, () =>
125
- Effect.gen(function*() {
126
- const engine = yield* SyncEngine
127
- yield* Console.log("Syncing with Confluence...")
128
- const result = yield* engine.sync()
129
- yield* Console.log(`Pulled: ${result.pulled}, Pushed: ${result.pushed}, Created: ${result.created}`)
130
- if (result.conflicts > 0) {
131
- yield* Console.warn(`Conflicts: ${result.conflicts}`)
132
- }
133
- if (result.errors.length > 0) {
134
- yield* Console.error("Errors:", result.errors.join("\n"))
135
- }
136
- })).pipe(Command.withDescription("Bidirectional sync with conflict detection"))
137
-
138
- // === Status command ===
139
- const statusCommand = Command.make("status", {}, () =>
140
- Effect.gen(function*() {
141
- const engine = yield* SyncEngine
142
- const result = yield* engine.status()
143
- yield* Console.log(`
144
- Sync Status:
145
- Synced: ${result.synced}
146
- Local Modified: ${result.localModified}
147
- Remote Modified: ${result.remoteModified}
148
- Conflicts: ${result.conflicts}
149
- Local Only: ${result.localOnly}
150
- Remote Only: ${result.remoteOnly}
151
- `)
152
- if (result.files.length > 0 && result.synced < result.files.length) {
153
- yield* Console.log("Changed files:")
154
- for (const file of result.files) {
155
- if (file._tag !== "Synced" && file._tag !== "RemoteOnly") {
156
- yield* Console.log(` [${file._tag}] ${file.path}`)
157
- } else if (file._tag === "RemoteOnly") {
158
- yield* Console.log(` [${file._tag}] ${file.page.title}`)
159
- }
160
- }
161
- }
162
- })).pipe(Command.withDescription("Show sync status"))
12
+ authCommand,
13
+ cloneCommand,
14
+ commitCommand,
15
+ deleteCommand,
16
+ diffCommand,
17
+ logCommand,
18
+ newCommand,
19
+ pullCommand,
20
+ pushCommand,
21
+ statusCommand
22
+ } from "./commands/index.js"
23
+ import { AppLayer, AuthOnlyLayer, CloneLayer, getLayerType, MinimalLayer } from "./commands/layers.js"
163
24
 
164
25
  // === Main command ===
165
26
  const confluence = Command.make("confluence").pipe(
166
27
  Command.withDescription("Sync Confluence pages to local markdown"),
167
- Command.withSubcommands([initCommand, pullCommand, pushCommand, syncCommand, statusCommand])
168
- )
169
-
170
- // === Build app layer ===
171
- // Dummy config layer for help/init
172
- const DummyConfigLayer = ConfluenceConfigLayerFromValues({
173
- rootPageId: "dummy" as PageId,
174
- baseUrl: "https://dummy.atlassian.net",
175
- docsPath: ".docs/confluence",
176
- excludePatterns: []
177
- })
178
-
179
- // Dummy client layer for help/init (will fail if actually used)
180
- const DummyConfluenceClientLayer = Layer.succeed(
181
- ConfluenceClient,
182
- ConfluenceClient.of({
183
- getPage: () => Effect.dieMessage("Not configured"),
184
- getChildren: () => Effect.dieMessage("Not configured"),
185
- getAllChildren: () => Effect.dieMessage("Not configured"),
186
- createPage: () => Effect.dieMessage("Not configured"),
187
- updatePage: () => Effect.dieMessage("Not configured"),
188
- deletePage: () => Effect.dieMessage("Not configured")
189
- })
190
- )
191
-
192
- // Dummy sync engine that will fail if actually used
193
- const DummySyncEngineLayer = Layer.succeed(
194
- SyncEngine,
195
- SyncEngine.of({
196
- pull: () => Effect.dieMessage("Not configured - run 'confluence init' first"),
197
- push: () => Effect.dieMessage("Not configured - run 'confluence init' first"),
198
- sync: () => Effect.dieMessage("Not configured - run 'confluence init' first"),
199
- status: () => Effect.dieMessage("Not configured - run 'confluence init' first")
200
- })
201
- )
202
-
203
- // Build client layer dynamically based on auth
204
- const ConfluenceClientLive = Layer.unwrapEffect(
205
- Effect.gen(function*() {
206
- const auth = yield* getAuth()
207
- const config = yield* ConfluenceConfig
208
-
209
- const clientConfig: ConfluenceClientConfig = {
210
- baseUrl: config.baseUrl,
211
- auth
212
- }
213
-
214
- return ConfluenceClientLayer(clientConfig)
215
- })
216
- )
217
-
218
- // Full app layer with all services
219
- const AppLayer = SyncEngineLayer.pipe(
220
- Layer.provideMerge(ConfluenceClientLive),
221
- Layer.provideMerge(MarkdownConverterLayer),
222
- Layer.provideMerge(LocalFileSystemLayer),
223
- Layer.provideMerge(ConfluenceConfigLayer()),
224
- Layer.provideMerge(NodeHttpClient.layer),
225
- Layer.provideMerge(NodeContext.layer)
226
- )
227
-
228
- // Minimal layer for init/help (dummy services, won't be invoked)
229
- const MinimalLayer = DummySyncEngineLayer.pipe(
230
- Layer.provideMerge(DummyConfluenceClientLayer),
231
- Layer.provideMerge(MarkdownConverterLayer),
232
- Layer.provideMerge(LocalFileSystemLayer),
233
- Layer.provideMerge(DummyConfigLayer),
234
- Layer.provideMerge(NodeTerminal.layer),
235
- Layer.provideMerge(NodeContext.layer)
28
+ Command.withSubcommands([
29
+ cloneCommand,
30
+ authCommand,
31
+ pullCommand,
32
+ pushCommand,
33
+ statusCommand,
34
+ commitCommand,
35
+ logCommand,
36
+ diffCommand,
37
+ newCommand,
38
+ deleteCommand
39
+ ])
236
40
  )
237
41
 
238
42
  // === Run CLI ===
@@ -241,29 +45,26 @@ const cli = Command.run(confluence, {
241
45
  version: pkg.version
242
46
  })
243
47
 
244
- // Check if we need the full layer based on command
245
- const needsFullLayer = () => {
246
- const args = process.argv
247
- const cmd = args[2]
248
- // init, --help, -h, --version don't need config
249
- if (!cmd || cmd === "init" || cmd === "--help" || cmd === "-h" || cmd === "--version") {
250
- return false
48
+ const layerType = getLayerType()
49
+ const layer = layerType === "full"
50
+ ? AppLayer
51
+ : layerType === "auth"
52
+ ? AuthOnlyLayer
53
+ : layerType === "clone"
54
+ ? CloneLayer
55
+ : MinimalLayer
56
+
57
+ // Suppress verbose Effect logs (e.g. token refresh messages)
58
+ const SilentLogger = Logger.replace(Logger.defaultLogger, Logger.none)
59
+
60
+ Effect.suspend(() => cli(process.argv)).pipe(
61
+ Effect.provide(layer),
62
+ Effect.provide(SilentLogger),
63
+ Logger.withMinimumLogLevel(LogLevel.None),
64
+ Effect.runPromiseExit
65
+ ).then((exit) => {
66
+ if (exit._tag === "Failure") {
67
+ handleError(exit.cause)
68
+ process.exit(1)
251
69
  }
252
- return true
253
- }
254
-
255
- const main = Effect.suspend(() => cli(process.argv))
256
-
257
- if (needsFullLayer()) {
258
- main.pipe(
259
- Effect.provide(AppLayer),
260
- Effect.tapErrorCause(Effect.logError),
261
- NodeRuntime.runMain
262
- )
263
- } else {
264
- main.pipe(
265
- Effect.provide(MinimalLayer),
266
- Effect.tapErrorCause(Effect.logError),
267
- NodeRuntime.runMain
268
- )
269
- }
70
+ })
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Authentication commands for Confluence CLI.
3
+ */
4
+ import { Command, Options, Prompt } from "@effect/cli"
5
+ import * as Console from "effect/Console"
6
+ import * as Effect from "effect/Effect"
7
+ import * as Option from "effect/Option"
8
+ import { ConfluenceAuth } from "../ConfluenceAuth.js"
9
+
10
+ // === Auth create command ===
11
+ const createCommand = Command.make("create", {}, () =>
12
+ Effect.gen(function*() {
13
+ yield* Console.log(`
14
+ Creating OAuth app in Atlassian Developer Console...
15
+
16
+ 1. Browser will open to create a new OAuth 2.0 (3LO) app
17
+ 2. Enter app name (e.g., "Confluence CLI")
18
+ 3. After creation, go to "Permissions" and add:
19
+ - Confluence API (granular): read:page:confluence, write:page:confluence, delete:page:confluence
20
+ - User Identity API: read:me
21
+ 4. Go to "Authorization" and set callback URL:
22
+ http://localhost:8585/callback
23
+ 5. Go to "Settings" and copy Client ID and Secret
24
+ 6. Run: confluence auth configure --client-id <ID> --client-secret <SECRET>
25
+ `)
26
+ const url = "https://developer.atlassian.com/console/myapps/create-3lo-app/"
27
+ yield* Effect.promise(() =>
28
+ import("node:child_process").then((cp) =>
29
+ new Promise<void>((resolve, reject) => {
30
+ const platform = process.platform
31
+ if (platform === "darwin") {
32
+ cp.execFile("open", [url], (err) => err ? reject(err) : resolve())
33
+ } else if (platform === "win32") {
34
+ cp.execFile("cmd", ["/c", "start", "", url], (err) => err ? reject(err) : resolve())
35
+ } else {
36
+ cp.execFile("xdg-open", [url], (err) => err ? reject(err) : resolve())
37
+ }
38
+ })
39
+ )
40
+ )
41
+ })).pipe(Command.withDescription("Create OAuth app in Atlassian Developer Console"))
42
+
43
+ // === Auth configure command ===
44
+ const clientIdOption = Options.text("client-id").pipe(
45
+ Options.withDescription("OAuth client ID from Atlassian Developer Console"),
46
+ Options.optional
47
+ )
48
+ const clientSecretOption = Options.text("client-secret").pipe(
49
+ Options.withDescription("OAuth client secret"),
50
+ Options.optional
51
+ )
52
+
53
+ const configureCommand = Command.make(
54
+ "configure",
55
+ { clientId: clientIdOption, clientSecret: clientSecretOption },
56
+ ({ clientId, clientSecret }) =>
57
+ Effect.gen(function*() {
58
+ const auth = yield* ConfluenceAuth
59
+
60
+ const rawClientId = Option.isSome(clientId)
61
+ ? clientId.value
62
+ : yield* Prompt.text({ message: "Enter OAuth client ID:" })
63
+ const rawClientSecret = Option.isSome(clientSecret)
64
+ ? clientSecret.value
65
+ : yield* Prompt.text({ message: "Enter OAuth client secret:" })
66
+
67
+ yield* auth.configure({ clientId: rawClientId, clientSecret: rawClientSecret })
68
+ yield* Console.log("OAuth configured. Run 'confluence auth login' to authenticate.")
69
+ })
70
+ ).pipe(Command.withDescription("Configure OAuth client credentials"))
71
+
72
+ // === Auth login command ===
73
+ const siteOption = Options.text("site").pipe(
74
+ Options.withDescription("Confluence site URL to use (for accounts with multiple sites)"),
75
+ Options.optional
76
+ )
77
+
78
+ const loginCommand = Command.make("login", { site: siteOption }, ({ site }) =>
79
+ Effect.gen(function*() {
80
+ const auth = yield* ConfluenceAuth
81
+ const result = yield* auth.login(Option.isSome(site) ? { siteUrl: site.value } : undefined)
82
+ if (Array.isArray(result) && result.length > 0) {
83
+ yield* Console.log("\nRe-run with --site to select a specific site.")
84
+ }
85
+ })).pipe(Command.withDescription("Authenticate with Atlassian via OAuth"))
86
+
87
+ // === Auth logout command ===
88
+ const logoutCommand = Command.make("logout", {}, () =>
89
+ Effect.gen(function*() {
90
+ const auth = yield* ConfluenceAuth
91
+ yield* auth.logout()
92
+ yield* Console.log("Logged out")
93
+ })).pipe(Command.withDescription("Remove stored authentication"))
94
+
95
+ // === Auth status command ===
96
+ const statusCommand = Command.make("status", {}, () =>
97
+ Effect.gen(function*() {
98
+ const auth = yield* ConfluenceAuth
99
+ const user = yield* auth.getCurrentUser()
100
+ if (user) {
101
+ yield* Console.log(`Logged in as: ${user.name} (${user.email})`)
102
+ } else {
103
+ yield* Console.log("Not logged in. Use 'confluence auth login' to authenticate.")
104
+ }
105
+ })).pipe(Command.withDescription("Show authentication status"))
106
+
107
+ // === Auth command group ===
108
+ export const authCommand = Command.make("auth").pipe(
109
+ Command.withDescription("Manage OAuth authentication"),
110
+ Command.withSubcommands([
111
+ createCommand,
112
+ configureCommand,
113
+ loginCommand,
114
+ logoutCommand,
115
+ statusCommand
116
+ ])
117
+ )
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Clone command for Confluence CLI.
3
+ */
4
+ import { Command, Options, Prompt } from "@effect/cli"
5
+ import * as NodeContext from "@effect/platform-node/NodeContext"
6
+ import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
7
+ import * as Console from "effect/Console"
8
+ import * as Effect from "effect/Effect"
9
+ import * as Layer from "effect/Layer"
10
+ import * as Option from "effect/Option"
11
+ import type { PageId } from "../Brand.js"
12
+ import { type ConfluenceClientConfig, layer as ConfluenceClientLayer } from "../ConfluenceClient.js"
13
+ import { createConfigFile, layerFromValues as ConfluenceConfigLayerFromValues } from "../ConfluenceConfig.js"
14
+ import { ConfigError } from "../ConfluenceError.js"
15
+ import { GitService, layer as GitServiceLayer } from "../GitService.js"
16
+ import { UserCacheLayer } from "../internal/userCache.js"
17
+ import { layer as LocalFileSystemLayer } from "../LocalFileSystem.js"
18
+ import { layer as MarkdownConverterLayer } from "../MarkdownConverter.js"
19
+ import { layer as SyncEngineLayer, SyncEngine } from "../SyncEngine.js"
20
+ import { getAuth } from "./shared.js"
21
+
22
+ const rootPageIdOption = Options.text("root-page-id").pipe(
23
+ Options.withDescription("Confluence root page ID to sync from"),
24
+ Options.optional
25
+ )
26
+
27
+ const baseUrlOption = Options.text("base-url").pipe(
28
+ Options.withDescription("Confluence Cloud base URL (e.g., https://yoursite.atlassian.net)"),
29
+ Options.optional
30
+ )
31
+
32
+ /** Validate page ID format */
33
+ const validatePageId = (input: string): Effect.Effect<string, ConfigError> =>
34
+ input.trim().length > 0
35
+ ? Effect.succeed(input.trim())
36
+ : Effect.fail(new ConfigError({ message: "Page ID cannot be empty" }))
37
+
38
+ /** Validate base URL format */
39
+ const validateBaseUrl = (input: string): Effect.Effect<string, ConfigError> => {
40
+ const pattern = /^https:\/\/[a-z0-9-]+\.atlassian\.net$/
41
+ return pattern.test(input)
42
+ ? Effect.succeed(input)
43
+ : Effect.fail(
44
+ new ConfigError({
45
+ message: `Invalid Confluence URL: ${input}. Expected format: https://yoursite.atlassian.net`
46
+ })
47
+ )
48
+ }
49
+
50
+ export const cloneCommand = Command.make(
51
+ "clone",
52
+ { rootPageId: rootPageIdOption, baseUrl: baseUrlOption },
53
+ ({ baseUrl, rootPageId }) =>
54
+ Effect.gen(function*() {
55
+ const git = yield* GitService
56
+
57
+ // Fail if .confluence already exists
58
+ const isGitInit = yield* git.isInitialized()
59
+ if (isGitInit) {
60
+ return yield* Effect.fail(
61
+ new ConfigError({ message: "Already cloned. Use 'confluence pull' to update." })
62
+ )
63
+ }
64
+
65
+ // Validate git is installed
66
+ yield* Console.log("Checking git installation...")
67
+ const gitVersion = yield* git.validateGit().pipe(
68
+ Effect.mapError(() =>
69
+ new ConfigError({ message: "Git is required but not installed. Please install git first." })
70
+ )
71
+ )
72
+ yield* Console.log(`Found git ${gitVersion}`)
73
+
74
+ const rawPageId = Option.isSome(rootPageId)
75
+ ? rootPageId.value
76
+ : yield* Prompt.text({ message: "Enter Confluence root page ID:" })
77
+ const rawUrl = Option.isSome(baseUrl)
78
+ ? baseUrl.value
79
+ : yield* Prompt.text({ message: "Enter Confluence base URL (e.g., https://yoursite.atlassian.net):" })
80
+
81
+ const pageId = yield* validatePageId(rawPageId)
82
+ const url = yield* validateBaseUrl(rawUrl)
83
+
84
+ const path = yield* createConfigFile(pageId, url)
85
+ yield* Console.log(`Created configuration file: ${path}`)
86
+
87
+ // Initialize git repo
88
+ yield* Console.log("Initializing git repository...")
89
+ yield* git.init().pipe(
90
+ Effect.mapError(() => new ConfigError({ message: "Failed to initialize git repository" }))
91
+ )
92
+
93
+ // Build services dynamically with the new config
94
+ yield* Console.log("Cloning pages from Confluence with full history...")
95
+
96
+ // Get auth
97
+ const auth = yield* getAuth()
98
+ const clientConfig: ConfluenceClientConfig = { baseUrl: url, auth }
99
+
100
+ // Build layers for the clone operation
101
+ const configLayer = ConfluenceConfigLayerFromValues({
102
+ rootPageId: pageId as PageId,
103
+ baseUrl: url,
104
+ docsPath: ".confluence/docs",
105
+ excludePatterns: [],
106
+ saveSource: false,
107
+ trackedPaths: ["**/*.md"]
108
+ })
109
+
110
+ const clientLayer = ConfluenceClientLayer(clientConfig).pipe(
111
+ Layer.provide(NodeHttpClient.layer)
112
+ )
113
+
114
+ const cloneLayer = SyncEngineLayer.pipe(
115
+ Layer.provideMerge(UserCacheLayer),
116
+ Layer.provideMerge(GitServiceLayer),
117
+ Layer.provideMerge(clientLayer),
118
+ Layer.provideMerge(MarkdownConverterLayer),
119
+ Layer.provideMerge(LocalFileSystemLayer),
120
+ Layer.provideMerge(configLayer),
121
+ Layer.provideMerge(NodeContext.layer)
122
+ )
123
+
124
+ const result = yield* Effect.gen(function*() {
125
+ const engine = yield* SyncEngine
126
+ const gitService = yield* GitService
127
+ const pullResult = yield* engine.pull({
128
+ force: true,
129
+ replayHistory: true,
130
+ onProgress: (current, total, message) => {
131
+ process.stdout.write(`\r Replaying history: ${current}/${total} - ${message}`)
132
+ }
133
+ })
134
+
135
+ // Create origin/confluence branch at HEAD to track remote state
136
+ yield* gitService.createBranch("origin/confluence")
137
+
138
+ return pullResult
139
+ }).pipe(Effect.provide(cloneLayer))
140
+
141
+ // Clear progress line and print final result
142
+ process.stdout.write("\r" + " ".repeat(80) + "\r")
143
+ yield* Console.log(`Cloned ${result.pulled} pages with ${result.commits} commits`)
144
+ })
145
+ ).pipe(Command.withDescription("Clone Confluence pages with full version history"))
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Delete command for Confluence CLI.
3
+ *
4
+ * Interactive mode: select page from tree, delete local file only.
5
+ * Push to actually delete from Confluence.
6
+ */
7
+ import { Command, Prompt } from "@effect/cli"
8
+ import * as FileSystem from "@effect/platform/FileSystem"
9
+ import * as Path from "@effect/platform/Path"
10
+ import * as Console from "effect/Console"
11
+ import * as Effect from "effect/Effect"
12
+ import { ConfluenceConfig } from "../ConfluenceConfig.js"
13
+ import { LocalFileSystem } from "../LocalFileSystem.js"
14
+ import { flattenPageTree } from "./pageTree.js"
15
+
16
+ export const deleteCommand = Command.make("delete", {}, () =>
17
+ Effect.gen(function*() {
18
+ const localFs = yield* LocalFileSystem
19
+ const config = yield* ConfluenceConfig
20
+ const pathService = yield* Path.Path
21
+ const fs = yield* FileSystem.FileSystem
22
+
23
+ const docsPath = pathService.join(process.cwd(), config.docsPath)
24
+
25
+ // Interactive mode - show page tree, delete local file
26
+ yield* Console.log("Scanning page structure...")
27
+ const tree = yield* localFs.buildPageTree(docsPath, config.rootPageId, "Root")
28
+
29
+ // Flatten to choices (exclude pages without pageId - they're not on Confluence)
30
+ const allChoices = flattenPageTree(tree)
31
+ const choices = allChoices.filter((c) => c.value.pageId !== null)
32
+
33
+ if (choices.length === 0) {
34
+ yield* Console.log("No pages found to delete. Run 'confluence clone' or 'confluence pull' first.")
35
+ return
36
+ }
37
+
38
+ const selected = yield* Prompt.select({
39
+ message: "Select page to delete:",
40
+ choices
41
+ })
42
+
43
+ if (!selected.pageId || !selected.path) {
44
+ yield* Console.log("Selected page has no pageId - cannot delete.")
45
+ return
46
+ }
47
+
48
+ // Delete local file
49
+ const filePath = pathService.join(docsPath, selected.path)
50
+ yield* fs.remove(filePath)
51
+
52
+ yield* Console.log(`Deleted: ${selected.path}`)
53
+ yield* Console.log("")
54
+ yield* Console.log("Next steps:")
55
+ yield* Console.log(" 1. Run 'confluence commit' to stage the deletion")
56
+ yield* Console.log(" 2. Run 'confluence push' to delete from Confluence")
57
+ })).pipe(Command.withDescription("Delete a page locally (push to remove from Confluence)"))
@@ -0,0 +1,32 @@
1
+ /**
2
+ * CLI error handler.
3
+ *
4
+ * We use custom error handling instead of built-in options because:
5
+ * - Cause.pretty() includes stack traces which are noisy for CLI users
6
+ * - Logger.pretty is for logging, not error output
7
+ * - NodeRuntime.runMain error reporting shows full cause structure
8
+ */
9
+ import * as Cause from "effect/Cause"
10
+
11
+ /**
12
+ * Print errors to stderr without stack traces.
13
+ */
14
+ export const handleError = <E>(cause: Cause.Cause<E>): void => {
15
+ if (Cause.isEmpty(cause)) return
16
+
17
+ for (const error of Cause.failures(cause)) {
18
+ if (error && typeof error === "object" && "message" in error) {
19
+ process.stderr.write(`${(error as { message: string }).message}\n`)
20
+ } else {
21
+ process.stderr.write(`${String(error)}\n`)
22
+ }
23
+ }
24
+
25
+ for (const defect of Cause.defects(cause)) {
26
+ if (defect instanceof Error) {
27
+ process.stderr.write(`Error: ${defect.message}\n`)
28
+ } else {
29
+ process.stderr.write(`Error: ${String(defect)}\n`)
30
+ }
31
+ }
32
+ }