@knpkv/confluence-to-markdown 0.2.0 → 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +73 -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 +366 -0
- package/dist/ConfluenceAuth.js.map +1 -0
- package/dist/ConfluenceClient.d.ts +26 -12
- package/dist/ConfluenceClient.d.ts.map +1 -1
- package/dist/ConfluenceClient.js +139 -97
- 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 +99 -16
- 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 +32 -21
- package/src/ConfluenceAuth.ts +581 -0
- package/src/ConfluenceClient.ts +230 -165
- 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/dist/SyncEngine.js
CHANGED
|
@@ -7,9 +7,13 @@ 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 { PageId } from "./Brand.js";
|
|
10
11
|
import { ConfluenceClient } from "./ConfluenceClient.js";
|
|
11
12
|
import { ConfluenceConfig } from "./ConfluenceConfig.js";
|
|
12
|
-
import {
|
|
13
|
+
import { FileSystemError, StructureError } from "./ConfluenceError.js";
|
|
14
|
+
import { GitService } from "./GitService.js";
|
|
15
|
+
import { computeHash, HashServiceLive } from "./internal/hashUtils.js";
|
|
16
|
+
import { UserCache } from "./internal/userCache.js";
|
|
13
17
|
import { LocalFileSystem } from "./LocalFileSystem.js";
|
|
14
18
|
import { MarkdownConverter } from "./MarkdownConverter.js";
|
|
15
19
|
/**
|
|
@@ -42,45 +46,194 @@ export const layer = Layer.effect(SyncEngine, Effect.gen(function* () {
|
|
|
42
46
|
const converter = yield* MarkdownConverter;
|
|
43
47
|
const localFs = yield* LocalFileSystem;
|
|
44
48
|
const pathService = yield* Path.Path;
|
|
49
|
+
const git = yield* GitService;
|
|
50
|
+
const userCache = yield* UserCache;
|
|
45
51
|
const docsPath = pathService.join(process.cwd(), config.docsPath);
|
|
52
|
+
/**
|
|
53
|
+
* Build a map of relative path (without .md) to pageId for resolving parents.
|
|
54
|
+
* e.g., "guide" -> pageId, "guide/getting-started" -> pageId
|
|
55
|
+
*/
|
|
56
|
+
const buildPageIdMap = () => Effect.gen(function* () {
|
|
57
|
+
const files = yield* localFs.listMarkdownFiles(docsPath);
|
|
58
|
+
const map = new Map();
|
|
59
|
+
for (const filePath of files) {
|
|
60
|
+
const localFile = yield* localFs.readMarkdownFile(filePath);
|
|
61
|
+
const relativePath = pathService.relative(docsPath, filePath);
|
|
62
|
+
const key = relativePath.replace(/\.md$/, "");
|
|
63
|
+
if (localFile.frontMatter?.pageId) {
|
|
64
|
+
map.set(key, localFile.frontMatter.pageId);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return map;
|
|
68
|
+
});
|
|
69
|
+
/**
|
|
70
|
+
* Resolve parent page ID from directory structure.
|
|
71
|
+
* Rule: foo/ contains children of foo.md
|
|
72
|
+
*/
|
|
73
|
+
const resolveParent = (filePath, pageIdMap) => Effect.gen(function* () {
|
|
74
|
+
const relativePath = pathService.relative(docsPath, filePath);
|
|
75
|
+
const dirPath = pathService.dirname(relativePath);
|
|
76
|
+
// Root level files -> parent is rootPageId
|
|
77
|
+
if (dirPath === ".") {
|
|
78
|
+
return config.rootPageId;
|
|
79
|
+
}
|
|
80
|
+
// Files in subdir -> parent is the directory's parent page
|
|
81
|
+
// e.g., "foo/bar.md" -> parent is "foo.md"
|
|
82
|
+
const parentKey = dirPath;
|
|
83
|
+
const parentPageId = pageIdMap.get(parentKey);
|
|
84
|
+
if (!parentPageId) {
|
|
85
|
+
// Check if the parent .md file exists
|
|
86
|
+
const parentMdPath = pathService.join(docsPath, `${parentKey}.md`);
|
|
87
|
+
const parentExists = yield* localFs.exists(parentMdPath);
|
|
88
|
+
if (!parentExists) {
|
|
89
|
+
return yield* Effect.fail(new StructureError({
|
|
90
|
+
path: filePath,
|
|
91
|
+
message: `Directory '${dirPath}/' has no parent page`,
|
|
92
|
+
advice: `Create '${parentKey}.md' first`
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
// Parent file exists but has no pageId (not pushed yet)
|
|
96
|
+
return yield* Effect.fail(new StructureError({
|
|
97
|
+
path: filePath,
|
|
98
|
+
message: `Parent page '${parentKey}.md' not yet pushed`,
|
|
99
|
+
advice: `Push parent before creating children`
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
return parentPageId;
|
|
103
|
+
});
|
|
104
|
+
/**
|
|
105
|
+
* Validate directory structure consistency.
|
|
106
|
+
* - Every directory foo/ must have a corresponding foo.md with pageId
|
|
107
|
+
*/
|
|
108
|
+
const validateStructure = () => Effect.gen(function* () {
|
|
109
|
+
const files = yield* localFs.listMarkdownFiles(docsPath);
|
|
110
|
+
const pageIdMap = yield* buildPageIdMap();
|
|
111
|
+
// Build set of directories that contain files
|
|
112
|
+
const dirsWithFiles = new Set();
|
|
113
|
+
for (const filePath of files) {
|
|
114
|
+
const relativePath = pathService.relative(docsPath, filePath);
|
|
115
|
+
const dirPath = pathService.dirname(relativePath);
|
|
116
|
+
if (dirPath !== ".") {
|
|
117
|
+
dirsWithFiles.add(dirPath);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Check each directory has a parent .md with pageId
|
|
121
|
+
// Rule: foo/ directory must have foo.md as parent
|
|
122
|
+
for (const dir of dirsWithFiles) {
|
|
123
|
+
const parentPageId = pageIdMap.get(dir);
|
|
124
|
+
if (!parentPageId) {
|
|
125
|
+
const parentMdPath = pathService.join(docsPath, `${dir}.md`);
|
|
126
|
+
const parentExists = yield* localFs.exists(parentMdPath);
|
|
127
|
+
if (!parentExists) {
|
|
128
|
+
return yield* Effect.fail(new StructureError({
|
|
129
|
+
path: dir,
|
|
130
|
+
message: `Directory '${dir}/' has no parent page`,
|
|
131
|
+
advice: `Create '${dir}.md' first`
|
|
132
|
+
}));
|
|
133
|
+
}
|
|
134
|
+
// Parent exists but not pushed - this is OK during push,
|
|
135
|
+
// as long as we push in order (parent before child)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Check parentId in front-matter matches directory structure
|
|
139
|
+
// Only validate new pages or pages where we can determine expected parent
|
|
140
|
+
for (const filePath of files) {
|
|
141
|
+
const localFile = yield* localFs.readMarkdownFile(filePath);
|
|
142
|
+
if (localFile.frontMatter?.parentId) {
|
|
143
|
+
const relativePath = pathService.relative(docsPath, filePath);
|
|
144
|
+
const dirPath = pathService.dirname(relativePath);
|
|
145
|
+
// Determine expected parent based on directory
|
|
146
|
+
// foo/bar.md -> parent should be foo.md (pageIdMap key: "foo")
|
|
147
|
+
let expectedParentId = null;
|
|
148
|
+
if (dirPath === ".") {
|
|
149
|
+
// Root level - parent should be rootPageId
|
|
150
|
+
// But if the parentId points elsewhere, it might be correct Confluence hierarchy
|
|
151
|
+
// Only validate if it's a new page (no pageId yet)
|
|
152
|
+
if (!localFile.frontMatter.pageId) {
|
|
153
|
+
expectedParentId = config.rootPageId;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
const parentPageId = pageIdMap.get(dirPath);
|
|
158
|
+
if (parentPageId) {
|
|
159
|
+
expectedParentId = parentPageId;
|
|
160
|
+
}
|
|
161
|
+
// If parent not in map, skip validation (parent might be outside our tree)
|
|
162
|
+
}
|
|
163
|
+
if (expectedParentId !== null && localFile.frontMatter.parentId !== expectedParentId) {
|
|
164
|
+
return yield* Effect.fail(new StructureError({
|
|
165
|
+
path: filePath,
|
|
166
|
+
message: `Page parentId '${localFile.frontMatter.parentId}' does not match directory location`,
|
|
167
|
+
advice: `Move file to correct directory or update parentId to '${expectedParentId}'`
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
/**
|
|
174
|
+
* Get user info with caching.
|
|
175
|
+
*/
|
|
176
|
+
const getUser = (accountId) => userCache.getOrFetch(accountId, client.getUser).pipe(Effect.catchAll(() => Effect.succeed(undefined)));
|
|
177
|
+
/**
|
|
178
|
+
* Convert version content to markdown and front-matter.
|
|
179
|
+
*/
|
|
180
|
+
const versionToMarkdown = (pageId, version, title, parentId, position) => Effect.gen(function* () {
|
|
181
|
+
const htmlContent = version.body?.storage?.value ?? "";
|
|
182
|
+
const markdown = yield* converter.htmlToMarkdown(htmlContent, {
|
|
183
|
+
includeRawSource: config.saveSource
|
|
184
|
+
});
|
|
185
|
+
const contentHash = yield* computeHash(markdown).pipe(Effect.provide(HashServiceLive));
|
|
186
|
+
// Get author info
|
|
187
|
+
const author = version.authorId ? yield* getUser(version.authorId) : undefined;
|
|
188
|
+
const frontMatter = {
|
|
189
|
+
pageId,
|
|
190
|
+
version: version.number,
|
|
191
|
+
title,
|
|
192
|
+
updated: new Date(version.createdAt),
|
|
193
|
+
...(parentId ? { parentId: parentId } : {}),
|
|
194
|
+
...(position !== undefined ? { position } : {}),
|
|
195
|
+
contentHash,
|
|
196
|
+
...(version.message ? { versionMessage: version.message } : {}),
|
|
197
|
+
...(author?.displayName ? { authorName: author.displayName } : {}),
|
|
198
|
+
...(author?.email ? { authorEmail: author.email } : {})
|
|
199
|
+
};
|
|
200
|
+
return { markdown, frontMatter };
|
|
201
|
+
});
|
|
46
202
|
/**
|
|
47
203
|
* Pull a single page and its children recursively.
|
|
204
|
+
* Returns { pulled, commits } count.
|
|
48
205
|
*/
|
|
49
|
-
const pullPage = (page, parentPath,
|
|
206
|
+
const pullPage = (page, parentPath, options, gitInitialized, knownParentId) => Effect.gen(function* () {
|
|
207
|
+
const pageId = page.id;
|
|
50
208
|
// Get children to determine if this is a folder
|
|
51
|
-
const children = yield* client.getAllChildren(
|
|
209
|
+
const children = yield* client.getAllChildren(pageId);
|
|
52
210
|
const hasChildren = children.length > 0;
|
|
53
|
-
const filePath = localFs.getPagePath(page.title, hasChildren, parentPath);
|
|
54
|
-
const dirPath = hasChildren ? localFs.getPageDir(page.title, parentPath) : parentPath;
|
|
211
|
+
const filePath = yield* localFs.getPagePath(page.title, hasChildren, parentPath);
|
|
212
|
+
const dirPath = hasChildren ? yield* localFs.getPageDir(page.title, parentPath) : parentPath;
|
|
55
213
|
// Get page content
|
|
56
|
-
const fullPage = yield* client.getPage(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
.map((child) => {
|
|
63
|
-
const pageUrl = `${config.baseUrl}/wiki/spaces/${config.spaceKey}/pages/${child.id}`;
|
|
64
|
-
return `- [${child.title}](${pageUrl})`;
|
|
65
|
-
})
|
|
66
|
-
.join("\n");
|
|
67
|
-
markdown = markdown.trim() + "\n\n## Child Pages\n\n" + childLinks + "\n";
|
|
68
|
-
}
|
|
69
|
-
const contentHash = computeHash(markdown);
|
|
70
|
-
// Check if we need to update
|
|
71
|
-
if (!force) {
|
|
214
|
+
const fullPage = yield* client.getPage(pageId);
|
|
215
|
+
// Determine parentId: use API response, fall back to known parent
|
|
216
|
+
const effectiveParentId = page.parentId ?? knownParentId;
|
|
217
|
+
// Check existing local version
|
|
218
|
+
let localVersion = 0;
|
|
219
|
+
if (!options.force) {
|
|
72
220
|
const exists = yield* localFs.exists(filePath);
|
|
73
221
|
if (exists) {
|
|
74
222
|
const localFile = yield* localFs.readMarkdownFile(filePath);
|
|
75
|
-
if (localFile.frontMatter
|
|
76
|
-
localFile.frontMatter.version
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
223
|
+
if (localFile.frontMatter) {
|
|
224
|
+
localVersion = localFile.frontMatter.version;
|
|
225
|
+
// If local is already at remote version, skip
|
|
226
|
+
if (localVersion >= fullPage.version.number) {
|
|
227
|
+
let childPulled = 0;
|
|
228
|
+
let childCommits = 0;
|
|
229
|
+
for (const child of children) {
|
|
230
|
+
// Pass current page's ID as parent for children
|
|
231
|
+
const result = yield* pullPage(child, dirPath, options, gitInitialized, pageId);
|
|
232
|
+
childPulled += result.pulled;
|
|
233
|
+
childCommits += result.commits;
|
|
234
|
+
}
|
|
235
|
+
return { pulled: childPulled, commits: childCommits };
|
|
82
236
|
}
|
|
83
|
-
return count;
|
|
84
237
|
}
|
|
85
238
|
}
|
|
86
239
|
}
|
|
@@ -88,113 +241,410 @@ export const layer = Layer.effect(SyncEngine, Effect.gen(function* () {
|
|
|
88
241
|
if (hasChildren) {
|
|
89
242
|
yield* localFs.ensureDir(dirPath);
|
|
90
243
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
244
|
+
let totalCommits = 0;
|
|
245
|
+
// If history replay is enabled and git is initialized, replay versions
|
|
246
|
+
let historyReplayFailed = false;
|
|
247
|
+
if (options.replayHistory && gitInitialized && localVersion < fullPage.version.number) {
|
|
248
|
+
// Fetch versions with body content since localVersion
|
|
249
|
+
const versions = yield* client.getPageVersions(pageId, { since: localVersion, includeBody: true });
|
|
250
|
+
// Sort by version number (oldest first)
|
|
251
|
+
const sortedVersions = [...versions].sort((a, b) => a.number - b.number);
|
|
252
|
+
const totalVersions = sortedVersions.length;
|
|
253
|
+
let versionIdx = 0;
|
|
254
|
+
for (const versionInfo of sortedVersions) {
|
|
255
|
+
versionIdx++;
|
|
256
|
+
// Report progress
|
|
257
|
+
if (options.onProgress) {
|
|
258
|
+
options.onProgress(versionIdx, totalVersions, `${fullPage.title} v${versionInfo.number}`);
|
|
259
|
+
}
|
|
260
|
+
// Check if body content is available from the versions list
|
|
261
|
+
const bodyContent = versionInfo.page?.body?.storage?.value;
|
|
262
|
+
if (!bodyContent) {
|
|
263
|
+
// No body content available - history replay not supported
|
|
264
|
+
historyReplayFailed = true;
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
// Build version content from the list response
|
|
268
|
+
const versionContent = {
|
|
269
|
+
number: versionInfo.number,
|
|
270
|
+
authorId: versionInfo.authorId,
|
|
271
|
+
createdAt: versionInfo.createdAt,
|
|
272
|
+
message: versionInfo.message,
|
|
273
|
+
body: {
|
|
274
|
+
storage: {
|
|
275
|
+
value: bodyContent,
|
|
276
|
+
representation: "storage"
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
const { frontMatter, markdown } = yield* versionToMarkdown(pageId, versionContent, versionInfo.page?.title ?? fullPage.title, effectiveParentId, page.position);
|
|
281
|
+
// Write file
|
|
282
|
+
yield* localFs.writeMarkdownFile(filePath, frontMatter, markdown);
|
|
283
|
+
// Save source HTML if configured
|
|
284
|
+
if (config.saveSource && versionContent.body?.storage?.value) {
|
|
285
|
+
const sourceFilePath = filePath.replace(/\.md$/, ".html");
|
|
286
|
+
yield* localFs.writeFile(sourceFilePath, versionContent.body.storage.value);
|
|
287
|
+
}
|
|
288
|
+
// Commit this version
|
|
289
|
+
const author = versionInfo.authorId ? yield* getUser(versionInfo.authorId) : undefined;
|
|
290
|
+
const commitMessage = versionInfo.message || `Update ${fullPage.title} (v${versionInfo.number})`;
|
|
291
|
+
yield* git.addAll();
|
|
292
|
+
const commitOptions = author
|
|
293
|
+
? {
|
|
294
|
+
message: commitMessage,
|
|
295
|
+
author: { name: author.displayName, email: author.email ?? "unknown@atlassian.com" },
|
|
296
|
+
date: new Date(versionInfo.createdAt)
|
|
297
|
+
}
|
|
298
|
+
: { message: commitMessage, date: new Date(versionInfo.createdAt) };
|
|
299
|
+
yield* git.commit(commitOptions).pipe(Effect.catchTag("GitNoChangesError", () => Effect.void));
|
|
300
|
+
totalCommits++;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Simple pull - either history replay is disabled, not initialized, or body not available
|
|
304
|
+
const needsSimplePull = historyReplayFailed || !options.replayHistory || !gitInitialized ||
|
|
305
|
+
localVersion >= fullPage.version.number;
|
|
306
|
+
if (needsSimplePull) {
|
|
307
|
+
if (historyReplayFailed) {
|
|
308
|
+
yield* Effect.logWarning("History replay not available. Confluence Cloud API does not return body content for historical versions. Falling back to simple pull.");
|
|
309
|
+
}
|
|
310
|
+
// Simple pull without history replay
|
|
311
|
+
const htmlContent = fullPage.body?.storage?.value ?? "";
|
|
312
|
+
let markdown = yield* converter.htmlToMarkdown(htmlContent, {
|
|
313
|
+
includeRawSource: config.saveSource
|
|
314
|
+
});
|
|
315
|
+
// Add child page links for index pages
|
|
316
|
+
if (hasChildren && config.spaceKey) {
|
|
317
|
+
const childLinks = children
|
|
318
|
+
.map((child) => {
|
|
319
|
+
const pageUrl = `${config.baseUrl}/wiki/spaces/${config.spaceKey}/pages/${child.id}`;
|
|
320
|
+
return `- [${child.title}](${pageUrl})`;
|
|
321
|
+
})
|
|
322
|
+
.join("\n");
|
|
323
|
+
markdown = markdown.trim() + "\n\n## Child Pages\n\n" + childLinks + "\n";
|
|
324
|
+
}
|
|
325
|
+
const contentHash = yield* computeHash(markdown).pipe(Effect.provide(HashServiceLive));
|
|
326
|
+
// Get author info
|
|
327
|
+
const author = fullPage.version.authorId ? yield* getUser(fullPage.version.authorId) : undefined;
|
|
328
|
+
// Write file
|
|
329
|
+
const frontMatter = {
|
|
330
|
+
pageId,
|
|
331
|
+
version: fullPage.version.number,
|
|
332
|
+
title: fullPage.title,
|
|
333
|
+
updated: fullPage.version.createdAt ? new Date(fullPage.version.createdAt) : new Date(),
|
|
334
|
+
...(effectiveParentId ? { parentId: effectiveParentId } : {}),
|
|
335
|
+
...(page.position !== undefined ? { position: page.position } : {}),
|
|
336
|
+
contentHash,
|
|
337
|
+
...(fullPage.version.message ? { versionMessage: fullPage.version.message } : {}),
|
|
338
|
+
...(author?.displayName ? { authorName: author.displayName } : {}),
|
|
339
|
+
...(author?.email ? { authorEmail: author.email } : {})
|
|
340
|
+
};
|
|
341
|
+
yield* localFs.writeMarkdownFile(filePath, frontMatter, markdown);
|
|
342
|
+
// Save source HTML if configured
|
|
343
|
+
if (config.saveSource && htmlContent) {
|
|
344
|
+
const sourceFilePath = filePath.replace(/\.md$/, ".html");
|
|
345
|
+
yield* localFs.writeFile(sourceFilePath, htmlContent);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
102
348
|
// Pull children
|
|
103
|
-
let
|
|
349
|
+
let childPulled = 0;
|
|
350
|
+
let childCommits = 0;
|
|
104
351
|
for (const child of children) {
|
|
105
|
-
|
|
352
|
+
// Pass current page's ID as parent for children
|
|
353
|
+
const result = yield* pullPage(child, dirPath, options, gitInitialized, pageId);
|
|
354
|
+
childPulled += result.pulled;
|
|
355
|
+
childCommits += result.commits;
|
|
106
356
|
}
|
|
107
|
-
return
|
|
357
|
+
return { pulled: 1 + childPulled, commits: totalCommits + childCommits };
|
|
108
358
|
});
|
|
109
359
|
const pull = (options) => Effect.gen(function* () {
|
|
110
360
|
yield* localFs.ensureDir(docsPath);
|
|
361
|
+
// Check if git is initialized
|
|
362
|
+
const gitInitialized = yield* git.isInitialized();
|
|
363
|
+
// Two-branch model: if origin/confluence exists, work on that branch first
|
|
364
|
+
const hasRemoteBranch = gitInitialized
|
|
365
|
+
? yield* git.branchExists("origin/confluence")
|
|
366
|
+
: false;
|
|
367
|
+
const originalBranch = gitInitialized ? yield* git.getCurrentBranch() : "";
|
|
368
|
+
if (hasRemoteBranch) {
|
|
369
|
+
// Switch to origin/confluence to pull updates there
|
|
370
|
+
yield* git.checkout("origin/confluence");
|
|
371
|
+
}
|
|
111
372
|
const rootPage = yield* client.getPage(config.rootPageId);
|
|
112
|
-
const
|
|
373
|
+
const result = yield* pullPage(rootPage, docsPath, options, gitInitialized);
|
|
374
|
+
// If git is initialized and we have changes but didn't replay history, auto-commit
|
|
375
|
+
if (gitInitialized && !options.replayHistory && result.pulled > 0) {
|
|
376
|
+
yield* git.addAll();
|
|
377
|
+
yield* git.commit({
|
|
378
|
+
message: `Pull from Confluence (${result.pulled} page${result.pulled !== 1 ? "s" : ""})`
|
|
379
|
+
}).pipe(Effect.catchTag("GitNoChangesError", () => Effect.void));
|
|
380
|
+
}
|
|
381
|
+
// Two-branch model: merge origin/confluence into current branch
|
|
382
|
+
if (hasRemoteBranch && originalBranch && originalBranch !== "origin/confluence") {
|
|
383
|
+
yield* git.checkout(originalBranch);
|
|
384
|
+
yield* git.merge("origin/confluence", {
|
|
385
|
+
message: `Merge remote changes from Confluence`
|
|
386
|
+
}).pipe(Effect.catchAll(() => Effect.void)); // May fail if no changes
|
|
387
|
+
}
|
|
113
388
|
return {
|
|
114
|
-
pulled,
|
|
389
|
+
pulled: result.pulled,
|
|
115
390
|
skipped: 0,
|
|
391
|
+
commits: result.commits,
|
|
116
392
|
errors: []
|
|
117
393
|
};
|
|
118
394
|
});
|
|
119
|
-
|
|
395
|
+
/**
|
|
396
|
+
* Push a single file's content to Confluence.
|
|
397
|
+
* Returns the canonical content after push.
|
|
398
|
+
*/
|
|
399
|
+
const pushFile = (filePath, revisionMessage, spaceId, pageIdMap) => Effect.gen(function* () {
|
|
400
|
+
const localFile = yield* localFs.readMarkdownFile(filePath);
|
|
401
|
+
// Handle new page creation
|
|
402
|
+
if (localFile.isNew || !localFile.frontMatter) {
|
|
403
|
+
// Get title from front-matter or filename
|
|
404
|
+
const relativePath = pathService.relative(docsPath, filePath);
|
|
405
|
+
const baseName = pathService.basename(filePath, ".md");
|
|
406
|
+
// For new pages, re-parse front-matter to get title
|
|
407
|
+
// The localFile only has the content (body), not the original front-matter
|
|
408
|
+
const title = yield* Effect.tryPromise({
|
|
409
|
+
try: async () => {
|
|
410
|
+
const fs = await import("node:fs/promises");
|
|
411
|
+
const matter = await import("gray-matter");
|
|
412
|
+
const rawFile = await fs.readFile(filePath, "utf-8");
|
|
413
|
+
const parsed = matter.default(rawFile);
|
|
414
|
+
return parsed.data.title ?? baseName;
|
|
415
|
+
},
|
|
416
|
+
catch: (cause) => new FileSystemError({ operation: "read", path: filePath, cause })
|
|
417
|
+
});
|
|
418
|
+
// Resolve parent from directory structure
|
|
419
|
+
const parentId = yield* resolveParent(filePath, pageIdMap);
|
|
420
|
+
// Convert markdown to HTML
|
|
421
|
+
const html = yield* converter.markdownToHtml(localFile.content);
|
|
422
|
+
// Create the page
|
|
423
|
+
const createdPage = yield* client.createPage({
|
|
424
|
+
spaceId,
|
|
425
|
+
parentId,
|
|
426
|
+
title,
|
|
427
|
+
body: {
|
|
428
|
+
representation: "storage",
|
|
429
|
+
value: html
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
// Set editor version to v2 (new editor)
|
|
433
|
+
yield* client.setEditorVersion(createdPage.id, "v2").pipe(Effect.catchAll((error) => {
|
|
434
|
+
// Log warning but don't fail the push
|
|
435
|
+
return Effect.logWarning(`Failed to set editor v2 for page ${createdPage.id}: ${error.message}`);
|
|
436
|
+
}));
|
|
437
|
+
// Fetch canonical content back from Confluence
|
|
438
|
+
const canonicalPage = yield* client.getPage(createdPage.id);
|
|
439
|
+
const canonicalHtml = canonicalPage.body?.storage?.value ?? "";
|
|
440
|
+
const canonicalMarkdown = yield* converter.htmlToMarkdown(canonicalHtml, {
|
|
441
|
+
includeRawSource: config.saveSource
|
|
442
|
+
});
|
|
443
|
+
const canonicalHash = yield* computeHash(canonicalMarkdown).pipe(Effect.provide(HashServiceLive));
|
|
444
|
+
// Write canonical content with full front-matter
|
|
445
|
+
const newFrontMatter = {
|
|
446
|
+
pageId: createdPage.id,
|
|
447
|
+
version: createdPage.version.number,
|
|
448
|
+
title,
|
|
449
|
+
updated: new Date(canonicalPage.version.createdAt ?? new Date().toISOString()),
|
|
450
|
+
parentId: parentId,
|
|
451
|
+
contentHash: canonicalHash
|
|
452
|
+
};
|
|
453
|
+
yield* localFs.writeMarkdownFile(filePath, newFrontMatter, canonicalMarkdown);
|
|
454
|
+
// Update pageIdMap with new page
|
|
455
|
+
const key = relativePath.replace(/\.md$/, "");
|
|
456
|
+
pageIdMap.set(key, createdPage.id);
|
|
457
|
+
return { pushed: false, created: true, newPageId: createdPage.id };
|
|
458
|
+
}
|
|
459
|
+
const fm = localFile.frontMatter;
|
|
460
|
+
const currentHash = yield* computeHash(localFile.content).pipe(Effect.provide(HashServiceLive));
|
|
461
|
+
if (currentHash === fm.contentHash) {
|
|
462
|
+
return { pushed: false, created: false };
|
|
463
|
+
}
|
|
464
|
+
// Fetch current version to avoid conflicts
|
|
465
|
+
const remotePage = yield* client.getPage(fm.pageId);
|
|
466
|
+
const html = yield* converter.markdownToHtml(localFile.content);
|
|
467
|
+
const updatedPage = yield* client.updatePage({
|
|
468
|
+
id: fm.pageId,
|
|
469
|
+
title: fm.title,
|
|
470
|
+
status: "current",
|
|
471
|
+
version: {
|
|
472
|
+
number: remotePage.version.number + 1,
|
|
473
|
+
message: revisionMessage
|
|
474
|
+
},
|
|
475
|
+
body: {
|
|
476
|
+
representation: "storage",
|
|
477
|
+
value: html
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
// Fetch canonical content back from Confluence
|
|
481
|
+
const canonicalPage = yield* client.getPage(fm.pageId);
|
|
482
|
+
const canonicalHtml = canonicalPage.body?.storage?.value ?? "";
|
|
483
|
+
const canonicalMarkdown = yield* converter.htmlToMarkdown(canonicalHtml, {
|
|
484
|
+
includeRawSource: config.saveSource
|
|
485
|
+
});
|
|
486
|
+
const canonicalHash = yield* computeHash(canonicalMarkdown).pipe(Effect.provide(HashServiceLive));
|
|
487
|
+
// Write canonical content with updated front-matter
|
|
488
|
+
const newFrontMatter = {
|
|
489
|
+
...fm,
|
|
490
|
+
version: updatedPage.version.number,
|
|
491
|
+
updated: new Date(canonicalPage.version.createdAt ?? new Date().toISOString()),
|
|
492
|
+
contentHash: canonicalHash
|
|
493
|
+
};
|
|
494
|
+
yield* localFs.writeMarkdownFile(filePath, newFrontMatter, canonicalMarkdown);
|
|
495
|
+
return { pushed: true, created: false };
|
|
496
|
+
});
|
|
497
|
+
/**
|
|
498
|
+
* Find commits that have unpushed changes.
|
|
499
|
+
* Uses two-branch model: finds commits in current branch not in origin/confluence.
|
|
500
|
+
* Returns commits from oldest to newest.
|
|
501
|
+
*/
|
|
502
|
+
const findUnpushedCommits = () => Effect.gen(function* () {
|
|
503
|
+
// Two-branch model: find commits in current branch not in origin/confluence
|
|
504
|
+
const hasRemoteBranch = yield* git.branchExists("origin/confluence");
|
|
505
|
+
if (hasRemoteBranch) {
|
|
506
|
+
// Use logRange to find commits not in origin/confluence
|
|
507
|
+
const commits = yield* git.logRange("origin/confluence", "HEAD");
|
|
508
|
+
return commits.map((c) => ({ hash: c.hash, message: c.message }));
|
|
509
|
+
}
|
|
510
|
+
// Fallback: no origin/confluence branch yet, use content hash comparison
|
|
120
511
|
const files = yield* localFs.listMarkdownFiles(docsPath);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
512
|
+
if (files.length === 0)
|
|
513
|
+
return [];
|
|
514
|
+
const allCommits = yield* git.log({ n: 100 });
|
|
515
|
+
if (allCommits.length === 0)
|
|
516
|
+
return [];
|
|
517
|
+
const unpushed = [];
|
|
518
|
+
for (const commit of allCommits) {
|
|
519
|
+
yield* git.checkout(commit.hash);
|
|
520
|
+
let hasChanges = false;
|
|
521
|
+
for (const filePath of files) {
|
|
522
|
+
const exists = yield* localFs.exists(filePath);
|
|
523
|
+
if (!exists)
|
|
524
|
+
continue;
|
|
127
525
|
const localFile = yield* localFs.readMarkdownFile(filePath);
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// For now, skip page creation - need space ID in config
|
|
132
|
-
errors.push(`Page creation requires space ID in config (not yet supported): ${filePath}`);
|
|
133
|
-
}
|
|
134
|
-
created++;
|
|
135
|
-
return;
|
|
526
|
+
if (!localFile.frontMatter) {
|
|
527
|
+
hasChanges = true;
|
|
528
|
+
break;
|
|
136
529
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return;
|
|
530
|
+
const currentHash = yield* computeHash(localFile.content).pipe(Effect.provide(HashServiceLive));
|
|
531
|
+
if (currentHash !== localFile.frontMatter.contentHash) {
|
|
532
|
+
hasChanges = true;
|
|
533
|
+
break;
|
|
142
534
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const updatedPage = yield* client.updatePage({
|
|
148
|
-
id: fm.pageId,
|
|
149
|
-
title: fm.title,
|
|
150
|
-
status: "current",
|
|
151
|
-
version: {
|
|
152
|
-
number: remotePage.version.number + 1,
|
|
153
|
-
message: "Updated via confluence-to-markdown"
|
|
154
|
-
},
|
|
155
|
-
body: {
|
|
156
|
-
representation: "storage",
|
|
157
|
-
value: html
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
// Update front-matter with new version
|
|
161
|
-
const newFrontMatter = {
|
|
162
|
-
...fm,
|
|
163
|
-
version: updatedPage.version.number,
|
|
164
|
-
updated: new Date(),
|
|
165
|
-
contentHash: currentHash
|
|
166
|
-
};
|
|
167
|
-
yield* localFs.writeMarkdownFile(filePath, newFrontMatter, localFile.content);
|
|
168
|
-
}
|
|
169
|
-
pushed++;
|
|
170
|
-
}).pipe(Effect.catchAll((error) => Effect.sync(() => {
|
|
171
|
-
errors.push(`Failed to push ${filePath}: ${error._tag === "ApiError" ? error.message : error._tag}`);
|
|
172
|
-
})));
|
|
535
|
+
}
|
|
536
|
+
if (!hasChanges)
|
|
537
|
+
break;
|
|
538
|
+
unpushed.push({ hash: commit.hash, message: commit.message });
|
|
173
539
|
}
|
|
174
|
-
return
|
|
540
|
+
return unpushed.reverse();
|
|
175
541
|
});
|
|
176
|
-
const
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
542
|
+
const push = (options) => Effect.gen(function* () {
|
|
543
|
+
// Validate structure before push
|
|
544
|
+
yield* validateStructure();
|
|
545
|
+
// Get spaceId from root page
|
|
546
|
+
const spaceId = yield* client.getSpaceId(config.rootPageId);
|
|
547
|
+
// Build pageId map for parent resolution
|
|
548
|
+
const pageIdMap = yield* buildPageIdMap();
|
|
549
|
+
const gitInitialized = yield* git.isInitialized();
|
|
550
|
+
// Get files and sort by depth (parent before child)
|
|
551
|
+
const files = yield* localFs.listMarkdownFiles(docsPath);
|
|
552
|
+
const sortedFiles = [...files].sort((a, b) => {
|
|
553
|
+
const depthA = pathService.relative(docsPath, a).split(pathService.sep).length;
|
|
554
|
+
const depthB = pathService.relative(docsPath, b).split(pathService.sep).length;
|
|
555
|
+
return depthA - depthB;
|
|
556
|
+
});
|
|
557
|
+
if (!gitInitialized) {
|
|
558
|
+
// Non-git mode: just push current content
|
|
559
|
+
let pushed = 0;
|
|
560
|
+
let created = 0;
|
|
561
|
+
const errors = [];
|
|
562
|
+
for (const filePath of sortedFiles) {
|
|
563
|
+
if (options.dryRun) {
|
|
564
|
+
pushed++;
|
|
565
|
+
continue;
|
|
184
566
|
}
|
|
567
|
+
const result = yield* pushFile(filePath, options.message ?? "Updated via confluence-to-markdown", spaceId, pageIdMap).pipe(Effect.catchAll((error) => Effect.succeed({
|
|
568
|
+
pushed: false,
|
|
569
|
+
created: false,
|
|
570
|
+
error: `Failed: ${error._tag}`
|
|
571
|
+
})));
|
|
572
|
+
if (result.error)
|
|
573
|
+
errors.push(result.error);
|
|
574
|
+
if (result.pushed)
|
|
575
|
+
pushed++;
|
|
576
|
+
if (result.created)
|
|
577
|
+
created++;
|
|
185
578
|
}
|
|
579
|
+
return { pushed, created, deleted: 0, skipped: 0, errors: errors };
|
|
186
580
|
}
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
581
|
+
// Git mode: push current HEAD state to Confluence
|
|
582
|
+
// For simplicity, we push the final state as a single Confluence version
|
|
583
|
+
// with the most recent commit message
|
|
584
|
+
const errors = [];
|
|
585
|
+
let pushed = 0;
|
|
586
|
+
let created = 0;
|
|
587
|
+
let deleted = 0;
|
|
588
|
+
// Get the most recent unpushed commit message for the revision
|
|
589
|
+
const unpushedCommits = yield* findUnpushedCommits();
|
|
590
|
+
if (unpushedCommits.length === 0) {
|
|
591
|
+
return { pushed: 0, created: 0, skipped: 0, deleted: 0, errors: [] };
|
|
592
|
+
}
|
|
593
|
+
if (options.dryRun) {
|
|
594
|
+
return {
|
|
595
|
+
pushed: unpushedCommits.length,
|
|
596
|
+
created: 0,
|
|
597
|
+
skipped: 0,
|
|
598
|
+
deleted: 0,
|
|
599
|
+
errors: []
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
// Use the last commit's message as the revision message
|
|
603
|
+
const lastCommit = unpushedCommits[unpushedCommits.length - 1];
|
|
604
|
+
const revisionMessage = options.message ?? lastCommit.message;
|
|
605
|
+
// Find deleted files by comparing origin/confluence with current HEAD
|
|
606
|
+
// Note: Git repo is inside .confluence/, so paths are relative to that
|
|
607
|
+
// (e.g., "docs/page.md" not ".confluence/docs/page.md")
|
|
608
|
+
const hasRemoteBranch = yield* git.branchExists("origin/confluence");
|
|
609
|
+
if (hasRemoteBranch) {
|
|
610
|
+
const deletedFiles = yield* git.getDeletedFiles("origin/confluence", "HEAD", "docs");
|
|
611
|
+
// Delete pages from Confluence
|
|
612
|
+
for (const deletedPath of deletedFiles) {
|
|
613
|
+
// Read the file from origin/confluence to get pageId
|
|
614
|
+
// deletedPath is already relative to git root (e.g., "docs/page.md")
|
|
615
|
+
const pageIdFromOrigin = yield* git.getFileContentAt("origin/confluence", deletedPath).pipe(Effect.map((content) => {
|
|
616
|
+
const match = content.match(/pageId:\s*['"]?(\d+)['"]?/);
|
|
617
|
+
return match ? match[1] : null;
|
|
618
|
+
}), Effect.catchAll(() => Effect.succeed(null)));
|
|
619
|
+
if (pageIdFromOrigin) {
|
|
620
|
+
yield* client.deletePage(PageId(pageIdFromOrigin)).pipe(Effect.tap(() => Effect.sync(() => deleted++)), Effect.catchAll((error) => {
|
|
621
|
+
errors.push(`Failed to delete page ${pageIdFromOrigin}: ${error.message}`);
|
|
622
|
+
return Effect.void;
|
|
623
|
+
}));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
for (const filePath of sortedFiles) {
|
|
628
|
+
const result = yield* pushFile(filePath, revisionMessage, spaceId, pageIdMap).pipe(Effect.catchAll((error) => Effect.succeed({
|
|
629
|
+
pushed: false,
|
|
630
|
+
created: false,
|
|
631
|
+
error: `Failed to push ${filePath}: ${error._tag}`
|
|
632
|
+
})));
|
|
633
|
+
if (result.error)
|
|
634
|
+
errors.push(result.error);
|
|
635
|
+
if (result.pushed)
|
|
636
|
+
pushed++;
|
|
637
|
+
if (result.created)
|
|
638
|
+
created++;
|
|
639
|
+
}
|
|
640
|
+
// Amend the last commit with canonical content
|
|
641
|
+
yield* git.addAll();
|
|
642
|
+
yield* git.amend({ noEdit: true }).pipe(Effect.catchAll(() => Effect.void));
|
|
643
|
+
// Two-branch model: update origin/confluence to match HEAD
|
|
644
|
+
if (hasRemoteBranch) {
|
|
645
|
+
yield* git.updateBranch("origin/confluence", "HEAD");
|
|
646
|
+
}
|
|
647
|
+
return { pushed, created, skipped: 0, deleted, errors: errors };
|
|
198
648
|
});
|
|
199
649
|
const status = () => Effect.gen(function* () {
|
|
200
650
|
const files = yield* localFs.listMarkdownFiles(docsPath);
|
|
@@ -213,7 +663,7 @@ export const layer = Layer.effect(SyncEngine, Effect.gen(function* () {
|
|
|
213
663
|
continue;
|
|
214
664
|
}
|
|
215
665
|
const fm = localFile.frontMatter;
|
|
216
|
-
const currentHash = computeHash(localFile.content);
|
|
666
|
+
const currentHash = yield* computeHash(localFile.content).pipe(Effect.provide(HashServiceLive));
|
|
217
667
|
// Fetch remote page
|
|
218
668
|
const remotePage = yield* Effect.either(client.getPage(fm.pageId));
|
|
219
669
|
if (remotePage._tag === "Left") {
|
|
@@ -260,7 +710,6 @@ export const layer = Layer.effect(SyncEngine, Effect.gen(function* () {
|
|
|
260
710
|
return SyncEngine.of({
|
|
261
711
|
pull,
|
|
262
712
|
push,
|
|
263
|
-
sync,
|
|
264
713
|
status
|
|
265
714
|
});
|
|
266
715
|
}));
|