feishu-md-sync 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +5 -0
- package/README.md +107 -0
- package/dist/adapters/feishu-adapter.d.ts +44 -0
- package/dist/adapters/feishu-adapter.js +1 -0
- package/dist/adapters/lark-cli-adapter.d.ts +45 -0
- package/dist/adapters/lark-cli-adapter.js +229 -0
- package/dist/cli/commands/core.d.ts +2 -0
- package/dist/cli/commands/core.js +135 -0
- package/dist/cli/commands/publish.d.ts +2 -0
- package/dist/cli/commands/publish.js +63 -0
- package/dist/cli/env.d.ts +27 -0
- package/dist/cli/env.js +113 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +52 -0
- package/dist/cli/output.d.ts +4 -0
- package/dist/cli/output.js +18 -0
- package/dist/config/sync-config.d.ts +13 -0
- package/dist/config/sync-config.js +60 -0
- package/dist/core/diff.d.ts +1 -0
- package/dist/core/diff.js +20 -0
- package/dist/core/doc-id.d.ts +12 -0
- package/dist/core/doc-id.js +41 -0
- package/dist/core/hash.d.ts +6 -0
- package/dist/core/hash.js +84 -0
- package/dist/core/markdown-canonical.d.ts +2 -0
- package/dist/core/markdown-canonical.js +11 -0
- package/dist/diff/run-diff.d.ts +23 -0
- package/dist/diff/run-diff.js +33 -0
- package/dist/feishu/types.d.ts +101 -0
- package/dist/feishu/types.js +1 -0
- package/dist/markdown/blocks.d.ts +5 -0
- package/dist/markdown/blocks.js +305 -0
- package/dist/markdown/from-blocks.d.ts +2 -0
- package/dist/markdown/from-blocks.js +110 -0
- package/dist/markdown/links.d.ts +3 -0
- package/dist/markdown/links.js +44 -0
- package/dist/merge/line-merge.d.ts +21 -0
- package/dist/merge/line-merge.js +270 -0
- package/dist/merge/merge-state.d.ts +33 -0
- package/dist/merge/merge-state.js +44 -0
- package/dist/merge/run-merge.d.ts +38 -0
- package/dist/merge/run-merge.js +139 -0
- package/dist/profiles/publish-profile.d.ts +9 -0
- package/dist/profiles/publish-profile.js +9 -0
- package/dist/publish/block-patch-plan.d.ts +34 -0
- package/dist/publish/block-patch-plan.js +223 -0
- package/dist/publish/block-state.d.ts +8 -0
- package/dist/publish/block-state.js +82 -0
- package/dist/publish/block-update.d.ts +4 -0
- package/dist/publish/block-update.js +40 -0
- package/dist/publish/profile-transform.d.ts +5 -0
- package/dist/publish/profile-transform.js +6 -0
- package/dist/publish/publish-plan.d.ts +35 -0
- package/dist/publish/publish-plan.js +113 -0
- package/dist/publish/run-publish.d.ts +25 -0
- package/dist/publish/run-publish.js +299 -0
- package/dist/publish/title.d.ts +9 -0
- package/dist/publish/title.js +20 -0
- package/dist/pull/run-pull.d.ts +23 -0
- package/dist/pull/run-pull.js +57 -0
- package/dist/receipts/publish-receipt.d.ts +49 -0
- package/dist/receipts/publish-receipt.js +52 -0
- package/dist/receipts/pull-receipt.d.ts +31 -0
- package/dist/receipts/pull-receipt.js +30 -0
- package/dist/status/run-status.d.ts +56 -0
- package/dist/status/run-status.js +131 -0
- package/dist/transform/include-tags.d.ts +6 -0
- package/dist/transform/include-tags.js +22 -0
- package/dist/transform/zilliz-publish.d.ts +5 -0
- package/dist/transform/zilliz-publish.js +42 -0
- package/dist/transform/zilliz-pull.d.ts +6 -0
- package/dist/transform/zilliz-pull.js +34 -0
- package/package.json +61 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { sha256, stableStringify } from '../core/hash.js';
|
|
2
|
+
import { feishuBlocksToMarkdown } from '../markdown/from-blocks.js';
|
|
3
|
+
import { isTextLikeBlockPairUpdateable } from './block-update.js';
|
|
4
|
+
export function planPublishBlockPatch(input) {
|
|
5
|
+
const state = {
|
|
6
|
+
operations: [],
|
|
7
|
+
warnings: []
|
|
8
|
+
};
|
|
9
|
+
const fallbackReason = planSequence({
|
|
10
|
+
parentBlockId: input.parentBlockId,
|
|
11
|
+
remoteBlocks: input.remoteBlocks,
|
|
12
|
+
desiredBlocks: input.desiredBlocks,
|
|
13
|
+
path: [],
|
|
14
|
+
state
|
|
15
|
+
});
|
|
16
|
+
const requiresCollaborationRiskConfirmation = state.operations.some((operation) => {
|
|
17
|
+
return operation.kind === 'update' || operation.kind === 'delete';
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
kind: 'publish-block-patch-plan',
|
|
21
|
+
safeToWrite: fallbackReason === undefined,
|
|
22
|
+
requiresCollaborationRiskConfirmation,
|
|
23
|
+
operations: fallbackReason ? [] : state.operations,
|
|
24
|
+
fallbackReason,
|
|
25
|
+
warnings: state.warnings
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function planSequence(input) {
|
|
29
|
+
if (blocksEquivalent(input.remoteBlocks, input.desiredBlocks))
|
|
30
|
+
return undefined;
|
|
31
|
+
const prefixLength = commonPrefixLength(input.remoteBlocks, input.desiredBlocks);
|
|
32
|
+
const suffixLength = commonSuffixLength(input.remoteBlocks, input.desiredBlocks, prefixLength);
|
|
33
|
+
const remoteMiddleEnd = input.remoteBlocks.length - suffixLength;
|
|
34
|
+
const desiredMiddleEnd = input.desiredBlocks.length - suffixLength;
|
|
35
|
+
const remoteMiddle = input.remoteBlocks.slice(prefixLength, remoteMiddleEnd);
|
|
36
|
+
const desiredMiddle = input.desiredBlocks.slice(prefixLength, desiredMiddleEnd);
|
|
37
|
+
if (remoteMiddle.length === 0 && desiredMiddle.length > 0) {
|
|
38
|
+
const unsupported = desiredMiddle.find((block) => !isWritableMarkdownBlockForPatch(block));
|
|
39
|
+
if (unsupported) {
|
|
40
|
+
return `create block_type ${unsupported.block_type} is unsupported at ${formatPath([...input.path, prefixLength])}`;
|
|
41
|
+
}
|
|
42
|
+
const insertAfterBlockId = insertAfterBlockIdForCreate(input.remoteBlocks, input.parentBlockId, prefixLength);
|
|
43
|
+
if (!insertAfterBlockId) {
|
|
44
|
+
return `create anchor is missing at ${formatPath([...input.path, prefixLength])}`;
|
|
45
|
+
}
|
|
46
|
+
input.state.operations.push({
|
|
47
|
+
kind: 'create',
|
|
48
|
+
parentBlockId: input.parentBlockId,
|
|
49
|
+
insertAfterBlockId,
|
|
50
|
+
index: prefixLength,
|
|
51
|
+
path: [...input.path, prefixLength],
|
|
52
|
+
blocks: desiredMiddle
|
|
53
|
+
});
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (remoteMiddle.length > 0 && desiredMiddle.length === 0) {
|
|
57
|
+
const unsupported = remoteMiddle.find((block) => !isWritableMarkdownBlockForPatch(block));
|
|
58
|
+
if (unsupported) {
|
|
59
|
+
return `delete block_type ${unsupported.block_type} is unsupported at ${formatPath([...input.path, prefixLength])}`;
|
|
60
|
+
}
|
|
61
|
+
const blockIds = blockIdsForDelete(remoteMiddle);
|
|
62
|
+
if (blockIds.length !== remoteMiddle.length) {
|
|
63
|
+
return `delete block id is missing at ${formatPath([...input.path, prefixLength])}`;
|
|
64
|
+
}
|
|
65
|
+
input.state.operations.push({
|
|
66
|
+
kind: 'delete',
|
|
67
|
+
parentBlockId: input.parentBlockId,
|
|
68
|
+
blockIds,
|
|
69
|
+
startIndex: prefixLength,
|
|
70
|
+
endIndex: remoteMiddleEnd,
|
|
71
|
+
path: [...input.path, prefixLength]
|
|
72
|
+
});
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (remoteMiddle.length !== desiredMiddle.length) {
|
|
76
|
+
return `block order or count changed at ${formatPath(input.path)}`;
|
|
77
|
+
}
|
|
78
|
+
for (let index = 0; index < remoteMiddle.length; index += 1) {
|
|
79
|
+
const remote = remoteMiddle[index];
|
|
80
|
+
const desired = desiredMiddle[index];
|
|
81
|
+
const absoluteIndex = prefixLength + index;
|
|
82
|
+
const childPath = [...input.path, absoluteIndex];
|
|
83
|
+
const fallbackReason = planBlockPair({
|
|
84
|
+
remote,
|
|
85
|
+
desired,
|
|
86
|
+
path: childPath,
|
|
87
|
+
state: input.state
|
|
88
|
+
});
|
|
89
|
+
if (fallbackReason)
|
|
90
|
+
return fallbackReason;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
function planBlockPair(input) {
|
|
95
|
+
if (blocksEquivalent([input.remote], [input.desired]))
|
|
96
|
+
return undefined;
|
|
97
|
+
if (isWhiteboardBlock(input.remote) || isWhiteboardBlock(input.desired)) {
|
|
98
|
+
return `whiteboard block changed at ${formatPath(input.path)}`;
|
|
99
|
+
}
|
|
100
|
+
if (hasBlockChildren(input.remote) || hasBlockChildren(input.desired)) {
|
|
101
|
+
if (!input.remote.block_id)
|
|
102
|
+
return `container block is missing block id at ${formatPath(input.path)}`;
|
|
103
|
+
if (input.remote.block_type !== input.desired.block_type) {
|
|
104
|
+
return `container block type changed at ${formatPath(input.path)}`;
|
|
105
|
+
}
|
|
106
|
+
if (!hasBlockChildren(input.remote) || !hasBlockChildren(input.desired)) {
|
|
107
|
+
return `container block structure changed at ${formatPath(input.path)}`;
|
|
108
|
+
}
|
|
109
|
+
if (!containerShellEquivalent(input.remote, input.desired)) {
|
|
110
|
+
return `container block shell changed at ${formatPath(input.path)}`;
|
|
111
|
+
}
|
|
112
|
+
return planSequence({
|
|
113
|
+
parentBlockId: input.remote.block_id,
|
|
114
|
+
remoteBlocks: input.remote.children,
|
|
115
|
+
desiredBlocks: input.desired.children,
|
|
116
|
+
path: input.path,
|
|
117
|
+
state: input.state
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (!isTextLikeBlockPairUpdateable(input.remote, input.desired)) {
|
|
121
|
+
return `unsupported block change at ${formatPath(input.path)}`;
|
|
122
|
+
}
|
|
123
|
+
input.state.operations.push({
|
|
124
|
+
kind: 'update',
|
|
125
|
+
remoteBlockId: input.remote.block_id,
|
|
126
|
+
path: input.path,
|
|
127
|
+
blockType: input.remote.block_type
|
|
128
|
+
});
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
function commonPrefixLength(remote, desired) {
|
|
132
|
+
const maxLength = Math.min(remote.length, desired.length);
|
|
133
|
+
for (let index = 0; index < maxLength; index += 1) {
|
|
134
|
+
if (!blocksEquivalent([remote[index]], [desired[index]]))
|
|
135
|
+
return index;
|
|
136
|
+
}
|
|
137
|
+
return maxLength;
|
|
138
|
+
}
|
|
139
|
+
function commonSuffixLength(remote, desired, prefixLength) {
|
|
140
|
+
const maxLength = Math.min(remote.length, desired.length) - prefixLength;
|
|
141
|
+
for (let offset = 0; offset < maxLength; offset += 1) {
|
|
142
|
+
const remoteIndex = remote.length - 1 - offset;
|
|
143
|
+
const desiredIndex = desired.length - 1 - offset;
|
|
144
|
+
if (!blocksEquivalent([remote[remoteIndex]], [desired[desiredIndex]]))
|
|
145
|
+
return offset;
|
|
146
|
+
}
|
|
147
|
+
return maxLength;
|
|
148
|
+
}
|
|
149
|
+
function blocksEquivalent(remote, desired) {
|
|
150
|
+
return hashComparableBlocks(remote) === hashComparableBlocks(desired) ||
|
|
151
|
+
(canUseCanonicalMarkdown(remote) && canUseCanonicalMarkdown(desired) && canonicalMarkdown(remote) === canonicalMarkdown(desired));
|
|
152
|
+
}
|
|
153
|
+
function containerShellEquivalent(remote, desired) {
|
|
154
|
+
return hashComparableBlocks([stripContainerChildren(remote)]) === hashComparableBlocks([stripContainerChildren(desired)]);
|
|
155
|
+
}
|
|
156
|
+
function hashComparableBlocks(blocks) {
|
|
157
|
+
return sha256(stableStringify(blocks.map(normalizeBlockForPatch)));
|
|
158
|
+
}
|
|
159
|
+
function normalizeBlockForPatch(value) {
|
|
160
|
+
if (Array.isArray(value)) {
|
|
161
|
+
return value.map(normalizeBlockForPatch);
|
|
162
|
+
}
|
|
163
|
+
if (value && typeof value === 'object') {
|
|
164
|
+
const normalized = {};
|
|
165
|
+
for (const [key, child] of Object.entries(value)) {
|
|
166
|
+
if (key === 'block_id')
|
|
167
|
+
continue;
|
|
168
|
+
if (key === 'parent_id')
|
|
169
|
+
continue;
|
|
170
|
+
if (key === 'merge_info')
|
|
171
|
+
continue;
|
|
172
|
+
normalized[key] = normalizeBlockForPatch(child);
|
|
173
|
+
}
|
|
174
|
+
return normalized;
|
|
175
|
+
}
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
function canonicalMarkdown(blocks) {
|
|
179
|
+
return feishuBlocksToMarkdown(blocks)
|
|
180
|
+
.replace(/\r\n/g, '\n')
|
|
181
|
+
.replace(/[ \t]+$/gm, '')
|
|
182
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
183
|
+
.trim();
|
|
184
|
+
}
|
|
185
|
+
function canUseCanonicalMarkdown(blocks) {
|
|
186
|
+
return blocks.every((block) => isWritableMarkdownBlockForPatch(block) && block.block_type !== 31);
|
|
187
|
+
}
|
|
188
|
+
function stripContainerChildren(block) {
|
|
189
|
+
const { children: _children, block_id: _blockId, parent_id: _parentId, ...rest } = block;
|
|
190
|
+
return rest;
|
|
191
|
+
}
|
|
192
|
+
function hasBlockChildren(block) {
|
|
193
|
+
return Array.isArray(block.children) && block.children.every(isFeishuBlock);
|
|
194
|
+
}
|
|
195
|
+
function isFeishuBlock(value) {
|
|
196
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value) && 'block_type' in value);
|
|
197
|
+
}
|
|
198
|
+
function isWhiteboardBlock(block) {
|
|
199
|
+
return 'whiteboard' in block;
|
|
200
|
+
}
|
|
201
|
+
function insertAfterBlockIdForCreate(remoteBlocks, parentBlockId, index) {
|
|
202
|
+
if (index === 0)
|
|
203
|
+
return parentBlockId;
|
|
204
|
+
const previous = remoteBlocks[index - 1];
|
|
205
|
+
return previous?.block_id;
|
|
206
|
+
}
|
|
207
|
+
function blockIdsForDelete(blocks) {
|
|
208
|
+
return blocks.flatMap((block) => block.block_id ? [block.block_id] : []);
|
|
209
|
+
}
|
|
210
|
+
function isWritableMarkdownBlockForPatch(block) {
|
|
211
|
+
const writableType = block.block_type === 2 || (block.block_type >= 3 && block.block_type <= 8) || block.block_type === 12 || block.block_type === 13 || block.block_type === 14 || block.block_type === 31;
|
|
212
|
+
if (!writableType)
|
|
213
|
+
return false;
|
|
214
|
+
if (Array.isArray(block.children) && block.children.length > 0)
|
|
215
|
+
return false;
|
|
216
|
+
if (block.block_type !== 31)
|
|
217
|
+
return true;
|
|
218
|
+
const cells = block.table?.cells ?? [];
|
|
219
|
+
return cells.every((cell) => isFeishuBlock(cell) && cell.block_type === 2 && (!Array.isArray(cell.children) || cell.children.length === 0));
|
|
220
|
+
}
|
|
221
|
+
function formatPath(path) {
|
|
222
|
+
return path.length === 0 ? '<root>' : path.join('.');
|
|
223
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FeishuBlock } from '../feishu/types.js';
|
|
2
|
+
export type PageBlock = FeishuBlock & {
|
|
3
|
+
block_id: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function findPageBlock(blocks: FeishuBlock[], documentId: string): PageBlock;
|
|
6
|
+
export declare function directChildBlocks(blocks: FeishuBlock[], pageBlock: FeishuBlock): FeishuBlock[];
|
|
7
|
+
export declare function comparableDirectChildBlocks(blocks: FeishuBlock[], pageBlock: FeishuBlock): FeishuBlock[];
|
|
8
|
+
export declare function renderableDirectChildBlocks(blocks: FeishuBlock[], pageBlock: FeishuBlock): FeishuBlock[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export function findPageBlock(blocks, documentId) {
|
|
2
|
+
const page = blocks.find((block) => block.block_type === 1) ?? blocks.find((block) => block.block_id === documentId);
|
|
3
|
+
if (!page?.block_id) {
|
|
4
|
+
throw new Error(`Could not find page block for document ${documentId}.`);
|
|
5
|
+
}
|
|
6
|
+
return page;
|
|
7
|
+
}
|
|
8
|
+
export function directChildBlocks(blocks, pageBlock) {
|
|
9
|
+
const byId = blockMapById(blocks);
|
|
10
|
+
const children = Array.isArray(pageBlock.children) ? pageBlock.children : [];
|
|
11
|
+
return children
|
|
12
|
+
.map((child) => (typeof child === 'string' ? byId.get(child) : child))
|
|
13
|
+
.filter((child) => Boolean(child));
|
|
14
|
+
}
|
|
15
|
+
export function comparableDirectChildBlocks(blocks, pageBlock) {
|
|
16
|
+
const byId = blockMapById(blocks);
|
|
17
|
+
return directChildBlocks(blocks, pageBlock).map((block) => comparableBlock(block, byId));
|
|
18
|
+
}
|
|
19
|
+
export function renderableDirectChildBlocks(blocks, pageBlock) {
|
|
20
|
+
const byId = blockMapById(blocks);
|
|
21
|
+
return directChildBlocks(blocks, pageBlock).flatMap((block) => renderableBlocks(block, byId));
|
|
22
|
+
}
|
|
23
|
+
function blockMapById(blocks) {
|
|
24
|
+
return new Map(blocks.flatMap((block) => block.block_id ? [[block.block_id, block]] : []));
|
|
25
|
+
}
|
|
26
|
+
function renderableBlocks(block, byId) {
|
|
27
|
+
if (block.block_type === 19 && Array.isArray(block.children)) {
|
|
28
|
+
return [resolveChildContainer(block, byId)];
|
|
29
|
+
}
|
|
30
|
+
if (block.block_type !== 49 || !Array.isArray(block.children)) {
|
|
31
|
+
return [comparableBlock(block, byId)];
|
|
32
|
+
}
|
|
33
|
+
return block.children.flatMap((childRef) => {
|
|
34
|
+
const child = typeof childRef === 'string' ? byId.get(childRef) : asBlock(childRef);
|
|
35
|
+
return child ? renderableBlocks(child, byId) : [];
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
function resolveChildContainer(block, byId) {
|
|
39
|
+
const children = Array.isArray(block.children)
|
|
40
|
+
? block.children
|
|
41
|
+
.map((childRef) => (typeof childRef === 'string' ? byId.get(childRef) : asBlock(childRef)))
|
|
42
|
+
.filter((child) => Boolean(child))
|
|
43
|
+
.map((child) => comparableBlock(child, byId))
|
|
44
|
+
: [];
|
|
45
|
+
return {
|
|
46
|
+
...block,
|
|
47
|
+
children
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function comparableBlock(block, byId) {
|
|
51
|
+
if (block.block_type === 49 && Array.isArray(block.children)) {
|
|
52
|
+
return resolveChildContainer(block, byId);
|
|
53
|
+
}
|
|
54
|
+
if (block.block_type !== 31 || !isTableBlock(block)) {
|
|
55
|
+
return block;
|
|
56
|
+
}
|
|
57
|
+
const cellRefs = block.table.cells ?? [];
|
|
58
|
+
const resolvedCells = cellRefs.map((cellRef) => {
|
|
59
|
+
const cellBlock = typeof cellRef === 'string' ? byId.get(cellRef) : asBlock(cellRef);
|
|
60
|
+
const firstChildRef = Array.isArray(cellBlock?.children) ? cellBlock.children[0] : undefined;
|
|
61
|
+
const firstChild = typeof firstChildRef === 'string' ? byId.get(firstChildRef) : asBlock(firstChildRef);
|
|
62
|
+
return firstChild ?? { block_type: 2, text: { elements: [], style: { align: 1 } } };
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
...block,
|
|
66
|
+
table: {
|
|
67
|
+
...block.table,
|
|
68
|
+
cells: resolvedCells
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function isTableBlock(block) {
|
|
73
|
+
return Boolean(block.table &&
|
|
74
|
+
typeof block.table === 'object' &&
|
|
75
|
+
!Array.isArray(block.table));
|
|
76
|
+
}
|
|
77
|
+
function asBlock(value) {
|
|
78
|
+
if (value && typeof value === 'object' && !Array.isArray(value) && 'block_type' in value) {
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { FeishuBlock, FeishuBlockUpdateRequest, TextElement } from '../feishu/types.js';
|
|
2
|
+
export declare function isTextLikeBlockPairUpdateable(remote: FeishuBlock, desired: FeishuBlock): boolean;
|
|
3
|
+
export declare function buildTextLikeBlockUpdateRequest(remote: FeishuBlock, desired: FeishuBlock): FeishuBlockUpdateRequest;
|
|
4
|
+
export declare function elementsForBlock(block: FeishuBlock): TextElement[] | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const TEXT_LIKE_KEYS = {
|
|
2
|
+
2: 'text',
|
|
3
|
+
3: 'heading1',
|
|
4
|
+
4: 'heading2',
|
|
5
|
+
5: 'heading3',
|
|
6
|
+
6: 'heading4',
|
|
7
|
+
7: 'heading5',
|
|
8
|
+
8: 'heading6',
|
|
9
|
+
12: 'bullet',
|
|
10
|
+
13: 'ordered',
|
|
11
|
+
14: 'code'
|
|
12
|
+
};
|
|
13
|
+
export function isTextLikeBlockPairUpdateable(remote, desired) {
|
|
14
|
+
if (!remote.block_id)
|
|
15
|
+
return false;
|
|
16
|
+
if (remote.block_type !== desired.block_type)
|
|
17
|
+
return false;
|
|
18
|
+
return Boolean(TEXT_LIKE_KEYS[remote.block_type] && elementsForBlock(desired));
|
|
19
|
+
}
|
|
20
|
+
export function buildTextLikeBlockUpdateRequest(remote, desired) {
|
|
21
|
+
if (!remote.block_id || !isTextLikeBlockPairUpdateable(remote, desired)) {
|
|
22
|
+
throw new Error(`Block ${remote.block_id ?? '<missing id>'} cannot be updated in place.`);
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
block_id: remote.block_id,
|
|
26
|
+
update_text_elements: {
|
|
27
|
+
elements: elementsForBlock(desired) ?? []
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export function elementsForBlock(block) {
|
|
32
|
+
const key = TEXT_LIKE_KEYS[block.block_type];
|
|
33
|
+
if (!key)
|
|
34
|
+
return null;
|
|
35
|
+
const value = block[key];
|
|
36
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
37
|
+
return null;
|
|
38
|
+
const elements = value.elements;
|
|
39
|
+
return Array.isArray(elements) ? elements : null;
|
|
40
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PublishProfileName } from '../profiles/publish-profile.js';
|
|
2
|
+
import { type PublishReceipt, type PublishReceiptTarget } from '../receipts/publish-receipt.js';
|
|
3
|
+
import type { PublishBlockPatchPlan } from './block-patch-plan.js';
|
|
4
|
+
export type PublishStrategy = 'no-op' | 'block-patch' | 'section-replace' | 'document-replace' | 'create-document';
|
|
5
|
+
export type PublishPlan = {
|
|
6
|
+
target: PublishReceiptTarget;
|
|
7
|
+
profile: PublishProfileName;
|
|
8
|
+
strategy: PublishStrategy;
|
|
9
|
+
safeToWrite: boolean;
|
|
10
|
+
remoteChanged: boolean;
|
|
11
|
+
localSourceHash: string;
|
|
12
|
+
publishDraftHash: string;
|
|
13
|
+
remoteSnapshotHash: string;
|
|
14
|
+
requiresCollaborationRiskConfirmation: boolean;
|
|
15
|
+
requiresUntrackedRemoteConfirmation: boolean;
|
|
16
|
+
blockPatch?: {
|
|
17
|
+
operations: PublishBlockPatchPlan['operations'];
|
|
18
|
+
requiresCollaborationRiskConfirmation: boolean;
|
|
19
|
+
fallbackReason?: string;
|
|
20
|
+
warnings: string[];
|
|
21
|
+
};
|
|
22
|
+
risks: string[];
|
|
23
|
+
warnings: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare function buildPublishPlan(input: {
|
|
26
|
+
target: PublishReceiptTarget;
|
|
27
|
+
profile: PublishProfileName;
|
|
28
|
+
localSource: string;
|
|
29
|
+
publishDraft: string;
|
|
30
|
+
remoteMarkdown: string;
|
|
31
|
+
receipt: PublishReceipt | undefined;
|
|
32
|
+
transformWarnings: string[];
|
|
33
|
+
createDocument?: boolean;
|
|
34
|
+
blockPatch?: PublishBlockPatchPlan;
|
|
35
|
+
}): PublishPlan;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { hashText } from '../receipts/publish-receipt.js';
|
|
2
|
+
export function buildPublishPlan(input) {
|
|
3
|
+
const localSourceHash = hashText(input.localSource);
|
|
4
|
+
const publishDraftHash = hashText(input.publishDraft);
|
|
5
|
+
const remoteSnapshotHash = hashText(input.remoteMarkdown);
|
|
6
|
+
if (input.target.kind === 'folder' || input.createDocument === true) {
|
|
7
|
+
return {
|
|
8
|
+
target: input.target,
|
|
9
|
+
profile: input.profile,
|
|
10
|
+
strategy: 'create-document',
|
|
11
|
+
safeToWrite: true,
|
|
12
|
+
remoteChanged: false,
|
|
13
|
+
localSourceHash,
|
|
14
|
+
publishDraftHash,
|
|
15
|
+
remoteSnapshotHash,
|
|
16
|
+
requiresCollaborationRiskConfirmation: false,
|
|
17
|
+
requiresUntrackedRemoteConfirmation: false,
|
|
18
|
+
risks: [],
|
|
19
|
+
warnings: input.transformWarnings
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const remoteChanged = input.receipt ? input.receipt.remoteSnapshotHash !== remoteSnapshotHash : false;
|
|
23
|
+
const risks = [];
|
|
24
|
+
if (!input.receipt)
|
|
25
|
+
risks.push('untracked remote: no publish receipt exists for this target');
|
|
26
|
+
if (remoteChanged)
|
|
27
|
+
risks.push('remote changed since last publish receipt');
|
|
28
|
+
if (publishDraftHash === remoteSnapshotHash) {
|
|
29
|
+
return {
|
|
30
|
+
target: input.target,
|
|
31
|
+
profile: input.profile,
|
|
32
|
+
strategy: 'no-op',
|
|
33
|
+
safeToWrite: true,
|
|
34
|
+
remoteChanged,
|
|
35
|
+
localSourceHash,
|
|
36
|
+
publishDraftHash,
|
|
37
|
+
remoteSnapshotHash,
|
|
38
|
+
requiresCollaborationRiskConfirmation: false,
|
|
39
|
+
requiresUntrackedRemoteConfirmation: false,
|
|
40
|
+
risks,
|
|
41
|
+
warnings: input.transformWarnings
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (input.blockPatch?.safeToWrite === true && !remoteChanged) {
|
|
45
|
+
if (input.blockPatch.operations.length === 0) {
|
|
46
|
+
return {
|
|
47
|
+
target: input.target,
|
|
48
|
+
profile: input.profile,
|
|
49
|
+
strategy: 'no-op',
|
|
50
|
+
safeToWrite: true,
|
|
51
|
+
remoteChanged,
|
|
52
|
+
localSourceHash,
|
|
53
|
+
publishDraftHash,
|
|
54
|
+
remoteSnapshotHash,
|
|
55
|
+
requiresCollaborationRiskConfirmation: false,
|
|
56
|
+
requiresUntrackedRemoteConfirmation: false,
|
|
57
|
+
blockPatch: {
|
|
58
|
+
operations: [],
|
|
59
|
+
requiresCollaborationRiskConfirmation: false,
|
|
60
|
+
fallbackReason: input.blockPatch.fallbackReason,
|
|
61
|
+
warnings: input.blockPatch.warnings
|
|
62
|
+
},
|
|
63
|
+
risks,
|
|
64
|
+
warnings: input.transformWarnings
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (!input.receipt) {
|
|
68
|
+
risks.push('untracked remote block-patch requires explicit confirmation');
|
|
69
|
+
}
|
|
70
|
+
if (input.blockPatch.requiresCollaborationRiskConfirmation) {
|
|
71
|
+
risks.push('changed blocks may lose comments, anchors, or block identity when replaced');
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
target: input.target,
|
|
75
|
+
profile: input.profile,
|
|
76
|
+
strategy: 'block-patch',
|
|
77
|
+
safeToWrite: Boolean(input.receipt) && !input.blockPatch.requiresCollaborationRiskConfirmation,
|
|
78
|
+
remoteChanged,
|
|
79
|
+
localSourceHash,
|
|
80
|
+
publishDraftHash,
|
|
81
|
+
remoteSnapshotHash,
|
|
82
|
+
requiresCollaborationRiskConfirmation: input.blockPatch.requiresCollaborationRiskConfirmation,
|
|
83
|
+
requiresUntrackedRemoteConfirmation: !input.receipt,
|
|
84
|
+
blockPatch: {
|
|
85
|
+
operations: input.blockPatch.operations,
|
|
86
|
+
requiresCollaborationRiskConfirmation: input.blockPatch.requiresCollaborationRiskConfirmation,
|
|
87
|
+
fallbackReason: input.blockPatch.fallbackReason,
|
|
88
|
+
warnings: input.blockPatch.warnings
|
|
89
|
+
},
|
|
90
|
+
risks,
|
|
91
|
+
warnings: input.transformWarnings
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (input.blockPatch && !input.blockPatch.safeToWrite && input.blockPatch.fallbackReason) {
|
|
95
|
+
risks.push(`block-patch unavailable: ${input.blockPatch.fallbackReason}`);
|
|
96
|
+
}
|
|
97
|
+
const strategy = 'document-replace';
|
|
98
|
+
risks.push('document replace can affect comments, anchors, block identity, and collaboration context');
|
|
99
|
+
return {
|
|
100
|
+
target: input.target,
|
|
101
|
+
profile: input.profile,
|
|
102
|
+
strategy,
|
|
103
|
+
safeToWrite: false,
|
|
104
|
+
remoteChanged,
|
|
105
|
+
localSourceHash,
|
|
106
|
+
publishDraftHash,
|
|
107
|
+
remoteSnapshotHash,
|
|
108
|
+
requiresCollaborationRiskConfirmation: false,
|
|
109
|
+
requiresUntrackedRemoteConfirmation: false,
|
|
110
|
+
risks,
|
|
111
|
+
warnings: input.transformWarnings
|
|
112
|
+
};
|
|
113
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { FeishuAdapter } from '../adapters/feishu-adapter.js';
|
|
2
|
+
import type { PublishProfileName } from '../profiles/publish-profile.js';
|
|
3
|
+
import { type PublishReceiptTarget } from '../receipts/publish-receipt.js';
|
|
4
|
+
import { type PublishPlan, type PublishStrategy } from './publish-plan.js';
|
|
5
|
+
export type RunPublishResult = {
|
|
6
|
+
mode: 'dry-run' | 'write';
|
|
7
|
+
plan: PublishPlan;
|
|
8
|
+
document?: {
|
|
9
|
+
documentId: string;
|
|
10
|
+
url?: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
export declare function runPublish(input: {
|
|
14
|
+
cwd: string;
|
|
15
|
+
file: string;
|
|
16
|
+
target: PublishReceiptTarget;
|
|
17
|
+
profile: PublishProfileName;
|
|
18
|
+
write: boolean;
|
|
19
|
+
create: boolean;
|
|
20
|
+
strategy: 'auto' | PublishStrategy;
|
|
21
|
+
confirmDestructive: boolean;
|
|
22
|
+
confirmCollaborationRisk?: boolean;
|
|
23
|
+
confirmUntrackedRemote?: boolean;
|
|
24
|
+
adapter: FeishuAdapter;
|
|
25
|
+
}): Promise<RunPublishResult>;
|