docz-cli 0.5.0 → 0.7.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.
@@ -1,6 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/client.ts
4
+ var ConflictError = class extends Error {
5
+ constructor(detail) {
6
+ super(
7
+ "Conflict: file has been modified by others. Please re-read the latest content and re-apply your changes."
8
+ );
9
+ this.detail = detail;
10
+ this.name = "ConflictError";
11
+ }
12
+ };
4
13
  var DocSyncClient = class {
5
14
  constructor(baseUrl, token) {
6
15
  this.baseUrl = baseUrl;
@@ -48,12 +57,28 @@ var DocSyncClient = class {
48
57
  `/api/spaces/${spaceId}/tree?path=${encodeURIComponent(path)}`
49
58
  );
50
59
  }
60
+ async treeFull(spaceId) {
61
+ return this.request(`/api/spaces/${spaceId}/tree/full`);
62
+ }
51
63
  // --- Blob ---
52
64
  async cat(spaceId, filepath) {
53
65
  return this.request(
54
66
  `/api/spaces/${spaceId}/blob/${encodeURIComponent(filepath)}`
55
67
  );
56
68
  }
69
+ async catWithRef(spaceId, filepath) {
70
+ const res = await fetch(
71
+ `${this.baseUrl}/api/spaces/${spaceId}/blob/${encodeURIComponent(filepath)}`,
72
+ { headers: { Authorization: `Bearer ${this.token}` } }
73
+ );
74
+ if (!res.ok) {
75
+ const body = await res.text().catch(() => "");
76
+ throw new Error(`${res.status} ${res.statusText}: ${body}`.trim());
77
+ }
78
+ const content = await res.text();
79
+ const ref = res.headers.get("X-Git-Ref") ?? "";
80
+ return { content, ref };
81
+ }
57
82
  // --- Write ---
58
83
  async upload(spaceId, dir, filename, content) {
59
84
  const blob = typeof content === "string" ? new Blob([content], { type: "application/octet-stream" }) : new Blob([content], { type: "application/octet-stream" });
@@ -65,6 +90,31 @@ var DocSyncClient = class {
65
90
  body: form
66
91
  });
67
92
  }
93
+ async save(spaceId, path, content, opts) {
94
+ const body = { path, content };
95
+ if (opts?.baseRef) body.base_ref = opts.baseRef;
96
+ if (opts?.message) body.message = opts.message;
97
+ const res = await fetch(
98
+ `${this.baseUrl}/api/spaces/${spaceId}/files/save`,
99
+ {
100
+ method: "POST",
101
+ headers: {
102
+ Authorization: `Bearer ${this.token}`,
103
+ "Content-Type": "application/json"
104
+ },
105
+ body: JSON.stringify(body)
106
+ }
107
+ );
108
+ if (res.status === 409) {
109
+ const data = await res.json();
110
+ throw new ConflictError(data);
111
+ }
112
+ if (!res.ok) {
113
+ const text = await res.text().catch(() => "");
114
+ throw new Error(`${res.status} ${res.statusText}: ${text}`.trim());
115
+ }
116
+ return res.json();
117
+ }
68
118
  async mkdir(spaceId, path) {
69
119
  await this.request(`/api/spaces/${spaceId}/files/mkdir`, {
70
120
  method: "POST",
@@ -86,6 +136,13 @@ var DocSyncClient = class {
86
136
  body: JSON.stringify({ old_path: from, new_path: to })
87
137
  });
88
138
  }
139
+ async rollback(spaceId, filePath, commitHash) {
140
+ await this.request(`/api/spaces/${spaceId}/files/rollback`, {
141
+ method: "POST",
142
+ headers: { "Content-Type": "application/json" },
143
+ body: JSON.stringify({ file_path: filePath, commit_hash: commitHash })
144
+ });
145
+ }
89
146
  // --- History ---
90
147
  async log(spaceId, filepath) {
91
148
  const path = filepath ? `/api/spaces/${spaceId}/log/${encodeURIComponent(filepath)}` : `/api/spaces/${spaceId}/log/`;
@@ -95,6 +152,48 @@ var DocSyncClient = class {
95
152
  async trash(spaceId) {
96
153
  return this.request(`/api/spaces/${spaceId}/trash`);
97
154
  }
155
+ async restore(spaceId, path, commit) {
156
+ await this.request(`/api/spaces/${spaceId}/trash/restore`, {
157
+ method: "POST",
158
+ headers: { "Content-Type": "application/json" },
159
+ body: JSON.stringify({ path, commit })
160
+ });
161
+ }
162
+ // --- Comments ---
163
+ async listComments(spaceId, filePath) {
164
+ return this.request(
165
+ `/api/spaces/${spaceId}/comments?path=${encodeURIComponent(filePath)}`
166
+ );
167
+ }
168
+ async createComment(spaceId, filePath, content) {
169
+ return this.request(`/api/spaces/${spaceId}/comments`, {
170
+ method: "POST",
171
+ headers: { "Content-Type": "application/json" },
172
+ body: JSON.stringify({ file_path: filePath, content })
173
+ });
174
+ }
175
+ async replyComment(spaceId, commentId, content) {
176
+ return this.request(
177
+ `/api/spaces/${spaceId}/comments/${commentId}/replies`,
178
+ {
179
+ method: "POST",
180
+ headers: { "Content-Type": "application/json" },
181
+ body: JSON.stringify({ content })
182
+ }
183
+ );
184
+ }
185
+ async closeComment(spaceId, commentId) {
186
+ return this.request(`/api/spaces/${spaceId}/comments/${commentId}`, {
187
+ method: "PUT",
188
+ headers: { "Content-Type": "application/json" },
189
+ body: JSON.stringify({ is_closed: true })
190
+ });
191
+ }
192
+ async deleteComment(spaceId, commentId) {
193
+ await this.request(`/api/spaces/${spaceId}/comments/${commentId}`, {
194
+ method: "DELETE"
195
+ });
196
+ }
98
197
  // --- Share Links ---
99
198
  async createShareLink(spaceId, filePath, opts) {
100
199
  const body = { file_path: filePath };
@@ -137,7 +236,9 @@ var DocSyncClient = class {
137
236
  async diffFile(spaceId, filePath, to, from) {
138
237
  const params = new URLSearchParams({ to });
139
238
  if (from) params.set("from", from);
140
- return this.request(`/api/spaces/${spaceId}/diff/${encodeURIComponent(filePath)}?${params}`);
239
+ return this.request(
240
+ `/api/spaces/${spaceId}/diff/${encodeURIComponent(filePath)}?${params}`
241
+ );
141
242
  }
142
243
  async diffSummary(spaceId, to, from) {
143
244
  const params = new URLSearchParams({ to });
@@ -192,7 +293,7 @@ function getConfigPath() {
192
293
 
193
294
  // src/commands.ts
194
295
  import { readFileSync as readFileSync2 } from "fs";
195
- import { basename, dirname } from "path";
296
+ import { basename } from "path";
196
297
  function getClient() {
197
298
  const token = getToken();
198
299
  if (!token) {
@@ -222,47 +323,76 @@ function formatSize(bytes) {
222
323
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
223
324
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
224
325
  }
225
- var SHORT_URL_RE = /\/s\/([^/]+)\/f\/([^/?\s]+)/;
226
- var SLUG_ONLY_RE = /\/s\/([^/?\s]+)\/?(?:\?.*)?$/;
227
326
  var SHARE_URL_RE = /\/share\/([^/?\s]+)/;
327
+ async function resolveSlug(client, slug) {
328
+ try {
329
+ const spaces = await client.listSpaces();
330
+ const found = spaces.find((s) => s.slug === slug);
331
+ if (found) return found;
332
+ } catch {
333
+ }
334
+ return client.resolveBySlug(slug);
335
+ }
228
336
  async function resolveUrl(client, input) {
229
- const fileMatch = input.match(SHORT_URL_RE);
337
+ let pathname;
338
+ try {
339
+ pathname = new URL(input).pathname;
340
+ } catch {
341
+ return null;
342
+ }
343
+ const fileMatch = pathname.match(/^\/s\/([^/]+)\/f\/([^/]+)$/);
230
344
  if (fileMatch) {
231
345
  const [, slug, fileId] = fileMatch;
232
346
  const space = await resolveSlug(client, slug);
233
347
  const ref = await client.resolveFileRef(fileId);
234
348
  return { spaceId: space.id, path: ref.path };
235
349
  }
236
- const slugMatch = input.match(SLUG_ONLY_RE);
350
+ const slugMatch = pathname.match(/^\/s\/([^/]+)(\/.*)?$/);
237
351
  if (slugMatch) {
238
- const [, slug] = slugMatch;
352
+ const [, slug, rest] = slugMatch;
239
353
  const space = await resolveSlug(client, slug);
240
- return { spaceId: space.id, path: "" };
354
+ const filePath = rest ? decodeURIComponent(rest.slice(1)) : "";
355
+ return { spaceId: space.id, path: filePath };
241
356
  }
242
- return null;
243
- }
244
- async function resolveSlug(client, slug) {
245
- try {
246
- const spaces = await client.listSpaces();
247
- const found = spaces.find((s) => s.slug === slug);
248
- if (found) return found;
249
- } catch {
357
+ const legacyMatch = pathname.match(/^\/spaces\/([^/]+)(\/.*)?$/);
358
+ if (legacyMatch) {
359
+ const [, spaceId, rest] = legacyMatch;
360
+ const filePath = rest ? decodeURIComponent(rest.slice(1)) : "";
361
+ return { spaceId, path: filePath };
250
362
  }
251
- return client.resolveBySlug(slug);
363
+ return null;
252
364
  }
253
365
  async function resolveTarget(client, args) {
254
366
  const first = args[0];
255
367
  if (first && (first.startsWith("http://") || first.startsWith("https://"))) {
256
368
  const result = await resolveUrl(client, first);
257
369
  if (result) return result;
370
+ throw new Error(
371
+ `Unrecognized DocSync URL: ${first}
372
+ Expected: /s/{slug}[/f/{fileId}], /s/{slug}[/path], or /spaces/{id}[/path]`
373
+ );
258
374
  }
259
375
  const { space, path } = parseTarget(args);
260
376
  const s = await client.resolveSpace(space);
261
377
  return { spaceId: s.id, path };
262
378
  }
379
+ async function resolveSpaceArg(client, input) {
380
+ if (input.startsWith("http://") || input.startsWith("https://")) {
381
+ const result = await resolveUrl(client, input);
382
+ if (result) return { id: result.spaceId };
383
+ throw new Error(
384
+ `Unrecognized DocSync URL: ${input}
385
+ Expected: /s/{slug}[/f/{fileId}], /s/{slug}[/path], or /spaces/{id}[/path]`
386
+ );
387
+ }
388
+ return client.resolveSpace(input);
389
+ }
263
390
  function parseExpires(value) {
264
391
  const match = value.match(/^(\d+)([dh])$/);
265
- if (!match) throw new Error(`Invalid expires format: "${value}". Use e.g. 7d, 24h, 30d`);
392
+ if (!match)
393
+ throw new Error(
394
+ `Invalid expires format: "${value}". Use e.g. 7d, 24h, 30d`
395
+ );
266
396
  const [, num, unit] = match;
267
397
  const ms = unit === "d" ? Number(num) * 864e5 : Number(num) * 36e5;
268
398
  return new Date(Date.now() + ms).toISOString();
@@ -272,6 +402,14 @@ function extractShareToken(input) {
272
402
  if (m) return m[1];
273
403
  return input;
274
404
  }
405
+ async function readStdin() {
406
+ const chunks = [];
407
+ for await (const chunk of process.stdin) {
408
+ chunks.push(chunk);
409
+ }
410
+ return Buffer.concat(chunks).toString("utf-8");
411
+ }
412
+ var MAX_SAVE_SIZE = 2 * 1024 * 1024;
275
413
  function registerCommands(program) {
276
414
  program.command("login").description("Configure DocSync credentials").option("-u, --url <url>", "DocSync server URL").option("-t, --token <token>", "API token").action(async (opts) => {
277
415
  const url = opts.url ?? getBaseUrl();
@@ -308,10 +446,10 @@ function registerCommands(program) {
308
446
  console.log(`${s.name} ${tag} ${s.member_count} members ${s.id}`);
309
447
  }
310
448
  });
311
- program.command("ls").description("List files \u2014 docz ls <space>[:<path>] or <url>").argument("<target...>").action(async (args) => {
449
+ program.command("ls").description("List files \u2014 docz ls <space>[:<path>] or <url>").argument("<target...>").option("-R, --recursive", "Recursively list all files").action(async (args, opts) => {
312
450
  const client = getClient();
313
451
  const { spaceId, path } = await resolveTarget(client, args);
314
- const entries = await client.ls(spaceId, path);
452
+ const entries = opts.recursive ? await client.treeFull(spaceId) : await client.ls(spaceId, path);
315
453
  if (entries.length === 0) {
316
454
  console.log("(empty)");
317
455
  return;
@@ -324,7 +462,7 @@ function registerCommands(program) {
324
462
  }
325
463
  }
326
464
  });
327
- program.command("cat").description("Read file content \u2014 docz cat <space>:<path> or <url>").argument("<target...>").action(async (args) => {
465
+ program.command("cat").description("Read file content \u2014 docz cat <space>:<path> or <url>").argument("<target...>").option("--ref", "Also output Git ref to stderr").action(async (args, opts) => {
328
466
  const client = getClient();
329
467
  const { spaceId, path } = await resolveTarget(client, args);
330
468
  if (!path) {
@@ -333,80 +471,100 @@ function registerCommands(program) {
333
471
  );
334
472
  process.exit(1);
335
473
  }
336
- const content = await client.cat(spaceId, path);
337
- process.stdout.write(content);
474
+ if (opts.ref) {
475
+ const result = await client.catWithRef(spaceId, path);
476
+ console.error(`ref: ${result.ref}`);
477
+ process.stdout.write(result.content);
478
+ } else {
479
+ const content = await client.cat(spaceId, path);
480
+ process.stdout.write(content);
481
+ }
338
482
  });
339
- program.command("upload").description("Upload file \u2014 docz upload <local-file> <space>[:<dir>]").argument("<file>", "Local file to upload").argument("<target...>").action(async (file, args) => {
340
- const { space, path: dir } = parseTarget(args);
483
+ program.command("upload").description(
484
+ "Upload file \u2014 docz upload <local-file> <space>[:<dir>] or <url>"
485
+ ).argument("<file>", "Local file to upload").argument("<target...>").action(async (file, args) => {
341
486
  const client = getClient();
342
- const s = await client.resolveSpace(space);
487
+ const { spaceId, path: dir } = await resolveTarget(client, args);
343
488
  const content = readFileSync2(file);
344
489
  const filename = basename(file);
345
490
  const targetDir = dir || "";
346
- const result = await client.upload(s.id, targetDir, filename, content);
491
+ const result = await client.upload(spaceId, targetDir, filename, content);
347
492
  console.log(`Uploaded: ${result.path}`);
348
493
  });
349
- program.command("write").description("Write content to file \u2014 docz write <space>:<path> <content>").argument("<target>", "space:dir/filename.md").argument("<content>", "File content (or - for stdin)").action(async (target, content) => {
350
- const { space, path } = parseTarget([target]);
351
- if (!path) {
352
- console.error(
353
- "Error: path is required. Usage: docz write <space>:<dir/filename> <content>"
354
- );
355
- process.exit(1);
356
- }
357
- const client = getClient();
358
- const s = await client.resolveSpace(space);
359
- const filename = basename(path);
360
- const dir = dirname(path);
361
- let body;
362
- if (content === "-") {
363
- const chunks = [];
364
- for await (const chunk of process.stdin) {
365
- chunks.push(chunk);
494
+ program.command("write").description(
495
+ "Write content to file \u2014 docz write <space>:<path> <content> or <url> <content>"
496
+ ).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(
497
+ async (target, content, opts) => {
498
+ const client = getClient();
499
+ const { spaceId, path } = await resolveTarget(client, [target]);
500
+ if (!path) {
501
+ console.error(
502
+ "Error: path is required. Usage: docz write <space>:<dir/filename> <content>"
503
+ );
504
+ process.exit(1);
505
+ }
506
+ const body = content === "-" ? await readStdin() : content;
507
+ if (Buffer.byteLength(body, "utf-8") > MAX_SAVE_SIZE) {
508
+ console.error(
509
+ "Error: content exceeds 2MB limit. Use `docz upload` for large files."
510
+ );
511
+ process.exit(1);
512
+ }
513
+ let baseRef;
514
+ if (!opts.force) {
515
+ try {
516
+ const existing = await client.catWithRef(spaceId, path);
517
+ baseRef = existing.ref;
518
+ } catch (err) {
519
+ const msg = err instanceof Error ? err.message : "";
520
+ if (!msg.includes("404")) throw err;
521
+ }
522
+ }
523
+ try {
524
+ const result = await client.save(spaceId, path, body, {
525
+ baseRef,
526
+ message: opts.message
527
+ });
528
+ console.log(`Written: ${result.path} (ref: ${result.ref})`);
529
+ } catch (err) {
530
+ if (err instanceof ConflictError) {
531
+ console.error(
532
+ "Error: file was modified by someone else. Please re-read the latest content and try again."
533
+ );
534
+ process.exit(1);
535
+ }
536
+ throw err;
366
537
  }
367
- body = Buffer.concat(chunks).toString("utf-8");
368
- } else {
369
- body = content;
370
538
  }
371
- const result = await client.upload(
372
- s.id,
373
- dir === "." ? "" : dir,
374
- filename,
375
- body
376
- );
377
- console.log(`Written: ${result.path}`);
378
- });
379
- program.command("mkdir").description("Create folder \u2014 docz mkdir <space>:<path>").argument("<target...>").action(async (args) => {
380
- const { space, path } = parseTarget(args);
539
+ );
540
+ program.command("mkdir").description("Create folder \u2014 docz mkdir <space>:<path> or <url>").argument("<target...>").action(async (args) => {
541
+ const client = getClient();
542
+ const { spaceId, path } = await resolveTarget(client, args);
381
543
  if (!path) {
382
544
  console.error("Error: path is required.");
383
545
  process.exit(1);
384
546
  }
385
- const client = getClient();
386
- const s = await client.resolveSpace(space);
387
- await client.mkdir(s.id, path);
547
+ await client.mkdir(spaceId, path);
388
548
  console.log(`Created: ${path}`);
389
549
  });
390
- program.command("rm").description("Delete file/folder \u2014 docz rm <space>:<path>").argument("<target...>").action(async (args) => {
391
- const { space, path } = parseTarget(args);
550
+ program.command("rm").description("Delete file/folder \u2014 docz rm <space>:<path> or <url>").argument("<target...>").action(async (args) => {
551
+ const client = getClient();
552
+ const { spaceId, path } = await resolveTarget(client, args);
392
553
  if (!path) {
393
554
  console.error("Error: path is required.");
394
555
  process.exit(1);
395
556
  }
396
- const client = getClient();
397
- const s = await client.resolveSpace(space);
398
- await client.rm(s.id, path);
557
+ await client.rm(spaceId, path);
399
558
  console.log(`Deleted: ${path} (recoverable from trash for 30 days)`);
400
559
  });
401
- program.command("mv").description("Rename/move \u2014 docz mv <space>:<from> <to>").argument("<target>", "space:old-path").argument("<to>", "new-path").action(async (target, to) => {
402
- const { space, path: from } = parseTarget([target]);
560
+ program.command("mv").description("Rename/move \u2014 docz mv <space>:<from> <to> or <url> <to>").argument("<target>", "space:old-path or short URL").argument("<to>", "new-path").action(async (target, to) => {
561
+ const client = getClient();
562
+ const { spaceId, path: from } = await resolveTarget(client, [target]);
403
563
  if (!from) {
404
564
  console.error("Error: source path is required.");
405
565
  process.exit(1);
406
566
  }
407
- const client = getClient();
408
- const s = await client.resolveSpace(space);
409
- await client.mv(s.id, from, to);
567
+ await client.mv(spaceId, from, to);
410
568
  console.log(`Moved: ${from} \u2192 ${to}`);
411
569
  });
412
570
  program.command("log").description("Show change history \u2014 docz log <space>[:<path>] or <url>").argument("<target...>").action(async (args) => {
@@ -421,9 +579,21 @@ function registerCommands(program) {
421
579
  console.log(`${l.hash} ${l.date} ${l.message}`);
422
580
  }
423
581
  });
582
+ program.command("rollback").description(
583
+ "Rollback file to a previous version \u2014 docz rollback <space>:<path> <commit> or <url> <commit>"
584
+ ).argument("<target>", "space:path or short URL").argument("<commit>", "Commit hash to rollback to").action(async (target, commit) => {
585
+ const client = getClient();
586
+ const { spaceId, path } = await resolveTarget(client, [target]);
587
+ if (!path) {
588
+ console.error("Error: file path is required.");
589
+ process.exit(1);
590
+ }
591
+ await client.rollback(spaceId, path, commit);
592
+ console.log(`Rolled back: ${path} \u2192 ${commit.substring(0, 7)}`);
593
+ });
424
594
  program.command("trash").description("Show deleted files \u2014 docz trash <space>").argument("<space>").action(async (spaceName) => {
425
595
  const client = getClient();
426
- const s = await client.resolveSpace(spaceName);
596
+ const s = await resolveSpaceArg(client, spaceName);
427
597
  const items = await client.trash(s.id);
428
598
  if (items.length === 0) {
429
599
  console.log("Trash is empty.");
@@ -435,32 +605,109 @@ function registerCommands(program) {
435
605
  );
436
606
  }
437
607
  });
438
- const share = program.command("share").description("Manage share links");
439
- share.command("create").description("Create share link \u2014 docz share create <space>:<path>").argument("<target>", "space:path").option("--expires <duration>", "Expiry duration (e.g. 7d, 24h)").option("--users <emails>", "Comma-separated user emails or IDs").option("--groups <ids>", "Comma-separated group IDs").action(async (target, opts) => {
440
- const { space, path } = parseTarget([target]);
608
+ program.command("restore").description(
609
+ "Restore file from trash \u2014 docz restore <space>:<path> <commit> or <url> <commit>"
610
+ ).argument("<target>", "space:path or short URL").argument("<commit>", "Commit hash from trash listing").action(async (target, commit) => {
611
+ const client = getClient();
612
+ const { spaceId, path } = await resolveTarget(client, [target]);
613
+ if (!path) {
614
+ console.error("Error: path is required.");
615
+ process.exit(1);
616
+ }
617
+ await client.restore(spaceId, path, commit);
618
+ console.log(`Restored: ${path}`);
619
+ });
620
+ const comment = program.command("comment").description("Manage file comments");
621
+ comment.command("list").description("List comments \u2014 docz comment list <space>:<path>").argument("<target...>").action(async (args) => {
622
+ const client = getClient();
623
+ const { spaceId, path } = await resolveTarget(client, args);
624
+ if (!path) {
625
+ console.error("Error: file path is required.");
626
+ process.exit(1);
627
+ }
628
+ const comments = await client.listComments(spaceId, path);
629
+ if (comments.length === 0) {
630
+ console.log("No comments.");
631
+ return;
632
+ }
633
+ for (const c of comments) {
634
+ const status = c.is_closed ? " [closed]" : "";
635
+ console.log(`#${c.id} ${c.user_name} (${c.created_at})${status}`);
636
+ console.log(` ${c.content}`);
637
+ for (const r of c.replies) {
638
+ console.log(` \u2192 ${r.user_name}: ${r.content}`);
639
+ }
640
+ }
641
+ });
642
+ comment.command("add").description(
643
+ "Add comment \u2014 docz comment add <space>:<path> <content> or <url> <content>"
644
+ ).argument("<target>", "space:path or short URL").argument("<content>", "Comment text (or - for stdin)").action(async (target, content) => {
645
+ const client = getClient();
646
+ const { spaceId, path } = await resolveTarget(client, [target]);
441
647
  if (!path) {
442
- console.error("Error: file path is required. Usage: docz share create <space>:<path>");
648
+ console.error("Error: file path is required.");
443
649
  process.exit(1);
444
650
  }
651
+ const body = content === "-" ? await readStdin() : content;
652
+ const c = await client.createComment(spaceId, path, body);
653
+ console.log(`Comment #${c.id} created.`);
654
+ });
655
+ comment.command("reply").description(
656
+ "Reply to comment \u2014 docz comment reply <space> <commentId> <content>"
657
+ ).argument("<space>").argument("<commentId>").argument("<content>", "Reply text (or - for stdin)").action(async (spaceName, commentId, content) => {
658
+ const client = getClient();
659
+ const s = await resolveSpaceArg(client, spaceName);
660
+ const body = content === "-" ? await readStdin() : content;
661
+ const r = await client.replyComment(s.id, Number(commentId), body);
662
+ console.log(`Reply #${r.id} created.`);
663
+ });
664
+ comment.command("close").description("Close comment \u2014 docz comment close <space> <commentId>").argument("<space>").argument("<commentId>").action(async (spaceName, commentId) => {
665
+ const client = getClient();
666
+ const s = await resolveSpaceArg(client, spaceName);
667
+ await client.closeComment(s.id, Number(commentId));
668
+ console.log(`Comment #${commentId} closed.`);
669
+ });
670
+ comment.command("rm").description("Delete comment \u2014 docz comment rm <space> <commentId>").argument("<space>").argument("<commentId>").action(async (spaceName, commentId) => {
445
671
  const client = getClient();
446
- const s = await client.resolveSpace(space);
447
- const apiOpts = {};
448
- if (opts.expires) apiOpts.expiresAt = parseExpires(opts.expires);
449
- if (opts.users) apiOpts.userIds = opts.users.split(",").map((s2) => s2.trim());
450
- if (opts.groups) apiOpts.groupIds = opts.groups.split(",").map((s2) => s2.trim());
451
- const link = await client.createShareLink(s.id, path, apiOpts);
452
- const baseUrl = getBaseUrl();
453
- console.log(`Created share link:`);
454
- console.log(` id: ${link.id}`);
455
- console.log(` token: ${link.token}`);
456
- console.log(` url: ${baseUrl}/share/${link.token}`);
457
- console.log(` expires: ${link.expires_at ?? "never"}`);
458
- if (link.user_ids?.length) console.log(` users: ${link.user_ids.join(", ")}`);
459
- if (link.group_ids?.length) console.log(` groups: ${link.group_ids.length}`);
672
+ const s = await resolveSpaceArg(client, spaceName);
673
+ await client.deleteComment(s.id, Number(commentId));
674
+ console.log(`Comment #${commentId} deleted.`);
460
675
  });
676
+ const share = program.command("share").description("Manage share links");
677
+ share.command("create").description(
678
+ "Create share link \u2014 docz share create <space>:<path> or <url>"
679
+ ).argument("<target>", "space:path or short URL").option("--expires <duration>", "Expiry duration (e.g. 7d, 24h)").option("--users <emails>", "Comma-separated user emails or IDs").option("--groups <ids>", "Comma-separated group IDs").action(
680
+ async (target, opts) => {
681
+ const client = getClient();
682
+ const { spaceId, path } = await resolveTarget(client, [target]);
683
+ if (!path) {
684
+ console.error(
685
+ "Error: file path is required. Usage: docz share create <space>:<path>"
686
+ );
687
+ process.exit(1);
688
+ }
689
+ const apiOpts = {};
690
+ if (opts.expires) apiOpts.expiresAt = parseExpires(opts.expires);
691
+ if (opts.users)
692
+ apiOpts.userIds = opts.users.split(",").map((s) => s.trim());
693
+ if (opts.groups)
694
+ apiOpts.groupIds = opts.groups.split(",").map((s) => s.trim());
695
+ const link = await client.createShareLink(spaceId, path, apiOpts);
696
+ const baseUrl = getBaseUrl();
697
+ console.log("Created share link:");
698
+ console.log(` id: ${link.id}`);
699
+ console.log(` token: ${link.token}`);
700
+ console.log(` url: ${baseUrl}/share/${link.token}`);
701
+ console.log(` expires: ${link.expires_at ?? "never"}`);
702
+ if (link.user_ids?.length)
703
+ console.log(` users: ${link.user_ids.join(", ")}`);
704
+ if (link.group_ids?.length)
705
+ console.log(` groups: ${link.group_ids.length}`);
706
+ }
707
+ );
461
708
  share.command("list").description("List share links \u2014 docz share list <space>").argument("<space>").option("--file <path>", "Filter by file path").action(async (spaceName, opts) => {
462
709
  const client = getClient();
463
- const s = await client.resolveSpace(spaceName);
710
+ const s = await resolveSpaceArg(client, spaceName);
464
711
  const links = await client.listShareLinks(s.id, opts.file);
465
712
  if (links.length === 0) {
466
713
  console.log("No share links.");
@@ -469,19 +716,25 @@ function registerCommands(program) {
469
716
  for (const l of links) {
470
717
  const expires = l.expires_at ?? "never";
471
718
  const creator = l.created_by_name ?? l.created_by_email ?? l.created_by;
472
- console.log(`${l.id} ${l.token} ${l.file_path} ${expires} ${creator}`);
719
+ console.log(
720
+ `${l.id} ${l.token} ${l.file_path} ${expires} ${creator}`
721
+ );
473
722
  }
474
723
  });
475
- share.command("update").description("Update share link \u2014 docz share update <space> <link-id>").argument("<space>").argument("<linkId>").option("--expires <duration>", "New expiry duration (e.g. 30d)").option("--users <emails>", "Comma-separated user emails or IDs").option("--groups <ids>", "Comma-separated group IDs").action(async (spaceName, linkId, opts) => {
476
- const client = getClient();
477
- const s = await client.resolveSpace(spaceName);
478
- const apiOpts = {};
479
- if (opts.expires) apiOpts.expiresAt = parseExpires(opts.expires);
480
- if (opts.users) apiOpts.userIds = opts.users.split(",").map((s2) => s2.trim());
481
- if (opts.groups) apiOpts.groupIds = opts.groups.split(",").map((s2) => s2.trim());
482
- await client.updateShareLink(s.id, linkId, apiOpts);
483
- console.log(`Updated share link: ${linkId}`);
484
- });
724
+ share.command("update").description("Update share link \u2014 docz share update <space> <link-id>").argument("<space>").argument("<linkId>").option("--expires <duration>", "New expiry duration (e.g. 30d)").option("--users <emails>", "Comma-separated user emails or IDs").option("--groups <ids>", "Comma-separated group IDs").action(
725
+ async (spaceName, linkId, opts) => {
726
+ const client = getClient();
727
+ const s = await resolveSpaceArg(client, spaceName);
728
+ const apiOpts = {};
729
+ if (opts.expires) apiOpts.expiresAt = parseExpires(opts.expires);
730
+ if (opts.users)
731
+ apiOpts.userIds = opts.users.split(",").map((s2) => s2.trim());
732
+ if (opts.groups)
733
+ apiOpts.groupIds = opts.groups.split(",").map((s2) => s2.trim());
734
+ await client.updateShareLink(s.id, linkId, apiOpts);
735
+ console.log(`Updated share link: ${linkId}`);
736
+ }
737
+ );
485
738
  share.command("cat").description("Read shared file \u2014 docz share cat <token-or-url>").argument("<token>", "Share token or full URL").option("--raw", "Output raw content only").action(async (tokenArg, opts) => {
486
739
  const client = getClient();
487
740
  const token = extractShareToken(tokenArg);
@@ -489,7 +742,9 @@ function registerCommands(program) {
489
742
  try {
490
743
  const info = await client.getSharedFileInfo(token);
491
744
  console.log(`File: ${info.file_path} (${info.space_name})`);
492
- console.log(`Shared by: ${info.created_by_name} | Expires: ${info.expires_at ?? "never"}`);
745
+ console.log(
746
+ `Shared by: ${info.created_by_name} | Expires: ${info.expires_at ?? "never"}`
747
+ );
493
748
  console.log("---");
494
749
  } catch (err) {
495
750
  const msg = err instanceof Error ? err.message : String(err);
@@ -512,7 +767,7 @@ function registerCommands(program) {
512
767
  });
513
768
  share.command("rm").description("Delete share link \u2014 docz share rm <space> <link-id>").argument("<space>").argument("<linkId>").action(async (spaceName, linkId) => {
514
769
  const client = getClient();
515
- const s = await client.resolveSpace(spaceName);
770
+ const s = await resolveSpaceArg(client, spaceName);
516
771
  await client.deleteShareLink(s.id, linkId);
517
772
  console.log(`Deleted share link: ${linkId}`);
518
773
  });
@@ -528,19 +783,20 @@ function registerCommands(program) {
528
783
  const ref = await client.getFileRef(spaceId, path);
529
784
  console.log(ref.url);
530
785
  });
531
- program.command("diff").description("Show changes \u2014 docz diff <space>[:<path>] <commit> [<from>]").argument("<target>", "space or space:path").argument("<to>", "Commit hash").argument("[from]", "From commit hash (default: to^)").action(async (target, to, from) => {
786
+ program.command("diff").description(
787
+ "Show changes \u2014 docz diff <space>[:<path>] <commit> [<from>] or <url> <commit> [<from>]"
788
+ ).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) => {
532
789
  const client = getClient();
533
- const { space, path } = parseTarget([target]);
534
- const s = await client.resolveSpace(space);
790
+ const { spaceId, path } = await resolveTarget(client, [target]);
535
791
  if (path) {
536
- const result = await client.diffFile(s.id, path, to, from);
792
+ const result = await client.diffFile(spaceId, path, to, from);
537
793
  if (result.diff) {
538
794
  process.stdout.write(result.diff);
539
795
  } else {
540
796
  console.log("No changes.");
541
797
  }
542
798
  } else {
543
- const result = await client.diffSummary(s.id, to, from);
799
+ const result = await client.diffSummary(spaceId, to, from);
544
800
  if (result.files.length === 0) {
545
801
  console.log("No changes.");
546
802
  return;
@@ -553,10 +809,11 @@ function registerCommands(program) {
553
809
  }
554
810
 
555
811
  export {
812
+ ConflictError,
556
813
  DocSyncClient,
557
814
  getBaseUrl,
558
815
  getToken,
559
816
  parseExpires,
560
817
  registerCommands
561
818
  };
562
- //# sourceMappingURL=chunk-EBJ32KUL.js.map
819
+ //# sourceMappingURL=chunk-BHXHIHWH.js.map