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,131 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { canonicalMarkdown, canonicalMarkdownHash } from '../core/markdown-canonical.js';
3
+ import { hashText, publishReceiptPath, readPublishReceipt } from '../receipts/publish-receipt.js';
4
+ import { applyPublishTransformForProfile } from '../publish/profile-transform.js';
5
+ export async function runStatus(input) {
6
+ return statusFromContext(await loadPublishStatusContext(input));
7
+ }
8
+ export async function loadPublishStatusContext(input) {
9
+ const localSource = await readFile(input.sourcePath, 'utf8');
10
+ const transform = applyPublishTransformForProfile(localSource, input.profile);
11
+ const remote = await input.adapter.fetchDocMarkdown({ doc: input.target.token });
12
+ const receipt = await readPublishReceipt({ cwd: input.cwd, target: input.target });
13
+ return {
14
+ cwd: input.cwd,
15
+ target: input.target,
16
+ sourcePath: input.sourcePath,
17
+ profile: input.profile,
18
+ localSource,
19
+ publishDraft: transform.markdown,
20
+ publishDraftCanonical: canonicalMarkdown(transform.markdown),
21
+ remoteMarkdown: remote.markdown,
22
+ remoteCanonical: canonicalMarkdown(remote.markdown),
23
+ remoteRevision: remote.revision,
24
+ receipt,
25
+ transformWarnings: transform.warnings
26
+ };
27
+ }
28
+ export function statusFromContext(context) {
29
+ const localSourceHash = hashText(context.localSource);
30
+ const publishDraftHash = hashText(context.publishDraft);
31
+ const publishDraftCanonicalHash = canonicalMarkdownHash(context.publishDraft);
32
+ const remoteSnapshotHash = hashText(context.remoteMarkdown);
33
+ const remoteCanonicalHash = canonicalMarkdownHash(context.remoteMarkdown);
34
+ const contentMatchesRemote = publishDraftCanonicalHash === remoteCanonicalHash;
35
+ const receiptPath = publishReceiptPath({ cwd: context.cwd, target: context.target });
36
+ if (!context.receipt) {
37
+ const state = 'untracked';
38
+ return {
39
+ target: context.target,
40
+ sourcePath: context.sourcePath,
41
+ profile: context.profile,
42
+ state,
43
+ localChanged: true,
44
+ remoteChanged: !contentMatchesRemote,
45
+ contentMatchesRemote,
46
+ hasReceipt: false,
47
+ receiptPath,
48
+ localSourceHash,
49
+ publishDraftHash,
50
+ publishDraftCanonicalHash,
51
+ remoteSnapshotHash,
52
+ remoteCanonicalHash,
53
+ remoteRevision: context.remoteRevision,
54
+ transformWarnings: context.transformWarnings,
55
+ recommendation: recommendationFor({ state, contentMatchesRemote })
56
+ };
57
+ }
58
+ const localChanged = context.receipt.publishDraftHash !== publishDraftHash;
59
+ const remoteChanged = context.receipt.remoteSnapshotHash !== remoteSnapshotHash;
60
+ const state = statusStateFor({ localChanged, remoteChanged });
61
+ return {
62
+ target: context.target,
63
+ sourcePath: context.sourcePath,
64
+ profile: context.profile,
65
+ state,
66
+ localChanged,
67
+ remoteChanged,
68
+ contentMatchesRemote,
69
+ hasReceipt: true,
70
+ receiptPath,
71
+ localSourceHash,
72
+ publishDraftHash,
73
+ publishDraftCanonicalHash,
74
+ remoteSnapshotHash,
75
+ remoteCanonicalHash,
76
+ remoteRevision: context.remoteRevision,
77
+ transformWarnings: context.transformWarnings,
78
+ recommendation: recommendationFor({ state, contentMatchesRemote })
79
+ };
80
+ }
81
+ function statusStateFor(input) {
82
+ if (input.localChanged && input.remoteChanged)
83
+ return 'diverged';
84
+ if (input.localChanged)
85
+ return 'local-changed';
86
+ if (input.remoteChanged)
87
+ return 'remote-changed';
88
+ return 'clean';
89
+ }
90
+ function recommendationFor(input) {
91
+ if (input.state === 'clean') {
92
+ return {
93
+ action: 'no-action',
94
+ reason: 'publish would be a no-op'
95
+ };
96
+ }
97
+ if (input.contentMatchesRemote) {
98
+ return {
99
+ action: 'publish-dry-run',
100
+ reason: 'content matches remote, but the publish receipt is stale'
101
+ };
102
+ }
103
+ if (input.state === 'local-changed') {
104
+ return {
105
+ action: 'publish-dry-run',
106
+ reason: 'local publish draft changed and remote still matches the last publish receipt'
107
+ };
108
+ }
109
+ if (input.state === 'remote-changed') {
110
+ return {
111
+ action: 'pull-review',
112
+ reason: 'remote changed since the last publish receipt'
113
+ };
114
+ }
115
+ if (input.state === 'diverged') {
116
+ return {
117
+ action: 'resolve-divergence',
118
+ reason: 'local publish draft and remote both changed since the last publish receipt'
119
+ };
120
+ }
121
+ if (input.contentMatchesRemote) {
122
+ return {
123
+ action: 'publish-dry-run',
124
+ reason: 'content matches remote, but no publish receipt exists for this target'
125
+ };
126
+ }
127
+ return {
128
+ action: 'adopt-or-replace',
129
+ reason: 'remote is untracked and differs from the current publish draft'
130
+ };
131
+ }
@@ -0,0 +1,6 @@
1
+ export declare function protectInlineSpans(line: string): {
2
+ protectedLine: string;
3
+ restore(value: string): string;
4
+ };
5
+ export declare function milvusOnly(value: string): string;
6
+ export declare function dualProductName(): string;
@@ -0,0 +1,22 @@
1
+ export function protectInlineSpans(line) {
2
+ const spans = [];
3
+ const protect = (value) => {
4
+ const token = `\u0000${spans.length}\u0000`;
5
+ spans.push(value);
6
+ return token;
7
+ };
8
+ const protectedLine = line
9
+ .replace(/<include\b[\s\S]*?<\/include>/g, protect)
10
+ .replace(/`[^`]*`/g, protect)
11
+ .replace(/\[[^\]]+\]\([^)]+\)/g, protect);
12
+ return {
13
+ protectedLine,
14
+ restore: (value) => value.replace(/\u0000(\d+)\u0000/g, (_, index) => spans[Number(index)] ?? '')
15
+ };
16
+ }
17
+ export function milvusOnly(value) {
18
+ return `<include target="milvus">${value}</include>`;
19
+ }
20
+ export function dualProductName() {
21
+ return '<include target="milvus">Milvus</include><include target="zilliz">Zilliz Cloud</include>';
22
+ }
@@ -0,0 +1,5 @@
1
+ export type PublishTransformResult = {
2
+ markdown: string;
3
+ warnings: string[];
4
+ };
5
+ export declare function applyZillizPublishTransform(markdown: string): PublishTransformResult;
@@ -0,0 +1,42 @@
1
+ import { dualProductName, milvusOnly, protectInlineSpans } from './include-tags.js';
2
+ const VERSIONED_MILVUS_PATTERN = /\bMilvus\s+v?\d+(?:\.\d+)*(?:\.x)?\b/;
3
+ const ORDINARY_MILVUS_PATTERN = /\bMilvus(?!\s+v?\d)(?![-\w])/g;
4
+ export function applyZillizPublishTransform(markdown) {
5
+ const warnings = [];
6
+ const lines = markdown.replace(/\r\n/g, '\n').split('\n');
7
+ let inCodeFence = false;
8
+ const transformed = lines.map((line) => {
9
+ if (/^```/.test(line.trim())) {
10
+ inCodeFence = !inCodeFence;
11
+ return line;
12
+ }
13
+ if (inCodeFence)
14
+ return line;
15
+ if (/^#{1,6}\s+/.test(line)) {
16
+ if (/\bMilvus\b/.test(line))
17
+ warnings.push(`Heading contains Milvus product wording and was not rewritten: ${line}`);
18
+ return line;
19
+ }
20
+ return transformLine(line);
21
+ }).join('\n');
22
+ return { markdown: transformed, warnings };
23
+ }
24
+ function transformLine(line) {
25
+ if (line.trim() === '')
26
+ return line;
27
+ const { protectedLine, restore } = protectInlineSpans(line);
28
+ const transformed = transformSentences(protectedLine);
29
+ return restore(transformed);
30
+ }
31
+ function transformSentences(line) {
32
+ return line
33
+ .split(/(?<=[.!?])(\s+)/)
34
+ .map((part) => {
35
+ if (/^\s+$/.test(part))
36
+ return part;
37
+ if (VERSIONED_MILVUS_PATTERN.test(part))
38
+ return milvusOnly(part);
39
+ return part.replace(ORDINARY_MILVUS_PATTERN, dualProductName());
40
+ })
41
+ .join('');
42
+ }
@@ -0,0 +1,6 @@
1
+ import type { PublishProfileName } from '../profiles/publish-profile.js';
2
+ export type PullTransformResult = {
3
+ markdown: string;
4
+ warnings: string[];
5
+ };
6
+ export declare function applyPullTransformForProfile(markdown: string, profile: PublishProfileName): PullTransformResult;
@@ -0,0 +1,34 @@
1
+ const SUPPORTED_TARGETS = new Set(['milvus', 'zilliz']);
2
+ const TAG_PATTERN = /<(include|exclude)\b([^>]*)>([\s\S]*?)<\/\1>/g;
3
+ const TARGET_ATTR_PATTERN = /\btarget\s*=\s*["']([^"']+)["']/;
4
+ export function applyPullTransformForProfile(markdown, profile) {
5
+ if (profile === 'none')
6
+ return { markdown, warnings: [] };
7
+ const warnings = [];
8
+ const transformed = markdown.replace(TAG_PATTERN, (full, tagName, attrs, content) => {
9
+ const targets = parseTargets(attrs);
10
+ if (!targets) {
11
+ warnings.push(`${tagName} tag without target attribute was left unchanged.`);
12
+ return full;
13
+ }
14
+ const unsupportedTargets = targets.filter((target) => !SUPPORTED_TARGETS.has(target));
15
+ if (unsupportedTargets.length > 0) {
16
+ warnings.push(`Unsupported ${tagName} target "${unsupportedTargets.join(', ')}"; left tag unchanged.`);
17
+ return full;
18
+ }
19
+ const matches = targets.includes(profile);
20
+ if (tagName === 'include')
21
+ return matches ? content : '';
22
+ return matches ? '' : content;
23
+ });
24
+ return { markdown: transformed, warnings };
25
+ }
26
+ function parseTargets(attrs) {
27
+ const match = attrs.match(TARGET_ATTR_PATTERN);
28
+ if (!match)
29
+ return undefined;
30
+ return match[1]
31
+ .split(/[,\s]+/)
32
+ .map((target) => target.trim())
33
+ .filter(Boolean);
34
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "feishu-md-sync",
3
+ "version": "0.1.0",
4
+ "description": "Dry-run-first sync between local Markdown and Feishu/Lark documents using the official lark-cli.",
5
+ "keywords": [
6
+ "feishu",
7
+ "lark",
8
+ "markdown",
9
+ "sync",
10
+ "cli"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/liyun95/feishu-md-sync.git",
16
+ "directory": "packages/cli"
17
+ },
18
+ "homepage": "https://liyun95.github.io/feishu-md-sync/",
19
+ "bugs": {
20
+ "url": "https://github.com/liyun95/feishu-md-sync/issues"
21
+ },
22
+ "type": "module",
23
+ "bin": {
24
+ "feishu-md-sync": "dist/cli/index.js"
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "README.md",
29
+ "LICENSE",
30
+ "NOTICE"
31
+ ],
32
+ "publishConfig": {
33
+ "access": "public",
34
+ "provenance": true
35
+ },
36
+ "scripts": {
37
+ "clean": "node scripts/clean-dist.mjs",
38
+ "build": "npm run clean && tsc -p tsconfig.json",
39
+ "prepack": "npm run build",
40
+ "test": "vitest run",
41
+ "test:live:feishu": "vitest run test/live-feishu-publish.test.ts && vitest run test/live-feishu-merge.test.ts",
42
+ "test:coverage": "vitest run --coverage",
43
+ "test:package": "npm run build && node scripts/package-smoke.mjs",
44
+ "typecheck": "tsc -p tsconfig.json --noEmit",
45
+ "dev": "tsx src/cli/index.ts"
46
+ },
47
+ "engines": {
48
+ "node": ">=20"
49
+ },
50
+ "dependencies": {
51
+ "commander": "^12.1.0",
52
+ "dotenv": "^16.4.7"
53
+ },
54
+ "devDependencies": {
55
+ "@types/node": "^22.10.2",
56
+ "@vitest/coverage-v8": "^3.2.4",
57
+ "tsx": "^4.19.2",
58
+ "typescript": "^5.7.2",
59
+ "vitest": "^3.2.4"
60
+ }
61
+ }