@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.
- package/CHANGELOG.md +60 -0
- package/LICENSE +21 -0
- package/README.md +282 -14
- package/dist/ConfluenceAuth.d.ts +76 -0
- package/dist/ConfluenceAuth.d.ts.map +1 -0
- package/dist/ConfluenceAuth.js +356 -0
- package/dist/ConfluenceAuth.js.map +1 -0
- package/dist/ConfluenceClient.d.ts +26 -2
- package/dist/ConfluenceClient.d.ts.map +1 -1
- package/dist/ConfluenceClient.js +98 -92
- package/dist/ConfluenceClient.js.map +1 -1
- package/dist/ConfluenceConfig.d.ts +4 -24
- package/dist/ConfluenceConfig.d.ts.map +1 -1
- package/dist/ConfluenceConfig.js +45 -7
- package/dist/ConfluenceConfig.js.map +1 -1
- package/dist/ConfluenceError.d.ts +89 -6
- package/dist/ConfluenceError.d.ts.map +1 -1
- package/dist/ConfluenceError.js +88 -5
- package/dist/ConfluenceError.js.map +1 -1
- package/dist/GitError.d.ts +103 -0
- package/dist/GitError.d.ts.map +1 -0
- package/dist/GitError.js +85 -0
- package/dist/GitError.js.map +1 -0
- package/dist/GitService.d.ts +175 -0
- package/dist/GitService.d.ts.map +1 -0
- package/dist/GitService.js +431 -0
- package/dist/GitService.js.map +1 -0
- package/dist/LocalFileSystem.d.ts +29 -4
- package/dist/LocalFileSystem.d.ts.map +1 -1
- package/dist/LocalFileSystem.js +80 -6
- package/dist/LocalFileSystem.js.map +1 -1
- package/dist/MarkdownConverter.d.ts +49 -2
- package/dist/MarkdownConverter.d.ts.map +1 -1
- package/dist/MarkdownConverter.js +73 -111
- package/dist/MarkdownConverter.js.map +1 -1
- package/dist/SchemaConverterError.d.ts +108 -0
- package/dist/SchemaConverterError.d.ts.map +1 -0
- package/dist/SchemaConverterError.js +84 -0
- package/dist/SchemaConverterError.js.map +1 -0
- package/dist/Schemas.d.ts +225 -1
- package/dist/Schemas.d.ts.map +1 -1
- package/dist/Schemas.js +155 -6
- package/dist/Schemas.js.map +1 -1
- package/dist/SyncEngine.d.ts +30 -20
- package/dist/SyncEngine.d.ts.map +1 -1
- package/dist/SyncEngine.js +566 -117
- package/dist/SyncEngine.js.map +1 -1
- package/dist/ast/BlockNode.d.ts +468 -0
- package/dist/ast/BlockNode.d.ts.map +1 -0
- package/dist/ast/BlockNode.js +319 -0
- package/dist/ast/BlockNode.js.map +1 -0
- package/dist/ast/Document.d.ts +244 -0
- package/dist/ast/Document.d.ts.map +1 -0
- package/dist/ast/Document.js +69 -0
- package/dist/ast/Document.js.map +1 -0
- package/dist/ast/InlineNode.d.ts +477 -0
- package/dist/ast/InlineNode.d.ts.map +1 -0
- package/dist/ast/InlineNode.js +263 -0
- package/dist/ast/InlineNode.js.map +1 -0
- package/dist/ast/MacroNode.d.ts +267 -0
- package/dist/ast/MacroNode.d.ts.map +1 -0
- package/dist/ast/MacroNode.js +164 -0
- package/dist/ast/MacroNode.js.map +1 -0
- package/dist/ast/index.d.ts +10 -0
- package/dist/ast/index.d.ts.map +1 -0
- package/dist/ast/index.js +14 -0
- package/dist/ast/index.js.map +1 -0
- package/dist/bin.js +33 -149
- package/dist/bin.js.map +1 -1
- package/dist/commands/auth.d.ts +15 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +86 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/clone.d.ts +12 -0
- package/dist/commands/clone.d.ts.map +1 -0
- package/dist/commands/clone.js +93 -0
- package/dist/commands/clone.js.map +1 -0
- package/dist/commands/delete.d.ts +13 -0
- package/dist/commands/delete.d.ts.map +1 -0
- package/dist/commands/delete.js +48 -0
- package/dist/commands/delete.js.map +1 -0
- package/dist/commands/errorHandler.d.ts +14 -0
- package/dist/commands/errorHandler.d.ts.map +1 -0
- package/dist/commands/errorHandler.js +33 -0
- package/dist/commands/errorHandler.js.map +1 -0
- package/dist/commands/git.d.ts +22 -0
- package/dist/commands/git.d.ts.map +1 -0
- package/dist/commands/git.js +72 -0
- package/dist/commands/git.js.map +1 -0
- package/dist/commands/index.d.ts +11 -0
- package/dist/commands/index.d.ts.map +1 -0
- package/dist/commands/index.js +11 -0
- package/dist/commands/index.js.map +1 -0
- package/dist/commands/layers.d.ts +31 -0
- package/dist/commands/layers.d.ts.map +1 -0
- package/dist/commands/layers.js +137 -0
- package/dist/commands/layers.js.map +1 -0
- package/dist/commands/new.d.ts +9 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +80 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/pageTree.d.ts +18 -0
- package/dist/commands/pageTree.d.ts.map +1 -0
- package/dist/commands/pageTree.js +20 -0
- package/dist/commands/pageTree.js.map +1 -0
- package/dist/commands/shared.d.ts +15 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +27 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/sync.d.ts +15 -0
- package/dist/commands/sync.d.ts.map +1 -0
- package/dist/commands/sync.js +101 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/NodeLayers.d.ts +7 -0
- package/dist/internal/NodeLayers.d.ts.map +1 -0
- package/dist/internal/NodeLayers.js +19 -0
- package/dist/internal/NodeLayers.js.map +1 -0
- package/dist/internal/frontmatter.d.ts +10 -0
- package/dist/internal/frontmatter.d.ts.map +1 -1
- package/dist/internal/frontmatter.js +16 -0
- package/dist/internal/frontmatter.js.map +1 -1
- package/dist/internal/gitCommands.d.ts +78 -0
- package/dist/internal/gitCommands.d.ts.map +1 -0
- package/dist/internal/gitCommands.js +156 -0
- package/dist/internal/gitCommands.js.map +1 -0
- package/dist/internal/hashUtils.d.ts +42 -1
- package/dist/internal/hashUtils.d.ts.map +1 -1
- package/dist/internal/hashUtils.js +38 -2
- package/dist/internal/hashUtils.js.map +1 -1
- package/dist/internal/oauthServer.d.ts +55 -0
- package/dist/internal/oauthServer.d.ts.map +1 -0
- package/dist/internal/oauthServer.js +110 -0
- package/dist/internal/oauthServer.js.map +1 -0
- package/dist/internal/pathUtils.d.ts +21 -4
- package/dist/internal/pathUtils.d.ts.map +1 -1
- package/dist/internal/pathUtils.js +24 -13
- package/dist/internal/pathUtils.js.map +1 -1
- package/dist/internal/tokenStorage.d.ts +75 -0
- package/dist/internal/tokenStorage.d.ts.map +1 -0
- package/dist/internal/tokenStorage.js +149 -0
- package/dist/internal/tokenStorage.js.map +1 -0
- package/dist/internal/userCache.d.ts +42 -0
- package/dist/internal/userCache.d.ts.map +1 -0
- package/dist/internal/userCache.js +51 -0
- package/dist/internal/userCache.js.map +1 -0
- package/dist/parsers/ConfluenceParser.d.ts +26 -0
- package/dist/parsers/ConfluenceParser.d.ts.map +1 -0
- package/dist/parsers/ConfluenceParser.js +792 -0
- package/dist/parsers/ConfluenceParser.js.map +1 -0
- package/dist/parsers/MarkdownParser.d.ts +26 -0
- package/dist/parsers/MarkdownParser.d.ts.map +1 -0
- package/dist/parsers/MarkdownParser.js +873 -0
- package/dist/parsers/MarkdownParser.js.map +1 -0
- package/dist/parsers/index.d.ts +8 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +8 -0
- package/dist/parsers/index.js.map +1 -0
- package/dist/schemas/ConfluenceSchema.d.ts +21 -0
- package/dist/schemas/ConfluenceSchema.d.ts.map +1 -0
- package/dist/schemas/ConfluenceSchema.js +38 -0
- package/dist/schemas/ConfluenceSchema.js.map +1 -0
- package/dist/schemas/ConversionSchema.d.ts +35 -0
- package/dist/schemas/ConversionSchema.d.ts.map +1 -0
- package/dist/schemas/ConversionSchema.js +208 -0
- package/dist/schemas/ConversionSchema.js.map +1 -0
- package/dist/schemas/MarkdownSchema.d.ts +21 -0
- package/dist/schemas/MarkdownSchema.d.ts.map +1 -0
- package/dist/schemas/MarkdownSchema.js +38 -0
- package/dist/schemas/MarkdownSchema.js.map +1 -0
- package/dist/schemas/hast/HastFromHtml.d.ts +27 -0
- package/dist/schemas/hast/HastFromHtml.d.ts.map +1 -0
- package/dist/schemas/hast/HastFromHtml.js +107 -0
- package/dist/schemas/hast/HastFromHtml.js.map +1 -0
- package/dist/schemas/hast/HastSchema.d.ts +195 -0
- package/dist/schemas/hast/HastSchema.d.ts.map +1 -0
- package/dist/schemas/hast/HastSchema.js +183 -0
- package/dist/schemas/hast/HastSchema.js.map +1 -0
- package/dist/schemas/hast/index.d.ts +9 -0
- package/dist/schemas/hast/index.d.ts.map +1 -0
- package/dist/schemas/hast/index.js +3 -0
- package/dist/schemas/hast/index.js.map +1 -0
- package/dist/schemas/index.d.ts +14 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +16 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/mdast/MdastFromMarkdown.d.ts +30 -0
- package/dist/schemas/mdast/MdastFromMarkdown.d.ts.map +1 -0
- package/dist/schemas/mdast/MdastFromMarkdown.js +79 -0
- package/dist/schemas/mdast/MdastFromMarkdown.js.map +1 -0
- package/dist/schemas/mdast/MdastSchema.d.ts +385 -0
- package/dist/schemas/mdast/MdastSchema.d.ts.map +1 -0
- package/dist/schemas/mdast/MdastSchema.js +266 -0
- package/dist/schemas/mdast/MdastSchema.js.map +1 -0
- package/dist/schemas/mdast/index.d.ts +10 -0
- package/dist/schemas/mdast/index.d.ts.map +1 -0
- package/dist/schemas/mdast/index.js +4 -0
- package/dist/schemas/mdast/index.js.map +1 -0
- package/dist/schemas/mdast/mdastToString.d.ts +13 -0
- package/dist/schemas/mdast/mdastToString.d.ts.map +1 -0
- package/dist/schemas/mdast/mdastToString.js +85 -0
- package/dist/schemas/mdast/mdastToString.js.map +1 -0
- package/dist/schemas/nodes/block/BlockSchema.d.ts +43 -0
- package/dist/schemas/nodes/block/BlockSchema.d.ts.map +1 -0
- package/dist/schemas/nodes/block/BlockSchema.js +634 -0
- package/dist/schemas/nodes/block/BlockSchema.js.map +1 -0
- package/dist/schemas/nodes/block/index.d.ts +7 -0
- package/dist/schemas/nodes/block/index.d.ts.map +1 -0
- package/dist/schemas/nodes/block/index.js +7 -0
- package/dist/schemas/nodes/block/index.js.map +1 -0
- package/dist/schemas/nodes/index.d.ts +9 -0
- package/dist/schemas/nodes/index.d.ts.map +1 -0
- package/dist/schemas/nodes/index.js +12 -0
- package/dist/schemas/nodes/index.js.map +1 -0
- package/dist/schemas/nodes/inline/InlineSchema.d.ts +48 -0
- package/dist/schemas/nodes/inline/InlineSchema.d.ts.map +1 -0
- package/dist/schemas/nodes/inline/InlineSchema.js +436 -0
- package/dist/schemas/nodes/inline/InlineSchema.js.map +1 -0
- package/dist/schemas/nodes/inline/index.d.ts +7 -0
- package/dist/schemas/nodes/inline/index.d.ts.map +1 -0
- package/dist/schemas/nodes/inline/index.js +7 -0
- package/dist/schemas/nodes/inline/index.js.map +1 -0
- package/dist/schemas/nodes/macro/MacroSchema.d.ts +27 -0
- package/dist/schemas/nodes/macro/MacroSchema.d.ts.map +1 -0
- package/dist/schemas/nodes/macro/MacroSchema.js +162 -0
- package/dist/schemas/nodes/macro/MacroSchema.js.map +1 -0
- package/dist/schemas/nodes/macro/index.d.ts +7 -0
- package/dist/schemas/nodes/macro/index.d.ts.map +1 -0
- package/dist/schemas/nodes/macro/index.js +7 -0
- package/dist/schemas/nodes/macro/index.js.map +1 -0
- package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts +24 -0
- package/dist/schemas/preprocessing/ConfluencePreprocessor.d.ts.map +1 -0
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js +351 -0
- package/dist/schemas/preprocessing/ConfluencePreprocessor.js.map +1 -0
- package/dist/schemas/preprocessing/index.d.ts +8 -0
- package/dist/schemas/preprocessing/index.d.ts.map +1 -0
- package/dist/schemas/preprocessing/index.js +2 -0
- package/dist/schemas/preprocessing/index.js.map +1 -0
- package/dist/serializers/ConfluenceSerializer.d.ts +30 -0
- package/dist/serializers/ConfluenceSerializer.d.ts.map +1 -0
- package/dist/serializers/ConfluenceSerializer.js +551 -0
- package/dist/serializers/ConfluenceSerializer.js.map +1 -0
- package/dist/serializers/MarkdownSerializer.d.ts +34 -0
- package/dist/serializers/MarkdownSerializer.d.ts.map +1 -0
- package/dist/serializers/MarkdownSerializer.js +355 -0
- package/dist/serializers/MarkdownSerializer.js.map +1 -0
- package/dist/serializers/index.d.ts +8 -0
- package/dist/serializers/index.d.ts.map +1 -0
- package/dist/serializers/index.js +8 -0
- package/dist/serializers/index.js.map +1 -0
- package/package.json +27 -16
- package/src/ConfluenceAuth.ts +571 -0
- package/src/ConfluenceClient.ts +188 -156
- package/src/ConfluenceConfig.ts +63 -7
- package/src/ConfluenceError.ts +110 -14
- package/src/GitError.ts +92 -0
- package/src/GitService.ts +859 -0
- package/src/LocalFileSystem.ts +179 -9
- package/src/MarkdownConverter.ts +126 -122
- package/src/SchemaConverterError.ts +108 -0
- package/src/Schemas.ts +223 -6
- package/src/SyncEngine.ts +745 -162
- package/src/ast/BlockNode.ts +425 -0
- package/src/ast/Document.ts +90 -0
- package/src/ast/InlineNode.ts +323 -0
- package/src/ast/MacroNode.ts +245 -0
- package/src/ast/index.ts +83 -0
- package/src/bin.ts +50 -249
- package/src/commands/auth.ts +117 -0
- package/src/commands/clone.ts +145 -0
- package/src/commands/delete.ts +57 -0
- package/src/commands/errorHandler.ts +32 -0
- package/src/commands/git.ts +114 -0
- package/src/commands/index.ts +10 -0
- package/src/commands/layers.ts +211 -0
- package/src/commands/new.ts +99 -0
- package/src/commands/pageTree.ts +40 -0
- package/src/commands/shared.ts +35 -0
- package/src/commands/sync.ts +129 -0
- package/src/index.ts +21 -1
- package/src/internal/NodeLayers.ts +21 -0
- package/src/internal/frontmatter.ts +21 -0
- package/src/internal/gitCommands.ts +229 -0
- package/src/internal/hashUtils.ts +65 -3
- package/src/internal/oauthServer.ts +199 -0
- package/src/internal/pathUtils.ts +34 -17
- package/src/internal/tokenStorage.ts +240 -0
- package/src/internal/userCache.ts +90 -0
- package/src/parsers/ConfluenceParser.ts +950 -0
- package/src/parsers/MarkdownParser.ts +1198 -0
- package/src/parsers/index.ts +8 -0
- package/src/schemas/ConfluenceSchema.ts +56 -0
- package/src/schemas/ConversionSchema.ts +318 -0
- package/src/schemas/MarkdownSchema.ts +56 -0
- package/src/schemas/hast/HastFromHtml.ts +153 -0
- package/src/schemas/hast/HastSchema.ts +274 -0
- package/src/schemas/hast/index.ts +35 -0
- package/src/schemas/index.ts +20 -0
- package/src/schemas/mdast/MdastFromMarkdown.ts +118 -0
- package/src/schemas/mdast/MdastSchema.ts +566 -0
- package/src/schemas/mdast/index.ts +59 -0
- package/src/schemas/mdast/mdastToString.ts +102 -0
- package/src/schemas/nodes/block/BlockSchema.ts +773 -0
- package/src/schemas/nodes/block/index.ts +13 -0
- package/src/schemas/nodes/index.ts +20 -0
- package/src/schemas/nodes/inline/InlineSchema.ts +523 -0
- package/src/schemas/nodes/inline/index.ts +14 -0
- package/src/schemas/nodes/macro/MacroSchema.ts +226 -0
- package/src/schemas/nodes/macro/index.ts +6 -0
- package/src/schemas/preprocessing/ConfluencePreprocessor.ts +446 -0
- package/src/schemas/preprocessing/index.ts +8 -0
- package/src/serializers/ConfluenceSerializer.ts +717 -0
- package/src/serializers/MarkdownSerializer.ts +493 -0
- package/src/serializers/index.ts +8 -0
- package/test/GitService.test.ts +209 -0
- package/test/MarkdownConverter.test.ts +37 -3
- package/test/Schemas.test.ts +97 -2
- package/test/ast/BlockNode.test.ts +265 -0
- package/test/ast/Document.test.ts +126 -0
- package/test/ast/InlineNode.test.ts +161 -0
- package/test/fixtures/integration-test.html.fixture +103 -0
- package/test/fixtures/integration-test.md.expected +257 -0
- package/test/integration.test.ts +269 -0
- package/test/oauthServer.test.ts +50 -0
- package/test/parsers/ConfluenceParser.test.ts +283 -0
- package/test/schemas/ConfluencePreprocessor.test.ts +180 -0
- package/test/schemas/ConversionSchema.test.ts +159 -0
- package/test/schemas/HastSchema.test.ts +138 -0
- package/test/schemas/MdastSchema.test.ts +145 -0
- package/test/schemas/nodes/block/BlockSchema.test.ts +173 -0
- package/test/schemas/nodes/inline/InlineSchema.test.ts +198 -0
- package/test/schemas/nodes/macro/MacroSchema.test.ts +142 -0
- package/test/tokenStorage.test.ts +99 -0
package/src/SyncEngine.ts
CHANGED
|
@@ -7,21 +7,18 @@ import * as Path from "@effect/platform/Path"
|
|
|
7
7
|
import * as Context from "effect/Context"
|
|
8
8
|
import * as Effect from "effect/Effect"
|
|
9
9
|
import * as Layer from "effect/Layer"
|
|
10
|
-
import
|
|
10
|
+
import { PageId } from "./Brand.js"
|
|
11
11
|
import { ConfluenceClient } from "./ConfluenceClient.js"
|
|
12
12
|
import { ConfluenceConfig } from "./ConfluenceConfig.js"
|
|
13
|
-
import type {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
RateLimitError
|
|
20
|
-
} from "./ConfluenceError.js"
|
|
21
|
-
import { computeHash } from "./internal/hashUtils.js"
|
|
13
|
+
import type { ApiError, ConversionError, FrontMatterError, RateLimitError } from "./ConfluenceError.js"
|
|
14
|
+
import { FileSystemError, StructureError } from "./ConfluenceError.js"
|
|
15
|
+
import type { GitServiceError } from "./GitService.js"
|
|
16
|
+
import { GitService } from "./GitService.js"
|
|
17
|
+
import { computeHash, HashServiceLive } from "./internal/hashUtils.js"
|
|
18
|
+
import { UserCache } from "./internal/userCache.js"
|
|
22
19
|
import { LocalFileSystem } from "./LocalFileSystem.js"
|
|
23
20
|
import { MarkdownConverter } from "./MarkdownConverter.js"
|
|
24
|
-
import type { PageFrontMatter, PageListItem, PageResponse } from "./Schemas.js"
|
|
21
|
+
import type { AtlassianUser, PageFrontMatter, PageListItem, PageResponse, PageVersionContent } from "./Schemas.js"
|
|
25
22
|
|
|
26
23
|
/**
|
|
27
24
|
* Sync status for a single page.
|
|
@@ -40,12 +37,34 @@ export type SyncStatus =
|
|
|
40
37
|
readonly remoteVersion: number
|
|
41
38
|
}
|
|
42
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Progress callback for version replay.
|
|
42
|
+
*/
|
|
43
|
+
export type ProgressCallback = (current: number, total: number, message: string) => void
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Options for pull operation.
|
|
47
|
+
*/
|
|
48
|
+
export interface PullOptions {
|
|
49
|
+
readonly force: boolean
|
|
50
|
+
/**
|
|
51
|
+
* Replay version history as individual git commits.
|
|
52
|
+
* Only applies when git is initialized.
|
|
53
|
+
*/
|
|
54
|
+
readonly replayHistory?: boolean
|
|
55
|
+
/**
|
|
56
|
+
* Progress callback for version replay.
|
|
57
|
+
*/
|
|
58
|
+
readonly onProgress?: ProgressCallback
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
/**
|
|
44
62
|
* Result of a pull operation.
|
|
45
63
|
*/
|
|
46
64
|
export interface PullResult {
|
|
47
65
|
readonly pulled: number
|
|
48
66
|
readonly skipped: number
|
|
67
|
+
readonly commits: number
|
|
49
68
|
readonly errors: ReadonlyArray<string>
|
|
50
69
|
}
|
|
51
70
|
|
|
@@ -55,21 +74,11 @@ export interface PullResult {
|
|
|
55
74
|
export interface PushResult {
|
|
56
75
|
readonly pushed: number
|
|
57
76
|
readonly created: number
|
|
77
|
+
readonly deleted: number
|
|
58
78
|
readonly skipped: number
|
|
59
79
|
readonly errors: ReadonlyArray<string>
|
|
60
80
|
}
|
|
61
81
|
|
|
62
|
-
/**
|
|
63
|
-
* Result of a sync operation.
|
|
64
|
-
*/
|
|
65
|
-
export interface SyncResult {
|
|
66
|
-
readonly pulled: number
|
|
67
|
-
readonly pushed: number
|
|
68
|
-
readonly created: number
|
|
69
|
-
readonly conflicts: number
|
|
70
|
-
readonly errors: ReadonlyArray<string>
|
|
71
|
-
}
|
|
72
|
-
|
|
73
82
|
/**
|
|
74
83
|
* Result of a status operation.
|
|
75
84
|
*/
|
|
@@ -83,7 +92,14 @@ export interface StatusResult {
|
|
|
83
92
|
readonly files: ReadonlyArray<SyncStatus>
|
|
84
93
|
}
|
|
85
94
|
|
|
86
|
-
type SyncError =
|
|
95
|
+
type SyncError =
|
|
96
|
+
| ApiError
|
|
97
|
+
| RateLimitError
|
|
98
|
+
| ConversionError
|
|
99
|
+
| FileSystemError
|
|
100
|
+
| FrontMatterError
|
|
101
|
+
| GitServiceError
|
|
102
|
+
| StructureError
|
|
87
103
|
|
|
88
104
|
/**
|
|
89
105
|
* Sync engine service for Confluence <-> Markdown operations.
|
|
@@ -110,17 +126,12 @@ export class SyncEngine extends Context.Tag(
|
|
|
110
126
|
/**
|
|
111
127
|
* Pull pages from Confluence to local markdown.
|
|
112
128
|
*/
|
|
113
|
-
readonly pull: (options:
|
|
129
|
+
readonly pull: (options: PullOptions) => Effect.Effect<PullResult, SyncError>
|
|
114
130
|
|
|
115
131
|
/**
|
|
116
132
|
* Push local markdown changes to Confluence.
|
|
117
133
|
*/
|
|
118
|
-
readonly push: (options: { dryRun: boolean }) => Effect.Effect<PushResult, SyncError>
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Bidirectional sync with conflict detection.
|
|
122
|
-
*/
|
|
123
|
-
readonly sync: () => Effect.Effect<SyncResult, SyncError | ConflictError>
|
|
134
|
+
readonly push: (options: { dryRun: boolean; message?: string }) => Effect.Effect<PushResult, SyncError>
|
|
124
135
|
|
|
125
136
|
/**
|
|
126
137
|
* Get sync status for all files.
|
|
@@ -137,7 +148,7 @@ export class SyncEngine extends Context.Tag(
|
|
|
137
148
|
export const layer: Layer.Layer<
|
|
138
149
|
SyncEngine,
|
|
139
150
|
never,
|
|
140
|
-
ConfluenceClient | ConfluenceConfig | MarkdownConverter | LocalFileSystem | Path.Path
|
|
151
|
+
ConfluenceClient | ConfluenceConfig | MarkdownConverter | LocalFileSystem | Path.Path | GitService | UserCache
|
|
141
152
|
> = Layer.effect(
|
|
142
153
|
SyncEngine,
|
|
143
154
|
Effect.gen(function*() {
|
|
@@ -146,59 +157,254 @@ export const layer: Layer.Layer<
|
|
|
146
157
|
const converter = yield* MarkdownConverter
|
|
147
158
|
const localFs = yield* LocalFileSystem
|
|
148
159
|
const pathService = yield* Path.Path
|
|
160
|
+
const git = yield* GitService
|
|
161
|
+
const userCache = yield* UserCache
|
|
149
162
|
|
|
150
163
|
const docsPath = pathService.join(process.cwd(), config.docsPath)
|
|
151
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Build a map of relative path (without .md) to pageId for resolving parents.
|
|
167
|
+
* e.g., "guide" -> pageId, "guide/getting-started" -> pageId
|
|
168
|
+
*/
|
|
169
|
+
const buildPageIdMap = (): Effect.Effect<Map<string, string>, SyncError> =>
|
|
170
|
+
Effect.gen(function*() {
|
|
171
|
+
const files = yield* localFs.listMarkdownFiles(docsPath)
|
|
172
|
+
const map = new Map<string, string>()
|
|
173
|
+
|
|
174
|
+
for (const filePath of files) {
|
|
175
|
+
const localFile = yield* localFs.readMarkdownFile(filePath)
|
|
176
|
+
const relativePath = pathService.relative(docsPath, filePath)
|
|
177
|
+
const key = relativePath.replace(/\.md$/, "")
|
|
178
|
+
|
|
179
|
+
if (localFile.frontMatter?.pageId) {
|
|
180
|
+
map.set(key, localFile.frontMatter.pageId)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return map
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolve parent page ID from directory structure.
|
|
189
|
+
* Rule: foo/ contains children of foo.md
|
|
190
|
+
*/
|
|
191
|
+
const resolveParent = (
|
|
192
|
+
filePath: string,
|
|
193
|
+
pageIdMap: Map<string, string>
|
|
194
|
+
): Effect.Effect<string, StructureError | FileSystemError> =>
|
|
195
|
+
Effect.gen(function*() {
|
|
196
|
+
const relativePath = pathService.relative(docsPath, filePath)
|
|
197
|
+
const dirPath = pathService.dirname(relativePath)
|
|
198
|
+
|
|
199
|
+
// Root level files -> parent is rootPageId
|
|
200
|
+
if (dirPath === ".") {
|
|
201
|
+
return config.rootPageId
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Files in subdir -> parent is the directory's parent page
|
|
205
|
+
// e.g., "foo/bar.md" -> parent is "foo.md"
|
|
206
|
+
const parentKey = dirPath
|
|
207
|
+
const parentPageId = pageIdMap.get(parentKey)
|
|
208
|
+
|
|
209
|
+
if (!parentPageId) {
|
|
210
|
+
// Check if the parent .md file exists
|
|
211
|
+
const parentMdPath = pathService.join(docsPath, `${parentKey}.md`)
|
|
212
|
+
const parentExists = yield* localFs.exists(parentMdPath)
|
|
213
|
+
|
|
214
|
+
if (!parentExists) {
|
|
215
|
+
return yield* Effect.fail(
|
|
216
|
+
new StructureError({
|
|
217
|
+
path: filePath,
|
|
218
|
+
message: `Directory '${dirPath}/' has no parent page`,
|
|
219
|
+
advice: `Create '${parentKey}.md' first`
|
|
220
|
+
})
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Parent file exists but has no pageId (not pushed yet)
|
|
225
|
+
return yield* Effect.fail(
|
|
226
|
+
new StructureError({
|
|
227
|
+
path: filePath,
|
|
228
|
+
message: `Parent page '${parentKey}.md' not yet pushed`,
|
|
229
|
+
advice: `Push parent before creating children`
|
|
230
|
+
})
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return parentPageId
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Validate directory structure consistency.
|
|
239
|
+
* - Every directory foo/ must have a corresponding foo.md with pageId
|
|
240
|
+
*/
|
|
241
|
+
const validateStructure = (): Effect.Effect<void, SyncError> =>
|
|
242
|
+
Effect.gen(function*() {
|
|
243
|
+
const files = yield* localFs.listMarkdownFiles(docsPath)
|
|
244
|
+
const pageIdMap = yield* buildPageIdMap()
|
|
245
|
+
|
|
246
|
+
// Build set of directories that contain files
|
|
247
|
+
const dirsWithFiles = new Set<string>()
|
|
248
|
+
for (const filePath of files) {
|
|
249
|
+
const relativePath = pathService.relative(docsPath, filePath)
|
|
250
|
+
const dirPath = pathService.dirname(relativePath)
|
|
251
|
+
if (dirPath !== ".") {
|
|
252
|
+
dirsWithFiles.add(dirPath)
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Check each directory has a parent .md with pageId
|
|
257
|
+
// Rule: foo/ directory must have foo.md as parent
|
|
258
|
+
for (const dir of dirsWithFiles) {
|
|
259
|
+
const parentPageId = pageIdMap.get(dir)
|
|
260
|
+
if (!parentPageId) {
|
|
261
|
+
const parentMdPath = pathService.join(docsPath, `${dir}.md`)
|
|
262
|
+
const parentExists = yield* localFs.exists(parentMdPath)
|
|
263
|
+
|
|
264
|
+
if (!parentExists) {
|
|
265
|
+
return yield* Effect.fail(
|
|
266
|
+
new StructureError({
|
|
267
|
+
path: dir,
|
|
268
|
+
message: `Directory '${dir}/' has no parent page`,
|
|
269
|
+
advice: `Create '${dir}.md' first`
|
|
270
|
+
})
|
|
271
|
+
)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Parent exists but not pushed - this is OK during push,
|
|
275
|
+
// as long as we push in order (parent before child)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Check parentId in front-matter matches directory structure
|
|
280
|
+
// Only validate new pages or pages where we can determine expected parent
|
|
281
|
+
for (const filePath of files) {
|
|
282
|
+
const localFile = yield* localFs.readMarkdownFile(filePath)
|
|
283
|
+
if (localFile.frontMatter?.parentId) {
|
|
284
|
+
const relativePath = pathService.relative(docsPath, filePath)
|
|
285
|
+
const dirPath = pathService.dirname(relativePath)
|
|
286
|
+
|
|
287
|
+
// Determine expected parent based on directory
|
|
288
|
+
// foo/bar.md -> parent should be foo.md (pageIdMap key: "foo")
|
|
289
|
+
let expectedParentId: string | null = null
|
|
290
|
+
if (dirPath === ".") {
|
|
291
|
+
// Root level - parent should be rootPageId
|
|
292
|
+
// But if the parentId points elsewhere, it might be correct Confluence hierarchy
|
|
293
|
+
// Only validate if it's a new page (no pageId yet)
|
|
294
|
+
if (!localFile.frontMatter.pageId) {
|
|
295
|
+
expectedParentId = config.rootPageId
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
const parentPageId = pageIdMap.get(dirPath)
|
|
299
|
+
if (parentPageId) {
|
|
300
|
+
expectedParentId = parentPageId
|
|
301
|
+
}
|
|
302
|
+
// If parent not in map, skip validation (parent might be outside our tree)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (expectedParentId !== null && localFile.frontMatter.parentId !== expectedParentId) {
|
|
306
|
+
return yield* Effect.fail(
|
|
307
|
+
new StructureError({
|
|
308
|
+
path: filePath,
|
|
309
|
+
message: `Page parentId '${localFile.frontMatter.parentId}' does not match directory location`,
|
|
310
|
+
advice: `Move file to correct directory or update parentId to '${expectedParentId}'`
|
|
311
|
+
})
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Get user info with caching.
|
|
320
|
+
*/
|
|
321
|
+
const getUser = (accountId: string): Effect.Effect<AtlassianUser | undefined, ApiError | RateLimitError> =>
|
|
322
|
+
userCache.getOrFetch(accountId, client.getUser).pipe(
|
|
323
|
+
Effect.catchAll(() => Effect.succeed(undefined))
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Convert version content to markdown and front-matter.
|
|
328
|
+
*/
|
|
329
|
+
const versionToMarkdown = (
|
|
330
|
+
pageId: PageId,
|
|
331
|
+
version: PageVersionContent,
|
|
332
|
+
title: string,
|
|
333
|
+
parentId?: string,
|
|
334
|
+
position?: number
|
|
335
|
+
): Effect.Effect<{ markdown: string; frontMatter: PageFrontMatter }, SyncError> =>
|
|
336
|
+
Effect.gen(function*() {
|
|
337
|
+
const htmlContent = version.body?.storage?.value ?? ""
|
|
338
|
+
const markdown = yield* converter.htmlToMarkdown(htmlContent, {
|
|
339
|
+
includeRawSource: config.saveSource
|
|
340
|
+
})
|
|
341
|
+
const contentHash = yield* computeHash(markdown).pipe(Effect.provide(HashServiceLive))
|
|
342
|
+
|
|
343
|
+
// Get author info
|
|
344
|
+
const author = version.authorId ? yield* getUser(version.authorId) : undefined
|
|
345
|
+
|
|
346
|
+
const frontMatter: PageFrontMatter = {
|
|
347
|
+
pageId,
|
|
348
|
+
version: version.number,
|
|
349
|
+
title,
|
|
350
|
+
updated: new Date(version.createdAt),
|
|
351
|
+
...(parentId ? { parentId: parentId as PageId } : {}),
|
|
352
|
+
...(position !== undefined ? { position } : {}),
|
|
353
|
+
contentHash,
|
|
354
|
+
...(version.message ? { versionMessage: version.message } : {}),
|
|
355
|
+
...(author?.displayName ? { authorName: author.displayName } : {}),
|
|
356
|
+
...(author?.email ? { authorEmail: author.email } : {})
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return { markdown, frontMatter }
|
|
360
|
+
})
|
|
361
|
+
|
|
152
362
|
/**
|
|
153
363
|
* Pull a single page and its children recursively.
|
|
364
|
+
* Returns { pulled, commits } count.
|
|
154
365
|
*/
|
|
155
366
|
const pullPage = (
|
|
156
367
|
page: PageListItem | PageResponse,
|
|
157
368
|
parentPath: string,
|
|
158
|
-
|
|
159
|
-
|
|
369
|
+
options: PullOptions,
|
|
370
|
+
gitInitialized: boolean,
|
|
371
|
+
knownParentId?: string
|
|
372
|
+
): Effect.Effect<{ pulled: number; commits: number }, SyncError> =>
|
|
160
373
|
Effect.gen(function*() {
|
|
374
|
+
const pageId = page.id as PageId
|
|
161
375
|
// Get children to determine if this is a folder
|
|
162
|
-
const children = yield* client.getAllChildren(
|
|
376
|
+
const children = yield* client.getAllChildren(pageId)
|
|
163
377
|
const hasChildren = children.length > 0
|
|
164
378
|
|
|
165
|
-
const filePath = localFs.getPagePath(page.title, hasChildren, parentPath)
|
|
166
|
-
const dirPath = hasChildren ? localFs.getPageDir(page.title, parentPath) : parentPath
|
|
379
|
+
const filePath = yield* localFs.getPagePath(page.title, hasChildren, parentPath)
|
|
380
|
+
const dirPath = hasChildren ? yield* localFs.getPageDir(page.title, parentPath) : parentPath
|
|
167
381
|
|
|
168
382
|
// Get page content
|
|
169
|
-
const fullPage = yield* client.getPage(
|
|
170
|
-
const htmlContent = fullPage.body?.storage?.value ?? ""
|
|
171
|
-
let markdown = yield* converter.htmlToMarkdown(htmlContent)
|
|
172
|
-
|
|
173
|
-
// Add child page links for index pages
|
|
174
|
-
if (hasChildren && config.spaceKey) {
|
|
175
|
-
const childLinks = children
|
|
176
|
-
.map((child) => {
|
|
177
|
-
const pageUrl = `${config.baseUrl}/wiki/spaces/${config.spaceKey}/pages/${child.id}`
|
|
178
|
-
return `- [${child.title}](${pageUrl})`
|
|
179
|
-
})
|
|
180
|
-
.join("\n")
|
|
181
|
-
markdown = markdown.trim() + "\n\n## Child Pages\n\n" + childLinks + "\n"
|
|
182
|
-
}
|
|
383
|
+
const fullPage = yield* client.getPage(pageId)
|
|
183
384
|
|
|
184
|
-
|
|
385
|
+
// Determine parentId: use API response, fall back to known parent
|
|
386
|
+
const effectiveParentId = page.parentId ?? knownParentId
|
|
185
387
|
|
|
186
|
-
// Check
|
|
187
|
-
|
|
388
|
+
// Check existing local version
|
|
389
|
+
let localVersion = 0
|
|
390
|
+
if (!options.force) {
|
|
188
391
|
const exists = yield* localFs.exists(filePath)
|
|
189
392
|
if (exists) {
|
|
190
393
|
const localFile = yield* localFs.readMarkdownFile(filePath)
|
|
191
|
-
if (
|
|
192
|
-
localFile.frontMatter
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
394
|
+
if (localFile.frontMatter) {
|
|
395
|
+
localVersion = localFile.frontMatter.version
|
|
396
|
+
// If local is already at remote version, skip
|
|
397
|
+
if (localVersion >= fullPage.version.number) {
|
|
398
|
+
let childPulled = 0
|
|
399
|
+
let childCommits = 0
|
|
400
|
+
for (const child of children) {
|
|
401
|
+
// Pass current page's ID as parent for children
|
|
402
|
+
const result = yield* pullPage(child, dirPath, options, gitInitialized, pageId)
|
|
403
|
+
childPulled += result.pulled
|
|
404
|
+
childCommits += result.commits
|
|
405
|
+
}
|
|
406
|
+
return { pulled: childPulled, commits: childCommits }
|
|
200
407
|
}
|
|
201
|
-
return count
|
|
202
408
|
}
|
|
203
409
|
}
|
|
204
410
|
}
|
|
@@ -208,146 +414,524 @@ export const layer: Layer.Layer<
|
|
|
208
414
|
yield* localFs.ensureDir(dirPath)
|
|
209
415
|
}
|
|
210
416
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
417
|
+
let totalCommits = 0
|
|
418
|
+
|
|
419
|
+
// If history replay is enabled and git is initialized, replay versions
|
|
420
|
+
let historyReplayFailed = false
|
|
421
|
+
if (options.replayHistory && gitInitialized && localVersion < fullPage.version.number) {
|
|
422
|
+
// Fetch versions with body content since localVersion
|
|
423
|
+
const versions = yield* client.getPageVersions(pageId, { since: localVersion, includeBody: true })
|
|
424
|
+
// Sort by version number (oldest first)
|
|
425
|
+
const sortedVersions = [...versions].sort((a, b) => a.number - b.number)
|
|
426
|
+
const totalVersions = sortedVersions.length
|
|
427
|
+
|
|
428
|
+
let versionIdx = 0
|
|
429
|
+
for (const versionInfo of sortedVersions) {
|
|
430
|
+
versionIdx++
|
|
431
|
+
// Report progress
|
|
432
|
+
if (options.onProgress) {
|
|
433
|
+
options.onProgress(versionIdx, totalVersions, `${fullPage.title} v${versionInfo.number}`)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Check if body content is available from the versions list
|
|
437
|
+
const bodyContent = versionInfo.page?.body?.storage?.value
|
|
438
|
+
if (!bodyContent) {
|
|
439
|
+
// No body content available - history replay not supported
|
|
440
|
+
historyReplayFailed = true
|
|
441
|
+
break
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Build version content from the list response
|
|
445
|
+
const versionContent = {
|
|
446
|
+
number: versionInfo.number,
|
|
447
|
+
authorId: versionInfo.authorId,
|
|
448
|
+
createdAt: versionInfo.createdAt,
|
|
449
|
+
message: versionInfo.message,
|
|
450
|
+
body: {
|
|
451
|
+
storage: {
|
|
452
|
+
value: bodyContent,
|
|
453
|
+
representation: "storage" as const
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const { frontMatter, markdown } = yield* versionToMarkdown(
|
|
458
|
+
pageId,
|
|
459
|
+
versionContent,
|
|
460
|
+
versionInfo.page?.title ?? fullPage.title,
|
|
461
|
+
effectiveParentId,
|
|
462
|
+
page.position
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
// Write file
|
|
466
|
+
yield* localFs.writeMarkdownFile(filePath, frontMatter, markdown)
|
|
467
|
+
|
|
468
|
+
// Save source HTML if configured
|
|
469
|
+
if (config.saveSource && versionContent.body?.storage?.value) {
|
|
470
|
+
const sourceFilePath = filePath.replace(/\.md$/, ".html")
|
|
471
|
+
yield* localFs.writeFile(sourceFilePath, versionContent.body.storage.value)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Commit this version
|
|
475
|
+
const author = versionInfo.authorId ? yield* getUser(versionInfo.authorId) : undefined
|
|
476
|
+
const commitMessage = versionInfo.message || `Update ${fullPage.title} (v${versionInfo.number})`
|
|
477
|
+
|
|
478
|
+
yield* git.addAll()
|
|
479
|
+
const commitOptions = author
|
|
480
|
+
? {
|
|
481
|
+
message: commitMessage,
|
|
482
|
+
author: { name: author.displayName, email: author.email ?? "unknown@atlassian.com" },
|
|
483
|
+
date: new Date(versionInfo.createdAt)
|
|
484
|
+
}
|
|
485
|
+
: { message: commitMessage, date: new Date(versionInfo.createdAt) }
|
|
486
|
+
yield* git.commit(commitOptions).pipe(Effect.catchTag("GitNoChangesError", () => Effect.void))
|
|
487
|
+
|
|
488
|
+
totalCommits++
|
|
489
|
+
}
|
|
220
490
|
}
|
|
221
491
|
|
|
222
|
-
|
|
492
|
+
// Simple pull - either history replay is disabled, not initialized, or body not available
|
|
493
|
+
const needsSimplePull = historyReplayFailed || !options.replayHistory || !gitInitialized ||
|
|
494
|
+
localVersion >= fullPage.version.number
|
|
495
|
+
|
|
496
|
+
if (needsSimplePull) {
|
|
497
|
+
if (historyReplayFailed) {
|
|
498
|
+
yield* Effect.logWarning(
|
|
499
|
+
"History replay not available. Confluence Cloud API does not return body content for historical versions. Falling back to simple pull."
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Simple pull without history replay
|
|
504
|
+
const htmlContent = fullPage.body?.storage?.value ?? ""
|
|
505
|
+
let markdown = yield* converter.htmlToMarkdown(htmlContent, {
|
|
506
|
+
includeRawSource: config.saveSource
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
// Add child page links for index pages
|
|
510
|
+
if (hasChildren && config.spaceKey) {
|
|
511
|
+
const childLinks = children
|
|
512
|
+
.map((child) => {
|
|
513
|
+
const pageUrl = `${config.baseUrl}/wiki/spaces/${config.spaceKey}/pages/${child.id}`
|
|
514
|
+
return `- [${child.title}](${pageUrl})`
|
|
515
|
+
})
|
|
516
|
+
.join("\n")
|
|
517
|
+
markdown = markdown.trim() + "\n\n## Child Pages\n\n" + childLinks + "\n"
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const contentHash = yield* computeHash(markdown).pipe(Effect.provide(HashServiceLive))
|
|
521
|
+
|
|
522
|
+
// Get author info
|
|
523
|
+
const author = fullPage.version.authorId ? yield* getUser(fullPage.version.authorId) : undefined
|
|
524
|
+
|
|
525
|
+
// Write file
|
|
526
|
+
const frontMatter: PageFrontMatter = {
|
|
527
|
+
pageId,
|
|
528
|
+
version: fullPage.version.number,
|
|
529
|
+
title: fullPage.title,
|
|
530
|
+
updated: fullPage.version.createdAt ? new Date(fullPage.version.createdAt) : new Date(),
|
|
531
|
+
...(effectiveParentId ? { parentId: effectiveParentId as PageId } : {}),
|
|
532
|
+
...(page.position !== undefined ? { position: page.position } : {}),
|
|
533
|
+
contentHash,
|
|
534
|
+
...(fullPage.version.message ? { versionMessage: fullPage.version.message } : {}),
|
|
535
|
+
...(author?.displayName ? { authorName: author.displayName } : {}),
|
|
536
|
+
...(author?.email ? { authorEmail: author.email } : {})
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
yield* localFs.writeMarkdownFile(filePath, frontMatter, markdown)
|
|
540
|
+
|
|
541
|
+
// Save source HTML if configured
|
|
542
|
+
if (config.saveSource && htmlContent) {
|
|
543
|
+
const sourceFilePath = filePath.replace(/\.md$/, ".html")
|
|
544
|
+
yield* localFs.writeFile(sourceFilePath, htmlContent)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
223
547
|
|
|
224
548
|
// Pull children
|
|
225
|
-
let
|
|
549
|
+
let childPulled = 0
|
|
550
|
+
let childCommits = 0
|
|
226
551
|
for (const child of children) {
|
|
227
|
-
|
|
552
|
+
// Pass current page's ID as parent for children
|
|
553
|
+
const result = yield* pullPage(child, dirPath, options, gitInitialized, pageId)
|
|
554
|
+
childPulled += result.pulled
|
|
555
|
+
childCommits += result.commits
|
|
228
556
|
}
|
|
229
557
|
|
|
230
|
-
return
|
|
558
|
+
return { pulled: 1 + childPulled, commits: totalCommits + childCommits }
|
|
231
559
|
})
|
|
232
560
|
|
|
233
|
-
const pull = (options:
|
|
561
|
+
const pull = (options: PullOptions): Effect.Effect<PullResult, SyncError> =>
|
|
234
562
|
Effect.gen(function*() {
|
|
235
563
|
yield* localFs.ensureDir(docsPath)
|
|
236
564
|
|
|
565
|
+
// Check if git is initialized
|
|
566
|
+
const gitInitialized = yield* git.isInitialized()
|
|
567
|
+
|
|
568
|
+
// Two-branch model: if origin/confluence exists, work on that branch first
|
|
569
|
+
const hasRemoteBranch = gitInitialized
|
|
570
|
+
? yield* git.branchExists("origin/confluence")
|
|
571
|
+
: false
|
|
572
|
+
const originalBranch = gitInitialized ? yield* git.getCurrentBranch() : ""
|
|
573
|
+
|
|
574
|
+
if (hasRemoteBranch) {
|
|
575
|
+
// Switch to origin/confluence to pull updates there
|
|
576
|
+
yield* git.checkout("origin/confluence")
|
|
577
|
+
}
|
|
578
|
+
|
|
237
579
|
const rootPage = yield* client.getPage(config.rootPageId)
|
|
238
|
-
const
|
|
580
|
+
const result = yield* pullPage(rootPage, docsPath, options, gitInitialized)
|
|
581
|
+
|
|
582
|
+
// If git is initialized and we have changes but didn't replay history, auto-commit
|
|
583
|
+
if (gitInitialized && !options.replayHistory && result.pulled > 0) {
|
|
584
|
+
yield* git.addAll()
|
|
585
|
+
yield* git.commit({
|
|
586
|
+
message: `Pull from Confluence (${result.pulled} page${result.pulled !== 1 ? "s" : ""})`
|
|
587
|
+
}).pipe(Effect.catchTag("GitNoChangesError", () => Effect.void))
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Two-branch model: merge origin/confluence into current branch
|
|
591
|
+
if (hasRemoteBranch && originalBranch && originalBranch !== "origin/confluence") {
|
|
592
|
+
yield* git.checkout(originalBranch)
|
|
593
|
+
yield* git.merge("origin/confluence", {
|
|
594
|
+
message: `Merge remote changes from Confluence`
|
|
595
|
+
}).pipe(Effect.catchAll(() => Effect.void)) // May fail if no changes
|
|
596
|
+
}
|
|
239
597
|
|
|
240
598
|
return {
|
|
241
|
-
pulled,
|
|
599
|
+
pulled: result.pulled,
|
|
242
600
|
skipped: 0,
|
|
601
|
+
commits: result.commits,
|
|
243
602
|
errors: [] as ReadonlyArray<string>
|
|
244
603
|
}
|
|
245
604
|
})
|
|
246
605
|
|
|
247
|
-
|
|
606
|
+
/**
|
|
607
|
+
* Push a single file's content to Confluence.
|
|
608
|
+
* Returns the canonical content after push.
|
|
609
|
+
*/
|
|
610
|
+
const pushFile = (
|
|
611
|
+
filePath: string,
|
|
612
|
+
revisionMessage: string,
|
|
613
|
+
spaceId: string,
|
|
614
|
+
pageIdMap: Map<string, string>
|
|
615
|
+
): Effect.Effect<
|
|
616
|
+
{ pushed: boolean; created: boolean; newPageId?: string; error?: string },
|
|
617
|
+
SyncError
|
|
618
|
+
> =>
|
|
248
619
|
Effect.gen(function*() {
|
|
620
|
+
const localFile = yield* localFs.readMarkdownFile(filePath)
|
|
621
|
+
|
|
622
|
+
// Handle new page creation
|
|
623
|
+
if (localFile.isNew || !localFile.frontMatter) {
|
|
624
|
+
// Get title from front-matter or filename
|
|
625
|
+
const relativePath = pathService.relative(docsPath, filePath)
|
|
626
|
+
const baseName = pathService.basename(filePath, ".md")
|
|
627
|
+
|
|
628
|
+
// For new pages, re-parse front-matter to get title
|
|
629
|
+
// The localFile only has the content (body), not the original front-matter
|
|
630
|
+
const title = yield* Effect.tryPromise({
|
|
631
|
+
try: async () => {
|
|
632
|
+
const fs = await import("node:fs/promises")
|
|
633
|
+
const matter = await import("gray-matter")
|
|
634
|
+
const rawFile = await fs.readFile(filePath, "utf-8")
|
|
635
|
+
const parsed = matter.default(rawFile)
|
|
636
|
+
return (parsed.data as { title?: string }).title ?? baseName
|
|
637
|
+
},
|
|
638
|
+
catch: (cause) => new FileSystemError({ operation: "read", path: filePath, cause })
|
|
639
|
+
})
|
|
640
|
+
|
|
641
|
+
// Resolve parent from directory structure
|
|
642
|
+
const parentId = yield* resolveParent(filePath, pageIdMap)
|
|
643
|
+
|
|
644
|
+
// Convert markdown to HTML
|
|
645
|
+
const html = yield* converter.markdownToHtml(localFile.content)
|
|
646
|
+
|
|
647
|
+
// Create the page
|
|
648
|
+
const createdPage = yield* client.createPage({
|
|
649
|
+
spaceId,
|
|
650
|
+
parentId,
|
|
651
|
+
title,
|
|
652
|
+
body: {
|
|
653
|
+
representation: "storage",
|
|
654
|
+
value: html
|
|
655
|
+
}
|
|
656
|
+
})
|
|
657
|
+
|
|
658
|
+
// Set editor version to v2 (new editor)
|
|
659
|
+
yield* client.setEditorVersion(createdPage.id as PageId, "v2").pipe(
|
|
660
|
+
Effect.catchAll((error) => {
|
|
661
|
+
// Log warning but don't fail the push
|
|
662
|
+
return Effect.logWarning(`Failed to set editor v2 for page ${createdPage.id}: ${error.message}`)
|
|
663
|
+
})
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
// Fetch canonical content back from Confluence
|
|
667
|
+
const canonicalPage = yield* client.getPage(createdPage.id as PageId)
|
|
668
|
+
const canonicalHtml = canonicalPage.body?.storage?.value ?? ""
|
|
669
|
+
const canonicalMarkdown = yield* converter.htmlToMarkdown(canonicalHtml, {
|
|
670
|
+
includeRawSource: config.saveSource
|
|
671
|
+
})
|
|
672
|
+
const canonicalHash = yield* computeHash(canonicalMarkdown).pipe(Effect.provide(HashServiceLive))
|
|
673
|
+
|
|
674
|
+
// Write canonical content with full front-matter
|
|
675
|
+
const newFrontMatter: PageFrontMatter = {
|
|
676
|
+
pageId: createdPage.id as PageId,
|
|
677
|
+
version: createdPage.version.number,
|
|
678
|
+
title,
|
|
679
|
+
updated: new Date(canonicalPage.version.createdAt ?? new Date().toISOString()),
|
|
680
|
+
parentId: parentId as PageId,
|
|
681
|
+
contentHash: canonicalHash
|
|
682
|
+
}
|
|
683
|
+
yield* localFs.writeMarkdownFile(filePath, newFrontMatter, canonicalMarkdown)
|
|
684
|
+
|
|
685
|
+
// Update pageIdMap with new page
|
|
686
|
+
const key = relativePath.replace(/\.md$/, "")
|
|
687
|
+
pageIdMap.set(key, createdPage.id)
|
|
688
|
+
|
|
689
|
+
return { pushed: false, created: true, newPageId: createdPage.id }
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const fm = localFile.frontMatter
|
|
693
|
+
const currentHash = yield* computeHash(localFile.content).pipe(Effect.provide(HashServiceLive))
|
|
694
|
+
|
|
695
|
+
if (currentHash === fm.contentHash) {
|
|
696
|
+
return { pushed: false, created: false }
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Fetch current version to avoid conflicts
|
|
700
|
+
const remotePage = yield* client.getPage(fm.pageId)
|
|
701
|
+
const html = yield* converter.markdownToHtml(localFile.content)
|
|
702
|
+
const updatedPage = yield* client.updatePage({
|
|
703
|
+
id: fm.pageId,
|
|
704
|
+
title: fm.title,
|
|
705
|
+
status: "current",
|
|
706
|
+
version: {
|
|
707
|
+
number: remotePage.version.number + 1,
|
|
708
|
+
message: revisionMessage
|
|
709
|
+
},
|
|
710
|
+
body: {
|
|
711
|
+
representation: "storage",
|
|
712
|
+
value: html
|
|
713
|
+
}
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
// Fetch canonical content back from Confluence
|
|
717
|
+
const canonicalPage = yield* client.getPage(fm.pageId)
|
|
718
|
+
const canonicalHtml = canonicalPage.body?.storage?.value ?? ""
|
|
719
|
+
const canonicalMarkdown = yield* converter.htmlToMarkdown(canonicalHtml, {
|
|
720
|
+
includeRawSource: config.saveSource
|
|
721
|
+
})
|
|
722
|
+
const canonicalHash = yield* computeHash(canonicalMarkdown).pipe(Effect.provide(HashServiceLive))
|
|
723
|
+
|
|
724
|
+
// Write canonical content with updated front-matter
|
|
725
|
+
const newFrontMatter: PageFrontMatter = {
|
|
726
|
+
...fm,
|
|
727
|
+
version: updatedPage.version.number,
|
|
728
|
+
updated: new Date(canonicalPage.version.createdAt ?? new Date().toISOString()),
|
|
729
|
+
contentHash: canonicalHash
|
|
730
|
+
}
|
|
731
|
+
yield* localFs.writeMarkdownFile(filePath, newFrontMatter, canonicalMarkdown)
|
|
732
|
+
|
|
733
|
+
return { pushed: true, created: false }
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Find commits that have unpushed changes.
|
|
738
|
+
* Uses two-branch model: finds commits in current branch not in origin/confluence.
|
|
739
|
+
* Returns commits from oldest to newest.
|
|
740
|
+
*/
|
|
741
|
+
const findUnpushedCommits = (): Effect.Effect<
|
|
742
|
+
ReadonlyArray<{ hash: string; message: string }>,
|
|
743
|
+
SyncError
|
|
744
|
+
> =>
|
|
745
|
+
Effect.gen(function*() {
|
|
746
|
+
// Two-branch model: find commits in current branch not in origin/confluence
|
|
747
|
+
const hasRemoteBranch = yield* git.branchExists("origin/confluence")
|
|
748
|
+
|
|
749
|
+
if (hasRemoteBranch) {
|
|
750
|
+
// Use logRange to find commits not in origin/confluence
|
|
751
|
+
const commits = yield* git.logRange("origin/confluence", "HEAD")
|
|
752
|
+
return commits.map((c) => ({ hash: c.hash, message: c.message }))
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Fallback: no origin/confluence branch yet, use content hash comparison
|
|
249
756
|
const files = yield* localFs.listMarkdownFiles(docsPath)
|
|
250
|
-
|
|
251
|
-
let created = 0
|
|
252
|
-
let skipped = 0
|
|
253
|
-
const errors: Array<string> = []
|
|
757
|
+
if (files.length === 0) return []
|
|
254
758
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const localFile = yield* localFs.readMarkdownFile(filePath)
|
|
759
|
+
const allCommits = yield* git.log({ n: 100 })
|
|
760
|
+
if (allCommits.length === 0) return []
|
|
258
761
|
|
|
259
|
-
|
|
260
|
-
// New file - create page
|
|
261
|
-
if (!options.dryRun) {
|
|
262
|
-
// For now, skip page creation - need space ID in config
|
|
263
|
-
errors.push(
|
|
264
|
-
`Page creation requires space ID in config (not yet supported): ${filePath}`
|
|
265
|
-
)
|
|
266
|
-
}
|
|
267
|
-
created++
|
|
268
|
-
return
|
|
269
|
-
}
|
|
762
|
+
const unpushed: Array<{ hash: string; message: string }> = []
|
|
270
763
|
|
|
271
|
-
|
|
272
|
-
|
|
764
|
+
for (const commit of allCommits) {
|
|
765
|
+
yield* git.checkout(commit.hash)
|
|
273
766
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
767
|
+
let hasChanges = false
|
|
768
|
+
for (const filePath of files) {
|
|
769
|
+
const exists = yield* localFs.exists(filePath)
|
|
770
|
+
if (!exists) continue
|
|
278
771
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
id: fm.pageId,
|
|
285
|
-
title: fm.title,
|
|
286
|
-
status: "current",
|
|
287
|
-
version: {
|
|
288
|
-
number: remotePage.version.number + 1,
|
|
289
|
-
message: "Updated via confluence-to-markdown"
|
|
290
|
-
},
|
|
291
|
-
body: {
|
|
292
|
-
representation: "storage",
|
|
293
|
-
value: html
|
|
294
|
-
}
|
|
295
|
-
})
|
|
772
|
+
const localFile = yield* localFs.readMarkdownFile(filePath)
|
|
773
|
+
if (!localFile.frontMatter) {
|
|
774
|
+
hasChanges = true
|
|
775
|
+
break
|
|
776
|
+
}
|
|
296
777
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
updated: new Date(),
|
|
302
|
-
contentHash: currentHash
|
|
303
|
-
}
|
|
304
|
-
yield* localFs.writeMarkdownFile(filePath, newFrontMatter, localFile.content)
|
|
778
|
+
const currentHash = yield* computeHash(localFile.content).pipe(Effect.provide(HashServiceLive))
|
|
779
|
+
if (currentHash !== localFile.frontMatter.contentHash) {
|
|
780
|
+
hasChanges = true
|
|
781
|
+
break
|
|
305
782
|
}
|
|
783
|
+
}
|
|
306
784
|
|
|
307
|
-
|
|
308
|
-
})
|
|
309
|
-
Effect.catchAll((error) =>
|
|
310
|
-
Effect.sync(() => {
|
|
311
|
-
errors.push(
|
|
312
|
-
`Failed to push ${filePath}: ${error._tag === "ApiError" ? error.message : error._tag}`
|
|
313
|
-
)
|
|
314
|
-
})
|
|
315
|
-
)
|
|
316
|
-
)
|
|
785
|
+
if (!hasChanges) break
|
|
786
|
+
unpushed.push({ hash: commit.hash, message: commit.message })
|
|
317
787
|
}
|
|
318
788
|
|
|
319
|
-
return
|
|
789
|
+
return unpushed.reverse()
|
|
320
790
|
})
|
|
321
791
|
|
|
322
|
-
const
|
|
792
|
+
const push = (options: { dryRun: boolean; message?: string }): Effect.Effect<PushResult, SyncError> =>
|
|
323
793
|
Effect.gen(function*() {
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
794
|
+
// Validate structure before push
|
|
795
|
+
yield* validateStructure()
|
|
796
|
+
|
|
797
|
+
// Get spaceId from root page
|
|
798
|
+
const spaceId = yield* client.getSpaceId(config.rootPageId)
|
|
799
|
+
|
|
800
|
+
// Build pageId map for parent resolution
|
|
801
|
+
const pageIdMap = yield* buildPageIdMap()
|
|
802
|
+
|
|
803
|
+
const gitInitialized = yield* git.isInitialized()
|
|
804
|
+
|
|
805
|
+
// Get files and sort by depth (parent before child)
|
|
806
|
+
const files = yield* localFs.listMarkdownFiles(docsPath)
|
|
807
|
+
const sortedFiles = [...files].sort((a, b) => {
|
|
808
|
+
const depthA = pathService.relative(docsPath, a).split(pathService.sep).length
|
|
809
|
+
const depthB = pathService.relative(docsPath, b).split(pathService.sep).length
|
|
810
|
+
return depthA - depthB
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
if (!gitInitialized) {
|
|
814
|
+
// Non-git mode: just push current content
|
|
815
|
+
let pushed = 0
|
|
816
|
+
let created = 0
|
|
817
|
+
const errors: Array<string> = []
|
|
818
|
+
|
|
819
|
+
for (const filePath of sortedFiles) {
|
|
820
|
+
if (options.dryRun) {
|
|
821
|
+
pushed++
|
|
822
|
+
continue
|
|
823
|
+
}
|
|
824
|
+
const result = yield* pushFile(
|
|
825
|
+
filePath,
|
|
826
|
+
options.message ?? "Updated via confluence-to-markdown",
|
|
827
|
+
spaceId,
|
|
828
|
+
pageIdMap
|
|
829
|
+
).pipe(
|
|
830
|
+
Effect.catchAll((error) =>
|
|
831
|
+
Effect.succeed({
|
|
832
|
+
pushed: false,
|
|
833
|
+
created: false,
|
|
834
|
+
error: `Failed: ${error._tag}`
|
|
835
|
+
})
|
|
836
|
+
)
|
|
837
|
+
)
|
|
838
|
+
if (result.error) errors.push(result.error)
|
|
839
|
+
if (result.pushed) pushed++
|
|
840
|
+
if (result.created) created++
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
return { pushed, created, deleted: 0, skipped: 0, errors: errors as ReadonlyArray<string> }
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// Git mode: push current HEAD state to Confluence
|
|
847
|
+
// For simplicity, we push the final state as a single Confluence version
|
|
848
|
+
// with the most recent commit message
|
|
849
|
+
const errors: Array<string> = []
|
|
850
|
+
let pushed = 0
|
|
851
|
+
let created = 0
|
|
852
|
+
let deleted = 0
|
|
853
|
+
|
|
854
|
+
// Get the most recent unpushed commit message for the revision
|
|
855
|
+
const unpushedCommits = yield* findUnpushedCommits()
|
|
856
|
+
if (unpushedCommits.length === 0) {
|
|
857
|
+
return { pushed: 0, created: 0, skipped: 0, deleted: 0, errors: [] as ReadonlyArray<string> }
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (options.dryRun) {
|
|
861
|
+
return {
|
|
862
|
+
pushed: unpushedCommits.length,
|
|
863
|
+
created: 0,
|
|
864
|
+
skipped: 0,
|
|
865
|
+
deleted: 0,
|
|
866
|
+
errors: [] as ReadonlyArray<string>
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Use the last commit's message as the revision message
|
|
871
|
+
const lastCommit = unpushedCommits[unpushedCommits.length - 1]!
|
|
872
|
+
const revisionMessage = options.message ?? lastCommit.message
|
|
873
|
+
|
|
874
|
+
// Find deleted files by comparing origin/confluence with current HEAD
|
|
875
|
+
// Note: Git repo is inside .confluence/, so paths are relative to that
|
|
876
|
+
// (e.g., "docs/page.md" not ".confluence/docs/page.md")
|
|
877
|
+
const hasRemoteBranch = yield* git.branchExists("origin/confluence")
|
|
878
|
+
if (hasRemoteBranch) {
|
|
879
|
+
const deletedFiles = yield* git.getDeletedFiles("origin/confluence", "HEAD", "docs")
|
|
880
|
+
|
|
881
|
+
// Delete pages from Confluence
|
|
882
|
+
for (const deletedPath of deletedFiles) {
|
|
883
|
+
// Read the file from origin/confluence to get pageId
|
|
884
|
+
// deletedPath is already relative to git root (e.g., "docs/page.md")
|
|
885
|
+
const pageIdFromOrigin = yield* git.getFileContentAt(
|
|
886
|
+
"origin/confluence",
|
|
887
|
+
deletedPath
|
|
888
|
+
).pipe(
|
|
889
|
+
Effect.map((content) => {
|
|
890
|
+
const match = content.match(/pageId:\s*['"]?(\d+)['"]?/)
|
|
891
|
+
return match ? match[1] : null
|
|
892
|
+
}),
|
|
893
|
+
Effect.catchAll(() => Effect.succeed(null))
|
|
894
|
+
)
|
|
895
|
+
|
|
896
|
+
if (pageIdFromOrigin) {
|
|
897
|
+
yield* client.deletePage(PageId(pageIdFromOrigin)).pipe(
|
|
898
|
+
Effect.tap(() => Effect.sync(() => deleted++)),
|
|
899
|
+
Effect.catchAll((error) => {
|
|
900
|
+
errors.push(`Failed to delete page ${pageIdFromOrigin}: ${error.message}`)
|
|
901
|
+
return Effect.void
|
|
902
|
+
})
|
|
333
903
|
)
|
|
334
904
|
}
|
|
335
905
|
}
|
|
336
906
|
}
|
|
337
907
|
|
|
338
|
-
|
|
339
|
-
|
|
908
|
+
for (const filePath of sortedFiles) {
|
|
909
|
+
const result = yield* pushFile(filePath, revisionMessage, spaceId, pageIdMap).pipe(
|
|
910
|
+
Effect.catchAll((error) =>
|
|
911
|
+
Effect.succeed({
|
|
912
|
+
pushed: false,
|
|
913
|
+
created: false,
|
|
914
|
+
error: `Failed to push ${filePath}: ${error._tag}`
|
|
915
|
+
})
|
|
916
|
+
)
|
|
917
|
+
)
|
|
918
|
+
if (result.error) errors.push(result.error)
|
|
919
|
+
if (result.pushed) pushed++
|
|
920
|
+
if (result.created) created++
|
|
921
|
+
}
|
|
340
922
|
|
|
341
|
-
//
|
|
342
|
-
|
|
923
|
+
// Amend the last commit with canonical content
|
|
924
|
+
yield* git.addAll()
|
|
925
|
+
yield* git.amend({ noEdit: true }).pipe(
|
|
926
|
+
Effect.catchAll(() => Effect.void)
|
|
927
|
+
)
|
|
343
928
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
created: pushResult.created,
|
|
348
|
-
conflicts: statusResult.conflicts,
|
|
349
|
-
errors: [...conflictErrors, ...pullResult.errors, ...pushResult.errors] as ReadonlyArray<string>
|
|
929
|
+
// Two-branch model: update origin/confluence to match HEAD
|
|
930
|
+
if (hasRemoteBranch) {
|
|
931
|
+
yield* git.updateBranch("origin/confluence", "HEAD")
|
|
350
932
|
}
|
|
933
|
+
|
|
934
|
+
return { pushed, created, skipped: 0, deleted, errors: errors as ReadonlyArray<string> }
|
|
351
935
|
})
|
|
352
936
|
|
|
353
937
|
const status = (): Effect.Effect<StatusResult, SyncError> =>
|
|
@@ -372,7 +956,7 @@ export const layer: Layer.Layer<
|
|
|
372
956
|
}
|
|
373
957
|
|
|
374
958
|
const fm = localFile.frontMatter
|
|
375
|
-
const currentHash = computeHash(localFile.content)
|
|
959
|
+
const currentHash = yield* computeHash(localFile.content).pipe(Effect.provide(HashServiceLive))
|
|
376
960
|
|
|
377
961
|
// Fetch remote page
|
|
378
962
|
const remotePage = yield* Effect.either(client.getPage(fm.pageId))
|
|
@@ -422,7 +1006,6 @@ export const layer: Layer.Layer<
|
|
|
422
1006
|
return SyncEngine.of({
|
|
423
1007
|
pull,
|
|
424
1008
|
push,
|
|
425
|
-
sync,
|
|
426
1009
|
status
|
|
427
1010
|
})
|
|
428
1011
|
})
|