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.
- docmost_cli/__init__.py +5 -0
- docmost_cli/__main__.py +18 -0
- docmost_cli/api/__init__.py +5 -0
- docmost_cli/api/attachments.py +30 -0
- docmost_cli/api/auth.py +202 -0
- docmost_cli/api/client.py +296 -0
- docmost_cli/api/comments.py +103 -0
- docmost_cli/api/pages.py +530 -0
- docmost_cli/api/pagination.py +94 -0
- docmost_cli/api/search.py +40 -0
- docmost_cli/api/spaces.py +141 -0
- docmost_cli/api/users.py +25 -0
- docmost_cli/api/workspace.py +43 -0
- docmost_cli/cli/__init__.py +3 -0
- docmost_cli/cli/attachment.py +30 -0
- docmost_cli/cli/comment.py +83 -0
- docmost_cli/cli/config_cmd.py +143 -0
- docmost_cli/cli/main.py +133 -0
- docmost_cli/cli/page.py +382 -0
- docmost_cli/cli/search.py +33 -0
- docmost_cli/cli/space.py +57 -0
- docmost_cli/cli/sync_cmd.py +122 -0
- docmost_cli/cli/user.py +25 -0
- docmost_cli/cli/workspace.py +40 -0
- docmost_cli/config/__init__.py +23 -0
- docmost_cli/config/settings.py +23 -0
- docmost_cli/config/store.py +160 -0
- docmost_cli/convert/__init__.py +3 -0
- docmost_cli/convert/prosemirror_to_md.py +300 -0
- docmost_cli/models/__init__.py +3 -0
- docmost_cli/models/common.py +3 -0
- docmost_cli/output/__init__.py +17 -0
- docmost_cli/output/formatter.py +85 -0
- docmost_cli/output/tree.py +66 -0
- docmost_cli/py.typed +0 -0
- docmost_cli/sync/__init__.py +57 -0
- docmost_cli/sync/diff.py +156 -0
- docmost_cli/sync/frontmatter.py +152 -0
- docmost_cli/sync/manifest.py +195 -0
- docmost_cli/sync/pull.py +158 -0
- docmost_cli/sync/push.py +374 -0
- docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-attachment.1 +57 -0
- docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-comment.1 +92 -0
- docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-config.1 +127 -0
- docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-page.1 +412 -0
- docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-search.1 +90 -0
- docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-space.1 +111 -0
- docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-sync.1 +206 -0
- docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-user.1 +39 -0
- docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli-workspace.1 +68 -0
- docmost_cli-0.4.0.data/data/share/man/man1/docmost-cli.1 +301 -0
- docmost_cli-0.4.0.dist-info/METADATA +241 -0
- docmost_cli-0.4.0.dist-info/RECORD +56 -0
- docmost_cli-0.4.0.dist-info/WHEEL +4 -0
- docmost_cli-0.4.0.dist-info/entry_points.txt +2 -0
- 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)
|
docmost_cli/api/users.py
ADDED
|
@@ -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,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}")
|
docmost_cli/cli/main.py
ADDED
|
@@ -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)
|