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,270 @@
|
|
|
1
|
+
export function mergeLines(input) {
|
|
2
|
+
if (input.local === input.remote) {
|
|
3
|
+
return cleanResult(input.local);
|
|
4
|
+
}
|
|
5
|
+
const base = splitLines(input.base);
|
|
6
|
+
const localChanges = changesFromBase(base, splitLines(input.local));
|
|
7
|
+
const remoteChanges = changesFromBase(base, splitLines(input.remote));
|
|
8
|
+
const output = [];
|
|
9
|
+
let baseIndex = 0;
|
|
10
|
+
let localIndex = 0;
|
|
11
|
+
let remoteIndex = 0;
|
|
12
|
+
let conflicts = 0;
|
|
13
|
+
while (baseIndex < base.length || localIndex < localChanges.length || remoteIndex < remoteChanges.length) {
|
|
14
|
+
const localChange = localChanges[localIndex];
|
|
15
|
+
const remoteChange = remoteChanges[remoteIndex];
|
|
16
|
+
if (!localChange && !remoteChange) {
|
|
17
|
+
output.push(...base.slice(baseIndex));
|
|
18
|
+
break;
|
|
19
|
+
}
|
|
20
|
+
const nextStart = Math.min(localChange?.baseStart ?? Number.POSITIVE_INFINITY, remoteChange?.baseStart ?? Number.POSITIVE_INFINITY);
|
|
21
|
+
output.push(...base.slice(baseIndex, nextStart));
|
|
22
|
+
baseIndex = nextStart;
|
|
23
|
+
if (localChange && (!remoteChange || isStrictlyBefore(localChange, remoteChange))) {
|
|
24
|
+
output.push(...localChange.lines);
|
|
25
|
+
baseIndex = localChange.baseEnd;
|
|
26
|
+
localIndex += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (remoteChange && (!localChange || isStrictlyBefore(remoteChange, localChange))) {
|
|
30
|
+
output.push(...remoteChange.lines);
|
|
31
|
+
baseIndex = remoteChange.baseEnd;
|
|
32
|
+
remoteIndex += 1;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const overlap = collectOverlappingChanges({
|
|
36
|
+
localChanges,
|
|
37
|
+
remoteChanges,
|
|
38
|
+
localIndex,
|
|
39
|
+
remoteIndex
|
|
40
|
+
});
|
|
41
|
+
localIndex = overlap.localIndex;
|
|
42
|
+
remoteIndex = overlap.remoteIndex;
|
|
43
|
+
baseIndex = overlap.baseEnd;
|
|
44
|
+
const localLines = applyChangesToBaseSlice(base, overlap.baseStart, overlap.baseEnd, overlap.local);
|
|
45
|
+
const remoteLines = applyChangesToBaseSlice(base, overlap.baseStart, overlap.baseEnd, overlap.remote);
|
|
46
|
+
if (joinLines(localLines) === joinLines(remoteLines)) {
|
|
47
|
+
output.push(...localLines);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (overlap.baseStart === overlap.baseEnd) {
|
|
51
|
+
output.push(...combineSamePositionInsertions(localLines, remoteLines));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
output.push(...conflictLines(localLines, remoteLines));
|
|
55
|
+
conflicts += 1;
|
|
56
|
+
}
|
|
57
|
+
return resultFromOutput(output, input.local, conflicts);
|
|
58
|
+
}
|
|
59
|
+
export function mergeWithoutBase(input) {
|
|
60
|
+
if (input.local === input.remote) {
|
|
61
|
+
return cleanResult(input.local);
|
|
62
|
+
}
|
|
63
|
+
const local = splitLines(input.local);
|
|
64
|
+
const remote = splitLines(input.remote);
|
|
65
|
+
const table = lcsTable(local, remote);
|
|
66
|
+
const output = [];
|
|
67
|
+
let i = 0;
|
|
68
|
+
let j = 0;
|
|
69
|
+
let conflicts = 0;
|
|
70
|
+
while (i < local.length || j < remote.length) {
|
|
71
|
+
if (i < local.length && j < remote.length && local[i] === remote[j]) {
|
|
72
|
+
output.push(local[i]);
|
|
73
|
+
i += 1;
|
|
74
|
+
j += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
const localChunk = [];
|
|
78
|
+
const remoteChunk = [];
|
|
79
|
+
while (i < local.length || j < remote.length) {
|
|
80
|
+
if (i < local.length && j < remote.length && local[i] === remote[j])
|
|
81
|
+
break;
|
|
82
|
+
if (i < local.length && (j === remote.length || table[i + 1][j] >= table[i][j + 1])) {
|
|
83
|
+
localChunk.push(local[i]);
|
|
84
|
+
i += 1;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (j < remote.length) {
|
|
88
|
+
remoteChunk.push(remote[j]);
|
|
89
|
+
j += 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
output.push(...conflictLines(localChunk, remoteChunk));
|
|
93
|
+
conflicts += 1;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
markdown: joinLines(output),
|
|
97
|
+
state: 'conflict',
|
|
98
|
+
conflicts,
|
|
99
|
+
changed: true
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function cleanResult(markdown) {
|
|
103
|
+
return {
|
|
104
|
+
markdown,
|
|
105
|
+
state: 'clean',
|
|
106
|
+
conflicts: 0,
|
|
107
|
+
changed: false
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function resultFromOutput(output, local, conflicts) {
|
|
111
|
+
const markdown = joinLines(output);
|
|
112
|
+
if (conflicts > 0) {
|
|
113
|
+
return {
|
|
114
|
+
markdown,
|
|
115
|
+
state: 'conflict',
|
|
116
|
+
conflicts,
|
|
117
|
+
changed: markdown !== local
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
markdown,
|
|
122
|
+
state: markdown === local ? 'clean' : 'merged',
|
|
123
|
+
conflicts: 0,
|
|
124
|
+
changed: markdown !== local
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function splitLines(markdown) {
|
|
128
|
+
return markdown.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
129
|
+
}
|
|
130
|
+
function joinLines(lines) {
|
|
131
|
+
return lines.join('');
|
|
132
|
+
}
|
|
133
|
+
function conflictLines(local, remote) {
|
|
134
|
+
return [
|
|
135
|
+
'<<<<<<< LOCAL\n',
|
|
136
|
+
...ensureTrailingNewline(local),
|
|
137
|
+
'=======\n',
|
|
138
|
+
...ensureTrailingNewline(remote),
|
|
139
|
+
'>>>>>>> REMOTE\n'
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
function ensureTrailingNewline(lines) {
|
|
143
|
+
if (lines.length === 0)
|
|
144
|
+
return [];
|
|
145
|
+
const last = lines.at(-1);
|
|
146
|
+
if (last?.endsWith('\n'))
|
|
147
|
+
return lines;
|
|
148
|
+
return [...lines.slice(0, -1), `${last}\n`];
|
|
149
|
+
}
|
|
150
|
+
function combineSamePositionInsertions(local, remote) {
|
|
151
|
+
let prefix = 0;
|
|
152
|
+
while (prefix < local.length && prefix < remote.length && local[prefix] === remote[prefix]) {
|
|
153
|
+
prefix += 1;
|
|
154
|
+
}
|
|
155
|
+
return [
|
|
156
|
+
...local.slice(0, prefix),
|
|
157
|
+
...local.slice(prefix),
|
|
158
|
+
...remote.slice(prefix)
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
function isStrictlyBefore(left, right) {
|
|
162
|
+
if (left.baseStart === right.baseStart)
|
|
163
|
+
return false;
|
|
164
|
+
return left.baseEnd <= right.baseStart;
|
|
165
|
+
}
|
|
166
|
+
function collectOverlappingChanges(input) {
|
|
167
|
+
const firstLocal = input.localChanges[input.localIndex];
|
|
168
|
+
const firstRemote = input.remoteChanges[input.remoteIndex];
|
|
169
|
+
let baseStart = Math.min(firstLocal?.baseStart ?? Number.POSITIVE_INFINITY, firstRemote?.baseStart ?? Number.POSITIVE_INFINITY);
|
|
170
|
+
let baseEnd = Math.max(firstLocal?.baseEnd ?? baseStart, firstRemote?.baseEnd ?? baseStart);
|
|
171
|
+
let localIndex = input.localIndex;
|
|
172
|
+
let remoteIndex = input.remoteIndex;
|
|
173
|
+
const local = [];
|
|
174
|
+
const remote = [];
|
|
175
|
+
let changed = true;
|
|
176
|
+
while (changed) {
|
|
177
|
+
changed = false;
|
|
178
|
+
while (localIndex < input.localChanges.length && input.localChanges[localIndex].baseStart <= baseEnd) {
|
|
179
|
+
const change = input.localChanges[localIndex];
|
|
180
|
+
local.push(change);
|
|
181
|
+
baseStart = Math.min(baseStart, change.baseStart);
|
|
182
|
+
baseEnd = Math.max(baseEnd, change.baseEnd);
|
|
183
|
+
localIndex += 1;
|
|
184
|
+
changed = true;
|
|
185
|
+
}
|
|
186
|
+
while (remoteIndex < input.remoteChanges.length && input.remoteChanges[remoteIndex].baseStart <= baseEnd) {
|
|
187
|
+
const change = input.remoteChanges[remoteIndex];
|
|
188
|
+
remote.push(change);
|
|
189
|
+
baseStart = Math.min(baseStart, change.baseStart);
|
|
190
|
+
baseEnd = Math.max(baseEnd, change.baseEnd);
|
|
191
|
+
remoteIndex += 1;
|
|
192
|
+
changed = true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
baseStart,
|
|
197
|
+
baseEnd,
|
|
198
|
+
local,
|
|
199
|
+
remote,
|
|
200
|
+
localIndex,
|
|
201
|
+
remoteIndex
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function applyChangesToBaseSlice(base, start, end, changes) {
|
|
205
|
+
const output = [];
|
|
206
|
+
let index = start;
|
|
207
|
+
for (const change of changes) {
|
|
208
|
+
output.push(...base.slice(index, change.baseStart));
|
|
209
|
+
output.push(...change.lines);
|
|
210
|
+
index = change.baseEnd;
|
|
211
|
+
}
|
|
212
|
+
output.push(...base.slice(index, end));
|
|
213
|
+
return output;
|
|
214
|
+
}
|
|
215
|
+
function changesFromBase(base, next) {
|
|
216
|
+
const table = lcsTable(base, next);
|
|
217
|
+
const changes = [];
|
|
218
|
+
let i = 0;
|
|
219
|
+
let j = 0;
|
|
220
|
+
let pendingStart;
|
|
221
|
+
let pendingEnd = 0;
|
|
222
|
+
let pendingLines = [];
|
|
223
|
+
const startPending = () => {
|
|
224
|
+
if (pendingStart !== undefined)
|
|
225
|
+
return;
|
|
226
|
+
pendingStart = i;
|
|
227
|
+
pendingEnd = i;
|
|
228
|
+
};
|
|
229
|
+
const flush = () => {
|
|
230
|
+
if (pendingStart === undefined)
|
|
231
|
+
return;
|
|
232
|
+
changes.push({
|
|
233
|
+
baseStart: pendingStart,
|
|
234
|
+
baseEnd: pendingEnd,
|
|
235
|
+
lines: pendingLines
|
|
236
|
+
});
|
|
237
|
+
pendingStart = undefined;
|
|
238
|
+
pendingEnd = 0;
|
|
239
|
+
pendingLines = [];
|
|
240
|
+
};
|
|
241
|
+
while (i < base.length || j < next.length) {
|
|
242
|
+
if (i < base.length && j < next.length && base[i] === next[j]) {
|
|
243
|
+
flush();
|
|
244
|
+
i += 1;
|
|
245
|
+
j += 1;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
startPending();
|
|
249
|
+
if (j < next.length && (i === base.length || table[i][j + 1] >= table[i + 1][j])) {
|
|
250
|
+
pendingLines.push(next[j]);
|
|
251
|
+
j += 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (i < base.length) {
|
|
255
|
+
i += 1;
|
|
256
|
+
pendingEnd = i;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
flush();
|
|
260
|
+
return changes;
|
|
261
|
+
}
|
|
262
|
+
function lcsTable(a, b) {
|
|
263
|
+
const table = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
264
|
+
for (let i = a.length - 1; i >= 0; i -= 1) {
|
|
265
|
+
for (let j = b.length - 1; j >= 0; j -= 1) {
|
|
266
|
+
table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return table;
|
|
270
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { PublishProfileName } from '../profiles/publish-profile.js';
|
|
2
|
+
import type { PublishReceiptTarget } from '../receipts/publish-receipt.js';
|
|
3
|
+
export type MergeStateFile = {
|
|
4
|
+
version: 1;
|
|
5
|
+
filePath: string;
|
|
6
|
+
target?: PublishReceiptTarget;
|
|
7
|
+
profile: PublishProfileName;
|
|
8
|
+
startedAt: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function mergeStateDir(input: {
|
|
11
|
+
cwd: string;
|
|
12
|
+
filePath: string;
|
|
13
|
+
}): string;
|
|
14
|
+
export declare function assertNoMergeState(input: {
|
|
15
|
+
cwd: string;
|
|
16
|
+
filePath: string;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
export declare function writeMergeState(input: {
|
|
19
|
+
cwd: string;
|
|
20
|
+
filePath: string;
|
|
21
|
+
originalMarkdown: string;
|
|
22
|
+
target?: PublishReceiptTarget;
|
|
23
|
+
profile: PublishProfileName;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
statePath: string;
|
|
26
|
+
}>;
|
|
27
|
+
export declare function restoreMergeState(input: {
|
|
28
|
+
cwd: string;
|
|
29
|
+
filePath: string;
|
|
30
|
+
}): Promise<{
|
|
31
|
+
restored: boolean;
|
|
32
|
+
statePath: string;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join, relative } from 'node:path';
|
|
4
|
+
export function mergeStateDir(input) {
|
|
5
|
+
const key = createHash('sha256').update(input.filePath).digest('hex').slice(0, 24);
|
|
6
|
+
return join(input.cwd, '.sync', 'feishu-md-sync', 'merge-state', key);
|
|
7
|
+
}
|
|
8
|
+
export async function assertNoMergeState(input) {
|
|
9
|
+
try {
|
|
10
|
+
await readFile(join(mergeStateDir(input), 'state.json'), 'utf8');
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === 'ENOENT')
|
|
14
|
+
return;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
throw new Error('A merge is already in progress for this file. Resolve it or run merge --abort before starting another merge.');
|
|
18
|
+
}
|
|
19
|
+
export async function writeMergeState(input) {
|
|
20
|
+
await assertNoMergeState({ cwd: input.cwd, filePath: input.filePath });
|
|
21
|
+
const dir = mergeStateDir({ cwd: input.cwd, filePath: input.filePath });
|
|
22
|
+
await mkdir(dir, { recursive: true });
|
|
23
|
+
await writeFile(join(dir, 'original.md'), input.originalMarkdown, 'utf8');
|
|
24
|
+
const state = {
|
|
25
|
+
version: 1,
|
|
26
|
+
filePath: relative(input.cwd, input.filePath),
|
|
27
|
+
target: input.target,
|
|
28
|
+
profile: input.profile,
|
|
29
|
+
startedAt: new Date().toISOString()
|
|
30
|
+
};
|
|
31
|
+
const statePath = join(dir, 'state.json');
|
|
32
|
+
await writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
|
|
33
|
+
return { statePath };
|
|
34
|
+
}
|
|
35
|
+
export async function restoreMergeState(input) {
|
|
36
|
+
const dir = mergeStateDir(input);
|
|
37
|
+
const statePath = join(dir, 'state.json');
|
|
38
|
+
const originalPath = join(dir, 'original.md');
|
|
39
|
+
const original = await readFile(originalPath, 'utf8');
|
|
40
|
+
await mkdir(dirname(input.filePath), { recursive: true });
|
|
41
|
+
await writeFile(input.filePath, original, 'utf8');
|
|
42
|
+
await rm(dir, { recursive: true, force: true });
|
|
43
|
+
return { restored: true, statePath };
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
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 MergeState } from './line-merge.js';
|
|
5
|
+
export type RunMergeMode = 'write' | 'check' | 'dry-run' | 'abort';
|
|
6
|
+
export type RunMergeState = MergeState | 'aborted';
|
|
7
|
+
export type RunMergeResult = {
|
|
8
|
+
mode: RunMergeMode;
|
|
9
|
+
state: RunMergeState;
|
|
10
|
+
file: string;
|
|
11
|
+
profile: PublishProfileName;
|
|
12
|
+
base?: {
|
|
13
|
+
source: 'explicit' | 'receipt' | 'current-local' | 'none';
|
|
14
|
+
hash?: string;
|
|
15
|
+
};
|
|
16
|
+
remote?: {
|
|
17
|
+
source: 'target' | 'remote-file';
|
|
18
|
+
hash: string;
|
|
19
|
+
savedPath?: string;
|
|
20
|
+
revision?: string;
|
|
21
|
+
};
|
|
22
|
+
summary: {
|
|
23
|
+
conflicts: number;
|
|
24
|
+
changed: boolean;
|
|
25
|
+
};
|
|
26
|
+
warnings: string[];
|
|
27
|
+
};
|
|
28
|
+
export declare function runMerge(input: {
|
|
29
|
+
cwd: string;
|
|
30
|
+
filePath: string;
|
|
31
|
+
profile: PublishProfileName;
|
|
32
|
+
mode: RunMergeMode;
|
|
33
|
+
target?: PublishReceiptTarget;
|
|
34
|
+
remotePath?: string;
|
|
35
|
+
basePath?: string;
|
|
36
|
+
saveRemotePath?: string;
|
|
37
|
+
adapter: FeishuAdapter;
|
|
38
|
+
}): Promise<RunMergeResult>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { hashText, readLocalBaseSnapshot, readPublishReceipt } from '../receipts/publish-receipt.js';
|
|
4
|
+
import { applyPullTransformForProfile } from '../transform/zilliz-pull.js';
|
|
5
|
+
import { mergeLines, mergeWithoutBase } from './line-merge.js';
|
|
6
|
+
import { assertNoMergeState, restoreMergeState, writeMergeState } from './merge-state.js';
|
|
7
|
+
export async function runMerge(input) {
|
|
8
|
+
if (input.mode === 'abort') {
|
|
9
|
+
await restoreMergeState({ cwd: input.cwd, filePath: input.filePath });
|
|
10
|
+
return {
|
|
11
|
+
mode: 'abort',
|
|
12
|
+
state: 'aborted',
|
|
13
|
+
file: input.filePath,
|
|
14
|
+
profile: input.profile,
|
|
15
|
+
summary: { conflicts: 0, changed: true },
|
|
16
|
+
warnings: []
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (!input.target && !input.remotePath) {
|
|
20
|
+
throw new Error('merge requires either --target or --remote');
|
|
21
|
+
}
|
|
22
|
+
if (input.target && input.remotePath) {
|
|
23
|
+
throw new Error('merge accepts only one of --target or --remote');
|
|
24
|
+
}
|
|
25
|
+
const local = await readFile(input.filePath, 'utf8');
|
|
26
|
+
assertNoConflictMarkers(local);
|
|
27
|
+
await assertNoMergeState({ cwd: input.cwd, filePath: input.filePath });
|
|
28
|
+
const remote = await resolveRemoteMarkdown(input);
|
|
29
|
+
const base = await resolveBaseMarkdown({ ...input, localMarkdown: local });
|
|
30
|
+
const merge = base.markdown === undefined
|
|
31
|
+
? mergeWithoutBase({ local, remote: remote.markdown })
|
|
32
|
+
: mergeLines({ base: base.markdown, local, remote: remote.markdown });
|
|
33
|
+
if (input.mode === 'write' && merge.changed) {
|
|
34
|
+
await writeMergeState({
|
|
35
|
+
cwd: input.cwd,
|
|
36
|
+
filePath: input.filePath,
|
|
37
|
+
originalMarkdown: local,
|
|
38
|
+
target: input.target,
|
|
39
|
+
profile: input.profile
|
|
40
|
+
});
|
|
41
|
+
await writeFile(input.filePath, merge.markdown, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
mode: input.mode,
|
|
45
|
+
state: merge.state,
|
|
46
|
+
file: input.filePath,
|
|
47
|
+
profile: input.profile,
|
|
48
|
+
base: base.summary,
|
|
49
|
+
remote: remote.summary,
|
|
50
|
+
summary: {
|
|
51
|
+
conflicts: merge.conflicts,
|
|
52
|
+
changed: merge.changed
|
|
53
|
+
},
|
|
54
|
+
warnings: [...base.warnings, ...remote.warnings]
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function assertNoConflictMarkers(markdown) {
|
|
58
|
+
if (/^<<<<<<< |^=======\s*$|^>>>>>>> /m.test(markdown)) {
|
|
59
|
+
throw new Error('Refusing to merge because the local file contains unresolved conflict markers.');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function resolveRemoteMarkdown(input) {
|
|
63
|
+
if (input.remotePath) {
|
|
64
|
+
const raw = await readFile(input.remotePath, 'utf8');
|
|
65
|
+
const transformed = applyPullTransformForProfile(raw, input.profile);
|
|
66
|
+
return {
|
|
67
|
+
markdown: transformed.markdown,
|
|
68
|
+
warnings: transformed.warnings,
|
|
69
|
+
summary: {
|
|
70
|
+
source: 'remote-file',
|
|
71
|
+
hash: hashText(transformed.markdown)
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const fetched = await input.adapter.fetchDocMarkdown({ doc: input.target.token });
|
|
76
|
+
const transformed = applyPullTransformForProfile(fetched.markdown, input.profile);
|
|
77
|
+
if (input.saveRemotePath) {
|
|
78
|
+
await mkdir(dirname(input.saveRemotePath), { recursive: true });
|
|
79
|
+
await writeFile(input.saveRemotePath, transformed.markdown, 'utf8');
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
markdown: transformed.markdown,
|
|
83
|
+
warnings: transformed.warnings,
|
|
84
|
+
summary: {
|
|
85
|
+
source: 'target',
|
|
86
|
+
hash: hashText(transformed.markdown),
|
|
87
|
+
savedPath: input.saveRemotePath,
|
|
88
|
+
revision: fetched.revision
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function resolveBaseMarkdown(input) {
|
|
93
|
+
if (input.basePath) {
|
|
94
|
+
const markdown = await readFile(input.basePath, 'utf8');
|
|
95
|
+
return {
|
|
96
|
+
markdown,
|
|
97
|
+
summary: {
|
|
98
|
+
source: 'explicit',
|
|
99
|
+
hash: hashText(markdown)
|
|
100
|
+
},
|
|
101
|
+
warnings: []
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (input.target) {
|
|
105
|
+
const receipt = await readPublishReceipt({ cwd: input.cwd, target: input.target });
|
|
106
|
+
if (receipt?.localBaseSnapshot) {
|
|
107
|
+
const markdown = await readLocalBaseSnapshot({ cwd: input.cwd, snapshot: receipt.localBaseSnapshot });
|
|
108
|
+
if (markdown !== undefined) {
|
|
109
|
+
return {
|
|
110
|
+
markdown,
|
|
111
|
+
summary: {
|
|
112
|
+
source: 'receipt',
|
|
113
|
+
hash: hashText(markdown)
|
|
114
|
+
},
|
|
115
|
+
warnings: []
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (receipt?.localSourceHash === hashText(input.localMarkdown)) {
|
|
120
|
+
return {
|
|
121
|
+
markdown: input.localMarkdown,
|
|
122
|
+
summary: {
|
|
123
|
+
source: 'current-local',
|
|
124
|
+
hash: hashText(input.localMarkdown)
|
|
125
|
+
},
|
|
126
|
+
warnings: [
|
|
127
|
+
'receipt has no readable local base snapshot; using current local file as merge base because it still matches the last published source'
|
|
128
|
+
]
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
markdown: undefined,
|
|
134
|
+
summary: {
|
|
135
|
+
source: 'none'
|
|
136
|
+
},
|
|
137
|
+
warnings: []
|
|
138
|
+
};
|
|
139
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const PUBLISH_PROFILES: readonly ["zilliz", "milvus", "none"];
|
|
2
|
+
export type PublishProfileName = typeof PUBLISH_PROFILES[number];
|
|
3
|
+
export type PublishProfileConfig = {
|
|
4
|
+
includeTargets?: string[];
|
|
5
|
+
excludeTargets?: string[];
|
|
6
|
+
productNameMarkup?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function isPublishProfileName(value: string): value is PublishProfileName;
|
|
9
|
+
export declare function parsePublishProfileName(value: string, label: string): PublishProfileName;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const PUBLISH_PROFILES = ['zilliz', 'milvus', 'none'];
|
|
2
|
+
export function isPublishProfileName(value) {
|
|
3
|
+
return PUBLISH_PROFILES.includes(value);
|
|
4
|
+
}
|
|
5
|
+
export function parsePublishProfileName(value, label) {
|
|
6
|
+
if (isPublishProfileName(value))
|
|
7
|
+
return value;
|
|
8
|
+
throw new Error(`Invalid ${label} ${value}. Expected zilliz, milvus, or none.`);
|
|
9
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { FeishuBlock } from '../feishu/types.js';
|
|
2
|
+
export type PublishBlockPatchOperation = {
|
|
3
|
+
kind: 'update';
|
|
4
|
+
remoteBlockId: string;
|
|
5
|
+
path: number[];
|
|
6
|
+
blockType: number;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'create';
|
|
9
|
+
parentBlockId: string;
|
|
10
|
+
insertAfterBlockId: string;
|
|
11
|
+
index: number;
|
|
12
|
+
path: number[];
|
|
13
|
+
blocks: FeishuBlock[];
|
|
14
|
+
} | {
|
|
15
|
+
kind: 'delete';
|
|
16
|
+
parentBlockId: string;
|
|
17
|
+
blockIds: string[];
|
|
18
|
+
startIndex: number;
|
|
19
|
+
endIndex: number;
|
|
20
|
+
path: number[];
|
|
21
|
+
};
|
|
22
|
+
export type PublishBlockPatchPlan = {
|
|
23
|
+
kind: 'publish-block-patch-plan';
|
|
24
|
+
safeToWrite: boolean;
|
|
25
|
+
requiresCollaborationRiskConfirmation: boolean;
|
|
26
|
+
operations: PublishBlockPatchOperation[];
|
|
27
|
+
fallbackReason?: string;
|
|
28
|
+
warnings: string[];
|
|
29
|
+
};
|
|
30
|
+
export declare function planPublishBlockPatch(input: {
|
|
31
|
+
parentBlockId: string;
|
|
32
|
+
remoteBlocks: FeishuBlock[];
|
|
33
|
+
desiredBlocks: FeishuBlock[];
|
|
34
|
+
}): PublishBlockPatchPlan;
|