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 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: ![screenshot](https://...)
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 `![${alt}](${url})`;
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-DTGG5V2F.js.map
1458
+ //# sourceMappingURL=chunk-26LVQHYL.js.map