@knpkv/confluence-to-markdown 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +62 -0
  3. package/dist/Brand.d.ts +77 -0
  4. package/dist/Brand.d.ts.map +1 -0
  5. package/dist/Brand.js +44 -0
  6. package/dist/Brand.js.map +1 -0
  7. package/dist/ConfluenceClient.d.ts +140 -0
  8. package/dist/ConfluenceClient.d.ts.map +1 -0
  9. package/dist/ConfluenceClient.js +195 -0
  10. package/dist/ConfluenceClient.js.map +1 -0
  11. package/dist/ConfluenceConfig.d.ts +83 -0
  12. package/dist/ConfluenceConfig.d.ts.map +1 -0
  13. package/dist/ConfluenceConfig.js +122 -0
  14. package/dist/ConfluenceConfig.js.map +1 -0
  15. package/dist/ConfluenceError.d.ts +178 -0
  16. package/dist/ConfluenceError.d.ts.map +1 -0
  17. package/dist/ConfluenceError.js +131 -0
  18. package/dist/ConfluenceError.js.map +1 -0
  19. package/dist/LocalFileSystem.d.ts +85 -0
  20. package/dist/LocalFileSystem.d.ts.map +1 -0
  21. package/dist/LocalFileSystem.js +101 -0
  22. package/dist/LocalFileSystem.js.map +1 -0
  23. package/dist/MarkdownConverter.d.ts +50 -0
  24. package/dist/MarkdownConverter.d.ts.map +1 -0
  25. package/dist/MarkdownConverter.js +151 -0
  26. package/dist/MarkdownConverter.js.map +1 -0
  27. package/dist/Schemas.d.ts +225 -0
  28. package/dist/Schemas.d.ts.map +1 -0
  29. package/dist/Schemas.js +164 -0
  30. package/dist/Schemas.js.map +1 -0
  31. package/dist/SyncEngine.d.ts +132 -0
  32. package/dist/SyncEngine.d.ts.map +1 -0
  33. package/dist/SyncEngine.js +267 -0
  34. package/dist/SyncEngine.js.map +1 -0
  35. package/dist/bin.d.ts +3 -0
  36. package/dist/bin.d.ts.map +1 -0
  37. package/dist/bin.js +163 -0
  38. package/dist/bin.js.map +1 -0
  39. package/dist/index.d.ts +16 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +16 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/internal/frontmatter.d.ts +38 -0
  44. package/dist/internal/frontmatter.d.ts.map +1 -0
  45. package/dist/internal/frontmatter.js +69 -0
  46. package/dist/internal/frontmatter.js.map +1 -0
  47. package/dist/internal/hashUtils.d.ts +11 -0
  48. package/dist/internal/hashUtils.d.ts.map +1 -0
  49. package/dist/internal/hashUtils.js +17 -0
  50. package/dist/internal/hashUtils.js.map +1 -0
  51. package/dist/internal/pathUtils.d.ts +41 -0
  52. package/dist/internal/pathUtils.d.ts.map +1 -0
  53. package/dist/internal/pathUtils.js +69 -0
  54. package/dist/internal/pathUtils.js.map +1 -0
  55. package/package.json +113 -0
  56. package/src/Brand.ts +104 -0
  57. package/src/ConfluenceClient.ts +387 -0
  58. package/src/ConfluenceConfig.ts +184 -0
  59. package/src/ConfluenceError.ts +193 -0
  60. package/src/LocalFileSystem.ts +225 -0
  61. package/src/MarkdownConverter.ts +187 -0
  62. package/src/Schemas.ts +235 -0
  63. package/src/SyncEngine.ts +429 -0
  64. package/src/bin.ts +269 -0
  65. package/src/index.ts +35 -0
  66. package/src/internal/frontmatter.ts +98 -0
  67. package/src/internal/hashUtils.ts +19 -0
  68. package/src/internal/pathUtils.ts +77 -0
  69. package/test/Brand.test.ts +72 -0
  70. package/test/MarkdownConverter.test.ts +108 -0
  71. package/test/Schemas.test.ts +99 -0
  72. package/tsconfig.json +32 -0
  73. package/vitest.config.integration.ts +12 -0
  74. package/vitest.config.ts +13 -0
@@ -0,0 +1,387 @@
1
+ /**
2
+ * Confluence REST API v2 client service.
3
+ *
4
+ * @module
5
+ */
6
+ import * as HttpClient from "@effect/platform/HttpClient"
7
+ import * as HttpClientRequest from "@effect/platform/HttpClientRequest"
8
+ import * as Context from "effect/Context"
9
+ import * as Effect from "effect/Effect"
10
+ import * as Layer from "effect/Layer"
11
+ import * as Schedule from "effect/Schedule"
12
+ import * as Schema from "effect/Schema"
13
+ import type { PageId } from "./Brand.js"
14
+ import { ApiError, RateLimitError } from "./ConfluenceError.js"
15
+ import type { PageChildrenResponse, PageListItem, PageResponse } from "./Schemas.js"
16
+ import { PageChildrenResponseSchema, PageResponseSchema } from "./Schemas.js"
17
+
18
+ /**
19
+ * Request to create a new page.
20
+ *
21
+ * @category Types
22
+ */
23
+ export interface CreatePageRequest {
24
+ readonly spaceId: string
25
+ readonly title: string
26
+ readonly parentId?: string
27
+ readonly body: {
28
+ readonly representation: "storage"
29
+ readonly value: string
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Request to update an existing page.
35
+ *
36
+ * @category Types
37
+ */
38
+ export interface UpdatePageRequest {
39
+ readonly id: string
40
+ readonly title: string
41
+ readonly status?: "current" | "draft"
42
+ readonly version: {
43
+ readonly number: number
44
+ readonly message?: string
45
+ }
46
+ readonly body: {
47
+ readonly representation: "storage"
48
+ readonly value: string
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Confluence REST API v2 client service.
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * import { ConfluenceClient } from "@knpkv/confluence-to-markdown/ConfluenceClient"
58
+ * import { Effect } from "effect"
59
+ *
60
+ * const program = Effect.gen(function* () {
61
+ * const client = yield* ConfluenceClient
62
+ * const page = yield* client.getPage("12345")
63
+ * console.log(page.title)
64
+ * })
65
+ * ```
66
+ *
67
+ * @category Client
68
+ */
69
+ export class ConfluenceClient extends Context.Tag(
70
+ "@knpkv/confluence-to-markdown/ConfluenceClient"
71
+ )<
72
+ ConfluenceClient,
73
+ {
74
+ /**
75
+ * Get a page by ID.
76
+ */
77
+ readonly getPage: (id: PageId) => Effect.Effect<PageResponse, ApiError | RateLimitError>
78
+
79
+ /**
80
+ * Get children of a page.
81
+ */
82
+ readonly getChildren: (id: PageId) => Effect.Effect<PageChildrenResponse, ApiError | RateLimitError>
83
+
84
+ /**
85
+ * Get all children recursively (handles pagination).
86
+ */
87
+ readonly getAllChildren: (id: PageId) => Effect.Effect<ReadonlyArray<PageListItem>, ApiError | RateLimitError>
88
+
89
+ /**
90
+ * Create a new page.
91
+ */
92
+ readonly createPage: (request: CreatePageRequest) => Effect.Effect<PageResponse, ApiError | RateLimitError>
93
+
94
+ /**
95
+ * Update an existing page.
96
+ */
97
+ readonly updatePage: (request: UpdatePageRequest) => Effect.Effect<PageResponse, ApiError | RateLimitError>
98
+
99
+ /**
100
+ * Delete a page.
101
+ */
102
+ readonly deletePage: (id: PageId) => Effect.Effect<void, ApiError | RateLimitError>
103
+ }
104
+ >() {}
105
+
106
+ /**
107
+ * Configuration for the Confluence client.
108
+ *
109
+ * @category Config
110
+ */
111
+ export interface ConfluenceClientConfig {
112
+ readonly baseUrl: string
113
+ readonly auth: {
114
+ readonly type: "token"
115
+ readonly email: string
116
+ readonly token: string
117
+ } | {
118
+ readonly type: "oauth2"
119
+ readonly accessToken: string
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Rate limit retry schedule with exponential backoff.
125
+ */
126
+ const rateLimitSchedule = Schedule.exponential("1 second").pipe(
127
+ Schedule.union(Schedule.spaced("30 seconds")),
128
+ Schedule.whileInput<RateLimitError | ApiError>((error) => error._tag === "RateLimitError"),
129
+ Schedule.intersect(Schedule.recurs(3))
130
+ )
131
+
132
+ /**
133
+ * Create the Confluence client service.
134
+ */
135
+ const make = (
136
+ config: ConfluenceClientConfig
137
+ ): Effect.Effect<Context.Tag.Service<typeof ConfluenceClient>, never, HttpClient.HttpClient> =>
138
+ Effect.gen(function*() {
139
+ const httpClient = yield* HttpClient.HttpClient
140
+
141
+ const authHeader = config.auth.type === "token"
142
+ ? `Basic ${Buffer.from(`${config.auth.email}:${config.auth.token}`).toString("base64")}`
143
+ : `Bearer ${config.auth.accessToken}`
144
+
145
+ const baseRequest = HttpClientRequest.get(`${config.baseUrl}/wiki/api/v2`).pipe(
146
+ HttpClientRequest.setHeader("Authorization", authHeader),
147
+ HttpClientRequest.setHeader("Accept", "application/json"),
148
+ HttpClientRequest.setHeader("Content-Type", "application/json")
149
+ )
150
+
151
+ /**
152
+ * Make an HTTP request to the Confluence API.
153
+ * Returns raw JSON - callers must validate with Schema.decodeUnknown.
154
+ */
155
+ const request = (
156
+ method: "GET" | "POST" | "PUT" | "DELETE",
157
+ path: string,
158
+ body?: unknown
159
+ ): Effect.Effect<unknown, ApiError | RateLimitError, never> =>
160
+ Effect.gen(function*() {
161
+ let req = baseRequest.pipe(
162
+ HttpClientRequest.setMethod(method),
163
+ HttpClientRequest.setUrl(`${config.baseUrl}/wiki/api/v2${path}`)
164
+ )
165
+
166
+ if (body !== undefined) {
167
+ req = HttpClientRequest.bodyJson(req, body).pipe(
168
+ Effect.catchAll(() => Effect.succeed(req)),
169
+ Effect.runSync
170
+ )
171
+ }
172
+
173
+ const response = yield* httpClient.execute(req).pipe(
174
+ Effect.mapError((error) =>
175
+ new ApiError({
176
+ status: 0,
177
+ message: `Request failed: ${error.message}`,
178
+ endpoint: path
179
+ })
180
+ )
181
+ )
182
+
183
+ if (response.status === 429) {
184
+ const retryAfterHeader = response.headers["retry-after"]
185
+ const retryAfter = retryAfterHeader ? parseInt(retryAfterHeader, 10) : undefined
186
+ return yield* Effect.fail(
187
+ retryAfter !== undefined
188
+ ? new RateLimitError({ retryAfter })
189
+ : new RateLimitError({})
190
+ )
191
+ }
192
+
193
+ if (response.status >= 400) {
194
+ const text = yield* response.text.pipe(
195
+ Effect.catchAll(() => Effect.succeed(""))
196
+ )
197
+ return yield* Effect.fail(
198
+ new ApiError({
199
+ status: response.status,
200
+ message: text || `HTTP ${response.status}`,
201
+ endpoint: path
202
+ })
203
+ )
204
+ }
205
+
206
+ if (method === "DELETE" && response.status === 204) {
207
+ return undefined
208
+ }
209
+
210
+ const json = yield* response.json.pipe(
211
+ Effect.mapError((error) =>
212
+ new ApiError({
213
+ status: response.status,
214
+ message: `Failed to parse response: ${error}`,
215
+ endpoint: path
216
+ })
217
+ )
218
+ )
219
+
220
+ return json
221
+ }).pipe(
222
+ Effect.retry(rateLimitSchedule)
223
+ )
224
+
225
+ const getPage = (id: PageId): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
226
+ Effect.gen(function*() {
227
+ const raw = yield* request(
228
+ "GET",
229
+ `/pages/${id}?body-format=storage`
230
+ )
231
+ return yield* Schema.decodeUnknown(PageResponseSchema)(raw).pipe(
232
+ Effect.mapError((error) =>
233
+ new ApiError({
234
+ status: 0,
235
+ message: `Invalid response schema: ${error.message}`,
236
+ endpoint: `/pages/${id}`,
237
+ pageId: id
238
+ })
239
+ )
240
+ )
241
+ })
242
+
243
+ const getChildren = (id: PageId): Effect.Effect<PageChildrenResponse, ApiError | RateLimitError> =>
244
+ Effect.gen(function*() {
245
+ const raw = yield* request(
246
+ "GET",
247
+ `/pages/${id}/children?body-format=storage`
248
+ )
249
+ return yield* Schema.decodeUnknown(PageChildrenResponseSchema)(raw).pipe(
250
+ Effect.mapError((error) =>
251
+ new ApiError({
252
+ status: 0,
253
+ message: `Invalid response schema: ${error.message}`,
254
+ endpoint: `/pages/${id}/children`,
255
+ pageId: id
256
+ })
257
+ )
258
+ )
259
+ })
260
+
261
+ const getAllChildren = (id: PageId): Effect.Effect<ReadonlyArray<PageListItem>, ApiError | RateLimitError> =>
262
+ Effect.gen(function*() {
263
+ const allChildren: Array<PageListItem> = []
264
+ let cursor: string | undefined
265
+ let iterations = 0
266
+ const maxIterations = 100 // Prevent unbounded pagination
267
+
268
+ do {
269
+ if (iterations >= maxIterations) {
270
+ return yield* Effect.fail(
271
+ new ApiError({
272
+ status: 0,
273
+ message: `Pagination limit exceeded: more than ${maxIterations} pages of children`,
274
+ endpoint: `/pages/${id}/children`,
275
+ pageId: id
276
+ })
277
+ )
278
+ }
279
+
280
+ const path = cursor
281
+ ? `/pages/${id}/children?body-format=storage&cursor=${cursor}`
282
+ : `/pages/${id}/children?body-format=storage`
283
+
284
+ const raw = yield* request("GET", path)
285
+ const response = yield* Schema.decodeUnknown(PageChildrenResponseSchema)(raw).pipe(
286
+ Effect.mapError((error) =>
287
+ new ApiError({
288
+ status: 0,
289
+ message: `Invalid response schema: ${error.message}`,
290
+ endpoint: path,
291
+ pageId: id
292
+ })
293
+ )
294
+ )
295
+
296
+ for (const child of response.results) {
297
+ allChildren.push(child)
298
+ }
299
+
300
+ // Extract cursor from next link if present
301
+ cursor = response._links?.next
302
+ ? new URL(response._links.next, config.baseUrl).searchParams.get("cursor") ?? undefined
303
+ : undefined
304
+
305
+ iterations++
306
+ } while (cursor)
307
+
308
+ return allChildren
309
+ })
310
+
311
+ const createPage = (req: CreatePageRequest): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
312
+ Effect.gen(function*() {
313
+ const raw = yield* request("POST", "/pages", req)
314
+ return yield* Schema.decodeUnknown(PageResponseSchema)(raw).pipe(
315
+ Effect.mapError((error) =>
316
+ new ApiError({
317
+ status: 0,
318
+ message: `Invalid response schema: ${error.message}`,
319
+ endpoint: "/pages"
320
+ })
321
+ )
322
+ )
323
+ })
324
+
325
+ const updatePage = (req: UpdatePageRequest): Effect.Effect<PageResponse, ApiError | RateLimitError> =>
326
+ Effect.gen(function*() {
327
+ const raw = yield* request("PUT", `/pages/${req.id}`, req)
328
+ return yield* Schema.decodeUnknown(PageResponseSchema)(raw).pipe(
329
+ Effect.mapError((error) =>
330
+ new ApiError({
331
+ status: 0,
332
+ message: `Invalid response schema: ${error.message}`,
333
+ endpoint: `/pages/${req.id}`,
334
+ pageId: req.id
335
+ })
336
+ )
337
+ )
338
+ })
339
+
340
+ const deletePage = (id: PageId): Effect.Effect<void, ApiError | RateLimitError> =>
341
+ request("DELETE", `/pages/${id}`).pipe(Effect.asVoid)
342
+
343
+ return ConfluenceClient.of({
344
+ getPage,
345
+ getChildren,
346
+ getAllChildren,
347
+ createPage,
348
+ updatePage,
349
+ deletePage
350
+ })
351
+ })
352
+
353
+ /**
354
+ * Layer that provides ConfluenceClient with direct configuration.
355
+ *
356
+ * @example
357
+ * ```typescript
358
+ * import { ConfluenceClient } from "@knpkv/confluence-to-markdown/ConfluenceClient"
359
+ * import { NodeHttpClient } from "@effect/platform-node"
360
+ * import { Effect } from "effect"
361
+ *
362
+ * const program = Effect.gen(function* () {
363
+ * const client = yield* ConfluenceClient
364
+ * const page = yield* client.getPage("12345")
365
+ * console.log(page.title)
366
+ * })
367
+ *
368
+ * Effect.runPromise(
369
+ * program.pipe(
370
+ * Effect.provide(ConfluenceClient.layer({
371
+ * baseUrl: "https://yoursite.atlassian.net",
372
+ * auth: {
373
+ * type: "token",
374
+ * email: "you@example.com",
375
+ * token: process.env.CONFLUENCE_API_KEY
376
+ * }
377
+ * })),
378
+ * Effect.provide(NodeHttpClient.layer)
379
+ * )
380
+ * )
381
+ * ```
382
+ *
383
+ * @category Layers
384
+ */
385
+ export const layer = (
386
+ config: ConfluenceClientConfig
387
+ ): Layer.Layer<ConfluenceClient, never, HttpClient.HttpClient> => Layer.effect(ConfluenceClient, make(config))
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Configuration service for Confluence sync.
3
+ *
4
+ * @module
5
+ */
6
+ import * as FileSystem from "@effect/platform/FileSystem"
7
+ import * as Path from "@effect/platform/Path"
8
+ import * as Context from "effect/Context"
9
+ import * as Effect from "effect/Effect"
10
+ import * as Layer from "effect/Layer"
11
+ import * as Schema from "effect/Schema"
12
+ import type { PageId } from "./Brand.js"
13
+ import { ConfigNotFoundError, ConfigParseError } from "./ConfluenceError.js"
14
+ import type { ConfluenceConfigFile } from "./Schemas.js"
15
+ import { ConfluenceConfigFileSchema } from "./Schemas.js"
16
+
17
+ /**
18
+ * Configuration service for Confluence operations.
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * import { ConfluenceConfig } from "@knpkv/confluence-to-markdown/ConfluenceConfig"
23
+ * import { Effect } from "effect"
24
+ *
25
+ * const program = Effect.gen(function* () {
26
+ * const config = yield* ConfluenceConfig
27
+ * console.log(config.rootPageId)
28
+ * console.log(config.baseUrl)
29
+ * })
30
+ * ```
31
+ *
32
+ * @category Config
33
+ */
34
+ export class ConfluenceConfig extends Context.Tag(
35
+ "@knpkv/confluence-to-markdown/ConfluenceConfig"
36
+ )<
37
+ ConfluenceConfig,
38
+ {
39
+ /** Root page ID to sync from */
40
+ readonly rootPageId: PageId
41
+ /** Confluence Cloud base URL */
42
+ readonly baseUrl: string
43
+ /** Optional space key */
44
+ readonly spaceKey?: string
45
+ /** Local docs path */
46
+ readonly docsPath: string
47
+ /** Glob patterns to exclude */
48
+ readonly excludePatterns: ReadonlyArray<string>
49
+ }
50
+ >() {}
51
+
52
+ /**
53
+ * Default config file name.
54
+ */
55
+ const CONFIG_FILE_NAME = ".confluence.json"
56
+
57
+ /**
58
+ * Load configuration from a file.
59
+ */
60
+ const loadConfig = (
61
+ configPath: string
62
+ ): Effect.Effect<ConfluenceConfigFile, ConfigNotFoundError | ConfigParseError, FileSystem.FileSystem> =>
63
+ Effect.gen(function*() {
64
+ const fs = yield* FileSystem.FileSystem
65
+
66
+ const exists = yield* fs.exists(configPath).pipe(
67
+ Effect.catchAll(() => Effect.succeed(false))
68
+ )
69
+ if (!exists) {
70
+ return yield* Effect.fail(new ConfigNotFoundError({ path: configPath }))
71
+ }
72
+
73
+ const content = yield* fs.readFileString(configPath).pipe(
74
+ Effect.mapError((cause) => new ConfigParseError({ path: configPath, cause }))
75
+ )
76
+
77
+ const json = yield* Effect.try({
78
+ try: () => JSON.parse(content) as unknown,
79
+ catch: (cause) => new ConfigParseError({ path: configPath, cause })
80
+ })
81
+
82
+ return yield* Schema.decodeUnknown(ConfluenceConfigFileSchema)(json).pipe(
83
+ Effect.mapError((cause) => new ConfigParseError({ path: configPath, cause }))
84
+ )
85
+ })
86
+
87
+ /**
88
+ * Layer that provides ConfluenceConfig from a config file.
89
+ *
90
+ * @example
91
+ * ```typescript
92
+ * import { ConfluenceConfig } from "@knpkv/confluence-to-markdown/ConfluenceConfig"
93
+ * import { NodeFileSystem } from "@effect/platform-node"
94
+ * import { Effect } from "effect"
95
+ *
96
+ * const program = Effect.gen(function* () {
97
+ * const config = yield* ConfluenceConfig
98
+ * console.log(config.rootPageId)
99
+ * })
100
+ *
101
+ * Effect.runPromise(
102
+ * program.pipe(
103
+ * Effect.provide(ConfluenceConfig.layer()),
104
+ * Effect.provide(NodeFileSystem.layer)
105
+ * )
106
+ * )
107
+ * ```
108
+ *
109
+ * @category Layers
110
+ */
111
+ export const layer = (
112
+ configPath?: string
113
+ ): Layer.Layer<ConfluenceConfig, ConfigNotFoundError | ConfigParseError, FileSystem.FileSystem | Path.Path> =>
114
+ Layer.effect(
115
+ ConfluenceConfig,
116
+ Effect.gen(function*() {
117
+ const path = yield* Path.Path
118
+ const resolvedPath = configPath ?? path.join(process.cwd(), CONFIG_FILE_NAME)
119
+ const config = yield* loadConfig(resolvedPath)
120
+
121
+ return ConfluenceConfig.of({
122
+ rootPageId: config.rootPageId,
123
+ baseUrl: config.baseUrl,
124
+ ...(config.spaceKey !== undefined ? { spaceKey: config.spaceKey } : {}),
125
+ docsPath: config.docsPath,
126
+ excludePatterns: config.excludePatterns
127
+ })
128
+ })
129
+ )
130
+
131
+ /**
132
+ * Layer that provides ConfluenceConfig with direct values.
133
+ *
134
+ * @category Layers
135
+ */
136
+ export const layerFromValues = (
137
+ config: ConfluenceConfigFile
138
+ ): Layer.Layer<ConfluenceConfig> =>
139
+ Layer.succeed(
140
+ ConfluenceConfig,
141
+ ConfluenceConfig.of({
142
+ rootPageId: config.rootPageId,
143
+ baseUrl: config.baseUrl,
144
+ ...(config.spaceKey !== undefined ? { spaceKey: config.spaceKey } : {}),
145
+ docsPath: config.docsPath,
146
+ excludePatterns: config.excludePatterns
147
+ })
148
+ )
149
+
150
+ /**
151
+ * Create a new config file.
152
+ *
153
+ * @category Utilities
154
+ */
155
+ export const createConfigFile = (
156
+ rootPageId: string,
157
+ baseUrl: string,
158
+ configPath?: string
159
+ ): Effect.Effect<string, ConfigParseError, FileSystem.FileSystem | Path.Path> =>
160
+ Effect.gen(function*() {
161
+ const fs = yield* FileSystem.FileSystem
162
+ const pathService = yield* Path.Path
163
+
164
+ const resolvedPath = configPath ?? pathService.join(process.cwd(), CONFIG_FILE_NAME)
165
+
166
+ const config: ConfluenceConfigFile = {
167
+ rootPageId: rootPageId as PageId,
168
+ baseUrl,
169
+ docsPath: ".docs/confluence",
170
+ excludePatterns: []
171
+ }
172
+
173
+ // Validate the config
174
+ yield* Schema.decodeUnknown(ConfluenceConfigFileSchema)(config).pipe(
175
+ Effect.mapError((cause) => new ConfigParseError({ path: resolvedPath, cause }))
176
+ )
177
+
178
+ const content = JSON.stringify(config, null, 2)
179
+ yield* fs.writeFileString(resolvedPath, content).pipe(
180
+ Effect.mapError((cause) => new ConfigParseError({ path: resolvedPath, cause }))
181
+ )
182
+
183
+ return resolvedPath
184
+ })