docz-cli 0.8.2 → 0.10.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/README.md +13 -0
- package/dist/{chunk-DTGG5V2F.js → chunk-26LVQHYL.js} +590 -2
- package/dist/chunk-26LVQHYL.js.map +1 -0
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/{mcp-GARRGX3L.js → mcp-X6AF2C32.js} +229 -3
- package/dist/mcp-X6AF2C32.js.map +1 -0
- package/package.json +8 -3
- package/plugins/nvim/README.md +26 -0
- package/plugins/nvim/lua/docz/init.lua +159 -0
- package/dist/chunk-DTGG5V2F.js.map +0 -1
- package/dist/mcp-GARRGX3L.js.map +0 -1
package/README.md
CHANGED
|
@@ -79,6 +79,7 @@ export DOCSYNC_API_TOKEN=<your-token>
|
|
|
79
79
|
| `ls <space>[:<path>]` | List files and folders |
|
|
80
80
|
| `cat <space>:<path>` | Read file content |
|
|
81
81
|
| `upload <file> <space>[:<dir>]` | Upload local file |
|
|
82
|
+
| `image upload <file>` | Upload image to OSS, get permanent public URL for Markdown |
|
|
82
83
|
| `write <space>:<path> <content>` | Write content to file (`-` for stdin) |
|
|
83
84
|
| `mkdir <space>:<path>` | Create folder |
|
|
84
85
|
| `rm <space>:<path>` | Delete file/folder (30-day trash) |
|
|
@@ -141,6 +142,16 @@ docz-cli upload ./report.pdf G160-研发:reports # Uploa
|
|
|
141
142
|
docz-cli mkdir G160-研发:new-project # Create folder
|
|
142
143
|
```
|
|
143
144
|
|
|
145
|
+
### Images
|
|
146
|
+
|
|
147
|
+
Upload images to OSS for embedding in Markdown documents. Returns a permanent public URL — visible in share links and blogs without login, and doesn't consume Space quota. Supports png/jpg/webp, max 5MB.
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
docz-cli image upload ./screenshot.png
|
|
151
|
+
# URL: https://<bucket>.oss-cn-beijing.aliyuncs.com/docz-markdown/2026/06/.../image.png
|
|
152
|
+
# Markdown: 
|
|
153
|
+
```
|
|
154
|
+
|
|
144
155
|
### Manage
|
|
145
156
|
|
|
146
157
|
```bash
|
|
@@ -254,6 +265,7 @@ Add to your MCP settings:
|
|
|
254
265
|
| `docz_list_files` | List files in a directory |
|
|
255
266
|
| `docz_read_file` | Read file content |
|
|
256
267
|
| `docz_upload_file` | Upload/create a file |
|
|
268
|
+
| `docz_upload_image` | Upload image to OSS, returns public URL for Markdown |
|
|
257
269
|
| `docz_mkdir` | Create a folder |
|
|
258
270
|
| `docz_delete` | Delete file/folder |
|
|
259
271
|
| `docz_file_history` | View change history |
|
|
@@ -285,6 +297,7 @@ docz-cli wraps the DocSync REST API:
|
|
|
285
297
|
| `ls` | `GET /api/spaces/{id}/tree?path=` |
|
|
286
298
|
| `cat` | `GET /api/spaces/{id}/blob/{path}` |
|
|
287
299
|
| `upload` / `write` | `POST /api/spaces/{id}/files/upload` or `POST /api/spaces/{id}/files/save` |
|
|
300
|
+
| `image upload` | `POST /api/assets/images` |
|
|
288
301
|
| `mkdir` | `POST /api/spaces/{id}/files/mkdir` |
|
|
289
302
|
| `rm` | `POST /api/spaces/{id}/files/delete` |
|
|
290
303
|
| `mv` | `POST /api/spaces/{id}/files/rename` |
|
|
@@ -115,6 +115,20 @@ var DocSyncClient = class {
|
|
|
115
115
|
body: form
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Upload an image to the server's OSS asset storage.
|
|
120
|
+
* Returns a permanent public URL that can be embedded in Markdown.
|
|
121
|
+
* Server limits: png/jpg/webp only, max 5MB.
|
|
122
|
+
*/
|
|
123
|
+
async uploadImage(content, filename) {
|
|
124
|
+
const blob = new Blob([content], { type: "application/octet-stream" });
|
|
125
|
+
const form = new FormData();
|
|
126
|
+
form.append("file", blob, filename);
|
|
127
|
+
return this.request("/api/assets/images", {
|
|
128
|
+
method: "POST",
|
|
129
|
+
body: form
|
|
130
|
+
});
|
|
131
|
+
}
|
|
118
132
|
async save(spaceId, path, content, opts) {
|
|
119
133
|
const body = { path, content };
|
|
120
134
|
if (opts?.baseRef) body.base_ref = opts.baseRef;
|
|
@@ -298,6 +312,301 @@ var DocSyncClient = class {
|
|
|
298
312
|
}
|
|
299
313
|
};
|
|
300
314
|
|
|
315
|
+
// src/collab/text.ts
|
|
316
|
+
import { createHash } from "crypto";
|
|
317
|
+
function collabHash(content) {
|
|
318
|
+
return `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
319
|
+
}
|
|
320
|
+
var CollabConflictError = class extends Error {
|
|
321
|
+
constructor(currentHash, baseHash) {
|
|
322
|
+
super(
|
|
323
|
+
`collaborative document changed since base_collab_hash (current: ${currentHash}, base: ${baseHash})`
|
|
324
|
+
);
|
|
325
|
+
this.currentHash = currentHash;
|
|
326
|
+
this.baseHash = baseHash;
|
|
327
|
+
this.name = "CollabConflictError";
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
var CollabBaseHashRequiredError = class extends Error {
|
|
331
|
+
constructor(currentHash) {
|
|
332
|
+
super(
|
|
333
|
+
`base_collab_hash is required unless force is explicitly enabled (current: ${currentHash})`
|
|
334
|
+
);
|
|
335
|
+
this.currentHash = currentHash;
|
|
336
|
+
this.name = "CollabBaseHashRequiredError";
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
function getYText(doc) {
|
|
340
|
+
return doc.getText("content");
|
|
341
|
+
}
|
|
342
|
+
function readText(doc) {
|
|
343
|
+
return getYText(doc).toString();
|
|
344
|
+
}
|
|
345
|
+
function replaceText(doc, nextContent, opts = {}) {
|
|
346
|
+
const ytext = getYText(doc);
|
|
347
|
+
const previous = ytext.toString();
|
|
348
|
+
const previousHash = collabHash(previous);
|
|
349
|
+
const baseHash = opts.baseHash;
|
|
350
|
+
if (!opts.force) {
|
|
351
|
+
if (!baseHash) {
|
|
352
|
+
throw new CollabBaseHashRequiredError(previousHash);
|
|
353
|
+
}
|
|
354
|
+
if (previousHash !== baseHash) {
|
|
355
|
+
throw new CollabConflictError(previousHash, baseHash);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
doc.transact(() => {
|
|
359
|
+
ytext.delete(0, ytext.length);
|
|
360
|
+
if (nextContent) ytext.insert(0, nextContent);
|
|
361
|
+
}, opts.origin ?? "docz-cli");
|
|
362
|
+
return { previousHash, hash: collabHash(nextContent) };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// src/collab/types.ts
|
|
366
|
+
var CollabUnknownError = class extends Error {
|
|
367
|
+
constructor(message) {
|
|
368
|
+
super(message);
|
|
369
|
+
this.name = "CollabUnknownError";
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
var CollabPublishError = class extends Error {
|
|
373
|
+
constructor(message, code) {
|
|
374
|
+
super(message);
|
|
375
|
+
this.code = code;
|
|
376
|
+
this.name = "CollabPublishError";
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// src/collab/room.ts
|
|
381
|
+
import { randomUUID } from "crypto";
|
|
382
|
+
import {
|
|
383
|
+
HocuspocusProvider,
|
|
384
|
+
HocuspocusProviderWebsocket
|
|
385
|
+
} from "@hocuspocus/provider";
|
|
386
|
+
import WebSocket from "ws";
|
|
387
|
+
import * as Y from "yjs";
|
|
388
|
+
|
|
389
|
+
// src/collab/roomName.ts
|
|
390
|
+
function decodePath(path) {
|
|
391
|
+
try {
|
|
392
|
+
return decodeURIComponent(path);
|
|
393
|
+
} catch {
|
|
394
|
+
return path;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function normalizeCollabFilePath(path) {
|
|
398
|
+
const decoded = decodePath(path.trim()).replace(/\\/g, "/");
|
|
399
|
+
const parts = [];
|
|
400
|
+
for (const part of decoded.split("/")) {
|
|
401
|
+
if (!part || part === ".") continue;
|
|
402
|
+
if (part === "..") throw new Error("invalid collab file path");
|
|
403
|
+
parts.push(part);
|
|
404
|
+
}
|
|
405
|
+
return parts.join("/");
|
|
406
|
+
}
|
|
407
|
+
function buildCollabDocumentName(spaceId, filePath) {
|
|
408
|
+
const sid = spaceId.trim();
|
|
409
|
+
const normalizedPath = normalizeCollabFilePath(filePath);
|
|
410
|
+
if (!sid || !normalizedPath)
|
|
411
|
+
throw new Error("space id and file path are required");
|
|
412
|
+
return `${sid}:${normalizedPath}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// src/collab/room.ts
|
|
416
|
+
function wsUrl(baseUrl) {
|
|
417
|
+
const u = new URL(baseUrl);
|
|
418
|
+
u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
|
|
419
|
+
u.pathname = "/collab";
|
|
420
|
+
u.search = "";
|
|
421
|
+
u.hash = "";
|
|
422
|
+
return u.toString();
|
|
423
|
+
}
|
|
424
|
+
function withTimeout(promise, ms, label) {
|
|
425
|
+
let timer;
|
|
426
|
+
const timeout = new Promise((_, reject) => {
|
|
427
|
+
timer = setTimeout(
|
|
428
|
+
() => reject(new CollabUnknownError(`${label} timed out after ${ms}ms`)),
|
|
429
|
+
ms
|
|
430
|
+
);
|
|
431
|
+
});
|
|
432
|
+
return Promise.race([promise, timeout]).finally(() => {
|
|
433
|
+
if (timer) clearTimeout(timer);
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
var CollabRoomClient = class {
|
|
437
|
+
doc = new Y.Doc();
|
|
438
|
+
provider = null;
|
|
439
|
+
websocketProvider = null;
|
|
440
|
+
openOptions = null;
|
|
441
|
+
pending = /* @__PURE__ */ new Map();
|
|
442
|
+
connected = false;
|
|
443
|
+
synced = false;
|
|
444
|
+
readOnly = false;
|
|
445
|
+
async open(options) {
|
|
446
|
+
const openOptions = {
|
|
447
|
+
...options,
|
|
448
|
+
path: normalizeCollabFilePath(options.path),
|
|
449
|
+
timeoutMs: options.timeoutMs ?? 3e4
|
|
450
|
+
};
|
|
451
|
+
this.openOptions = openOptions;
|
|
452
|
+
const opened = new Promise((resolve, reject) => {
|
|
453
|
+
const websocketProvider = new HocuspocusProviderWebsocket({
|
|
454
|
+
url: wsUrl(openOptions.baseUrl),
|
|
455
|
+
WebSocketPolyfill: WebSocket,
|
|
456
|
+
parameters: {
|
|
457
|
+
space_id: openOptions.spaceId,
|
|
458
|
+
file_path: openOptions.path,
|
|
459
|
+
client: openOptions.client,
|
|
460
|
+
client_version: openOptions.clientVersion
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
this.websocketProvider = websocketProvider;
|
|
464
|
+
const provider = new HocuspocusProvider({
|
|
465
|
+
websocketProvider,
|
|
466
|
+
name: buildCollabDocumentName(openOptions.spaceId, openOptions.path),
|
|
467
|
+
document: this.doc,
|
|
468
|
+
token: () => openOptions.token,
|
|
469
|
+
onAuthenticated: () => {
|
|
470
|
+
},
|
|
471
|
+
onAuthenticationFailed: (data) => {
|
|
472
|
+
reject(
|
|
473
|
+
new CollabPublishError(
|
|
474
|
+
`collab authentication failed: ${data.reason}`,
|
|
475
|
+
"auth_failed"
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
},
|
|
479
|
+
onStatus: ({ status }) => {
|
|
480
|
+
this.connected = status === "connected";
|
|
481
|
+
},
|
|
482
|
+
onSynced: ({ state }) => {
|
|
483
|
+
this.synced = state;
|
|
484
|
+
if (state) resolve();
|
|
485
|
+
},
|
|
486
|
+
onStateless: ({ payload }) => {
|
|
487
|
+
this.handleStateless(payload);
|
|
488
|
+
},
|
|
489
|
+
onDisconnect: () => {
|
|
490
|
+
this.connected = false;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
this.provider = provider;
|
|
494
|
+
});
|
|
495
|
+
await withTimeout(opened, openOptions.timeoutMs, "collab open");
|
|
496
|
+
return this.read();
|
|
497
|
+
}
|
|
498
|
+
read() {
|
|
499
|
+
if (!this.openOptions) throw new Error("collab room is not open");
|
|
500
|
+
const content = readText(this.doc);
|
|
501
|
+
return {
|
|
502
|
+
spaceId: this.openOptions.spaceId,
|
|
503
|
+
path: this.openOptions.path,
|
|
504
|
+
content,
|
|
505
|
+
collabHash: collabHash(content),
|
|
506
|
+
connected: this.connected && this.synced,
|
|
507
|
+
readOnly: this.readOnly
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
write(content, opts = {}) {
|
|
511
|
+
if (!this.openOptions) throw new Error("collab room is not open");
|
|
512
|
+
if (this.readOnly)
|
|
513
|
+
throw new CollabPublishError("collab room is read-only", "read_only");
|
|
514
|
+
const result = replaceText(this.doc, content, {
|
|
515
|
+
baseHash: opts.baseHash,
|
|
516
|
+
force: opts.force,
|
|
517
|
+
origin: this.openOptions.client
|
|
518
|
+
});
|
|
519
|
+
return {
|
|
520
|
+
spaceId: this.openOptions.spaceId,
|
|
521
|
+
path: this.openOptions.path,
|
|
522
|
+
previousHash: result.previousHash,
|
|
523
|
+
collabHash: result.hash
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
async publish(timeoutMs) {
|
|
527
|
+
if (!this.provider || !this.openOptions)
|
|
528
|
+
throw new Error("collab room is not open");
|
|
529
|
+
const reqId = randomUUID();
|
|
530
|
+
const ms = timeoutMs ?? this.openOptions.timeoutMs ?? 3e4;
|
|
531
|
+
const result = new Promise((resolve, reject) => {
|
|
532
|
+
const timer = setTimeout(() => {
|
|
533
|
+
this.pending.delete(reqId);
|
|
534
|
+
reject(
|
|
535
|
+
new CollabUnknownError(`collab publish ack timed out after ${ms}ms`)
|
|
536
|
+
);
|
|
537
|
+
}, ms);
|
|
538
|
+
this.pending.set(reqId, { resolve, reject, timer });
|
|
539
|
+
});
|
|
540
|
+
this.provider.sendStateless(
|
|
541
|
+
JSON.stringify({
|
|
542
|
+
type: "publish",
|
|
543
|
+
reqId,
|
|
544
|
+
client: this.openOptions.client,
|
|
545
|
+
client_version: this.openOptions.clientVersion
|
|
546
|
+
})
|
|
547
|
+
);
|
|
548
|
+
return result;
|
|
549
|
+
}
|
|
550
|
+
close() {
|
|
551
|
+
for (const [reqId, pending] of this.pending) {
|
|
552
|
+
clearTimeout(pending.timer);
|
|
553
|
+
pending.reject(
|
|
554
|
+
new CollabUnknownError("collab room closed before publish ack")
|
|
555
|
+
);
|
|
556
|
+
this.pending.delete(reqId);
|
|
557
|
+
}
|
|
558
|
+
this.provider?.destroy();
|
|
559
|
+
this.websocketProvider?.destroy();
|
|
560
|
+
this.provider = null;
|
|
561
|
+
this.websocketProvider = null;
|
|
562
|
+
this.doc.destroy();
|
|
563
|
+
}
|
|
564
|
+
handleStateless(payload) {
|
|
565
|
+
let msg;
|
|
566
|
+
try {
|
|
567
|
+
msg = JSON.parse(payload);
|
|
568
|
+
} catch {
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (!msg.reqId) return;
|
|
572
|
+
const pending = this.pending.get(msg.reqId);
|
|
573
|
+
if (!pending) return;
|
|
574
|
+
this.pending.delete(msg.reqId);
|
|
575
|
+
clearTimeout(pending.timer);
|
|
576
|
+
if (msg.type === "publish_ack" || msg.type === "recreate_after_delete_ack") {
|
|
577
|
+
if (!this.openOptions) {
|
|
578
|
+
pending.reject(
|
|
579
|
+
new CollabUnknownError("collab room closed before publish ack")
|
|
580
|
+
);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
pending.resolve({
|
|
584
|
+
spaceId: this.openOptions.spaceId,
|
|
585
|
+
path: this.openOptions.path,
|
|
586
|
+
ref: msg.ref || "",
|
|
587
|
+
contentRef: msg.content_ref || "",
|
|
588
|
+
externalBackup: msg.external_backup || ""
|
|
589
|
+
});
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
pending.reject(
|
|
593
|
+
new CollabPublishError(
|
|
594
|
+
`collab publish failed${msg.code ? `: ${msg.code}` : ""}`,
|
|
595
|
+
msg.code
|
|
596
|
+
)
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
async function withCollabRoom(options, fn) {
|
|
601
|
+
const room = new CollabRoomClient();
|
|
602
|
+
try {
|
|
603
|
+
await room.open(options);
|
|
604
|
+
return await fn(room);
|
|
605
|
+
} finally {
|
|
606
|
+
room.close();
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
301
610
|
// src/config.ts
|
|
302
611
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
303
612
|
import { homedir } from "os";
|
|
@@ -331,8 +640,98 @@ function getConfigPath() {
|
|
|
331
640
|
}
|
|
332
641
|
|
|
333
642
|
// src/commands.ts
|
|
334
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
643
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, statSync } from "fs";
|
|
335
644
|
import { basename } from "path";
|
|
645
|
+
|
|
646
|
+
// src/collab/bridge.ts
|
|
647
|
+
import readline from "readline";
|
|
648
|
+
function send(value) {
|
|
649
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
650
|
+
`);
|
|
651
|
+
}
|
|
652
|
+
async function startCollabBridge(openRoom) {
|
|
653
|
+
let room = null;
|
|
654
|
+
let cleanupObserver = null;
|
|
655
|
+
const rl = readline.createInterface({
|
|
656
|
+
input: process.stdin,
|
|
657
|
+
crlfDelay: Number.POSITIVE_INFINITY
|
|
658
|
+
});
|
|
659
|
+
function attachObserver(nextRoom) {
|
|
660
|
+
cleanupObserver?.();
|
|
661
|
+
const ytext = nextRoom.doc.getText("content");
|
|
662
|
+
const observer = () => {
|
|
663
|
+
const content = ytext.toString();
|
|
664
|
+
send({ event: "document_change", content, hash: collabHash(content) });
|
|
665
|
+
};
|
|
666
|
+
ytext.observe(observer);
|
|
667
|
+
cleanupObserver = () => ytext.unobserve(observer);
|
|
668
|
+
}
|
|
669
|
+
function closeRoom() {
|
|
670
|
+
cleanupObserver?.();
|
|
671
|
+
cleanupObserver = null;
|
|
672
|
+
room?.close();
|
|
673
|
+
room = null;
|
|
674
|
+
}
|
|
675
|
+
for await (const line of rl) {
|
|
676
|
+
if (!line.trim()) continue;
|
|
677
|
+
let req;
|
|
678
|
+
try {
|
|
679
|
+
req = JSON.parse(line);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
send({ event: "error", code: "bad_json", message: String(err) });
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
try {
|
|
685
|
+
if (req.method === "open") {
|
|
686
|
+
const target = String(req.params?.target ?? "");
|
|
687
|
+
closeRoom();
|
|
688
|
+
room = await openRoom(target);
|
|
689
|
+
attachObserver(room);
|
|
690
|
+
const current = room.read();
|
|
691
|
+
send({
|
|
692
|
+
id: req.id,
|
|
693
|
+
result: {
|
|
694
|
+
content: current.content,
|
|
695
|
+
hash: current.collabHash,
|
|
696
|
+
read_only: current.readOnly
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
send({
|
|
700
|
+
event: "opened",
|
|
701
|
+
content: current.content,
|
|
702
|
+
hash: current.collabHash
|
|
703
|
+
});
|
|
704
|
+
} else if (req.method === "local_change") {
|
|
705
|
+
if (!room) throw new Error("room is not open");
|
|
706
|
+
const content = String(req.params?.content ?? "");
|
|
707
|
+
const baseHash = req.params?.base_hash ? String(req.params.base_hash) : void 0;
|
|
708
|
+
const result = room.write(content, {
|
|
709
|
+
baseHash,
|
|
710
|
+
force: req.params?.force === true
|
|
711
|
+
});
|
|
712
|
+
send({ id: req.id, result: { hash: result.collabHash } });
|
|
713
|
+
} else if (req.method === "publish") {
|
|
714
|
+
if (!room) throw new Error("room is not open");
|
|
715
|
+
send({ id: req.id, result: await room.publish() });
|
|
716
|
+
} else if (req.method === "status") {
|
|
717
|
+
send({ id: req.id, result: room ? room.read() : { connected: false } });
|
|
718
|
+
} else if (req.method === "close") {
|
|
719
|
+
closeRoom();
|
|
720
|
+
send({ id: req.id, result: { ok: true } });
|
|
721
|
+
} else {
|
|
722
|
+
throw new Error(`unknown method: ${req.method}`);
|
|
723
|
+
}
|
|
724
|
+
} catch (err) {
|
|
725
|
+
send({
|
|
726
|
+
id: req.id,
|
|
727
|
+
error: err instanceof Error ? err.message : String(err)
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
closeRoom();
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/commands.ts
|
|
336
735
|
function getClient() {
|
|
337
736
|
const token = getToken();
|
|
338
737
|
if (!token) {
|
|
@@ -343,6 +742,32 @@ function getClient() {
|
|
|
343
742
|
}
|
|
344
743
|
return new DocSyncClient(getBaseUrl(), token);
|
|
345
744
|
}
|
|
745
|
+
function getRequiredToken() {
|
|
746
|
+
const token = getToken();
|
|
747
|
+
if (!token) {
|
|
748
|
+
console.error(
|
|
749
|
+
"Error: No token configured.\nRun `docz login` or set DOCSYNC_API_TOKEN environment variable."
|
|
750
|
+
);
|
|
751
|
+
process.exit(1);
|
|
752
|
+
}
|
|
753
|
+
return token;
|
|
754
|
+
}
|
|
755
|
+
async function buildCollabOpenOptions(target, opts = {}) {
|
|
756
|
+
const api = getClient();
|
|
757
|
+
const { spaceId, path } = await resolveTarget(api, [target]);
|
|
758
|
+
if (!path) {
|
|
759
|
+
throw new Error("file path is required for collaborative editing");
|
|
760
|
+
}
|
|
761
|
+
return {
|
|
762
|
+
baseUrl: getBaseUrl(),
|
|
763
|
+
token: getRequiredToken(),
|
|
764
|
+
spaceId,
|
|
765
|
+
path,
|
|
766
|
+
client: opts.client ?? "docz-cli",
|
|
767
|
+
clientVersion: opts.clientVersion ?? "0.10.0",
|
|
768
|
+
timeoutMs: opts.timeout
|
|
769
|
+
};
|
|
770
|
+
}
|
|
346
771
|
function parseTarget(args) {
|
|
347
772
|
if (args.length === 0) {
|
|
348
773
|
console.error(
|
|
@@ -449,6 +874,31 @@ async function readStdin() {
|
|
|
449
874
|
return Buffer.concat(chunks).toString("utf-8");
|
|
450
875
|
}
|
|
451
876
|
var MAX_SAVE_SIZE = 2 * 1024 * 1024;
|
|
877
|
+
var IMAGE_EXTS = ["png", "jpg", "jpeg", "webp"];
|
|
878
|
+
var IMAGE_MAX_SIZE = 5 * 1024 * 1024;
|
|
879
|
+
function readImageFile(filePath) {
|
|
880
|
+
if (!existsSync2(filePath)) {
|
|
881
|
+
return { error: `File not found: ${filePath}` };
|
|
882
|
+
}
|
|
883
|
+
const filename = basename(filePath);
|
|
884
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
885
|
+
if (!IMAGE_EXTS.includes(ext)) {
|
|
886
|
+
return {
|
|
887
|
+
error: `Unsupported image type ".${ext}". Supported: ${IMAGE_EXTS.join(", ")}`
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
const size = statSync(filePath).size;
|
|
891
|
+
if (size > IMAGE_MAX_SIZE) {
|
|
892
|
+
return {
|
|
893
|
+
error: `Image too large (${formatSize(size)}). Max size: 5 MB`
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
return { content: readFileSync2(filePath), filename };
|
|
897
|
+
}
|
|
898
|
+
function markdownImageRef(filename, url) {
|
|
899
|
+
const alt = filename.replace(/\.[^.]+$/, "");
|
|
900
|
+
return ``;
|
|
901
|
+
}
|
|
452
902
|
function registerCommands(program) {
|
|
453
903
|
program.command("login").description("Configure DocSync credentials").option("-u, --url <url>", "DocSync server URL").option("-t, --token <token>", "API token").action(async (opts) => {
|
|
454
904
|
const url = opts.url ?? getBaseUrl();
|
|
@@ -532,6 +982,20 @@ function registerCommands(program) {
|
|
|
532
982
|
const result = await client.upload(spaceId, targetDir, filename, content);
|
|
533
983
|
console.log(`Uploaded: ${result.path}`);
|
|
534
984
|
});
|
|
985
|
+
const image = program.command("image").description("Image asset operations");
|
|
986
|
+
image.command("upload").description(
|
|
987
|
+
"Upload image to OSS, returns a permanent public URL for Markdown embedding"
|
|
988
|
+
).argument("<file>", "Local image file (png/jpg/webp, max 5MB)").action(async (file) => {
|
|
989
|
+
const read = readImageFile(file);
|
|
990
|
+
if ("error" in read) {
|
|
991
|
+
console.error(`Error: ${read.error}`);
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
const client = getClient();
|
|
995
|
+
const result = await client.uploadImage(read.content, read.filename);
|
|
996
|
+
console.log(`URL: ${result.url}`);
|
|
997
|
+
console.log(`Markdown: ${markdownImageRef(read.filename, result.url)}`);
|
|
998
|
+
});
|
|
535
999
|
program.command("write").description(
|
|
536
1000
|
"Write content to file \u2014 docz write <space>:<path> <content> or <url> <content>"
|
|
537
1001
|
).argument("<target>", "space:dir/filename.md or short URL").argument("<content>", "File content (or - for stdin)").option("--force", "Skip conflict detection").option("-m, --message <msg>", "Custom commit message").action(
|
|
@@ -834,6 +1298,124 @@ function registerCommands(program) {
|
|
|
834
1298
|
const ref = await client.getFileRef(spaceId, path);
|
|
835
1299
|
console.log(ref.url);
|
|
836
1300
|
});
|
|
1301
|
+
const collab = program.command("collab").description("Collaborative editing via Docz realtime rooms");
|
|
1302
|
+
collab.command("cat").description(
|
|
1303
|
+
"Read collaborative document \u2014 docz collab cat <space>:<path> or <url>"
|
|
1304
|
+
).argument("<target>", "space:path or short URL").option("--raw", "Output raw content only").option("--timeout <ms>", "Open timeout in milliseconds", Number).action(
|
|
1305
|
+
async (target, opts) => {
|
|
1306
|
+
const open = await buildCollabOpenOptions(target, {
|
|
1307
|
+
timeout: opts.timeout
|
|
1308
|
+
});
|
|
1309
|
+
await withCollabRoom(open, async (room) => {
|
|
1310
|
+
const result = room.read();
|
|
1311
|
+
if (!opts.raw) {
|
|
1312
|
+
console.error(`collab_hash: ${result.collabHash}`);
|
|
1313
|
+
console.error(`read_only: ${result.readOnly ? "true" : "false"}`);
|
|
1314
|
+
console.error("---");
|
|
1315
|
+
}
|
|
1316
|
+
process.stdout.write(result.content);
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
);
|
|
1320
|
+
collab.command("write").description(
|
|
1321
|
+
"Write collaborative document \u2014 docz collab write <space>:<path> <content>"
|
|
1322
|
+
).argument("<target>", "space:path or short URL").argument("<content>", "File content (or - for stdin)").option("--base-collab-hash <hash>", "Hash returned by docz collab cat").option("--force", "Skip collaborative hash conflict detection").option("--no-publish", "Only update realtime room, do not flush to repo").option("--timeout <ms>", "Open/publish timeout in milliseconds", Number).action(
|
|
1323
|
+
async (target, content, opts) => {
|
|
1324
|
+
const open = await buildCollabOpenOptions(target, {
|
|
1325
|
+
timeout: opts.timeout
|
|
1326
|
+
});
|
|
1327
|
+
const body = content === "-" ? await readStdin() : content;
|
|
1328
|
+
if (Buffer.byteLength(body, "utf-8") > MAX_SAVE_SIZE) {
|
|
1329
|
+
console.error(
|
|
1330
|
+
"Error: content exceeds 2MB limit. Use `docz upload` for large files."
|
|
1331
|
+
);
|
|
1332
|
+
process.exit(1);
|
|
1333
|
+
}
|
|
1334
|
+
try {
|
|
1335
|
+
await withCollabRoom(open, async (room) => {
|
|
1336
|
+
const write = room.write(body, {
|
|
1337
|
+
baseHash: opts.baseCollabHash,
|
|
1338
|
+
force: opts.force
|
|
1339
|
+
});
|
|
1340
|
+
if (opts.publish === false) {
|
|
1341
|
+
console.log(
|
|
1342
|
+
`Updated collaborative room (collab_hash: ${write.collabHash})`
|
|
1343
|
+
);
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
const published = await room.publish(opts.timeout);
|
|
1347
|
+
console.log(
|
|
1348
|
+
`Published: ${published.path} (ref: ${published.ref}, collab_hash: ${write.collabHash})`
|
|
1349
|
+
);
|
|
1350
|
+
if (published.externalBackup) {
|
|
1351
|
+
console.log(`External backup: ${published.externalBackup}`);
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
} catch (err) {
|
|
1355
|
+
if (err instanceof CollabConflictError) {
|
|
1356
|
+
console.error(
|
|
1357
|
+
`Error: collaborative document changed. Re-read and retry. current=${err.currentHash} base=${err.baseHash}`
|
|
1358
|
+
);
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
if (err instanceof CollabBaseHashRequiredError) {
|
|
1362
|
+
console.error(
|
|
1363
|
+
`Error: --base-collab-hash is required unless --force is set. Re-read first to get the latest hash. current=${err.currentHash}`
|
|
1364
|
+
);
|
|
1365
|
+
process.exit(1);
|
|
1366
|
+
}
|
|
1367
|
+
if (err instanceof CollabUnknownError) {
|
|
1368
|
+
console.error(
|
|
1369
|
+
`Unknown state: ${err.message}. The server may have processed the publish; please re-read before retrying.`
|
|
1370
|
+
);
|
|
1371
|
+
process.exit(75);
|
|
1372
|
+
}
|
|
1373
|
+
throw err;
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
);
|
|
1377
|
+
collab.command("publish").description(
|
|
1378
|
+
"Flush collaborative document to repo \u2014 docz collab publish <space>:<path>"
|
|
1379
|
+
).argument("<target>", "space:path or short URL").option("--timeout <ms>", "Open/publish timeout in milliseconds", Number).action(async (target, opts) => {
|
|
1380
|
+
const open = await buildCollabOpenOptions(target, {
|
|
1381
|
+
timeout: opts.timeout
|
|
1382
|
+
});
|
|
1383
|
+
try {
|
|
1384
|
+
await withCollabRoom(open, async (room) => {
|
|
1385
|
+
const result = await room.publish(opts.timeout);
|
|
1386
|
+
console.log(`Published: ${result.path} (ref: ${result.ref})`);
|
|
1387
|
+
if (result.externalBackup) {
|
|
1388
|
+
console.log(`External backup: ${result.externalBackup}`);
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
} catch (err) {
|
|
1392
|
+
if (err instanceof CollabUnknownError) {
|
|
1393
|
+
console.error(
|
|
1394
|
+
`Unknown state: ${err.message}. The server may have processed the publish; please re-read before retrying.`
|
|
1395
|
+
);
|
|
1396
|
+
process.exit(75);
|
|
1397
|
+
}
|
|
1398
|
+
throw err;
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
collab.command("bridge").description("Start local JSONL bridge for terminal editors").option("--client <name>", "Client name sent to Docz", "docz.nvim").option(
|
|
1402
|
+
"--client-version <version>",
|
|
1403
|
+
"Client version sent to Docz",
|
|
1404
|
+
"0.10.0"
|
|
1405
|
+
).option("--timeout <ms>", "Open/publish timeout in milliseconds", Number).action(
|
|
1406
|
+
async (opts) => {
|
|
1407
|
+
await startCollabBridge(async (target) => {
|
|
1408
|
+
const open = await buildCollabOpenOptions(target, {
|
|
1409
|
+
client: opts.client,
|
|
1410
|
+
clientVersion: opts.clientVersion,
|
|
1411
|
+
timeout: opts.timeout
|
|
1412
|
+
});
|
|
1413
|
+
const room = new CollabRoomClient();
|
|
1414
|
+
await room.open(open);
|
|
1415
|
+
return room;
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
);
|
|
837
1419
|
program.command("diff").description(
|
|
838
1420
|
"Show changes \u2014 docz diff <space>[:<path>] <commit> [<from>] or <url> <commit> [<from>]"
|
|
839
1421
|
).argument("<target>", "space or space:path or short URL").argument("<to>", "Commit hash").argument("[from]", "From commit hash (default: to^)").action(async (target, to, from) => {
|
|
@@ -862,9 +1444,15 @@ function registerCommands(program) {
|
|
|
862
1444
|
export {
|
|
863
1445
|
ConflictError,
|
|
864
1446
|
DocSyncClient,
|
|
1447
|
+
CollabConflictError,
|
|
1448
|
+
CollabBaseHashRequiredError,
|
|
1449
|
+
CollabUnknownError,
|
|
1450
|
+
withCollabRoom,
|
|
865
1451
|
getBaseUrl,
|
|
866
1452
|
getToken,
|
|
867
1453
|
parseExpires,
|
|
1454
|
+
readImageFile,
|
|
1455
|
+
markdownImageRef,
|
|
868
1456
|
registerCommands
|
|
869
1457
|
};
|
|
870
|
-
//# sourceMappingURL=chunk-
|
|
1458
|
+
//# sourceMappingURL=chunk-26LVQHYL.js.map
|