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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +5 -0
  3. package/README.md +107 -0
  4. package/dist/adapters/feishu-adapter.d.ts +44 -0
  5. package/dist/adapters/feishu-adapter.js +1 -0
  6. package/dist/adapters/lark-cli-adapter.d.ts +45 -0
  7. package/dist/adapters/lark-cli-adapter.js +229 -0
  8. package/dist/cli/commands/core.d.ts +2 -0
  9. package/dist/cli/commands/core.js +135 -0
  10. package/dist/cli/commands/publish.d.ts +2 -0
  11. package/dist/cli/commands/publish.js +63 -0
  12. package/dist/cli/env.d.ts +27 -0
  13. package/dist/cli/env.js +113 -0
  14. package/dist/cli/index.d.ts +2 -0
  15. package/dist/cli/index.js +52 -0
  16. package/dist/cli/output.d.ts +4 -0
  17. package/dist/cli/output.js +18 -0
  18. package/dist/config/sync-config.d.ts +13 -0
  19. package/dist/config/sync-config.js +60 -0
  20. package/dist/core/diff.d.ts +1 -0
  21. package/dist/core/diff.js +20 -0
  22. package/dist/core/doc-id.d.ts +12 -0
  23. package/dist/core/doc-id.js +41 -0
  24. package/dist/core/hash.d.ts +6 -0
  25. package/dist/core/hash.js +84 -0
  26. package/dist/core/markdown-canonical.d.ts +2 -0
  27. package/dist/core/markdown-canonical.js +11 -0
  28. package/dist/diff/run-diff.d.ts +23 -0
  29. package/dist/diff/run-diff.js +33 -0
  30. package/dist/feishu/types.d.ts +101 -0
  31. package/dist/feishu/types.js +1 -0
  32. package/dist/markdown/blocks.d.ts +5 -0
  33. package/dist/markdown/blocks.js +305 -0
  34. package/dist/markdown/from-blocks.d.ts +2 -0
  35. package/dist/markdown/from-blocks.js +110 -0
  36. package/dist/markdown/links.d.ts +3 -0
  37. package/dist/markdown/links.js +44 -0
  38. package/dist/merge/line-merge.d.ts +21 -0
  39. package/dist/merge/line-merge.js +270 -0
  40. package/dist/merge/merge-state.d.ts +33 -0
  41. package/dist/merge/merge-state.js +44 -0
  42. package/dist/merge/run-merge.d.ts +38 -0
  43. package/dist/merge/run-merge.js +139 -0
  44. package/dist/profiles/publish-profile.d.ts +9 -0
  45. package/dist/profiles/publish-profile.js +9 -0
  46. package/dist/publish/block-patch-plan.d.ts +34 -0
  47. package/dist/publish/block-patch-plan.js +223 -0
  48. package/dist/publish/block-state.d.ts +8 -0
  49. package/dist/publish/block-state.js +82 -0
  50. package/dist/publish/block-update.d.ts +4 -0
  51. package/dist/publish/block-update.js +40 -0
  52. package/dist/publish/profile-transform.d.ts +5 -0
  53. package/dist/publish/profile-transform.js +6 -0
  54. package/dist/publish/publish-plan.d.ts +35 -0
  55. package/dist/publish/publish-plan.js +113 -0
  56. package/dist/publish/run-publish.d.ts +25 -0
  57. package/dist/publish/run-publish.js +299 -0
  58. package/dist/publish/title.d.ts +9 -0
  59. package/dist/publish/title.js +20 -0
  60. package/dist/pull/run-pull.d.ts +23 -0
  61. package/dist/pull/run-pull.js +57 -0
  62. package/dist/receipts/publish-receipt.d.ts +49 -0
  63. package/dist/receipts/publish-receipt.js +52 -0
  64. package/dist/receipts/pull-receipt.d.ts +31 -0
  65. package/dist/receipts/pull-receipt.js +30 -0
  66. package/dist/status/run-status.d.ts +56 -0
  67. package/dist/status/run-status.js +131 -0
  68. package/dist/transform/include-tags.d.ts +6 -0
  69. package/dist/transform/include-tags.js +22 -0
  70. package/dist/transform/zilliz-publish.d.ts +5 -0
  71. package/dist/transform/zilliz-publish.js +42 -0
  72. package/dist/transform/zilliz-pull.d.ts +6 -0
  73. package/dist/transform/zilliz-pull.js +34 -0
  74. 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;