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,382 @@
1
+ """Page subcommands."""
2
+
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import typer
8
+
9
+ from docmost_cli.api.pages import (
10
+ build_page_tree,
11
+ copy_page,
12
+ create_and_place_page,
13
+ delete_page,
14
+ duplicate_page,
15
+ export_page,
16
+ get_page_children,
17
+ get_page_content,
18
+ get_page_history,
19
+ get_page_info,
20
+ import_page,
21
+ list_recent_pages,
22
+ move_page,
23
+ update_page_content,
24
+ update_page_meta,
25
+ )
26
+ from docmost_cli.api.pagination import extract_id, extract_items
27
+ from docmost_cli.api.spaces import resolve_space_id
28
+ from docmost_cli.cli.main import get_client, state
29
+ from docmost_cli.output.formatter import (
30
+ print_content,
31
+ print_content_with_meta,
32
+ print_error,
33
+ print_result,
34
+ print_table,
35
+ )
36
+ from docmost_cli.output.tree import print_tree
37
+
38
+ __all__ = ["page_app"]
39
+
40
+ page_app = typer.Typer(name="page", help="Page operations.")
41
+
42
+
43
+ def _resolve_content(
44
+ content: str | None,
45
+ file: Path | None,
46
+ stdin: bool,
47
+ ) -> str | None:
48
+ """Resolve content from --content, --file, or --stdin.
49
+
50
+ These are mutually exclusive. Returns None if no source provided.
51
+
52
+ Args:
53
+ content: Inline content string.
54
+ file: Path to content file.
55
+ stdin: Whether to read from stdin.
56
+
57
+ Returns:
58
+ Resolved content string, or None.
59
+ """
60
+ sources = sum([content is not None, file is not None, stdin])
61
+ if sources > 1:
62
+ print_error("Only one of --content, --file, or --stdin may be specified.")
63
+ if sources == 0:
64
+ return None
65
+ if content is not None:
66
+ # Interpret common escape sequences so --content "Line 1\n\nLine 2" works
67
+ return content.replace("\\n", "\n").replace("\\t", "\t")
68
+ if file is not None:
69
+ if not file.exists():
70
+ print_error(f"File not found: {file}")
71
+ return file.read_text(encoding="utf-8")
72
+ # stdin
73
+ if sys.stdin.isatty():
74
+ print_error(
75
+ "No input piped to stdin. "
76
+ "Use --content or --file instead, or pipe input: "
77
+ "echo '# Page' | docmost-cli page create ..."
78
+ )
79
+ return sys.stdin.read()
80
+
81
+
82
+ @page_app.command("create")
83
+ def page_create_cmd(
84
+ space_slug: str = typer.Argument(help="Space slug to create the page in"),
85
+ title: str = typer.Option(..., "--title", help="Page title (required)"),
86
+ content: str | None = typer.Option(None, "--content", help="Markdown content string"),
87
+ file: Path | None = typer.Option(None, "--file", help="Read content from file"),
88
+ stdin: bool = typer.Option(False, "--stdin", help="Read content from stdin"),
89
+ parent: str | None = typer.Option(None, "--parent", help="Parent page ID"),
90
+ icon: str | None = typer.Option(None, "--icon", help="Page icon emoji"),
91
+ ) -> None:
92
+ """Create a new page via Markdown import.
93
+
94
+ See also: page move (reposition), page children (list children).
95
+ """
96
+ resolved = _resolve_content(content, file, stdin) or ""
97
+ client = get_client()
98
+ space_id = resolve_space_id(client, space_slug)
99
+
100
+ page_id = create_and_place_page(
101
+ client,
102
+ space_id=space_id,
103
+ title=title,
104
+ content=resolved,
105
+ parent_page_id=parent,
106
+ icon=icon,
107
+ )
108
+
109
+ msg = f"Created page '{title}' in space '{space_slug}'"
110
+ if not resolved:
111
+ msg = f"Created empty page '{title}' in space '{space_slug}'"
112
+ print_result(page_id, msg)
113
+
114
+
115
+ @page_app.command("update")
116
+ def page_update_cmd(
117
+ page_id: str = typer.Argument(help="Page ID to update"),
118
+ title: str | None = typer.Option(None, "--title", help="New title"),
119
+ icon: str | None = typer.Option(None, "--icon", help="Page icon emoji"),
120
+ content: str | None = typer.Option(None, "--content", help="New content (Markdown)"),
121
+ file: Path | None = typer.Option(None, "--file", help="Read content from file"),
122
+ stdin: bool = typer.Option(False, "--stdin", help="Read content from stdin"),
123
+ ) -> None:
124
+ """Update an existing page's title, icon, and/or content.
125
+
126
+ See also: page move (reposition), page get (view current content).
127
+ """
128
+ resolved = _resolve_content(content, file, stdin)
129
+ if title is None and icon is None and resolved is None:
130
+ print_error("At least one of --title, --icon, --content, --file, or --stdin is required.")
131
+
132
+ client = get_client()
133
+ info = get_page_info(client, page_id)
134
+ page_title = info.get("title", page_id)
135
+
136
+ if title is not None or icon is not None:
137
+ update_page_meta(client, page_id=page_id, title=title, icon=icon)
138
+ if title is not None:
139
+ page_title = title
140
+
141
+ if resolved is not None:
142
+ update_page_content(client, page_id=page_id, content=resolved)
143
+
144
+ print_result(page_id, f"Updated page '{page_title}'")
145
+
146
+
147
+ @page_app.command("delete")
148
+ def page_delete_cmd(
149
+ page_id: str = typer.Argument(help="Page ID to delete"),
150
+ ) -> None:
151
+ """Delete a page (requires confirmation unless --yes).
152
+
153
+ See also: page duplicate (copy before deleting).
154
+ """
155
+ client = get_client()
156
+ info = get_page_info(client, page_id)
157
+ page_title = info.get("title", page_id)
158
+
159
+ if not state.yes:
160
+ typer.confirm(f"Delete page '{page_title}' ({page_id})?", abort=True)
161
+
162
+ delete_page(client, page_id)
163
+ print_result(page_id, f"Deleted page '{page_title}'")
164
+
165
+
166
+ @page_app.command("move")
167
+ def page_move_cmd(
168
+ page_id: str = typer.Argument(help="Page ID to move"),
169
+ parent: str | None = typer.Option(None, "--parent", help="New parent page ID"),
170
+ space: str | None = typer.Option(None, "--space", help="Target space slug"),
171
+ position: str | None = typer.Option(None, "--position", help="Position among siblings"),
172
+ ) -> None:
173
+ """Move a page to a new location.
174
+
175
+ See also: page children (find targets), page list --tree (view hierarchy).
176
+ """
177
+ if parent is None and space is None and position is None:
178
+ print_error("At least one of --parent, --space, or --position is required.")
179
+
180
+ client = get_client()
181
+ target_space_id = None
182
+ if space is not None:
183
+ target_space_id = resolve_space_id(client, space)
184
+
185
+ move_page(
186
+ client,
187
+ page_id=page_id,
188
+ parent_page_id=parent,
189
+ space_id=target_space_id,
190
+ position=position,
191
+ )
192
+ print_result(page_id, f"Moved page '{page_id}'")
193
+
194
+
195
+ @page_app.command("list")
196
+ def page_list_cmd(
197
+ space_slug: str = typer.Argument(help="Space slug to list pages in"),
198
+ limit: int | None = typer.Option(None, "--limit", help="Max results (default: 50)"),
199
+ cursor: str | None = typer.Option(None, "--cursor", help="Pagination cursor"),
200
+ tree: bool = typer.Option(False, "--tree", help="Show as indented tree"),
201
+ json_mode: bool = typer.Option(False, "--json", help="Output as JSON array"),
202
+ ) -> None:
203
+ """List pages in a space.
204
+
205
+ See also: page children (list by parent), page get (single page).
206
+ """
207
+ client = get_client()
208
+ space_id = resolve_space_id(client, space_slug)
209
+
210
+ if tree:
211
+ pages = build_page_tree(client, space_id)
212
+ print_tree(pages)
213
+ return
214
+
215
+ result = list_recent_pages(client, space_id, limit=limit, cursor=cursor)
216
+ items = extract_items(result)
217
+ columns = ["id", "title", "icon", "updatedAt", "parentPageId"]
218
+ print_table(items, columns, json_mode=json_mode)
219
+
220
+
221
+ @page_app.command("get")
222
+ def page_get_cmd(
223
+ page_id: str = typer.Argument(help="Page ID to retrieve"),
224
+ raw: bool = typer.Option(False, "--raw", help="Output ProseMirror JSON instead of Markdown"),
225
+ meta: bool = typer.Option(False, "--meta", help="Prepend YAML frontmatter with metadata"),
226
+ ) -> None:
227
+ """Get page content as Markdown.
228
+
229
+ See also: page list --json (batch retrieval), page export (to file).
230
+ """
231
+ client = get_client()
232
+
233
+ if raw:
234
+ # Raw mode: reuse get_page_content which handles Enterprise/Community fallback
235
+ info = get_page_content(client, page_id)
236
+ pm_content = info.get("content")
237
+ if not pm_content:
238
+ print_error("No content available for raw output.", exit_code=1)
239
+ sys.stdout.write(json.dumps(pm_content, indent=2) + "\n")
240
+ return
241
+
242
+ # Normal mode: get content and convert to Markdown
243
+ info = get_page_content(client, page_id)
244
+ pm_content = info.get("content")
245
+ if not pm_content:
246
+ print_error("Page has no content.", exit_code=1)
247
+
248
+ from docmost_cli.convert.prosemirror_to_md import convert_to_markdown
249
+
250
+ markdown = convert_to_markdown(pm_content)
251
+
252
+ if meta:
253
+ metadata = {
254
+ "id": info.get("id", ""),
255
+ "title": info.get("title", ""),
256
+ "parent_id": info.get("parentPageId", ""),
257
+ "space_id": info.get("spaceId", ""),
258
+ "created": info.get("createdAt", ""),
259
+ "updated": info.get("updatedAt", ""),
260
+ }
261
+ print_content_with_meta(markdown, metadata)
262
+ else:
263
+ print_content(markdown)
264
+
265
+
266
+ @page_app.command("duplicate")
267
+ def page_duplicate_cmd(
268
+ page_id: str = typer.Argument(help="Page ID to duplicate"),
269
+ ) -> None:
270
+ """Duplicate a page."""
271
+ client = get_client()
272
+ info = get_page_info(client, page_id)
273
+ page_title = info.get("title", page_id)
274
+ result = duplicate_page(client, page_id)
275
+ new_id = extract_id(result)
276
+ print_result(new_id, f"Duplicated page '{page_title}'")
277
+
278
+
279
+ @page_app.command("copy")
280
+ def page_copy_cmd(
281
+ page_id: str = typer.Argument(help="Page ID to copy"),
282
+ space: str = typer.Option(..., "--space", help="Target space slug (required)"),
283
+ ) -> None:
284
+ """Copy a page to a different space."""
285
+ client = get_client()
286
+ info = get_page_info(client, page_id)
287
+ page_title = info.get("title", page_id)
288
+ target_space_id = resolve_space_id(client, space)
289
+ result = copy_page(client, page_id, target_space_id)
290
+ new_id = extract_id(result)
291
+ print_result(new_id, f"Copied page '{page_title}' to space '{space}'")
292
+
293
+
294
+ @page_app.command("children")
295
+ def page_children_cmd(
296
+ page_id: str = typer.Argument(help="Page ID to list children for"),
297
+ json_mode: bool = typer.Option(False, "--json", help="Output as JSON array"),
298
+ ) -> None:
299
+ """List child pages of a parent.
300
+
301
+ See also: page list --tree (full hierarchy), page move (reposition).
302
+ """
303
+ client = get_client()
304
+ result = get_page_children(client, page_id)
305
+ items = extract_items(result)
306
+ columns = ["id", "title", "icon", "updatedAt"]
307
+ print_table(items, columns, json_mode=json_mode)
308
+
309
+
310
+ @page_app.command("history")
311
+ def page_history_cmd(
312
+ page_id: str = typer.Argument(help="Page ID to show history for"),
313
+ limit: int | None = typer.Option(None, "--limit", help="Max results"),
314
+ json_mode: bool = typer.Option(False, "--json", help="Output as JSON array"),
315
+ ) -> None:
316
+ """Show page version history."""
317
+ client = get_client()
318
+ result = get_page_history(client, page_id, limit=limit)
319
+ items = extract_items(result)
320
+ columns = ["id", "creatorId", "createdAt"]
321
+ print_table(items, columns, json_mode=json_mode)
322
+
323
+
324
+ @page_app.command("export")
325
+ def page_export_cmd(
326
+ page_id: str = typer.Argument(help="Page ID to export"),
327
+ fmt: str = typer.Option("md", "--format", help="Export format: md or html"),
328
+ output: Path | None = typer.Option(None, "--output", help="Write to file instead of stdout"),
329
+ ) -> None:
330
+ """Export page content."""
331
+ client = get_client()
332
+ content = export_page(client, page_id, fmt=fmt)
333
+
334
+ if output:
335
+ if output.exists() and not state.yes:
336
+ typer.confirm(f"File '{output}' already exists. Overwrite?", abort=True)
337
+ output.write_text(str(content), encoding="utf-8")
338
+ from rich.console import Console
339
+
340
+ Console(stderr=True).print(f"Exported to {output}")
341
+ else:
342
+ print_content(str(content))
343
+
344
+
345
+ @page_app.command("import")
346
+ def page_import_cmd(
347
+ space_slug: str = typer.Argument(help="Space slug to import into"),
348
+ file: Path = typer.Option(..., "--file", help="Markdown or HTML file to import"),
349
+ title: str | None = typer.Option(None, "--title", help="Override page title"),
350
+ parent: str | None = typer.Option(None, "--parent", help="Parent page ID"),
351
+ ) -> None:
352
+ """Import a file as a new page."""
353
+ if not file.exists():
354
+ print_error(f"File not found: {file}")
355
+
356
+ client = get_client()
357
+ space_id = resolve_space_id(client, space_slug)
358
+
359
+ # Read file once
360
+ file_bytes = file.read_bytes()
361
+ file_text = file_bytes.decode("utf-8", errors="replace")
362
+
363
+ # Auto-detect title: flag > H1 in file > filename stem
364
+ detected_title = title
365
+ if not detected_title:
366
+ for line in file_text.split("\n"):
367
+ stripped = line.strip()
368
+ if stripped.startswith("# ") and not stripped.startswith("## "):
369
+ detected_title = stripped[2:].strip()
370
+ break
371
+ if not detected_title:
372
+ detected_title = file.stem
373
+
374
+ result = import_page(
375
+ client,
376
+ space_id=space_id,
377
+ file_name=file.name,
378
+ file_bytes=file_bytes,
379
+ parent_page_id=parent,
380
+ )
381
+ new_id = extract_id(result)
382
+ print_result(new_id, f"Imported '{detected_title}' from {file.name}")
@@ -0,0 +1,33 @@
1
+ """Search subcommand."""
2
+
3
+ import typer
4
+
5
+ from docmost_cli.api.pagination import extract_items
6
+ from docmost_cli.api.search import search
7
+ from docmost_cli.api.spaces import resolve_space_id
8
+ from docmost_cli.cli.main import get_client
9
+ from docmost_cli.output.formatter import print_table
10
+
11
+ __all__ = ["search_app"]
12
+
13
+ search_app = typer.Typer(name="search", help="Search across the wiki.")
14
+
15
+
16
+ @search_app.command("query")
17
+ def search_cmd(
18
+ query: str = typer.Argument(help="Search query"),
19
+ space: str | None = typer.Option(None, "--space", help="Filter by space slug"),
20
+ limit: int | None = typer.Option(None, "--limit", help="Max results (default: 20)"),
21
+ type_filter: str | None = typer.Option(None, "--type", help="Filter: page or attachment"),
22
+ json_mode: bool = typer.Option(False, "--json", help="Output as JSON array"),
23
+ ) -> None:
24
+ """Full-text search across the wiki."""
25
+ client = get_client()
26
+ space_id = None
27
+ if space is not None:
28
+ space_id = resolve_space_id(client, space)
29
+
30
+ result = search(client, query, space_id=space_id, result_type=type_filter, limit=limit)
31
+ items = extract_items(result)
32
+ columns = ["id", "title", "highlight"]
33
+ print_table(items, columns, json_mode=json_mode)
@@ -0,0 +1,57 @@
1
+ """Space subcommands."""
2
+
3
+ import typer
4
+
5
+ from docmost_cli.api.pagination import extract_id, extract_items
6
+ from docmost_cli.api.spaces import (
7
+ create_space,
8
+ list_spaces,
9
+ resolve_space_id,
10
+ update_space,
11
+ )
12
+ from docmost_cli.cli.main import get_client
13
+ from docmost_cli.output.formatter import print_error, print_result, print_table
14
+
15
+ __all__ = ["space_app"]
16
+
17
+ space_app = typer.Typer(name="space", help="Space operations.")
18
+
19
+
20
+ @space_app.command("list")
21
+ def space_list_cmd(
22
+ json_mode: bool = typer.Option(False, "--json", help="Output as JSON array"),
23
+ ) -> None:
24
+ """List all spaces."""
25
+ client = get_client()
26
+ result = list_spaces(client)
27
+ items = extract_items(result)
28
+ columns = ["id", "name", "slug", "description"]
29
+ print_table(items, columns, json_mode=json_mode)
30
+
31
+
32
+ @space_app.command("create")
33
+ def space_create_cmd(
34
+ name: str = typer.Option(..., "--name", help="Space name (required)"),
35
+ slug: str | None = typer.Option(None, "--slug", help="Space slug (auto-generated if omitted)"),
36
+ description: str | None = typer.Option(None, "--description", help="Space description"),
37
+ ) -> None:
38
+ """Create a new space."""
39
+ client = get_client()
40
+ result = create_space(client, name=name, slug=slug, description=description)
41
+ space_id = extract_id(result)
42
+ print_result(space_id, f"Created space '{name}'")
43
+
44
+
45
+ @space_app.command("update")
46
+ def space_update_cmd(
47
+ space_slug: str = typer.Argument(help="Space slug to update"),
48
+ name: str | None = typer.Option(None, "--name", help="New space name"),
49
+ description: str | None = typer.Option(None, "--description", help="New description"),
50
+ ) -> None:
51
+ """Update an existing space."""
52
+ if name is None and description is None:
53
+ print_error("At least one of --name or --description is required.")
54
+ client = get_client()
55
+ space_id = resolve_space_id(client, space_slug)
56
+ update_space(client, space_id=space_id, name=name, description=description)
57
+ print_result(space_id, f"Updated space '{space_slug}'")
@@ -0,0 +1,122 @@
1
+ """Sync subcommands."""
2
+
3
+ from pathlib import Path
4
+
5
+ import typer
6
+
7
+ from docmost_cli.cli.main import get_client, state
8
+ from docmost_cli.output.formatter import print_error
9
+
10
+ __all__ = ["sync_app"]
11
+
12
+ sync_app = typer.Typer(name="sync", help="Sync space pages to/from local directory.")
13
+
14
+
15
+ @sync_app.command("pull")
16
+ def sync_pull_cmd(
17
+ space_slug: str = typer.Argument(help="Space slug to pull pages from"),
18
+ dir_path: Path = typer.Option(
19
+ None, "--dir", help="Target directory (default: ./<space-slug>/)"
20
+ ),
21
+ force: bool = typer.Option(False, "--force", help="Overwrite local changes without warning"),
22
+ ) -> None:
23
+ """Download all pages from a space to local Markdown files.
24
+
25
+ Creates a directory with one .md file per page (with YAML frontmatter)
26
+ and a .docmost-manifest.json tracking sync state.
27
+
28
+ See also: sync push (upload changes), sync status (show changes).
29
+ """
30
+ from docmost_cli.sync.pull import pull_space
31
+
32
+ client = get_client()
33
+ target = dir_path or Path(space_slug)
34
+ pull_space(client, space_slug, target, force=force)
35
+
36
+
37
+ @sync_app.command("status")
38
+ def sync_status_cmd(
39
+ space_slug: str = typer.Argument(help="Space slug to check"),
40
+ dir_path: Path = typer.Option(
41
+ None, "--dir", help="Directory to check (default: ./<space-slug>/)"
42
+ ),
43
+ ) -> None:
44
+ """Show changes between local files and last-pulled state.
45
+
46
+ See also: sync push (upload changes), sync pull (download from server).
47
+ """
48
+ import sys
49
+
50
+ from docmost_cli.sync.diff import compute_diff
51
+ from docmost_cli.sync.manifest import load_manifest
52
+
53
+ target = dir_path or Path(space_slug)
54
+ manifest = load_manifest(target)
55
+ if manifest is None:
56
+ print_error(f"No manifest found in '{target}'. Run 'sync pull' first.")
57
+ return # unreachable (print_error exits), but makes control flow explicit
58
+
59
+ diff = compute_diff(manifest, target)
60
+
61
+ if not diff.has_changes:
62
+ sys.stdout.write("No changes.\n")
63
+ return
64
+
65
+ if diff.new:
66
+ sys.stdout.write(f" New: {len(diff.new)} file(s)\n")
67
+ for c in diff.new:
68
+ sys.stdout.write(f" + {c.filename}\n")
69
+ if diff.modified:
70
+ sys.stdout.write(f" Modified: {len(diff.modified)} file(s)\n")
71
+ for c in diff.modified:
72
+ types = ", ".join(ct.value for ct in c.changes if ct.name != "MOVED")
73
+ sys.stdout.write(f" ~ {c.filename} ({types})\n")
74
+ if diff.moved:
75
+ move_only = [c for c in diff.moved if c not in diff.modified]
76
+ if move_only:
77
+ sys.stdout.write(f" Moved: {len(move_only)} file(s)\n")
78
+ for c in move_only:
79
+ sys.stdout.write(f" -> {c.filename}\n")
80
+ if diff.deleted:
81
+ sys.stdout.write(f" Deleted: {len(diff.deleted)} file(s)\n")
82
+ for c in diff.deleted:
83
+ entry = c.manifest_entry or {}
84
+ sys.stdout.write(f" - {entry.get('filename', '?')}\n")
85
+ sys.stdout.write(f" Unchanged: {diff.unchanged} file(s)\n")
86
+
87
+
88
+ @sync_app.command("push")
89
+ def sync_push_cmd(
90
+ space_slug: str = typer.Argument(help="Space slug to push changes to"),
91
+ dir_path: Path = typer.Option(
92
+ None, "--dir", help="Source directory (default: ./<space-slug>/)"
93
+ ),
94
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show changes without executing"),
95
+ delete: bool = typer.Option(False, "--delete", help="Delete server pages not found locally"),
96
+ ) -> None:
97
+ """Upload local changes to Docmost server.
98
+
99
+ Requires a prior 'sync pull' to establish the manifest.
100
+ Use --dry-run to preview changes before applying.
101
+
102
+ See also: sync status (preview changes), sync pull (download from server).
103
+ """
104
+ from docmost_cli.sync.diff import compute_diff
105
+ from docmost_cli.sync.manifest import load_manifest
106
+ from docmost_cli.sync.push import push_space
107
+
108
+ client = get_client()
109
+ target = dir_path or Path(space_slug)
110
+
111
+ # Pre-compute diff once — reused for confirmation prompt and push_space
112
+ pre_diff = None
113
+ if not dry_run and not state.yes:
114
+ manifest = load_manifest(target)
115
+ if manifest is None:
116
+ print_error(f"No manifest found in '{target}'. Run 'sync pull' first.")
117
+ return
118
+ pre_diff = compute_diff(manifest, target)
119
+ if pre_diff.has_changes:
120
+ typer.confirm("Push changes?", abort=True)
121
+
122
+ push_space(client, space_slug, target, dry_run=dry_run, delete=delete, diff=pre_diff)
@@ -0,0 +1,25 @@
1
+ """User subcommands."""
2
+
3
+ from typing import Any
4
+
5
+ import typer
6
+
7
+ from docmost_cli.api.users import get_current_user
8
+ from docmost_cli.cli.main import get_client
9
+ from docmost_cli.output.formatter import print_key_value
10
+
11
+ __all__ = ["user_app"]
12
+
13
+ user_app = typer.Typer(name="user", help="Current user info.")
14
+
15
+
16
+ @user_app.command("me")
17
+ def user_me_cmd() -> None:
18
+ """Show authenticated user info."""
19
+ client = get_client()
20
+ result = get_current_user(client)
21
+ display: dict[str, Any] = {}
22
+ for key in ["email", "name", "id", "role", "createdAt"]:
23
+ if key in result:
24
+ display[key] = result[key]
25
+ print_key_value(display)
@@ -0,0 +1,40 @@
1
+ """Workspace subcommands."""
2
+
3
+ from typing import Any
4
+
5
+ import typer
6
+
7
+ from docmost_cli.api.pagination import extract_items
8
+ from docmost_cli.api.workspace import get_workspace_info, list_workspace_members
9
+ from docmost_cli.cli.main import get_client
10
+ from docmost_cli.output.formatter import print_key_value, print_table
11
+
12
+ __all__ = ["workspace_app"]
13
+
14
+ workspace_app = typer.Typer(name="workspace", help="Workspace info.")
15
+
16
+
17
+ @workspace_app.command("info")
18
+ def workspace_info_cmd() -> None:
19
+ """Show workspace details."""
20
+ client = get_client()
21
+ result = get_workspace_info(client)
22
+ data = result.get("data", result)
23
+ display: dict[str, Any] = {}
24
+ for key in ["name", "id", "description", "memberCount", "createdAt"]:
25
+ if key in data:
26
+ display[key] = data[key]
27
+ print_key_value(display)
28
+
29
+
30
+ @workspace_app.command("members")
31
+ def workspace_members_cmd(
32
+ limit: int | None = typer.Option(None, "--limit", help="Max results"),
33
+ json_mode: bool = typer.Option(False, "--json", help="Output as JSON array"),
34
+ ) -> None:
35
+ """List workspace members."""
36
+ client = get_client()
37
+ result = list_workspace_members(client, limit=limit)
38
+ items = extract_items(result)
39
+ columns = ["id", "email", "name", "role"]
40
+ print_table(items, columns, json_mode=json_mode)
@@ -0,0 +1,23 @@
1
+ """Configuration management: settings model and config file I/O."""
2
+
3
+ from docmost_cli.config.settings import DocmostSettings
4
+ from docmost_cli.config.store import (
5
+ get_cache_dir,
6
+ get_config_path,
7
+ load_settings,
8
+ read_config,
9
+ read_profile,
10
+ set_config_value,
11
+ write_config,
12
+ )
13
+
14
+ __all__ = [
15
+ "DocmostSettings",
16
+ "get_cache_dir",
17
+ "get_config_path",
18
+ "load_settings",
19
+ "read_config",
20
+ "read_profile",
21
+ "set_config_value",
22
+ "write_config",
23
+ ]