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,141 @@
1
+ """Space API methods."""
2
+
3
+ from typing import Any
4
+
5
+ from docmost_cli.api.client import DocmostClient
6
+ from docmost_cli.api.pagination import build_body
7
+ from docmost_cli.output.formatter import print_error
8
+
9
+ __all__ = [
10
+ "create_space",
11
+ "get_space_info",
12
+ "list_spaces",
13
+ "resolve_space_id",
14
+ "update_space",
15
+ ]
16
+
17
+
18
+ def list_spaces(
19
+ client: DocmostClient,
20
+ *,
21
+ limit: int | None = None,
22
+ cursor: str | None = None,
23
+ ) -> dict[str, Any]:
24
+ """List spaces with optional pagination.
25
+
26
+ Args:
27
+ client: Authenticated Docmost client.
28
+ limit: Max results to return.
29
+ cursor: Pagination cursor.
30
+
31
+ Returns:
32
+ Raw API response dict.
33
+ """
34
+ body = build_body({}, limit=limit, cursor=cursor)
35
+ return client.post("/spaces", json=body)
36
+
37
+
38
+ def get_space_info(
39
+ client: DocmostClient,
40
+ *,
41
+ slug: str | None = None,
42
+ space_id: str | None = None,
43
+ ) -> dict[str, Any]:
44
+ """Get space info by slug or ID.
45
+
46
+ Args:
47
+ client: Authenticated Docmost client.
48
+ slug: Space slug (e.g., "engineering").
49
+ space_id: Space UUID.
50
+
51
+ Returns:
52
+ Space info dict.
53
+ """
54
+ if space_id:
55
+ result = client.post("/spaces/info", json={"spaceId": space_id})
56
+ return result.get("data", result)
57
+ if slug:
58
+ # /spaces/info only accepts spaceId, so search by slug in the full list
59
+ return _find_space_by_slug(client, slug)
60
+ print_error("Either slug or space_id is required.", exit_code=1)
61
+
62
+
63
+ def _find_space_by_slug(client: DocmostClient, slug: str) -> dict[str, Any]:
64
+ """Find a space by slug from the spaces list.
65
+
66
+ Args:
67
+ client: Authenticated Docmost client.
68
+ slug: Space slug to find.
69
+
70
+ Returns:
71
+ Space info dict.
72
+ """
73
+ from docmost_cli.api.pagination import extract_items
74
+
75
+ result = list_spaces(client)
76
+ items = extract_items(result)
77
+ for space in items:
78
+ if space.get("slug") == slug:
79
+ return space
80
+ print_error(f"Space '{slug}' not found.", exit_code=4)
81
+
82
+
83
+ def resolve_space_id(client: DocmostClient, slug: str) -> str:
84
+ """Resolve a space slug to its UUID.
85
+
86
+ Args:
87
+ client: Authenticated Docmost client.
88
+ slug: Space slug.
89
+
90
+ Returns:
91
+ Space UUID string.
92
+ """
93
+ info = get_space_info(client, slug=slug)
94
+ space_id = info.get("id")
95
+ if not space_id:
96
+ print_error(f"Space '{slug}' not found.", exit_code=4)
97
+ return space_id
98
+
99
+
100
+ def create_space(
101
+ client: DocmostClient,
102
+ *,
103
+ name: str,
104
+ slug: str | None = None,
105
+ description: str | None = None,
106
+ ) -> dict[str, Any]:
107
+ """Create a new space.
108
+
109
+ Args:
110
+ client: Authenticated Docmost client.
111
+ name: Space name.
112
+ slug: Space slug (auto-generated if omitted).
113
+ description: Space description.
114
+
115
+ Returns:
116
+ Raw API response dict.
117
+ """
118
+ body = build_body({"name": name}, slug=slug, description=description)
119
+ return client.post("/spaces/create", json=body)
120
+
121
+
122
+ def update_space(
123
+ client: DocmostClient,
124
+ *,
125
+ space_id: str,
126
+ name: str | None = None,
127
+ description: str | None = None,
128
+ ) -> dict[str, Any]:
129
+ """Update an existing space.
130
+
131
+ Args:
132
+ client: Authenticated Docmost client.
133
+ space_id: Space UUID.
134
+ name: New space name.
135
+ description: New description.
136
+
137
+ Returns:
138
+ Raw API response dict.
139
+ """
140
+ body = build_body({"spaceId": space_id}, name=name, description=description)
141
+ return client.post("/spaces/update", json=body)
@@ -0,0 +1,25 @@
1
+ """User API methods."""
2
+
3
+ from typing import Any
4
+
5
+ from docmost_cli.api.client import DocmostClient
6
+
7
+ __all__ = [
8
+ "get_current_user",
9
+ ]
10
+
11
+
12
+ def get_current_user(client: DocmostClient) -> dict[str, Any]:
13
+ """Get the currently authenticated user's info.
14
+
15
+ Args:
16
+ client: Authenticated Docmost client.
17
+
18
+ Returns:
19
+ Unwrapped user info dict.
20
+ """
21
+ result = client.post("/users/me", json={})
22
+ data = result.get("data", result)
23
+ if "user" in data and isinstance(data["user"], dict):
24
+ return data["user"]
25
+ return data
@@ -0,0 +1,43 @@
1
+ """Workspace API methods."""
2
+
3
+ from typing import Any
4
+
5
+ from docmost_cli.api.client import DocmostClient
6
+ from docmost_cli.api.pagination import build_body
7
+
8
+ __all__ = [
9
+ "get_workspace_info",
10
+ "list_workspace_members",
11
+ ]
12
+
13
+
14
+ def get_workspace_info(client: DocmostClient) -> dict[str, Any]:
15
+ """Get workspace information.
16
+
17
+ Args:
18
+ client: Authenticated Docmost client.
19
+
20
+ Returns:
21
+ Raw API response dict with workspace details.
22
+ """
23
+ return client.post("/workspace/info", json={})
24
+
25
+
26
+ def list_workspace_members(
27
+ client: DocmostClient,
28
+ *,
29
+ limit: int | None = None,
30
+ cursor: str | None = None,
31
+ ) -> dict[str, Any]:
32
+ """List workspace members with optional pagination.
33
+
34
+ Args:
35
+ client: Authenticated Docmost client.
36
+ limit: Max results to return.
37
+ cursor: Pagination cursor.
38
+
39
+ Returns:
40
+ Raw API response dict with members list.
41
+ """
42
+ body = build_body({}, limit=limit, cursor=cursor)
43
+ return client.post("/workspace/members", json=body)
@@ -0,0 +1,3 @@
1
+ """CLI command modules."""
2
+
3
+ __all__: list[str] = []
@@ -0,0 +1,30 @@
1
+ """Attachment subcommands."""
2
+
3
+ import typer
4
+
5
+ from docmost_cli.api.attachments import search_attachments
6
+ from docmost_cli.api.pagination import extract_items
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__ = ["attachment_app"]
12
+
13
+ attachment_app = typer.Typer(name="attachment", help="Attachment operations.")
14
+
15
+
16
+ @attachment_app.command("search")
17
+ def attachment_search_cmd(
18
+ query: str = typer.Argument(..., help="Search query string"),
19
+ space: str | None = typer.Option(None, "--space", help="Space slug to scope search"),
20
+ json_mode: bool = typer.Option(False, "--json", help="Output as JSON array"),
21
+ ) -> None:
22
+ """Search attachments."""
23
+ client = get_client()
24
+ space_id = None
25
+ if space:
26
+ space_id = resolve_space_id(client, space)
27
+ result = search_attachments(client, query, space_id=space_id)
28
+ items = extract_items(result)
29
+ columns = ["id", "fileName", "type"]
30
+ print_table(items, columns, json_mode=json_mode)
@@ -0,0 +1,83 @@
1
+ """Comment subcommands."""
2
+
3
+ from typing import Any
4
+
5
+ import typer
6
+
7
+ from docmost_cli.api.comments import create_comment, list_comments, update_comment
8
+ from docmost_cli.api.pagination import extract_id, extract_items
9
+ from docmost_cli.cli.main import get_client
10
+ from docmost_cli.output.formatter import print_result, print_table
11
+
12
+ __all__ = ["comment_app"]
13
+
14
+ comment_app = typer.Typer(name="comment", help="Comment operations.")
15
+
16
+
17
+ def _extract_text_from_prosemirror(doc: dict[str, Any]) -> str:
18
+ """Extract plain text from a ProseMirror document for display.
19
+
20
+ Args:
21
+ doc: ProseMirror document dict.
22
+
23
+ Returns:
24
+ Plain text string, truncated to ~100 chars.
25
+ """
26
+ texts: list[str] = []
27
+
28
+ def walk(node: dict[str, Any]) -> None:
29
+ if node.get("type") == "text":
30
+ texts.append(node.get("text", ""))
31
+ for child in node.get("content", []):
32
+ if isinstance(child, dict):
33
+ walk(child)
34
+
35
+ walk(doc)
36
+ full = " ".join(texts)
37
+ if len(full) > 100:
38
+ return full[:97] + "..."
39
+ return full
40
+
41
+
42
+ @comment_app.command("list")
43
+ def comment_list_cmd(
44
+ page_id: str = typer.Argument(help="Page ID to list comments for"),
45
+ json_mode: bool = typer.Option(False, "--json", help="Output as JSON array"),
46
+ ) -> None:
47
+ """List comments on a page."""
48
+ client = get_client()
49
+ result = list_comments(client, page_id)
50
+ items = extract_items(result)
51
+
52
+ # For table display, extract text from ProseMirror content
53
+ if not json_mode:
54
+ for item in items:
55
+ content = item.get("content")
56
+ if isinstance(content, dict):
57
+ item["content"] = _extract_text_from_prosemirror(content)
58
+
59
+ columns = ["id", "content", "creatorId", "createdAt"]
60
+ print_table(items, columns, json_mode=json_mode)
61
+
62
+
63
+ @comment_app.command("create")
64
+ def comment_create_cmd(
65
+ page_id: str = typer.Argument(help="Page ID to comment on"),
66
+ content: str = typer.Option(..., "--content", help="Comment text (required)"),
67
+ ) -> None:
68
+ """Add a comment to a page."""
69
+ client = get_client()
70
+ result = create_comment(client, page_id=page_id, content=content)
71
+ comment_id = extract_id(result)
72
+ print_result(comment_id, f"Created comment on page '{page_id}'")
73
+
74
+
75
+ @comment_app.command("update")
76
+ def comment_update_cmd(
77
+ comment_id: str = typer.Argument(help="Comment ID to update"),
78
+ content: str = typer.Option(..., "--content", help="New comment text (required)"),
79
+ ) -> None:
80
+ """Update an existing comment."""
81
+ client = get_client()
82
+ update_comment(client, comment_id=comment_id, content=content)
83
+ print_result(comment_id, f"Updated comment '{comment_id}'")
@@ -0,0 +1,143 @@
1
+ """Config management subcommands: init, show, set, test."""
2
+
3
+ import sys
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from docmost_cli.api.auth import AuthError
10
+ from docmost_cli.config.store import (
11
+ get_config_path,
12
+ read_config,
13
+ set_config_value,
14
+ write_config,
15
+ )
16
+ from docmost_cli.output.formatter import print_error
17
+
18
+ __all__ = ["config_app"]
19
+
20
+ config_app = typer.Typer(name="config", help="Manage configuration.")
21
+ _console = Console(stderr=True)
22
+
23
+
24
+ def _get_effective_config_path() -> str | None:
25
+ """Get the config path from global state (set via --config global option)."""
26
+ from docmost_cli.cli.main import state
27
+
28
+ return str(state.config_path) if state.config_path else None
29
+
30
+
31
+ def _mask(value: str) -> str:
32
+ """Mask a secret value, showing only the first 4 chars."""
33
+ if len(value) <= 4:
34
+ return "****"
35
+ return value[:4] + "*" * (len(value) - 4)
36
+
37
+
38
+ @config_app.command("init")
39
+ def config_init(
40
+ profile: str = typer.Option("default", "--profile", "-p", help="Profile to configure"),
41
+ ) -> None:
42
+ """Interactive setup wizard for Docmost CLI configuration."""
43
+ _console.print("[bold]Docmost CLI Configuration[/bold]\n")
44
+
45
+ url = typer.prompt("Docmost URL (e.g., https://docs.example.com)")
46
+ url = url.rstrip("/")
47
+
48
+ _console.print("\nAuthentication method:")
49
+ _console.print(" 1. API key (Enterprise edition)")
50
+ _console.print(" 2. Email + password (Community/AGPL edition)")
51
+ auth_choice = typer.prompt("Choose", type=int, default=1)
52
+
53
+ config_values: dict[str, str] = {"url": url}
54
+
55
+ if auth_choice == 1:
56
+ api_key = typer.prompt("API key")
57
+ config_values["api_key"] = api_key
58
+ else:
59
+ email = typer.prompt("Email")
60
+ password = typer.prompt("Password", hide_input=True)
61
+ config_values["email"] = email
62
+ config_values["password"] = password
63
+
64
+ path = get_config_path(_get_effective_config_path())
65
+ config = read_config(path)
66
+ config[profile] = config_values
67
+ write_config(config, path)
68
+
69
+ _console.print(f"\n[green]Configuration saved to {path}[/green]")
70
+ _console.print(f"Profile: [bold]{profile}[/bold]")
71
+ _console.print("\nRun [bold]docmost-cli config test[/bold] to verify connectivity.")
72
+
73
+
74
+ @config_app.command("show")
75
+ def config_show() -> None:
76
+ """Show current configuration (secrets are masked)."""
77
+ from docmost_cli.cli.main import state
78
+
79
+ if state.settings:
80
+ values: dict[str, str] = {
81
+ "url": state.settings.url or "",
82
+ "api_key": state.settings.api_key or "",
83
+ "email": state.settings.email or "",
84
+ "password": state.settings.password or "",
85
+ "profile": state.settings.profile,
86
+ }
87
+ else:
88
+ values = {"profile": "default"}
89
+
90
+ table = Table(title=f"Configuration — profile '{values.get('profile', 'default')}'")
91
+ table.add_column("Key", style="bold")
92
+ table.add_column("Value")
93
+
94
+ secret_keys = {"api_key", "password"}
95
+ for key, value in values.items():
96
+ if not value:
97
+ continue
98
+ display = _mask(value) if key in secret_keys else value
99
+ table.add_row(key, display)
100
+
101
+ console = Console()
102
+ console.print(table)
103
+
104
+
105
+ @config_app.command("set")
106
+ def config_set(
107
+ key: str = typer.Argument(help="Config key to set"),
108
+ value: str = typer.Argument(help="Value to set"),
109
+ profile: str = typer.Option("default", "--profile", "-p", help="Profile to update"),
110
+ ) -> None:
111
+ """Set a configuration value."""
112
+ valid_keys = {"url", "api_key", "email", "password"}
113
+ if key not in valid_keys:
114
+ print_error(f"Unknown config key '{key}'. Valid keys: {', '.join(sorted(valid_keys))}")
115
+
116
+ path = get_config_path(_get_effective_config_path())
117
+ set_config_value(key, value, profile, path)
118
+ _console.print(f"Set [bold]{key}[/bold] in profile '{profile}'")
119
+
120
+
121
+ @config_app.command("test")
122
+ def config_test() -> None:
123
+ """Test connectivity and authentication."""
124
+ from docmost_cli.cli.main import get_client
125
+
126
+ _console.print("Testing connection...\n")
127
+
128
+ try:
129
+ client = get_client()
130
+ except (AuthError, SystemExit) as exc:
131
+ print_error(f"Configuration error: {exc}", exit_code=3)
132
+
133
+ try:
134
+ from docmost_cli.api.users import get_current_user
135
+
136
+ result = get_current_user(client)
137
+ except SystemExit:
138
+ sys.exit(3)
139
+
140
+ name = result.get("name", result.get("email", "Unknown"))
141
+ _console.print("[green]Connected successfully![/green]")
142
+ _console.print(f"Authenticated as: [bold]{name}[/bold]")
143
+ _console.print(f"URL: {client._base_url}")
@@ -0,0 +1,133 @@
1
+ """Top-level typer app with global options and subcommand registration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ import typer
10
+
11
+ from docmost_cli.api.client import DocmostClient
12
+ from docmost_cli.config.store import load_settings
13
+ from docmost_cli.output.formatter import print_error
14
+
15
+ if TYPE_CHECKING:
16
+ from docmost_cli.config.settings import DocmostSettings
17
+
18
+ __all__ = ["_ensure_utf8_stdio", "app", "get_client", "state"]
19
+
20
+
21
+ class State:
22
+ """Global state shared across subcommands."""
23
+
24
+ def __init__(self) -> None:
25
+ self.settings: DocmostSettings | None = None
26
+ self.client: DocmostClient | None = None
27
+ self.config_path: Path | None = None
28
+ self.verbose: bool = False
29
+ self.yes: bool = False
30
+
31
+
32
+ state = State()
33
+
34
+ app = typer.Typer(
35
+ name="docmost-cli",
36
+ help="CLI tool for managing Docmost wiki instances.",
37
+ no_args_is_help=True,
38
+ )
39
+
40
+
41
+ def _ensure_utf8_stdio() -> None:
42
+ """Reconfigure stdout/stderr to UTF-8 on Windows.
43
+
44
+ Needed so emoji and Rich box-drawing chars work on cp1252 consoles.
45
+ Also called in __main__.py for 'python -m docmost_cli' invocation.
46
+ """
47
+ if sys.platform == "win32":
48
+ try:
49
+ sys.stdout.reconfigure(encoding="utf-8")
50
+ sys.stderr.reconfigure(encoding="utf-8")
51
+ except (AttributeError, ValueError):
52
+ pass
53
+
54
+
55
+ @app.callback()
56
+ def main(
57
+ profile: str = typer.Option("default", "--profile", "-p", help="Config profile name"),
58
+ url: str | None = typer.Option(None, "--url", help="Override Docmost URL"),
59
+ api_key: str | None = typer.Option(None, "--api-key", help="Override API key"),
60
+ yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompts"),
61
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Debug logging"),
62
+ config: str | None = typer.Option(None, "--config", help="Path to config file"),
63
+ ) -> None:
64
+ """Docmost CLI — manage your Docmost wiki from the terminal."""
65
+ _ensure_utf8_stdio()
66
+ cli_overrides: dict[str, str] = {}
67
+ if url is not None:
68
+ cli_overrides["url"] = url
69
+ if api_key is not None:
70
+ cli_overrides["api_key"] = api_key
71
+
72
+ state.config_path = Path(config) if config else None
73
+ state.settings = load_settings(
74
+ profile=profile,
75
+ config_path=state.config_path,
76
+ cli_overrides=cli_overrides if cli_overrides else None,
77
+ )
78
+ state.verbose = verbose
79
+ state.yes = yes
80
+
81
+
82
+ def get_client() -> DocmostClient:
83
+ """Get or create the DocmostClient.
84
+
85
+ Called by commands that need API access. Creates the client lazily
86
+ so commands like 'config show' don't require a valid API connection.
87
+
88
+ Returns:
89
+ A configured DocmostClient instance.
90
+ """
91
+ if state.client is None:
92
+ if state.settings is None:
93
+ print_error("Not configured. Run 'docmost-cli config init'.", exit_code=1)
94
+ state.client = DocmostClient(state.settings, verbose=state.verbose)
95
+ return state.client
96
+
97
+
98
+ # Register subcommand groups
99
+ from docmost_cli.cli.config_cmd import config_app # noqa: E402
100
+
101
+ app.add_typer(config_app)
102
+
103
+ from docmost_cli.cli.page import page_app # noqa: E402
104
+
105
+ app.add_typer(page_app)
106
+
107
+ from docmost_cli.cli.space import space_app # noqa: E402
108
+
109
+ app.add_typer(space_app)
110
+
111
+ from docmost_cli.cli.comment import comment_app # noqa: E402
112
+
113
+ app.add_typer(comment_app)
114
+
115
+ from docmost_cli.cli.search import search_app # noqa: E402
116
+
117
+ app.add_typer(search_app)
118
+
119
+ from docmost_cli.cli.attachment import attachment_app # noqa: E402
120
+
121
+ app.add_typer(attachment_app)
122
+
123
+ from docmost_cli.cli.workspace import workspace_app # noqa: E402
124
+
125
+ app.add_typer(workspace_app)
126
+
127
+ from docmost_cli.cli.user import user_app # noqa: E402
128
+
129
+ app.add_typer(user_app)
130
+
131
+ from docmost_cli.cli.sync_cmd import sync_app # noqa: E402
132
+
133
+ app.add_typer(sync_app)