docmost-cli 0.4.0__py3-none-any.whl

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.
Files changed (56) hide show
  1. docmost_cli/__init__.py +5 -0
  2. docmost_cli/__main__.py +18 -0
  3. docmost_cli/api/__init__.py +5 -0
  4. docmost_cli/api/attachments.py +30 -0
  5. docmost_cli/api/auth.py +202 -0
  6. docmost_cli/api/client.py +296 -0
  7. docmost_cli/api/comments.py +103 -0
  8. docmost_cli/api/pages.py +530 -0
  9. docmost_cli/api/pagination.py +94 -0
  10. docmost_cli/api/search.py +40 -0
  11. docmost_cli/api/spaces.py +141 -0
  12. docmost_cli/api/users.py +25 -0
  13. docmost_cli/api/workspace.py +43 -0
  14. docmost_cli/cli/__init__.py +3 -0
  15. docmost_cli/cli/attachment.py +30 -0
  16. docmost_cli/cli/comment.py +83 -0
  17. docmost_cli/cli/config_cmd.py +143 -0
  18. docmost_cli/cli/main.py +133 -0
  19. docmost_cli/cli/page.py +382 -0
  20. docmost_cli/cli/search.py +33 -0
  21. docmost_cli/cli/space.py +57 -0
  22. docmost_cli/cli/sync_cmd.py +122 -0
  23. docmost_cli/cli/user.py +25 -0
  24. docmost_cli/cli/workspace.py +40 -0
  25. docmost_cli/config/__init__.py +23 -0
  26. docmost_cli/config/settings.py +23 -0
  27. docmost_cli/config/store.py +160 -0
  28. docmost_cli/convert/__init__.py +3 -0
  29. docmost_cli/convert/prosemirror_to_md.py +300 -0
  30. docmost_cli/models/__init__.py +3 -0
  31. docmost_cli/models/common.py +3 -0
  32. docmost_cli/output/__init__.py +17 -0
  33. docmost_cli/output/formatter.py +85 -0
  34. docmost_cli/output/tree.py +66 -0
  35. docmost_cli/py.typed +0 -0
  36. docmost_cli/sync/__init__.py +57 -0
  37. docmost_cli/sync/diff.py +156 -0
  38. docmost_cli/sync/frontmatter.py +152 -0
  39. docmost_cli/sync/manifest.py +195 -0
  40. docmost_cli/sync/pull.py +158 -0
  41. docmost_cli/sync/push.py +374 -0
  42. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-attachment.1 +57 -0
  43. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-comment.1 +92 -0
  44. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-config.1 +127 -0
  45. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-page.1 +412 -0
  46. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-search.1 +90 -0
  47. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-space.1 +111 -0
  48. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-sync.1 +206 -0
  49. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-user.1 +39 -0
  50. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-workspace.1 +68 -0
  51. docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli.1 +301 -0
  52. docmost_cli-0.4.0.dist-info/METADATA +241 -0
  53. docmost_cli-0.4.0.dist-info/RECORD +56 -0
  54. docmost_cli-0.4.0.dist-info/WHEEL +4 -0
  55. docmost_cli-0.4.0.dist-info/entry_points.txt +2 -0
  56. docmost_cli-0.4.0.dist-info/licenses/LICENSE +661 -0
@@ -0,0 +1,158 @@
1
+ """Pull space pages from Docmost server to local directory."""
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from docmost_cli.api.client import DocmostClient
8
+ from docmost_cli.output.formatter import _err_console as _err
9
+
10
+ __all__ = ["PullResult", "flatten_tree", "pull_space"]
11
+
12
+
13
+ @dataclass
14
+ class PullResult:
15
+ """Result of a pull operation."""
16
+
17
+ pages_pulled: int
18
+ dir_path: Path
19
+
20
+
21
+ def flatten_tree(
22
+ pages: list[dict[str, Any]],
23
+ parent_id: str | None = None,
24
+ ) -> list[dict[str, Any]]:
25
+ """Flatten nested page tree into a flat list with parent_id.
26
+
27
+ Args:
28
+ pages: Nested page tree from build_page_tree.
29
+ parent_id: Parent page ID for current level.
30
+
31
+ Returns:
32
+ Flat list of dicts with: id, title, icon, parent_id
33
+ """
34
+ result: list[dict[str, Any]] = []
35
+ for page in pages:
36
+ result.append(
37
+ {
38
+ "id": page["id"],
39
+ "title": page.get("title", ""),
40
+ "icon": page.get("icon") or "",
41
+ "parent_id": parent_id,
42
+ }
43
+ )
44
+ children = page.get("children", [])
45
+ if children:
46
+ result.extend(flatten_tree(children, parent_id=page["id"]))
47
+ return result
48
+
49
+
50
+ def pull_space(
51
+ client: DocmostClient,
52
+ space_slug: str,
53
+ dir_path: Path,
54
+ *,
55
+ force: bool = False,
56
+ ) -> PullResult:
57
+ """Pull all pages from a space to a local directory.
58
+
59
+ Algorithm:
60
+ 1. Resolve space slug to ID
61
+ 2. Build full page tree
62
+ 3. Flatten tree to list with parent_id
63
+ 4. For each page: fetch content, convert to markdown, write file
64
+ 5. Write manifest LAST (atomic commit point)
65
+
66
+ Args:
67
+ client: Authenticated Docmost client.
68
+ space_slug: Space slug identifier.
69
+ dir_path: Target directory path.
70
+ force: Overwrite existing files without warning.
71
+
72
+ Returns:
73
+ PullResult with count and path.
74
+ """
75
+ from docmost_cli.api.pages import build_page_tree, get_page_content
76
+ from docmost_cli.api.spaces import resolve_space_id
77
+ from docmost_cli.convert.prosemirror_to_md import convert_to_markdown
78
+ from docmost_cli.output.formatter import print_error
79
+ from docmost_cli.sync.frontmatter import write_sync_file
80
+ from docmost_cli.sync.manifest import (
81
+ build_manifest,
82
+ build_page_entry,
83
+ compute_content_hash,
84
+ load_manifest,
85
+ sanitize_filename,
86
+ save_manifest,
87
+ )
88
+
89
+ # 1. Resolve space
90
+ space_id = resolve_space_id(client, space_slug)
91
+
92
+ # 2. Build page tree
93
+ _err.print(f"Fetching page tree for '{space_slug}'...")
94
+ tree = build_page_tree(client, space_id)
95
+
96
+ # 3. Flatten
97
+ flat_pages = flatten_tree(tree)
98
+ total = len(flat_pages)
99
+
100
+ if total == 0:
101
+ # Empty space -- create dir + empty manifest
102
+ dir_path.mkdir(parents=True, exist_ok=True)
103
+ manifest = build_manifest(space_slug, space_id, [])
104
+ save_manifest(dir_path, manifest)
105
+ _err.print(f"Pulled 0 pages from '{space_slug}' -> {dir_path}")
106
+ return PullResult(pages_pulled=0, dir_path=dir_path)
107
+
108
+ # 4. Check target directory
109
+ if dir_path.exists():
110
+ existing_manifest = load_manifest(dir_path)
111
+ if existing_manifest and not force:
112
+ print_error(
113
+ f"Directory '{dir_path}' already has synced data. Use --force to overwrite."
114
+ )
115
+ # If dir exists with no manifest OR force is set, proceed
116
+
117
+ dir_path.mkdir(parents=True, exist_ok=True)
118
+
119
+ # 5. Fetch content and write files
120
+ page_entries: list[dict[str, Any]] = []
121
+ for i, page_info in enumerate(flat_pages, 1):
122
+ page_id = page_info["id"]
123
+ title = page_info["title"]
124
+ _err.print(f"Pulling {i}/{total}: {title}")
125
+
126
+ # Fetch content
127
+ content_data = get_page_content(client, page_id)
128
+ pm_content = content_data.get("content")
129
+
130
+ markdown = convert_to_markdown(pm_content) if pm_content else ""
131
+
132
+ # Generate filename and write file
133
+ filename = sanitize_filename(title, page_id)
134
+ metadata = {
135
+ "id": page_id,
136
+ "title": title,
137
+ "parent_id": page_info["parent_id"] or "",
138
+ "icon": page_info["icon"],
139
+ }
140
+ write_sync_file(dir_path / filename, metadata, markdown)
141
+
142
+ # Build manifest entry
143
+ content_hash = compute_content_hash(markdown)
144
+ entry = build_page_entry(
145
+ title=title,
146
+ filename=filename,
147
+ parent_id=page_info["parent_id"],
148
+ icon=page_info["icon"],
149
+ content_hash=content_hash,
150
+ )
151
+ page_entries.append({"id": page_id, **entry})
152
+
153
+ # 6. Write manifest LAST
154
+ manifest = build_manifest(space_slug, space_id, page_entries)
155
+ save_manifest(dir_path, manifest)
156
+
157
+ _err.print(f"Pulled {total} pages from '{space_slug}' -> {dir_path}")
158
+ return PullResult(pages_pulled=total, dir_path=dir_path)
@@ -0,0 +1,374 @@
1
+ """Push local changes to Docmost server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ from docmost_cli.output.formatter import _err_console as _err
9
+ from docmost_cli.sync.diff import ChangeType, SyncDiff
10
+
11
+ if TYPE_CHECKING:
12
+ from pathlib import Path
13
+
14
+ from docmost_cli.api.client import DocmostClient
15
+
16
+ __all__ = ["PushResult", "push_space"]
17
+
18
+
19
+ @dataclass
20
+ class PushResult:
21
+ """Result of a push operation."""
22
+
23
+ created: int = 0
24
+ updated: int = 0
25
+ moved: int = 0
26
+ deleted: int = 0
27
+ unchanged: int = 0
28
+ id_remaps: dict[str, str] = field(default_factory=dict) # old_id -> new_id
29
+
30
+
31
+ def push_space(
32
+ client: DocmostClient,
33
+ space_slug: str,
34
+ dir_path: Path,
35
+ *,
36
+ dry_run: bool = False,
37
+ delete: bool = False,
38
+ diff: SyncDiff | None = None,
39
+ ) -> PushResult:
40
+ """Push local changes to Docmost server.
41
+
42
+ Args:
43
+ client: Authenticated Docmost client.
44
+ space_slug: Space slug identifier.
45
+ dir_path: Directory containing synced files.
46
+ dry_run: If True, show plan without executing changes.
47
+ delete: If True, delete server pages not found locally.
48
+ diff: Pre-computed diff (avoids recomputing if caller already has it).
49
+
50
+ Returns:
51
+ PushResult with counts and any ID remaps.
52
+ """
53
+ from docmost_cli.api.pages import (
54
+ POSITION_FIRST,
55
+ create_and_place_page,
56
+ delete_page,
57
+ move_page,
58
+ try_update_page_content,
59
+ update_page_meta,
60
+ )
61
+ from docmost_cli.api.spaces import resolve_space_id
62
+ from docmost_cli.output.formatter import print_error
63
+ from docmost_cli.sync.diff import compute_diff
64
+ from docmost_cli.sync.frontmatter import write_sync_file
65
+ from docmost_cli.sync.manifest import (
66
+ build_page_entry,
67
+ compute_content_hash,
68
+ load_manifest,
69
+ save_manifest,
70
+ )
71
+
72
+ space_id = resolve_space_id(client, space_slug)
73
+
74
+ manifest = load_manifest(dir_path)
75
+ if manifest is None:
76
+ print_error(f"No manifest found in '{dir_path}'. Run 'sync pull' first.")
77
+
78
+ if diff is None:
79
+ diff = compute_diff(manifest, dir_path)
80
+ result = PushResult(unchanged=diff.unchanged)
81
+
82
+ if not diff.has_changes:
83
+ _err.print("No changes to push.")
84
+ return result
85
+
86
+ # Display summary
87
+ _print_summary(diff)
88
+
89
+ if dry_run:
90
+ _print_dry_run(diff)
91
+ return result
92
+
93
+ # --- Execute changes ---
94
+
95
+ enterprise: bool | None = None # Cached edition detection
96
+ id_remap: dict[str, str] = {} # old_id -> new_id
97
+
98
+ # Phase A: Create new pages (topological order)
99
+ existing_ids = set(manifest.get("pages", {}).keys())
100
+ sorted_new = _topological_sort(diff.new, existing_ids)
101
+
102
+ for change in sorted_new:
103
+ meta = change.local_meta or {}
104
+ body = change.local_body or ""
105
+ title = meta.get("title", "Untitled")
106
+ parent_id = meta.get("parent_id", "").strip() or None
107
+ icon = meta.get("icon", "").strip()
108
+
109
+ # Resolve parent_id through remap table
110
+ if parent_id and parent_id in id_remap:
111
+ parent_id = id_remap[parent_id]
112
+
113
+ _err.print(f" Creating: {title}")
114
+ new_id = create_and_place_page(
115
+ client,
116
+ space_id=space_id,
117
+ title=title,
118
+ content=body,
119
+ parent_page_id=parent_id,
120
+ icon=icon,
121
+ )
122
+
123
+ # Write ID back to frontmatter
124
+ meta["id"] = new_id
125
+ write_sync_file(dir_path / change.filename, meta, body)
126
+
127
+ # Update manifest
128
+ content_hash = compute_content_hash(body)
129
+ manifest["pages"][new_id] = build_page_entry(
130
+ title=title,
131
+ filename=change.filename,
132
+ parent_id=parent_id,
133
+ icon=icon,
134
+ content_hash=content_hash,
135
+ )
136
+ existing_ids.add(new_id)
137
+ result.created += 1
138
+
139
+ # Phase B: Update modified pages
140
+ for change in diff.modified:
141
+ meta = change.local_meta or {}
142
+ body = change.local_body or ""
143
+ page_id = change.page_id
144
+ title = meta.get("title", "")
145
+ parent_id = meta.get("parent_id", "").strip() or None
146
+ icon = meta.get("icon", "").strip()
147
+
148
+ has_content_change = ChangeType.CONTENT_CHANGED in change.changes
149
+ has_meta_change = bool(change.changes & {ChangeType.TITLE_CHANGED, ChangeType.ICON_CHANGED})
150
+
151
+ # Content update
152
+ if has_content_change:
153
+ if enterprise is None:
154
+ # First attempt: probe and update in one call
155
+ enterprise = try_update_page_content(client, page_id=page_id, content=body)
156
+ if enterprise:
157
+ _err.print(f" Updated (Enterprise): {title}")
158
+ elif enterprise:
159
+ try_update_page_content(client, page_id=page_id, content=body)
160
+ _err.print(f" Updated: {title}")
161
+
162
+ if not enterprise:
163
+ # Community: safe create-then-delete
164
+ _err.print(f" Replacing: {title}")
165
+ new_id = _community_replace(
166
+ client,
167
+ space_id=space_id,
168
+ old_page_id=page_id,
169
+ title=title,
170
+ content=body,
171
+ parent_id=parent_id,
172
+ icon=icon,
173
+ )
174
+ id_remap[page_id] = new_id
175
+ meta["id"] = new_id
176
+ write_sync_file(dir_path / change.filename, meta, body)
177
+ manifest["pages"].pop(page_id, None)
178
+ page_id = new_id
179
+
180
+ # Meta update (title/icon) — skip if community update already recreated the page
181
+ if has_meta_change and not (has_content_change and not enterprise):
182
+ _err.print(f" Metadata: {title}")
183
+ update_page_meta(
184
+ client,
185
+ page_id=page_id,
186
+ title=title if ChangeType.TITLE_CHANGED in change.changes else None,
187
+ icon=icon if ChangeType.ICON_CHANGED in change.changes else None,
188
+ )
189
+
190
+ # Update manifest entry
191
+ content_hash = compute_content_hash(body)
192
+ manifest["pages"][page_id] = build_page_entry(
193
+ title=title,
194
+ filename=change.filename,
195
+ parent_id=parent_id,
196
+ icon=icon,
197
+ content_hash=content_hash,
198
+ )
199
+ result.updated += 1
200
+
201
+ # Phase B2: Move pages (that weren't already handled as part of modified)
202
+ modified_ids = {c.page_id for c in diff.modified}
203
+ for change in diff.moved:
204
+ if change.page_id in modified_ids:
205
+ continue
206
+
207
+ meta = change.local_meta or {}
208
+ page_id = change.page_id
209
+ parent_id = meta.get("parent_id", "").strip() or None
210
+ title = meta.get("title", page_id)
211
+
212
+ # Check remap
213
+ if page_id in id_remap:
214
+ page_id = id_remap[page_id]
215
+ if parent_id and parent_id in id_remap:
216
+ parent_id = id_remap[parent_id]
217
+
218
+ _err.print(f" Moving: {title}")
219
+ move_page(
220
+ client,
221
+ page_id=page_id,
222
+ parent_page_id=parent_id,
223
+ position=POSITION_FIRST,
224
+ )
225
+
226
+ # Update manifest
227
+ if page_id in manifest["pages"]:
228
+ manifest["pages"][page_id]["parent_id"] = parent_id
229
+ result.moved += 1
230
+
231
+ # Phase C: Deletions
232
+ if diff.deleted:
233
+ if delete:
234
+ for change in diff.deleted:
235
+ entry = change.manifest_entry or {}
236
+ _err.print(f" Deleting: {entry.get('title', change.page_id)}")
237
+ delete_page(client, change.page_id)
238
+ manifest["pages"].pop(change.page_id, None)
239
+ result.deleted += 1
240
+ else:
241
+ _err.print(
242
+ f" [yellow]{len(diff.deleted)} page(s) on server not found locally. "
243
+ "Use --delete to remove.[/yellow]"
244
+ )
245
+
246
+ # Save ID remaps
247
+ result.id_remaps = id_remap
248
+ if id_remap:
249
+ _err.print(
250
+ f"[yellow]Community edition: {len(id_remap)} page(s) got new IDs. "
251
+ "Internal wiki links may need updating.[/yellow]"
252
+ )
253
+
254
+ # Save manifest
255
+ save_manifest(dir_path, manifest)
256
+
257
+ _err.print(
258
+ f"Pushed to '{space_slug}': "
259
+ f"{result.created} created, {result.updated} updated, "
260
+ f"{result.moved} moved, {result.deleted} deleted"
261
+ )
262
+ return result
263
+
264
+
265
+ def _community_replace(
266
+ client: DocmostClient,
267
+ *,
268
+ space_id: str,
269
+ old_page_id: str,
270
+ title: str,
271
+ content: str,
272
+ parent_id: str | None,
273
+ icon: str,
274
+ ) -> str:
275
+ """Safe content update for Community edition: create new, then delete old.
276
+
277
+ The old page is only deleted after the new one is confirmed created.
278
+
279
+ Returns:
280
+ New page ID.
281
+ """
282
+ from docmost_cli.api.pages import create_and_place_page, delete_page
283
+
284
+ new_id = create_and_place_page(
285
+ client,
286
+ space_id=space_id,
287
+ title=title,
288
+ content=content,
289
+ parent_page_id=parent_id,
290
+ icon=icon,
291
+ )
292
+ delete_page(client, old_page_id)
293
+ return new_id
294
+
295
+
296
+ def _topological_sort(new_changes: list, existing_ids: set[str]) -> list:
297
+ """Sort new pages so parents are created before children.
298
+
299
+ Pages with no parent or whose parent already exists on the server
300
+ are placed first. Pages whose parent_id references a server ID not
301
+ yet in the resolved set are deferred. Note: new pages have empty
302
+ page_id, so cross-references between new pages are not supported —
303
+ only references to existing server IDs are resolved.
304
+
305
+ Args:
306
+ new_changes: List of PageChange with NEW type.
307
+ existing_ids: Set of page IDs already on the server.
308
+
309
+ Returns:
310
+ Sorted list of PageChange.
311
+ """
312
+ result = []
313
+ remaining = list(new_changes)
314
+ resolved = set(existing_ids)
315
+
316
+ max_iterations = len(remaining) + 1
317
+ for _ in range(max_iterations):
318
+ if not remaining:
319
+ break
320
+ next_remaining = []
321
+ for change in remaining:
322
+ meta = change.local_meta or {}
323
+ parent_id = meta.get("parent_id", "").strip() or None
324
+ if parent_id is None or parent_id in resolved:
325
+ result.append(change)
326
+ else:
327
+ next_remaining.append(change)
328
+ if len(next_remaining) == len(remaining):
329
+ # No progress — circular or broken parent reference — add remaining
330
+ result.extend(next_remaining)
331
+ break
332
+ remaining = next_remaining
333
+
334
+ return result
335
+
336
+
337
+ def _print_summary(diff: SyncDiff) -> None:
338
+ """Print change summary to stderr."""
339
+ lines: list[str] = []
340
+ if diff.new:
341
+ lines.append(f" Create: {len(diff.new)} page(s)")
342
+ if diff.modified:
343
+ lines.append(f" Update: {len(diff.modified)} page(s)")
344
+ if diff.moved:
345
+ move_only = [c for c in diff.moved if c not in diff.modified]
346
+ if move_only:
347
+ lines.append(f" Move: {len(move_only)} page(s)")
348
+ if diff.deleted:
349
+ lines.append(f" Delete: {len(diff.deleted)} page(s)")
350
+ lines.append(f" Unchanged: {diff.unchanged} page(s)")
351
+ _err.print("Push plan:")
352
+ for line in lines:
353
+ _err.print(line)
354
+
355
+
356
+ def _print_dry_run(diff: SyncDiff) -> None:
357
+ """Print detailed plan to stdout for scripting."""
358
+ import sys
359
+
360
+ for change in diff.new:
361
+ meta = change.local_meta or {}
362
+ sys.stdout.write(f"CREATE {change.filename} ({meta.get('title', '?')})\n")
363
+ for change in diff.modified:
364
+ types = ", ".join(c.value for c in change.changes if c != ChangeType.MOVED)
365
+ sys.stdout.write(f"UPDATE {change.filename} ({types})\n")
366
+ for change in diff.moved:
367
+ if change not in diff.modified:
368
+ meta = change.local_meta or {}
369
+ sys.stdout.write(
370
+ f"MOVE {change.filename} -> parent:{meta.get('parent_id', 'root')}\n"
371
+ )
372
+ for change in diff.deleted:
373
+ entry = change.manifest_entry or {}
374
+ sys.stdout.write(f"DELETE {entry.get('filename', '?')} ({entry.get('title', '?')})\n")
@@ -0,0 +1,57 @@
1
+ .\" Manual page for docmost-cli-attachment(1)
2
+ .\" Source: SPECIFICATION.md section 4.6, src/docmost_cli/cli/attachment.py
3
+ .TH DOCMOST\-CLI\-ATTACHMENT 1 "2026\-03\-22" "docmost\-cli 0.4.0" "User Commands"
4
+ .SH NAME
5
+ docmost\-cli\-attachment \- attachment operations for Docmost wiki
6
+ .SH SYNOPSIS
7
+ .B docmost\-cli attachment search
8
+ .I query
9
+ .RB [ \-\-space
10
+ .IR slug ]
11
+ .RB [ \-\-json ]
12
+ .SH DESCRIPTION
13
+ Commands for working with file attachments in a Docmost wiki.
14
+ .SH SUBCOMMANDS
15
+ .SS search
16
+ Search for attachments by name or content.
17
+ .PP
18
+ .I query
19
+ is the search string.
20
+ .TP
21
+ .BI \-\-space " slug"
22
+ Restrict search to attachments in a specific space.
23
+ .TP
24
+ .B \-\-json
25
+ Output as a JSON array instead of a Rich table.
26
+ .PP
27
+ Table columns: id, fileName, type.
28
+ .SH EXAMPLES
29
+ Search for attachments:
30
+ .PP
31
+ .RS 4
32
+ .EX
33
+ $ docmost\-cli attachment search "architecture diagram"
34
+ .EE
35
+ .RE
36
+ .PP
37
+ Search within a space:
38
+ .PP
39
+ .RS 4
40
+ .EX
41
+ $ docmost\-cli attachment search "logo" \-\-space design
42
+ .EE
43
+ .RE
44
+ .SH EXIT STATUS
45
+ See
46
+ .BR docmost\-cli (1).
47
+ .SH SEE ALSO
48
+ .BR docmost\-cli (1),
49
+ .BR docmost\-cli\-search (1),
50
+ .BR docmost\-cli\-page (1)
51
+ .SH AUTHORS
52
+ Georg
53
+ .MT georg@mann\-mouse.at
54
+ .ME .
55
+ .SH COPYRIGHT
56
+ Copyright \(co 2026 Georg.
57
+ License AGPLv3+: GNU Affero General Public License version 3 or later.
@@ -0,0 +1,92 @@
1
+ .\" Manual page for docmost-cli-comment(1)
2
+ .\" Source: SPECIFICATION.md section 4.4, src/docmost_cli/cli/comment.py
3
+ .TH DOCMOST\-CLI\-COMMENT 1 "2026\-03\-22" "docmost\-cli 0.4.0" "User Commands"
4
+ .SH NAME
5
+ docmost\-cli\-comment \- comment operations for Docmost wiki pages
6
+ .SH SYNOPSIS
7
+ .B docmost\-cli comment list
8
+ .I page\-id
9
+ .RB [ \-\-json ]
10
+ .br
11
+ .B docmost\-cli comment create
12
+ .I page\-id
13
+ .BI \-\-content " text"
14
+ .br
15
+ .B docmost\-cli comment update
16
+ .I comment\-id
17
+ .BI \-\-content " text"
18
+ .SH DESCRIPTION
19
+ Commands for managing comments on Docmost wiki pages.
20
+ Comments are displayed as plain text extracted from the underlying
21
+ ProseMirror content, truncated to approximately 100 characters in
22
+ table output.
23
+ .SH SUBCOMMANDS
24
+ .SS list
25
+ List comments on a page.
26
+ .PP
27
+ .I page\-id
28
+ is the UUID of the page whose comments to list.
29
+ .TP
30
+ .B \-\-json
31
+ Output as a JSON array instead of a Rich table.
32
+ .PP
33
+ Table columns: id, content, creatorId, createdAt.
34
+ .SS create
35
+ Add a comment to a page.
36
+ .PP
37
+ .I page\-id
38
+ is the UUID of the page to comment on.
39
+ .TP
40
+ .BI \-\-content " text"
41
+ Comment text.
42
+ Required.
43
+ .PP
44
+ Outputs the new comment ID to stdout.
45
+ .SS update
46
+ Update an existing comment.
47
+ .PP
48
+ .I comment\-id
49
+ is the UUID of the comment to update.
50
+ .TP
51
+ .BI \-\-content " text"
52
+ New comment text.
53
+ Required.
54
+ .PP
55
+ Outputs the updated comment ID to stdout.
56
+ .SH EXAMPLES
57
+ List comments on a page:
58
+ .PP
59
+ .RS 4
60
+ .EX
61
+ $ docmost\-cli comment list 019a2a69\-xxxx
62
+ .EE
63
+ .RE
64
+ .PP
65
+ Add a comment:
66
+ .PP
67
+ .RS 4
68
+ .EX
69
+ $ docmost\-cli comment create 019a2a69\-xxxx \-\-content "Looks good, ship it!"
70
+ .EE
71
+ .RE
72
+ .PP
73
+ Update a comment:
74
+ .PP
75
+ .RS 4
76
+ .EX
77
+ $ docmost\-cli comment update 019b3c8f\-yyyy \-\-content "Updated review notes"
78
+ .EE
79
+ .RE
80
+ .SH EXIT STATUS
81
+ See
82
+ .BR docmost\-cli (1).
83
+ .SH SEE ALSO
84
+ .BR docmost\-cli (1),
85
+ .BR docmost\-cli\-page (1)
86
+ .SH AUTHORS
87
+ Georg
88
+ .MT georg@mann\-mouse.at
89
+ .ME .
90
+ .SH COPYRIGHT
91
+ Copyright \(co 2026 Georg.
92
+ License AGPLv3+: GNU Affero General Public License version 3 or later.