yuque-cookie-plugin 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/.env.example +5 -0
- package/README.md +253 -0
- package/bin/yuque-local.mjs +3 -0
- package/dist/auth.js +439 -0
- package/dist/cli.js +692 -0
- package/dist/client-cookie.js +544 -0
- package/dist/download-utils.js +579 -0
- package/dist/downloader.js +388 -0
- package/dist/editor-bridge.js +240 -0
- package/dist/fs-utils.js +9 -0
- package/dist/lake-diff.js +113 -0
- package/dist/lake-heading-numbering.js +29 -0
- package/dist/lake-insert.js +34 -0
- package/dist/lake-markdown.js +54 -0
- package/dist/lake-parser.js +79 -0
- package/dist/lake-transform.js +12 -0
- package/dist/reports.js +10 -0
- package/dist/serve-book.js +134 -0
- package/dist/types.js +1 -0
- package/docs/docker-quickstart.md +96 -0
- package/docs/native-lake-capability.md +101 -0
- package/docs/npm-release.md +91 -0
- package/docs/real-acceptance.md +146 -0
- package/docs/usage-zh.md +580 -0
- package/package.json +71 -0
- package/skills/yuque-cookie-plugin/SKILL.md +58 -0
- package/skills/yuque-cookie-plugin/commands.yaml +61 -0
- package/skills/yuque-cookie-plugin/reference/agent-spec.md +51 -0
- package/vendor/lake-editor/CodeMirror.js +1 -0
- package/vendor/lake-editor/antd.4.24.13.css +26886 -0
- package/vendor/lake-editor/doc.css +23603 -0
- package/vendor/lake-editor/doc.umd.js +2 -0
- package/vendor/lake-editor/katex.js +18829 -0
- package/vendor/lake-editor/lake-editor-icon.js +1 -0
- package/vendor/lake-editor/react-dom.production.min.js +267 -0
- package/vendor/lake-editor/react.production.min.js +31 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from 'node:process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { cp, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { existsSync } from 'node:fs';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { YuqueCookieClient } from "./client-cookie.js";
|
|
8
|
+
import { diffLakeSnapshots } from "./lake-diff.js";
|
|
9
|
+
import { writeJson } from "./fs-utils.js";
|
|
10
|
+
import { getConfigFilePath, getCredentialStatus, getCredentials, loginWithBrowser, recordCredentialValidation } from "./auth.js";
|
|
11
|
+
import { serializeHtmlToLake } from "./editor-bridge.js";
|
|
12
|
+
import { snapshotToMarkdown } from "./lake-markdown.js";
|
|
13
|
+
import { downloadBook, downloadDocs } from "./downloader.js";
|
|
14
|
+
import { normalizeDownloadOptions } from "./download-utils.js";
|
|
15
|
+
import { serveBook } from "./serve-book.js";
|
|
16
|
+
import { enableLakeHeadingNumbering } from "./lake-heading-numbering.js";
|
|
17
|
+
import { extractFirstImageParagraph, insertLakeBlock } from "./lake-insert.js";
|
|
18
|
+
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
function help() {
|
|
20
|
+
console.log(`yuque-local
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
yuque-local auth login [--manual]
|
|
24
|
+
yuque-local auth status [doc-or-book-url] [--json]
|
|
25
|
+
yuque-local doctor [--json]
|
|
26
|
+
yuque-local skill install|status [--json]
|
|
27
|
+
yuque-local book create --name <name> --slug <slug> [--description <text>] [--public]
|
|
28
|
+
yuque-local book download <book-url> [--dist-dir download] [--incremental]
|
|
29
|
+
yuque-local doc create <book-url> --title <title> --markdown-file <file> [--number-headings]
|
|
30
|
+
yuque-local doc download <...doc-urls> [--dist-dir download]
|
|
31
|
+
|
|
32
|
+
Legacy flat commands remain supported:
|
|
33
|
+
yuque-local inspect <doc-or-book-url>
|
|
34
|
+
yuque-local auth-status <doc-or-book-url>
|
|
35
|
+
yuque-local login
|
|
36
|
+
yuque-local create-book --name <name> --slug <slug> [--description <text>] [--public]
|
|
37
|
+
yuque-local create-doc <book-url> --title <title> --markdown-file <file> [--number-headings] [--slug <slug>] [--no-toc] [--no-native-lake]
|
|
38
|
+
yuque-local upload-attach <doc-or-book-url> --file <file>
|
|
39
|
+
yuque-local insert-image <doc-url> --file <image> [--after-text <text>] [--dry-run]
|
|
40
|
+
yuque-local insert-attachment <doc-url> --file <file> [--after-text <text>] [--dry-run]
|
|
41
|
+
yuque-local snapshot <doc-url> --out <file>
|
|
42
|
+
yuque-local diff-lake <before.json> <after.json>
|
|
43
|
+
yuque-local editor-serialize --html-file <file> --out <lake-file>
|
|
44
|
+
yuque-local apply-lake <doc-url> --lake-file <body_asl.html> [--dry-run]
|
|
45
|
+
yuque-local lake-to-markdown <snapshot.json> --out <file>
|
|
46
|
+
yuque-local format-article <doc-url> --html-file <file> [--dry-run]
|
|
47
|
+
yuque-local update-lake <doc-url> --lake-file <body_asl.html>
|
|
48
|
+
yuque-local download-book <book-url> [--dist-dir download] [--incremental]
|
|
49
|
+
yuque-local download-doc <...doc-urls> [--dist-dir download]
|
|
50
|
+
yuque-local serve-book <book-path> [--port 5173]
|
|
51
|
+
|
|
52
|
+
Options:
|
|
53
|
+
--out <file> Output path
|
|
54
|
+
--lake-file <file> Lake body_asl HTML file
|
|
55
|
+
--html-file <file> HTML input file
|
|
56
|
+
--markdown-file <file> Markdown input file for create-doc
|
|
57
|
+
--file <file> Local file for upload-attach / insert-image / insert-attachment
|
|
58
|
+
--after-text <text> Insert image/attachment after first block containing text; otherwise append
|
|
59
|
+
--title <title> Document title
|
|
60
|
+
--name <name> Book name
|
|
61
|
+
--description <text> Book description
|
|
62
|
+
--slug <slug> Optional custom slug; omit to let Yuque generate it
|
|
63
|
+
--no-toc Create doc without attaching it to book TOC
|
|
64
|
+
--toc-action <action> TOC action, default: appendByDocs
|
|
65
|
+
--target-uuid <uuid> TOC target uuid, default: root
|
|
66
|
+
--number-headings Enable Yuque native heading numbering in Lake
|
|
67
|
+
--no-native-lake Skip final Lake-native rewrite after Markdown import
|
|
68
|
+
--public Create public book when supported
|
|
69
|
+
--dry-run Build a plan/report without writing online content
|
|
70
|
+
--keep-temp Keep editor bridge temp files for debugging
|
|
71
|
+
--json Emit a stable Agent-friendly JSON envelope for supported meta commands
|
|
72
|
+
--manual auth login: read cookies from terminal instead of starting a browser page
|
|
73
|
+
--session-env <name> Env var for _yuque_session, default: YUQUE_SESSION
|
|
74
|
+
--ctoken-env <name> Env var for yuque_ctoken, default: YUQUE_CTOKEN
|
|
75
|
+
--home-url <url> Optional user/org Yuque home URL, e.g. https://www.yuque.com/your-login/
|
|
76
|
+
--cookie-key <name> Extra Yuque cookie key, e.g. verified_books
|
|
77
|
+
--cookie-value <value> Extra Yuque cookie value for --cookie-key
|
|
78
|
+
--api-host <url> Override Yuque API host for tests or enterprise deployments
|
|
79
|
+
--dist-dir <dir> Download output dir, default: download
|
|
80
|
+
--ignore-img Do not download markdown images
|
|
81
|
+
--ignore-attachments Do not download attachments, or pass comma-separated extensions
|
|
82
|
+
--toc Add markdown table of contents per document
|
|
83
|
+
--incremental Skip unchanged docs based on progress.json
|
|
84
|
+
--hide-footer Do not append update/original footer
|
|
85
|
+
--quiet Suppress downloader progress lines; final JSON remains
|
|
86
|
+
--port <port> serve-book port, default: 5173
|
|
87
|
+
--host <host> serve-book host, default: localhost
|
|
88
|
+
--force Recreate VitePress config
|
|
89
|
+
--config-only Generate serve-book VitePress config and exit
|
|
90
|
+
-h, --help Show help
|
|
91
|
+
`);
|
|
92
|
+
}
|
|
93
|
+
function parseArgs(argv) {
|
|
94
|
+
const positional = [];
|
|
95
|
+
const flags = {};
|
|
96
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
97
|
+
const arg = argv[i];
|
|
98
|
+
if (!arg.startsWith('-')) {
|
|
99
|
+
positional.push(arg);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (arg === '-h' || arg === '--help') {
|
|
103
|
+
flags.help = true;
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const eq = arg.indexOf('=');
|
|
107
|
+
if (eq !== -1) {
|
|
108
|
+
flags[toCamel(arg.slice(0, eq).replace(/^--?/, ''))] = arg.slice(eq + 1);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const key = toCamel(arg.replace(/^--?/, ''));
|
|
112
|
+
const next = argv[i + 1];
|
|
113
|
+
if (next && !next.startsWith('-')) {
|
|
114
|
+
flags[key] = next;
|
|
115
|
+
i += 1;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
flags[key] = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { positional, flags };
|
|
122
|
+
}
|
|
123
|
+
function normalizeInvocation(argv) {
|
|
124
|
+
const [first, second, ...tail] = argv;
|
|
125
|
+
if (!first)
|
|
126
|
+
return { command: undefined, rest: [] };
|
|
127
|
+
if (!second || second.startsWith('-'))
|
|
128
|
+
return { command: first, rest: argv.slice(1) };
|
|
129
|
+
const grouped = {
|
|
130
|
+
auth: {
|
|
131
|
+
login: 'login',
|
|
132
|
+
status: 'auth-status'
|
|
133
|
+
},
|
|
134
|
+
book: {
|
|
135
|
+
create: 'create-book',
|
|
136
|
+
download: 'download-book',
|
|
137
|
+
inspect: 'inspect'
|
|
138
|
+
},
|
|
139
|
+
doc: {
|
|
140
|
+
create: 'create-doc',
|
|
141
|
+
download: 'download-doc',
|
|
142
|
+
inspect: 'inspect',
|
|
143
|
+
snapshot: 'snapshot',
|
|
144
|
+
'apply-lake': 'apply-lake',
|
|
145
|
+
'update-lake': 'update-lake',
|
|
146
|
+
format: 'format-article',
|
|
147
|
+
'insert-image': 'insert-image',
|
|
148
|
+
'insert-attachment': 'insert-attachment',
|
|
149
|
+
upload: 'upload-attach'
|
|
150
|
+
},
|
|
151
|
+
lake: {
|
|
152
|
+
diff: 'diff-lake',
|
|
153
|
+
serialize: 'editor-serialize',
|
|
154
|
+
'to-markdown': 'lake-to-markdown'
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
const mapped = grouped[first]?.[second];
|
|
158
|
+
if (mapped)
|
|
159
|
+
return { command: mapped, rest: tail };
|
|
160
|
+
return { command: first, rest: argv.slice(1) };
|
|
161
|
+
}
|
|
162
|
+
function toCamel(value) {
|
|
163
|
+
return value.replace(/-([a-z])/g, (_match, char) => char.toUpperCase());
|
|
164
|
+
}
|
|
165
|
+
async function createClient(flags) {
|
|
166
|
+
let credentials;
|
|
167
|
+
try {
|
|
168
|
+
credentials = getCredentials(flags);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
console.log(error instanceof Error ? error.message : String(error));
|
|
172
|
+
await loginWithBrowser(flags);
|
|
173
|
+
credentials = getCredentials(flags);
|
|
174
|
+
}
|
|
175
|
+
return new YuqueCookieClient({
|
|
176
|
+
session: credentials.session,
|
|
177
|
+
ctoken: credentials.ctoken,
|
|
178
|
+
extraCookies: credentials.extraCookies,
|
|
179
|
+
host: typeof flags.apiHost === 'string' ? flags.apiHost : undefined
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
function isAuthLikeError(error) {
|
|
183
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
184
|
+
return /401|403|Failed to parse appData|Missing Yuque credentials|登录|login|unauthorized|forbidden/i.test(message);
|
|
185
|
+
}
|
|
186
|
+
function withReloginHint(error) {
|
|
187
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
188
|
+
if (!isAuthLikeError(error))
|
|
189
|
+
return error instanceof Error ? error : new Error(message);
|
|
190
|
+
return new Error(`${message}\nYuque credentials may be expired. Run "npm run yuque-local -- login" and paste fresh _yuque_session + yuque_ctoken.`);
|
|
191
|
+
}
|
|
192
|
+
function jsonResult(status, message, data, metadata = {}) {
|
|
193
|
+
return { status, message, data, error: null, metadata };
|
|
194
|
+
}
|
|
195
|
+
function printJsonResult(result) {
|
|
196
|
+
console.log(JSON.stringify(result, null, 2));
|
|
197
|
+
}
|
|
198
|
+
function printCommandResult(data, flags, message, status = 'success') {
|
|
199
|
+
if (flags.json) {
|
|
200
|
+
printJsonResult(jsonResult(status, message, data));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
console.log(JSON.stringify(data, null, 2));
|
|
204
|
+
}
|
|
205
|
+
function shouldPrintJsonEnvelopeFromArgv() {
|
|
206
|
+
return process.argv.includes('--json') || process.argv.some((arg) => arg.startsWith('--json='));
|
|
207
|
+
}
|
|
208
|
+
function printError(error) {
|
|
209
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
210
|
+
if (shouldPrintJsonEnvelopeFromArgv()) {
|
|
211
|
+
console.error(JSON.stringify({
|
|
212
|
+
status: 'error',
|
|
213
|
+
message: 'yuque-local command failed',
|
|
214
|
+
data: null,
|
|
215
|
+
error: {
|
|
216
|
+
type: error instanceof Error ? error.name : 'Error',
|
|
217
|
+
message,
|
|
218
|
+
retryable: isAuthLikeError(error)
|
|
219
|
+
},
|
|
220
|
+
metadata: {
|
|
221
|
+
relogin: isAuthLikeError(error) ? 'yuque-local auth login' : undefined
|
|
222
|
+
}
|
|
223
|
+
}, null, 2));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
console.error(`yuque-local error: ${message}`);
|
|
227
|
+
}
|
|
228
|
+
async function commandDoctor(flags) {
|
|
229
|
+
const packageJsonPath = path.join(PACKAGE_ROOT, 'package.json');
|
|
230
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
231
|
+
const status = getCredentialStatus(flags);
|
|
232
|
+
const skillSource = path.join(PACKAGE_ROOT, 'skills', 'yuque-cookie-plugin');
|
|
233
|
+
const data = {
|
|
234
|
+
package: {
|
|
235
|
+
name: packageJson.name || 'yuque-cookie-plugin',
|
|
236
|
+
version: packageJson.version || '0.0.0',
|
|
237
|
+
root: PACKAGE_ROOT
|
|
238
|
+
},
|
|
239
|
+
runtime: {
|
|
240
|
+
node: process.version,
|
|
241
|
+
platform: process.platform,
|
|
242
|
+
arch: process.arch
|
|
243
|
+
},
|
|
244
|
+
auth: {
|
|
245
|
+
config_file: getConfigFilePath(),
|
|
246
|
+
...status
|
|
247
|
+
},
|
|
248
|
+
skill: {
|
|
249
|
+
source: skillSource,
|
|
250
|
+
source_exists: existsSync(skillSource),
|
|
251
|
+
codex_target: path.join(process.env.CODEX_HOME || path.join(process.env.HOME || '', '.codex'), 'skills', 'yuque-cookie-plugin'),
|
|
252
|
+
agents_target: path.join(process.env.HOME || '', '.agents', 'skills', 'yuque-cookie-plugin')
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
if (flags.json) {
|
|
256
|
+
printJsonResult(jsonResult('success', 'doctor completed', data));
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
console.log(JSON.stringify(data, null, 2));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
async function commandSkill(action, flags) {
|
|
263
|
+
if (!action || action === 'status') {
|
|
264
|
+
const source = path.join(PACKAGE_ROOT, 'skills', 'yuque-cookie-plugin');
|
|
265
|
+
const targets = skillTargets();
|
|
266
|
+
const data = {
|
|
267
|
+
source,
|
|
268
|
+
source_exists: existsSync(source),
|
|
269
|
+
targets: targets.map((target) => ({
|
|
270
|
+
path: target,
|
|
271
|
+
installed: existsSync(path.join(target, 'SKILL.md'))
|
|
272
|
+
}))
|
|
273
|
+
};
|
|
274
|
+
if (flags.json)
|
|
275
|
+
printJsonResult(jsonResult('success', 'skill status completed', data));
|
|
276
|
+
else
|
|
277
|
+
console.log(JSON.stringify(data, null, 2));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (action !== 'install')
|
|
281
|
+
throw new Error(`Unknown skill action: ${action}`);
|
|
282
|
+
const source = path.join(PACKAGE_ROOT, 'skills', 'yuque-cookie-plugin');
|
|
283
|
+
if (!existsSync(path.join(source, 'SKILL.md')))
|
|
284
|
+
throw new Error(`Skill source is missing: ${source}`);
|
|
285
|
+
const installed = [];
|
|
286
|
+
for (const target of skillTargets()) {
|
|
287
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
288
|
+
await cp(source, target, { recursive: true, force: true });
|
|
289
|
+
installed.push(target);
|
|
290
|
+
}
|
|
291
|
+
const data = { source, installed };
|
|
292
|
+
if (flags.json)
|
|
293
|
+
printJsonResult(jsonResult('success', 'skill installed', data));
|
|
294
|
+
else
|
|
295
|
+
console.log(JSON.stringify(data, null, 2));
|
|
296
|
+
}
|
|
297
|
+
function skillTargets() {
|
|
298
|
+
const home = process.env.HOME || '';
|
|
299
|
+
const codexHome = process.env.CODEX_HOME || (home ? path.join(home, '.codex') : '');
|
|
300
|
+
return [
|
|
301
|
+
codexHome ? path.join(codexHome, 'skills', 'yuque-cookie-plugin') : '',
|
|
302
|
+
home ? path.join(home, '.agents', 'skills', 'yuque-cookie-plugin') : ''
|
|
303
|
+
].filter(Boolean);
|
|
304
|
+
}
|
|
305
|
+
async function main() {
|
|
306
|
+
const invocation = normalizeInvocation(process.argv.slice(2));
|
|
307
|
+
const command = invocation.command;
|
|
308
|
+
const rest = invocation.rest;
|
|
309
|
+
const { positional, flags } = parseArgs(rest);
|
|
310
|
+
if (!command || command === 'help' || command === '--help' || flags.help) {
|
|
311
|
+
help();
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (command === 'diff-lake') {
|
|
315
|
+
const [beforePath, afterPath] = positional;
|
|
316
|
+
if (!beforePath || !afterPath)
|
|
317
|
+
throw new Error('Missing before/after snapshot path.');
|
|
318
|
+
const before = JSON.parse(await readFile(beforePath, 'utf8'));
|
|
319
|
+
const after = JSON.parse(await readFile(afterPath, 'utf8'));
|
|
320
|
+
printCommandResult(diffLakeSnapshots(before, after), flags, 'lake diff completed');
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (command === 'editor-serialize') {
|
|
324
|
+
if (!flags.htmlFile || typeof flags.htmlFile !== 'string')
|
|
325
|
+
throw new Error('Missing --html-file.');
|
|
326
|
+
if (!flags.out || typeof flags.out !== 'string')
|
|
327
|
+
throw new Error('Missing --out.');
|
|
328
|
+
const html = await readFile(flags.htmlFile, 'utf8');
|
|
329
|
+
const result = await serializeHtmlToLake({ html, out: flags.out, keepTemp: Boolean(flags.keepTemp) });
|
|
330
|
+
printCommandResult({
|
|
331
|
+
ok: true,
|
|
332
|
+
serializer: result.serializer,
|
|
333
|
+
out: flags.out,
|
|
334
|
+
length: result.lake.length
|
|
335
|
+
}, flags, 'editor serialization completed');
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (command === 'lake-to-markdown') {
|
|
339
|
+
const [snapshotPath] = positional;
|
|
340
|
+
if (!snapshotPath)
|
|
341
|
+
throw new Error('Missing snapshot path.');
|
|
342
|
+
if (!flags.out || typeof flags.out !== 'string')
|
|
343
|
+
throw new Error('Missing --out.');
|
|
344
|
+
const snapshot = JSON.parse(await readFile(snapshotPath, 'utf8'));
|
|
345
|
+
const markdown = snapshotToMarkdown(snapshot);
|
|
346
|
+
await writeFile(flags.out, markdown, 'utf8');
|
|
347
|
+
if (flags.json)
|
|
348
|
+
printCommandResult({ ok: true, out: flags.out, bytes: Buffer.byteLength(markdown) }, flags, 'markdown written');
|
|
349
|
+
else
|
|
350
|
+
console.log(`Markdown written: ${flags.out}`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (command === 'login') {
|
|
354
|
+
await loginWithBrowser(flags);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (command === 'doctor') {
|
|
358
|
+
await commandDoctor(flags);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (command === 'skill') {
|
|
362
|
+
await commandSkill(positional[0], flags);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (command === 'auth-status') {
|
|
366
|
+
const [url] = positional;
|
|
367
|
+
const status = getCredentialStatus(flags);
|
|
368
|
+
if (!url) {
|
|
369
|
+
printCommandResult(status, flags, 'auth status loaded');
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const client = await createClient(flags);
|
|
373
|
+
try {
|
|
374
|
+
const inspect = await client.inspect(url);
|
|
375
|
+
await recordCredentialValidation(true);
|
|
376
|
+
printCommandResult({
|
|
377
|
+
...getCredentialStatus(flags),
|
|
378
|
+
valid: true,
|
|
379
|
+
checked_at: new Date().toISOString(),
|
|
380
|
+
inspect
|
|
381
|
+
}, flags, 'auth status valid');
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
await recordCredentialValidation(false, error);
|
|
385
|
+
printCommandResult({
|
|
386
|
+
...getCredentialStatus(flags),
|
|
387
|
+
valid: false,
|
|
388
|
+
checked_at: new Date().toISOString(),
|
|
389
|
+
error: error instanceof Error ? error.message : String(error),
|
|
390
|
+
relogin: 'yuque-local auth login'
|
|
391
|
+
}, flags, 'auth status invalid', 'error');
|
|
392
|
+
process.exitCode = 1;
|
|
393
|
+
}
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
if (command === 'serve-book') {
|
|
397
|
+
const [bookPath] = positional;
|
|
398
|
+
if (!bookPath)
|
|
399
|
+
throw new Error('Missing book path.');
|
|
400
|
+
const result = await serveBook(bookPath, {
|
|
401
|
+
port: typeof flags.port === 'string' ? Number(flags.port) : undefined,
|
|
402
|
+
host: typeof flags.host === 'string' ? flags.host : undefined,
|
|
403
|
+
force: Boolean(flags.force),
|
|
404
|
+
configOnly: Boolean(flags.configOnly)
|
|
405
|
+
});
|
|
406
|
+
if (flags.configOnly)
|
|
407
|
+
printCommandResult({ ok: true, ...result }, flags, 'serve-book config generated');
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const client = await createClient(flags);
|
|
411
|
+
try {
|
|
412
|
+
if (command === 'inspect') {
|
|
413
|
+
const [url] = positional;
|
|
414
|
+
if (!url)
|
|
415
|
+
throw new Error('Missing Yuque URL.');
|
|
416
|
+
const info = await client.inspect(url);
|
|
417
|
+
await recordCredentialValidation(true);
|
|
418
|
+
printCommandResult(info, flags, 'inspect completed');
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (command === 'snapshot') {
|
|
422
|
+
const [url] = positional;
|
|
423
|
+
if (!url)
|
|
424
|
+
throw new Error('Missing Yuque doc URL.');
|
|
425
|
+
if (!flags.out || typeof flags.out !== 'string')
|
|
426
|
+
throw new Error('Missing --out.');
|
|
427
|
+
const snapshot = await client.snapshotDoc(url);
|
|
428
|
+
await writeJson(flags.out, snapshot);
|
|
429
|
+
await recordCredentialValidation(true);
|
|
430
|
+
if (flags.json)
|
|
431
|
+
printCommandResult({ ok: true, out: flags.out, snapshot }, flags, 'snapshot written');
|
|
432
|
+
else
|
|
433
|
+
console.log(`Snapshot written: ${flags.out}`);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (command === 'create-book') {
|
|
437
|
+
if (!flags.name || typeof flags.name !== 'string')
|
|
438
|
+
throw new Error('Missing --name.');
|
|
439
|
+
if (!flags.slug || typeof flags.slug !== 'string')
|
|
440
|
+
throw new Error('Missing --slug.');
|
|
441
|
+
const result = await client.createBook({
|
|
442
|
+
name: flags.name,
|
|
443
|
+
slug: flags.slug,
|
|
444
|
+
description: typeof flags.description === 'string' ? flags.description : undefined,
|
|
445
|
+
public: Boolean(flags.public)
|
|
446
|
+
});
|
|
447
|
+
await recordCredentialValidation(true);
|
|
448
|
+
printCommandResult(result, flags, 'book created');
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (command === 'create-doc') {
|
|
452
|
+
const [bookUrl] = positional;
|
|
453
|
+
if (!bookUrl)
|
|
454
|
+
throw new Error('Missing Yuque book URL.');
|
|
455
|
+
if (!flags.title || typeof flags.title !== 'string')
|
|
456
|
+
throw new Error('Missing --title.');
|
|
457
|
+
if (!flags.markdownFile || typeof flags.markdownFile !== 'string')
|
|
458
|
+
throw new Error('Missing --markdown-file.');
|
|
459
|
+
let body = await readFile(flags.markdownFile, 'utf8');
|
|
460
|
+
if (!body.trim())
|
|
461
|
+
throw new Error('Markdown file is empty.');
|
|
462
|
+
const result = await client.createMarkdownDoc(bookUrl, {
|
|
463
|
+
title: flags.title,
|
|
464
|
+
body,
|
|
465
|
+
slug: typeof flags.slug === 'string' ? flags.slug : undefined,
|
|
466
|
+
attachToToc: !flags.noToc,
|
|
467
|
+
tocAction: typeof flags.tocAction === 'string' ? flags.tocAction : undefined,
|
|
468
|
+
targetUuid: typeof flags.targetUuid === 'string' ? flags.targetUuid : undefined
|
|
469
|
+
});
|
|
470
|
+
if (!flags.noNativeLake && typeof result.url === 'string') {
|
|
471
|
+
const snapshot = await client.snapshotDoc(result.url);
|
|
472
|
+
const lake = snapshot.edit?.body_asl || snapshot.lake?.sourcecode || '';
|
|
473
|
+
const nativeLake = flags.numberHeadings ? enableLakeHeadingNumbering(lake) : lake;
|
|
474
|
+
const applyResult = await client.applyLakeDoc(result.url, nativeLake, {
|
|
475
|
+
command: flags.numberHeadings ? 'native-lake-number-headings' : 'native-lake'
|
|
476
|
+
});
|
|
477
|
+
Object.assign(result, {
|
|
478
|
+
final_format: 'lake',
|
|
479
|
+
native_lake: {
|
|
480
|
+
ok: true,
|
|
481
|
+
source: 'yuque-generated-body_asl',
|
|
482
|
+
apply: applyResult
|
|
483
|
+
},
|
|
484
|
+
...(flags.numberHeadings
|
|
485
|
+
? {
|
|
486
|
+
heading_numbering: {
|
|
487
|
+
ok: true,
|
|
488
|
+
mode: 'lake-native',
|
|
489
|
+
data_lake_index_type: '2'
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
: {})
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
await recordCredentialValidation(true);
|
|
496
|
+
printCommandResult(result, flags, 'document created');
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
if (command === 'upload-attach') {
|
|
500
|
+
const [url] = positional;
|
|
501
|
+
if (!url)
|
|
502
|
+
throw new Error('Missing Yuque URL.');
|
|
503
|
+
if (!flags.file || typeof flags.file !== 'string')
|
|
504
|
+
throw new Error('Missing --file.');
|
|
505
|
+
const result = await client.uploadAttach(url, flags.file);
|
|
506
|
+
await recordCredentialValidation(true);
|
|
507
|
+
printCommandResult(result, flags, 'attachment uploaded');
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (command === 'insert-image') {
|
|
511
|
+
const [url] = positional;
|
|
512
|
+
if (!url)
|
|
513
|
+
throw new Error('Missing Yuque doc URL.');
|
|
514
|
+
if (!flags.file || typeof flags.file !== 'string')
|
|
515
|
+
throw new Error('Missing --file.');
|
|
516
|
+
const uploaded = await client.uploadAttach(url, flags.file);
|
|
517
|
+
const uploadData = uploaded.upload;
|
|
518
|
+
const imageUrl = typeof uploadData?.url === 'string' ? uploadData.url : '';
|
|
519
|
+
if (!imageUrl)
|
|
520
|
+
throw new Error('Upload succeeded but response did not include upload.url.');
|
|
521
|
+
const imageHtml = `<p><img src="${escapeHtmlAttr(imageUrl)}" alt="${escapeHtmlAttr(String(uploadData?.filename || path.basename(flags.file)))}"></p>`;
|
|
522
|
+
const serialized = await serializeHtmlToLake({ html: imageHtml, keepTemp: Boolean(flags.keepTemp) });
|
|
523
|
+
const imageBlock = extractFirstImageParagraph(serialized.lake);
|
|
524
|
+
const snapshot = await client.snapshotDoc(url);
|
|
525
|
+
const currentLake = snapshot.edit?.body_asl || snapshot.lake?.sourcecode || '';
|
|
526
|
+
const nextLake = insertLakeBlock(currentLake, imageBlock, typeof flags.afterText === 'string' ? flags.afterText : undefined);
|
|
527
|
+
const applyResult = await client.applyLakeDoc(url, nextLake, {
|
|
528
|
+
dryRun: Boolean(flags.dryRun),
|
|
529
|
+
command: 'insert-image'
|
|
530
|
+
});
|
|
531
|
+
await recordCredentialValidation(true);
|
|
532
|
+
printCommandResult({
|
|
533
|
+
ok: true,
|
|
534
|
+
dry_run: Boolean(flags.dryRun),
|
|
535
|
+
uploaded,
|
|
536
|
+
serializer: serialized.serializer,
|
|
537
|
+
image_block_length: imageBlock.length,
|
|
538
|
+
inserted_after_text: typeof flags.afterText === 'string' ? flags.afterText : null,
|
|
539
|
+
apply: applyResult
|
|
540
|
+
}, flags, 'image inserted', flags.dryRun ? 'dry_run' : 'success');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (command === 'insert-attachment') {
|
|
544
|
+
const [url] = positional;
|
|
545
|
+
if (!url)
|
|
546
|
+
throw new Error('Missing Yuque doc URL.');
|
|
547
|
+
if (!flags.file || typeof flags.file !== 'string')
|
|
548
|
+
throw new Error('Missing --file.');
|
|
549
|
+
const uploaded = await client.uploadAttach(url, flags.file);
|
|
550
|
+
const uploadData = uploaded.upload;
|
|
551
|
+
if (!uploadData?.url)
|
|
552
|
+
throw new Error('Upload succeeded but response did not include upload.url.');
|
|
553
|
+
const attachmentBlock = buildAttachmentBlock(uploadData, flags.file, url);
|
|
554
|
+
const snapshot = await client.snapshotDoc(url);
|
|
555
|
+
const currentLake = snapshot.edit?.body_asl || snapshot.lake?.sourcecode || '';
|
|
556
|
+
const nextLake = insertLakeBlock(currentLake, attachmentBlock, typeof flags.afterText === 'string' ? flags.afterText : undefined);
|
|
557
|
+
const applyResult = await client.applyLakeDoc(url, nextLake, {
|
|
558
|
+
dryRun: Boolean(flags.dryRun),
|
|
559
|
+
command: 'insert-attachment'
|
|
560
|
+
});
|
|
561
|
+
await recordCredentialValidation(true);
|
|
562
|
+
printCommandResult({
|
|
563
|
+
ok: true,
|
|
564
|
+
dry_run: Boolean(flags.dryRun),
|
|
565
|
+
uploaded,
|
|
566
|
+
attachment_block_length: attachmentBlock.length,
|
|
567
|
+
inserted_after_text: typeof flags.afterText === 'string' ? flags.afterText : null,
|
|
568
|
+
apply: applyResult
|
|
569
|
+
}, flags, 'attachment inserted', flags.dryRun ? 'dry_run' : 'success');
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (command === 'update-lake') {
|
|
573
|
+
const [url] = positional;
|
|
574
|
+
if (!url)
|
|
575
|
+
throw new Error('Missing Yuque doc URL.');
|
|
576
|
+
if (!flags.lakeFile || typeof flags.lakeFile !== 'string')
|
|
577
|
+
throw new Error('Missing --lake-file.');
|
|
578
|
+
const lake = await readFile(flags.lakeFile, 'utf8');
|
|
579
|
+
const result = await client.updateLakeDoc(url, lake);
|
|
580
|
+
await recordCredentialValidation(true);
|
|
581
|
+
printCommandResult(result, flags, 'lake updated');
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (command === 'apply-lake') {
|
|
585
|
+
const [url] = positional;
|
|
586
|
+
if (!url)
|
|
587
|
+
throw new Error('Missing Yuque doc URL.');
|
|
588
|
+
if (!flags.lakeFile || typeof flags.lakeFile !== 'string')
|
|
589
|
+
throw new Error('Missing --lake-file.');
|
|
590
|
+
const lake = await readFile(flags.lakeFile, 'utf8');
|
|
591
|
+
const result = await client.applyLakeDoc(url, lake, {
|
|
592
|
+
dryRun: Boolean(flags.dryRun),
|
|
593
|
+
command: 'apply-lake'
|
|
594
|
+
});
|
|
595
|
+
await recordCredentialValidation(true);
|
|
596
|
+
printCommandResult(result, flags, 'lake applied', flags.dryRun ? 'dry_run' : 'success');
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (command === 'format-article') {
|
|
600
|
+
const [url] = positional;
|
|
601
|
+
if (!url)
|
|
602
|
+
throw new Error('Missing Yuque doc URL.');
|
|
603
|
+
if (!flags.htmlFile || typeof flags.htmlFile !== 'string')
|
|
604
|
+
throw new Error('Missing --html-file.');
|
|
605
|
+
const html = await readFile(flags.htmlFile, 'utf8');
|
|
606
|
+
const serialized = await serializeHtmlToLake({ html, keepTemp: Boolean(flags.keepTemp) });
|
|
607
|
+
const result = await client.applyLakeDoc(url, serialized.lake, {
|
|
608
|
+
dryRun: Boolean(flags.dryRun),
|
|
609
|
+
command: 'format-article'
|
|
610
|
+
});
|
|
611
|
+
printCommandResult({
|
|
612
|
+
serializer: serialized.serializer,
|
|
613
|
+
lake_length: serialized.lake.length,
|
|
614
|
+
...result
|
|
615
|
+
}, flags, 'article formatted', flags.dryRun ? 'dry_run' : 'success');
|
|
616
|
+
await recordCredentialValidation(true);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (command === 'download-book') {
|
|
620
|
+
const [url] = positional;
|
|
621
|
+
if (!url)
|
|
622
|
+
throw new Error('Missing Yuque book URL.');
|
|
623
|
+
const result = await downloadBook(client, url, normalizeDownloadOptions(flags));
|
|
624
|
+
await recordCredentialValidation(true);
|
|
625
|
+
printCommandResult(result, flags, 'book downloaded');
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
if (command === 'download-doc') {
|
|
629
|
+
if (!positional.length)
|
|
630
|
+
throw new Error('Missing Yuque doc URL.');
|
|
631
|
+
const result = await downloadDocs(client, positional, normalizeDownloadOptions(flags));
|
|
632
|
+
if (result.ok !== false || Number(result.downloaded || 0) > 0)
|
|
633
|
+
await recordCredentialValidation(true);
|
|
634
|
+
printCommandResult(result, flags, 'documents downloaded', result.ok === false ? 'error' : 'success');
|
|
635
|
+
if (result.ok === false && Number(result.downloaded || 0) === 0)
|
|
636
|
+
process.exitCode = 1;
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
await recordCredentialValidation(false, error);
|
|
642
|
+
throw withReloginHint(error);
|
|
643
|
+
}
|
|
644
|
+
throw new Error(`Unknown command: ${command}`);
|
|
645
|
+
}
|
|
646
|
+
main().catch((error) => {
|
|
647
|
+
printError(error);
|
|
648
|
+
process.exitCode = 1;
|
|
649
|
+
});
|
|
650
|
+
function escapeHtmlAttr(value) {
|
|
651
|
+
return value
|
|
652
|
+
.replace(/&/g, '&')
|
|
653
|
+
.replace(/"/g, '"')
|
|
654
|
+
.replace(/</g, '<')
|
|
655
|
+
.replace(/>/g, '>');
|
|
656
|
+
}
|
|
657
|
+
function buildAttachmentBlock(upload, filePath, docUrl) {
|
|
658
|
+
const filename = String(upload.filename || path.basename(filePath));
|
|
659
|
+
const ext = String(upload.extname || path.extname(filename).replace(/^\./, '') || '');
|
|
660
|
+
const downloadUrl = typeof upload.url === 'string' ? upload.url : '';
|
|
661
|
+
const previewUrl = buildOfficePreviewUrl(upload, docUrl) || downloadUrl;
|
|
662
|
+
const data = {
|
|
663
|
+
id: upload.attachment_id,
|
|
664
|
+
attachmentId: upload.attachment_id,
|
|
665
|
+
name: filename,
|
|
666
|
+
filename,
|
|
667
|
+
size: upload.size,
|
|
668
|
+
filesize: upload.size,
|
|
669
|
+
ext,
|
|
670
|
+
filekey: upload.filekey,
|
|
671
|
+
src: previewUrl,
|
|
672
|
+
url: previewUrl,
|
|
673
|
+
previewUrl,
|
|
674
|
+
preview_url: previewUrl,
|
|
675
|
+
downloadUrl,
|
|
676
|
+
download_url: downloadUrl,
|
|
677
|
+
status: 'done',
|
|
678
|
+
mode: upload.mode || 'private'
|
|
679
|
+
};
|
|
680
|
+
return `<p><card type="inline" name="file" value="data:${encodeURIComponent(JSON.stringify(data))}"></card></p>`;
|
|
681
|
+
}
|
|
682
|
+
function buildOfficePreviewUrl(upload, docUrl) {
|
|
683
|
+
if (typeof upload.filekey !== 'string' || !upload.filekey.trim())
|
|
684
|
+
return '';
|
|
685
|
+
try {
|
|
686
|
+
const origin = new URL(docUrl).origin;
|
|
687
|
+
return `${origin}/office/${upload.filekey}?from=${encodeURIComponent(docUrl)}`;
|
|
688
|
+
}
|
|
689
|
+
catch {
|
|
690
|
+
return '';
|
|
691
|
+
}
|
|
692
|
+
}
|