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,27 @@
|
|
|
1
|
+
export type CliEnvLoadOptions = {
|
|
2
|
+
argv?: string[];
|
|
3
|
+
cwd?: string;
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
moduleUrl?: string;
|
|
6
|
+
};
|
|
7
|
+
export type CliEnvLoadReport = {
|
|
8
|
+
cwd: string;
|
|
9
|
+
explicitEnvFile?: string;
|
|
10
|
+
attemptedFiles: string[];
|
|
11
|
+
loadedFiles: string[];
|
|
12
|
+
};
|
|
13
|
+
export type AuthDoctorReport = {
|
|
14
|
+
envFiles: Array<{
|
|
15
|
+
path: string;
|
|
16
|
+
loaded: boolean;
|
|
17
|
+
explicit: boolean;
|
|
18
|
+
}>;
|
|
19
|
+
larkCli: {
|
|
20
|
+
command: string;
|
|
21
|
+
identity: 'auto' | 'bot' | 'user';
|
|
22
|
+
identityEnv?: string;
|
|
23
|
+
warning?: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
export declare function loadCliEnv(options?: CliEnvLoadOptions): CliEnvLoadReport;
|
|
27
|
+
export declare function buildAuthDoctorReport(report: CliEnvLoadReport, env?: NodeJS.ProcessEnv): AuthDoctorReport;
|
package/dist/cli/env.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { config as loadDotenv } from 'dotenv';
|
|
5
|
+
export function loadCliEnv(options = {}) {
|
|
6
|
+
const argv = options.argv ?? process.argv;
|
|
7
|
+
const cwd = options.cwd ?? process.cwd();
|
|
8
|
+
const env = options.env ?? process.env;
|
|
9
|
+
const explicitEnvFile = envFileFromArgv(argv, cwd);
|
|
10
|
+
const attemptedFiles = candidateEnvFiles({
|
|
11
|
+
cwd,
|
|
12
|
+
explicitEnvFile,
|
|
13
|
+
moduleUrl: options.moduleUrl
|
|
14
|
+
});
|
|
15
|
+
const loadedFiles = [];
|
|
16
|
+
for (const file of attemptedFiles) {
|
|
17
|
+
if (!existsSync(file)) {
|
|
18
|
+
if (file === explicitEnvFile) {
|
|
19
|
+
throw new Error(`--env-file does not exist: ${file}`);
|
|
20
|
+
}
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const result = loadDotenv({
|
|
24
|
+
path: file,
|
|
25
|
+
processEnv: env,
|
|
26
|
+
override: file === explicitEnvFile
|
|
27
|
+
});
|
|
28
|
+
if (result.error)
|
|
29
|
+
throw result.error;
|
|
30
|
+
loadedFiles.push(file);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
cwd,
|
|
34
|
+
explicitEnvFile,
|
|
35
|
+
attemptedFiles,
|
|
36
|
+
loadedFiles
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function buildAuthDoctorReport(report, env = process.env) {
|
|
40
|
+
const identityEnv = env.FEISHU_MD_SYNC_LARK_AS;
|
|
41
|
+
const identity = identityEnv === 'bot' || identityEnv === 'user' ? identityEnv : 'auto';
|
|
42
|
+
return {
|
|
43
|
+
envFiles: report.attemptedFiles.map((file) => ({
|
|
44
|
+
path: file,
|
|
45
|
+
loaded: report.loadedFiles.includes(file),
|
|
46
|
+
explicit: file === report.explicitEnvFile
|
|
47
|
+
})),
|
|
48
|
+
larkCli: {
|
|
49
|
+
command: 'lark-cli auth status',
|
|
50
|
+
identity,
|
|
51
|
+
identityEnv,
|
|
52
|
+
warning: identityEnv && identity === 'auto'
|
|
53
|
+
? 'FEISHU_MD_SYNC_LARK_AS must be "bot" or "user"; falling back to lark-cli default identity.'
|
|
54
|
+
: undefined
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function candidateEnvFiles(input) {
|
|
59
|
+
const files = [];
|
|
60
|
+
if (input.explicitEnvFile)
|
|
61
|
+
files.push(input.explicitEnvFile);
|
|
62
|
+
files.push(resolve(input.cwd, '.env'));
|
|
63
|
+
for (const root of packageRootsFromModule(input.moduleUrl)) {
|
|
64
|
+
files.push(join(root, '.env'));
|
|
65
|
+
}
|
|
66
|
+
return Array.from(new Set(files));
|
|
67
|
+
}
|
|
68
|
+
function envFileFromArgv(argv, cwd) {
|
|
69
|
+
for (let index = argv.length - 1; index >= 0; index -= 1) {
|
|
70
|
+
const arg = argv[index];
|
|
71
|
+
if (arg === '--env-file') {
|
|
72
|
+
const value = argv[index + 1];
|
|
73
|
+
if (!value)
|
|
74
|
+
throw new Error('--env-file requires a path.');
|
|
75
|
+
return resolveEnvPath(value, cwd);
|
|
76
|
+
}
|
|
77
|
+
if (arg.startsWith('--env-file=')) {
|
|
78
|
+
return resolveEnvPath(arg.slice('--env-file='.length), cwd);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function resolveEnvPath(value, cwd) {
|
|
84
|
+
return isAbsolute(value) ? value : resolve(cwd, value);
|
|
85
|
+
}
|
|
86
|
+
function packageRootsFromModule(moduleUrl) {
|
|
87
|
+
if (!moduleUrl?.startsWith('file:'))
|
|
88
|
+
return [];
|
|
89
|
+
let current = dirname(fileURLToPath(moduleUrl));
|
|
90
|
+
const roots = [];
|
|
91
|
+
while (true) {
|
|
92
|
+
const packagePath = join(current, 'package.json');
|
|
93
|
+
if (existsSync(packagePath)) {
|
|
94
|
+
roots.push(current);
|
|
95
|
+
if (isWorkspaceRoot(packagePath))
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
const parent = dirname(current);
|
|
99
|
+
if (parent === current)
|
|
100
|
+
break;
|
|
101
|
+
current = parent;
|
|
102
|
+
}
|
|
103
|
+
return roots;
|
|
104
|
+
}
|
|
105
|
+
function isWorkspaceRoot(packagePath) {
|
|
106
|
+
try {
|
|
107
|
+
const parsed = JSON.parse(readFileSync(packagePath, 'utf8'));
|
|
108
|
+
return Array.isArray(parsed.workspaces) || Boolean(parsed.workspaces);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { registerCoreCommands } from './commands/core.js';
|
|
4
|
+
import { registerPublishCommand } from './commands/publish.js';
|
|
5
|
+
import { buildAuthDoctorReport, loadCliEnv } from './env.js';
|
|
6
|
+
import { printFormatted } from './output.js';
|
|
7
|
+
const topLevelCommands = ['publish', 'status', 'pull', 'diff', 'merge', 'doctor', 'help'];
|
|
8
|
+
main().catch((error) => {
|
|
9
|
+
const message = error.message;
|
|
10
|
+
console.error(message);
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
});
|
|
13
|
+
async function main() {
|
|
14
|
+
const envLoadReport = loadCliEnv({ moduleUrl: import.meta.url });
|
|
15
|
+
const program = new Command();
|
|
16
|
+
program
|
|
17
|
+
.name('feishu-md-sync')
|
|
18
|
+
.description('Sync local Markdown with Feishu/Lark online documents. Defaults to dry-run for remote writes.');
|
|
19
|
+
registerPublishCommand(program);
|
|
20
|
+
registerCoreCommands(program);
|
|
21
|
+
const doctor = program
|
|
22
|
+
.command('doctor')
|
|
23
|
+
.description('inspect local feishu-md-sync configuration');
|
|
24
|
+
doctor
|
|
25
|
+
.command('auth')
|
|
26
|
+
.description('show lark-cli auth hints and loaded .env files')
|
|
27
|
+
.option('--format <format>', 'output format: pretty | json', 'pretty')
|
|
28
|
+
.action(async (opts) => {
|
|
29
|
+
printFormatted(buildAuthDoctorReport(envLoadReport), opts.format);
|
|
30
|
+
});
|
|
31
|
+
const unknownCommand = findUnknownTopLevelCommand(process.argv.slice(2), topLevelCommands);
|
|
32
|
+
if (unknownCommand) {
|
|
33
|
+
throw new Error(`error: unknown command '${unknownCommand}'`);
|
|
34
|
+
}
|
|
35
|
+
await program.parseAsync();
|
|
36
|
+
}
|
|
37
|
+
function findUnknownTopLevelCommand(args, knownCommands) {
|
|
38
|
+
const known = new Set(knownCommands);
|
|
39
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
40
|
+
const arg = args[index];
|
|
41
|
+
if (arg === '--env-file') {
|
|
42
|
+
index += 1;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (arg.startsWith('--env-file='))
|
|
46
|
+
continue;
|
|
47
|
+
if (arg.startsWith('-'))
|
|
48
|
+
continue;
|
|
49
|
+
return known.has(arg) ? undefined : arg;
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function printJson(value) {
|
|
2
|
+
console.log(JSON.stringify(value, null, 2));
|
|
3
|
+
}
|
|
4
|
+
export function printFormatted(value, format) {
|
|
5
|
+
if (format === 'json') {
|
|
6
|
+
printJson(value);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (typeof value === 'string') {
|
|
10
|
+
console.log(value);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
printJson(value);
|
|
14
|
+
}
|
|
15
|
+
export function setFailedExitCode(condition) {
|
|
16
|
+
if (condition)
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type PublishProfileConfig, type PublishProfileName } from '../profiles/publish-profile.js';
|
|
2
|
+
export type SyncConfig = {
|
|
3
|
+
defaultProfile?: PublishProfileName;
|
|
4
|
+
profiles: Record<string, PublishProfileConfig>;
|
|
5
|
+
};
|
|
6
|
+
export type LoadSyncConfigInput = {
|
|
7
|
+
cwd: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function resolvePublishProfile(input: {
|
|
10
|
+
cliProfile?: string;
|
|
11
|
+
config: SyncConfig;
|
|
12
|
+
}): PublishProfileName;
|
|
13
|
+
export declare function loadSyncConfig(input: LoadSyncConfigInput): Promise<SyncConfig>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { parsePublishProfileName } from '../profiles/publish-profile.js';
|
|
4
|
+
export function resolvePublishProfile(input) {
|
|
5
|
+
if (input.cliProfile)
|
|
6
|
+
return parsePublishProfileName(input.cliProfile, '--profile');
|
|
7
|
+
return input.config.defaultProfile ?? 'none';
|
|
8
|
+
}
|
|
9
|
+
export async function loadSyncConfig(input) {
|
|
10
|
+
const path = join(input.cwd, 'feishu-md-sync.config.json');
|
|
11
|
+
let raw;
|
|
12
|
+
try {
|
|
13
|
+
raw = await readFile(path, 'utf8');
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
if (error.code === 'ENOENT')
|
|
17
|
+
return { profiles: {} };
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
const parsed = JSON.parse(raw);
|
|
21
|
+
if (!isRecord(parsed)) {
|
|
22
|
+
throw new Error('feishu-md-sync.config.json must contain a JSON object.');
|
|
23
|
+
}
|
|
24
|
+
const defaultProfile = typeof parsed.defaultProfile === 'string'
|
|
25
|
+
? parsePublishProfileName(parsed.defaultProfile, 'defaultProfile')
|
|
26
|
+
: undefined;
|
|
27
|
+
return {
|
|
28
|
+
defaultProfile,
|
|
29
|
+
profiles: parseProfiles(parsed.profiles)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function parseProfiles(value) {
|
|
33
|
+
if (value === undefined)
|
|
34
|
+
return {};
|
|
35
|
+
if (!isRecord(value))
|
|
36
|
+
throw new Error('profiles must be a JSON object.');
|
|
37
|
+
const profiles = {};
|
|
38
|
+
for (const [name, rawProfile] of Object.entries(value)) {
|
|
39
|
+
parsePublishProfileName(name, 'profile name');
|
|
40
|
+
if (!isRecord(rawProfile))
|
|
41
|
+
throw new Error(`profiles.${name} must be a JSON object.`);
|
|
42
|
+
profiles[name] = {
|
|
43
|
+
includeTargets: parseStringArray(rawProfile.includeTargets, `profiles.${name}.includeTargets`),
|
|
44
|
+
excludeTargets: parseStringArray(rawProfile.excludeTargets, `profiles.${name}.excludeTargets`),
|
|
45
|
+
productNameMarkup: typeof rawProfile.productNameMarkup === 'boolean' ? rawProfile.productNameMarkup : undefined
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return profiles;
|
|
49
|
+
}
|
|
50
|
+
function parseStringArray(value, label) {
|
|
51
|
+
if (value === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === 'string')) {
|
|
54
|
+
throw new Error(`${label} must be an array of strings.`);
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
function isRecord(value) {
|
|
59
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function unifiedDiff(leftName: string, rightName: string, left: string, right: string): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function unifiedDiff(leftName, rightName, left, right) {
|
|
2
|
+
const leftLines = left.split('\n');
|
|
3
|
+
const rightLines = right.split('\n');
|
|
4
|
+
const max = Math.max(leftLines.length, rightLines.length);
|
|
5
|
+
const lines = [`--- ${leftName}`, `+++ ${rightName}`];
|
|
6
|
+
for (let index = 0; index < max; index += 1) {
|
|
7
|
+
const a = leftLines[index];
|
|
8
|
+
const b = rightLines[index];
|
|
9
|
+
if (a === b) {
|
|
10
|
+
if (a !== undefined)
|
|
11
|
+
lines.push(` ${a}`);
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
if (a !== undefined)
|
|
15
|
+
lines.push(`-${a}`);
|
|
16
|
+
if (b !== undefined)
|
|
17
|
+
lines.push(`+${b}`);
|
|
18
|
+
}
|
|
19
|
+
return `${lines.join('\n')}\n`;
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type FeishuTarget = {
|
|
2
|
+
kind: 'docx';
|
|
3
|
+
token: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'wiki';
|
|
6
|
+
token: string;
|
|
7
|
+
} | {
|
|
8
|
+
kind: 'folder';
|
|
9
|
+
token: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function parseFeishuDocId(input: string): string;
|
|
12
|
+
export declare function parseFeishuTarget(input: string): FeishuTarget;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const DOC_ID_PATTERN = /^[A-Za-z0-9]{16,}$/;
|
|
2
|
+
export function parseFeishuDocId(input) {
|
|
3
|
+
const target = parseFeishuTarget(input);
|
|
4
|
+
if (target.kind !== 'docx') {
|
|
5
|
+
throw new Error(`Wiki URL requires API resolution before it can be used as a docx document: ${input}`);
|
|
6
|
+
}
|
|
7
|
+
return target.token;
|
|
8
|
+
}
|
|
9
|
+
export function parseFeishuTarget(input) {
|
|
10
|
+
const trimmed = input.trim();
|
|
11
|
+
if (!trimmed) {
|
|
12
|
+
throw new Error('Feishu document ID or URL is required.');
|
|
13
|
+
}
|
|
14
|
+
if (DOC_ID_PATTERN.test(trimmed)) {
|
|
15
|
+
return { kind: 'docx', token: trimmed };
|
|
16
|
+
}
|
|
17
|
+
let url;
|
|
18
|
+
try {
|
|
19
|
+
url = new URL(trimmed);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new Error(`Invalid Feishu document ID or URL: ${input}`);
|
|
23
|
+
}
|
|
24
|
+
const docxMatch = url.pathname.match(/\/docx\/([A-Za-z0-9]+)/);
|
|
25
|
+
if (docxMatch) {
|
|
26
|
+
return { kind: 'docx', token: docxMatch[1] };
|
|
27
|
+
}
|
|
28
|
+
const docsMatch = url.pathname.match(/\/docs\/([A-Za-z0-9]+)/);
|
|
29
|
+
if (docsMatch) {
|
|
30
|
+
return { kind: 'docx', token: docsMatch[1] };
|
|
31
|
+
}
|
|
32
|
+
const wikiMatch = url.pathname.match(/\/wiki\/([A-Za-z0-9]+)/);
|
|
33
|
+
if (wikiMatch) {
|
|
34
|
+
return { kind: 'wiki', token: wikiMatch[1] };
|
|
35
|
+
}
|
|
36
|
+
const folderMatch = url.pathname.match(/\/drive\/folder\/([A-Za-z0-9]+)/);
|
|
37
|
+
if (folderMatch) {
|
|
38
|
+
return { kind: 'folder', token: folderMatch[1] };
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`Could not find a supported Feishu token in URL: ${input}`);
|
|
41
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FeishuBlock } from '../feishu/types.js';
|
|
2
|
+
export declare function sha256(value: string): string;
|
|
3
|
+
export declare function hashSource(content: string): string;
|
|
4
|
+
export declare function stableStringify(value: unknown): string;
|
|
5
|
+
export declare function normalizeBlockForHash(block: FeishuBlock): unknown;
|
|
6
|
+
export declare function hashBlocks(blocks: FeishuBlock[]): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
export function sha256(value) {
|
|
3
|
+
return createHash('sha256').update(value, 'utf8').digest('hex');
|
|
4
|
+
}
|
|
5
|
+
export function hashSource(content) {
|
|
6
|
+
return sha256(content);
|
|
7
|
+
}
|
|
8
|
+
export function stableStringify(value) {
|
|
9
|
+
if (Array.isArray(value)) {
|
|
10
|
+
return `[${value.map(stableStringify).join(',')}]`;
|
|
11
|
+
}
|
|
12
|
+
if (value && typeof value === 'object') {
|
|
13
|
+
const obj = value;
|
|
14
|
+
const keys = Object.keys(obj).sort();
|
|
15
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(obj[key])}`).join(',')}}`;
|
|
16
|
+
}
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
}
|
|
19
|
+
export function normalizeBlockForHash(block) {
|
|
20
|
+
return normalizeValue(block);
|
|
21
|
+
}
|
|
22
|
+
export function hashBlocks(blocks) {
|
|
23
|
+
return sha256(stableStringify(blocks.map(normalizeBlockForHash)));
|
|
24
|
+
}
|
|
25
|
+
function normalizeValue(value) {
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return mergeAdjacentTextRuns(value.map(normalizeValue));
|
|
28
|
+
}
|
|
29
|
+
if (value && typeof value === 'object') {
|
|
30
|
+
const normalized = {};
|
|
31
|
+
for (const [key, child] of Object.entries(value)) {
|
|
32
|
+
if (key === 'block_id')
|
|
33
|
+
continue;
|
|
34
|
+
if (key === 'parent_id')
|
|
35
|
+
continue;
|
|
36
|
+
if (key === 'children')
|
|
37
|
+
continue;
|
|
38
|
+
if (key === 'merge_info')
|
|
39
|
+
continue;
|
|
40
|
+
if (key === 'column_width' && isDefaultColumnWidth(child))
|
|
41
|
+
continue;
|
|
42
|
+
if (key === 'folded' && child === false)
|
|
43
|
+
continue;
|
|
44
|
+
if (key === 'align' && child === 1)
|
|
45
|
+
continue;
|
|
46
|
+
if (key === 'wrap' && child === false)
|
|
47
|
+
continue;
|
|
48
|
+
normalized[key] = normalizeValue(child);
|
|
49
|
+
}
|
|
50
|
+
return normalized;
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
function isDefaultColumnWidth(value) {
|
|
55
|
+
return Array.isArray(value) && value.every((item) => item === 100);
|
|
56
|
+
}
|
|
57
|
+
function mergeAdjacentTextRuns(values) {
|
|
58
|
+
const merged = [];
|
|
59
|
+
for (const value of values) {
|
|
60
|
+
const previous = merged[merged.length - 1];
|
|
61
|
+
if (isTextRun(previous) && isTextRun(value) && sameTextRunStyle(previous, value)) {
|
|
62
|
+
const previousRun = previous.text_run;
|
|
63
|
+
const currentRun = value.text_run;
|
|
64
|
+
previousRun.content = `${previousRun.content}${currentRun.content}`;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
merged.push(value);
|
|
68
|
+
}
|
|
69
|
+
return merged;
|
|
70
|
+
}
|
|
71
|
+
function sameTextRunStyle(previous, current) {
|
|
72
|
+
if (!isTextRun(previous) || !isTextRun(current))
|
|
73
|
+
return false;
|
|
74
|
+
return stableStringify(previous.text_run.text_element_style) === stableStringify(current.text_run.text_element_style);
|
|
75
|
+
}
|
|
76
|
+
function isTextRun(value) {
|
|
77
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
78
|
+
return false;
|
|
79
|
+
const textRun = value.text_run;
|
|
80
|
+
return Boolean(textRun &&
|
|
81
|
+
typeof textRun === 'object' &&
|
|
82
|
+
!Array.isArray(textRun) &&
|
|
83
|
+
typeof textRun.content === 'string');
|
|
84
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { hashText } from '../receipts/publish-receipt.js';
|
|
2
|
+
export function canonicalMarkdown(markdown) {
|
|
3
|
+
return markdown
|
|
4
|
+
.replace(/\r\n/g, '\n')
|
|
5
|
+
.replace(/[ \t]+$/gm, '')
|
|
6
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
7
|
+
.trim();
|
|
8
|
+
}
|
|
9
|
+
export function canonicalMarkdownHash(markdown) {
|
|
10
|
+
return hashText(canonicalMarkdown(markdown));
|
|
11
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
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 PublishStatusResult } from '../status/run-status.js';
|
|
5
|
+
export type RunDiffResult = {
|
|
6
|
+
mode: 'read-only';
|
|
7
|
+
target: PublishReceiptTarget;
|
|
8
|
+
sourcePath: string;
|
|
9
|
+
profile: PublishProfileName;
|
|
10
|
+
left: 'remote-current';
|
|
11
|
+
right: 'publish-draft';
|
|
12
|
+
hasDiff: boolean;
|
|
13
|
+
diff: string;
|
|
14
|
+
status: PublishStatusResult;
|
|
15
|
+
};
|
|
16
|
+
export declare function runDiff(input: {
|
|
17
|
+
cwd: string;
|
|
18
|
+
sourcePath: string;
|
|
19
|
+
target: PublishReceiptTarget;
|
|
20
|
+
profile: PublishProfileName;
|
|
21
|
+
adapter: FeishuAdapter;
|
|
22
|
+
}): Promise<RunDiffResult>;
|
|
23
|
+
export declare function diffSummaryLines(result: RunDiffResult): string[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { unifiedDiff } from '../core/diff.js';
|
|
2
|
+
import { loadPublishStatusContext, statusFromContext } from '../status/run-status.js';
|
|
3
|
+
export async function runDiff(input) {
|
|
4
|
+
const context = await loadPublishStatusContext(input);
|
|
5
|
+
const hasDiff = context.remoteCanonical !== context.publishDraftCanonical;
|
|
6
|
+
return {
|
|
7
|
+
mode: 'read-only',
|
|
8
|
+
target: input.target,
|
|
9
|
+
sourcePath: input.sourcePath,
|
|
10
|
+
profile: input.profile,
|
|
11
|
+
left: 'remote-current',
|
|
12
|
+
right: 'publish-draft',
|
|
13
|
+
hasDiff,
|
|
14
|
+
diff: hasDiff ? unifiedDiff('remote-current', 'publish-draft', context.remoteCanonical, context.publishDraftCanonical) : '',
|
|
15
|
+
status: statusFromContext(context)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function diffSummaryLines(result) {
|
|
19
|
+
const lines = [
|
|
20
|
+
`state: ${result.status.state}`,
|
|
21
|
+
`local changed: ${result.status.localChanged}`,
|
|
22
|
+
`remote changed: ${result.status.remoteChanged}`,
|
|
23
|
+
`content matches remote: ${result.status.contentMatchesRemote}`,
|
|
24
|
+
`recommendation: ${result.status.recommendation.action} - ${result.status.recommendation.reason}`,
|
|
25
|
+
''
|
|
26
|
+
];
|
|
27
|
+
if (!result.hasDiff) {
|
|
28
|
+
lines.push('no diff');
|
|
29
|
+
return lines;
|
|
30
|
+
}
|
|
31
|
+
lines.push(result.diff.trimEnd());
|
|
32
|
+
return lines;
|
|
33
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type TextElementStyle = {
|
|
2
|
+
bold?: boolean;
|
|
3
|
+
italic?: boolean;
|
|
4
|
+
strikethrough?: boolean;
|
|
5
|
+
underline?: boolean;
|
|
6
|
+
inline_code?: boolean;
|
|
7
|
+
background_color?: number;
|
|
8
|
+
link?: {
|
|
9
|
+
url: string;
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
export type TextElement = {
|
|
13
|
+
text_run?: {
|
|
14
|
+
content: string;
|
|
15
|
+
text_element_style: TextElementStyle;
|
|
16
|
+
};
|
|
17
|
+
mention_doc?: {
|
|
18
|
+
title?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
token?: string;
|
|
21
|
+
obj_type?: number;
|
|
22
|
+
text_element_style?: TextElementStyle;
|
|
23
|
+
};
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
export type FeishuBlock = {
|
|
27
|
+
block_id?: string;
|
|
28
|
+
block_type: number;
|
|
29
|
+
children?: string[] | FeishuBlock[];
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
export type FeishuBlocksState = {
|
|
33
|
+
documentId: string;
|
|
34
|
+
blocks: FeishuBlock[];
|
|
35
|
+
};
|
|
36
|
+
export type FeishuBlockUpdateRequest = {
|
|
37
|
+
block_id: string;
|
|
38
|
+
update_text_elements?: {
|
|
39
|
+
elements: TextElement[];
|
|
40
|
+
};
|
|
41
|
+
update_text_style?: {
|
|
42
|
+
fields: string;
|
|
43
|
+
style: {
|
|
44
|
+
language?: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
replace_image?: {
|
|
48
|
+
token: string;
|
|
49
|
+
width?: number;
|
|
50
|
+
height?: number;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export type WriteResult = {
|
|
54
|
+
deleted: number;
|
|
55
|
+
created: number;
|
|
56
|
+
updated?: number;
|
|
57
|
+
skipped: boolean;
|
|
58
|
+
};
|
|
59
|
+
export type FeishuDriveFile = {
|
|
60
|
+
token?: string;
|
|
61
|
+
document_id?: string;
|
|
62
|
+
obj_token?: string;
|
|
63
|
+
revision_id?: number;
|
|
64
|
+
name?: string;
|
|
65
|
+
type?: string;
|
|
66
|
+
url?: string;
|
|
67
|
+
[key: string]: unknown;
|
|
68
|
+
};
|
|
69
|
+
export type BitableTable = {
|
|
70
|
+
table_id?: string;
|
|
71
|
+
name?: string;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
};
|
|
74
|
+
export type BitableField = {
|
|
75
|
+
field_id?: string;
|
|
76
|
+
field_name?: string;
|
|
77
|
+
type?: number;
|
|
78
|
+
[key: string]: unknown;
|
|
79
|
+
};
|
|
80
|
+
export type BitableRecord = {
|
|
81
|
+
record_id?: string;
|
|
82
|
+
fields?: Record<string, unknown>;
|
|
83
|
+
[key: string]: unknown;
|
|
84
|
+
};
|
|
85
|
+
export interface FeishuDocClient {
|
|
86
|
+
getDocumentBlocks(documentId: string): Promise<FeishuBlock[]>;
|
|
87
|
+
overwriteDocumentMarkdown?(documentId: string, markdown: string): Promise<void>;
|
|
88
|
+
uploadMediaFile?(input: {
|
|
89
|
+
filePath: string;
|
|
90
|
+
parentNode: string;
|
|
91
|
+
parentType: string;
|
|
92
|
+
}): Promise<{
|
|
93
|
+
token: string;
|
|
94
|
+
contentType?: string;
|
|
95
|
+
}>;
|
|
96
|
+
deleteChildren(documentId: string, parentBlockId: string, startIndex: number, endIndex: number): Promise<void>;
|
|
97
|
+
createChildren(documentId: string, parentBlockId: string, blocks: FeishuBlock[], options?: {
|
|
98
|
+
index?: number;
|
|
99
|
+
}): Promise<FeishuBlock[]>;
|
|
100
|
+
batchUpdateBlocks?(documentId: string, requests: FeishuBlockUpdateRequest[]): Promise<FeishuBlock[]>;
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FeishuBlock, TextElement } from '../feishu/types.js';
|
|
2
|
+
export declare const LANG_IDS: Record<string, number>;
|
|
3
|
+
export declare function languageIdForMarkdownLanguage(language: string): number;
|
|
4
|
+
export declare function markdownToFeishuBlocks(markdown: string): FeishuBlock[];
|
|
5
|
+
export declare function parseInlineText(text: string): TextElement[];
|