reviso-cli 0.1.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.
- reviso_cli/__init__.py +8 -0
- reviso_cli/__main__.py +6 -0
- reviso_cli/access.py +49 -0
- reviso_cli/cli/__init__.py +276 -0
- reviso_cli/cli/doctor.py +88 -0
- reviso_cli/cli/guest_comments.py +61 -0
- reviso_cli/cli/invites.py +72 -0
- reviso_cli/cli/versions.py +86 -0
- reviso_cli/client/__init__.py +231 -0
- reviso_cli/client/collab.py +39 -0
- reviso_cli/client/errors.py +28 -0
- reviso_cli/client/http.py +59 -0
- reviso_cli/client/participation.py +120 -0
- reviso_cli/command_journal.py +142 -0
- reviso_cli/document_route_keys.py +35 -0
- reviso_cli/document_search.py +82 -0
- reviso_cli/errors.py +6 -0
- reviso_cli/ids.py +17 -0
- reviso_cli/local.py +53 -0
- reviso_cli/origin.py +35 -0
- reviso_cli/paths.py +67 -0
- reviso_cli/scope.py +65 -0
- reviso_cli-0.1.0.dist-info/METADATA +146 -0
- reviso_cli-0.1.0.dist-info/RECORD +28 -0
- reviso_cli-0.1.0.dist-info/WHEEL +5 -0
- reviso_cli-0.1.0.dist-info/entry_points.txt +2 -0
- reviso_cli-0.1.0.dist-info/licenses/LICENSE +25 -0
- reviso_cli-0.1.0.dist-info/top_level.txt +1 -0
reviso_cli/__init__.py
ADDED
reviso_cli/__main__.py
ADDED
reviso_cli/access.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Access levels used by invites and share links.
|
|
2
|
+
|
|
3
|
+
The three levels below are the vocabulary a user types and sees. What each level
|
|
4
|
+
resolves to on the server is deliberately not represented here: the server owns
|
|
5
|
+
that mapping, and the client sends the level, not an expanded result.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
ACCESS_TO_ROLE = {
|
|
10
|
+
"view": "viewer",
|
|
11
|
+
"comment": "commenter",
|
|
12
|
+
"edit": "editor",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
VALID_ACCESS = tuple(ACCESS_TO_ROLE)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def normalize_access(access: str) -> str:
|
|
19
|
+
value = str(access or "comment").strip()
|
|
20
|
+
if value not in ACCESS_TO_ROLE:
|
|
21
|
+
raise ValueError("access must be view, comment, or edit")
|
|
22
|
+
return value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def role_for_access(access: str) -> str:
|
|
26
|
+
return ACCESS_TO_ROLE[normalize_access(access)]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def access_from_body(body: dict) -> str:
|
|
30
|
+
if str(body.get("role", "")).strip():
|
|
31
|
+
raise ValueError("role is no longer accepted; pass access")
|
|
32
|
+
return normalize_access(str(body.get("access") or "comment"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def invite_access_from_body(body: dict) -> str:
|
|
36
|
+
return access_from_body(body)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def invite_for_public(item: dict) -> dict:
|
|
40
|
+
result = {key: value for key, value in item.items() if key != "role"}
|
|
41
|
+
result["access"] = normalize_access(str(item.get("access") or "comment"))
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def invite_list_for_public(result: dict) -> dict:
|
|
46
|
+
return {
|
|
47
|
+
**result,
|
|
48
|
+
"invites": [invite_for_public(item) for item in result.get("invites", [])],
|
|
49
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Command line dispatcher. Every server call goes through RevisoClient."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .. import command_journal
|
|
10
|
+
from ..client import RevisoClient
|
|
11
|
+
from ..document_route_keys import public_document_path
|
|
12
|
+
from ..document_search import search_documents
|
|
13
|
+
from ..errors import ValidationError
|
|
14
|
+
from ..local import read_mapping, write_mapping # noqa: F401
|
|
15
|
+
from ..origin import DEFAULT_SERVER_URL
|
|
16
|
+
from ..paths import config_path, packet_path, read_json, write_json
|
|
17
|
+
from ..scope import resolve_agent_create_scope
|
|
18
|
+
from .doctor import doctor_report, print_doctor
|
|
19
|
+
from .guest_comments import add_guest_comment_commands
|
|
20
|
+
from .invites import add_invite_commands
|
|
21
|
+
from .versions import add_version_commands, format_file_arg
|
|
22
|
+
|
|
23
|
+
# Response blocks that are browser UI state. The human status line drops them;
|
|
24
|
+
# --json keeps them.
|
|
25
|
+
ACCESS_KEYS = ("effective_access", "security_policy")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _load_config() -> dict:
|
|
29
|
+
path = config_path()
|
|
30
|
+
if path.exists():
|
|
31
|
+
return read_json(path)
|
|
32
|
+
return {}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _server_url() -> str:
|
|
36
|
+
cfg = _load_config()
|
|
37
|
+
return os.environ.get("REVISO_SERVER",
|
|
38
|
+
cfg.get("server", DEFAULT_SERVER_URL))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _automation_key(cfg: dict) -> str:
|
|
42
|
+
return (os.environ.get("REVISO_AUTOMATION_KEY")
|
|
43
|
+
or cfg.get("automation_key", ""))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _client() -> RevisoClient:
|
|
47
|
+
return RevisoClient(_server_url(), _automation_key(_load_config()) or None)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_auth(args: argparse.Namespace) -> None:
|
|
51
|
+
if args.action == "login":
|
|
52
|
+
key = args.key or os.environ.get("REVISO_AUTOMATION_KEY", "")
|
|
53
|
+
server = args.server or os.environ.get("REVISO_SERVER",
|
|
54
|
+
DEFAULT_SERVER_URL)
|
|
55
|
+
if not key:
|
|
56
|
+
print("No automation key. Create one in Reviso under Settings -> "
|
|
57
|
+
"Integrations, then set REVISO_AUTOMATION_KEY or pass --key.")
|
|
58
|
+
return
|
|
59
|
+
write_json(config_path(), {"server": server, "automation_key": key})
|
|
60
|
+
print(f"Saved to {config_path()}")
|
|
61
|
+
elif args.action == "status":
|
|
62
|
+
cfg = _load_config()
|
|
63
|
+
print(f"server: {cfg.get('server', '(not set)')}")
|
|
64
|
+
print(f"key: {'set' if cfg.get('automation_key') else '(not set)'}")
|
|
65
|
+
print(f"config: {config_path()}")
|
|
66
|
+
else:
|
|
67
|
+
print(f"unknown auth action: {args.action}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def cmd_publish(args: argparse.Namespace) -> None:
|
|
71
|
+
path = Path(args.file)
|
|
72
|
+
scope = _workspace_scope(args.workspace_id or "", args.parent_id or "")
|
|
73
|
+
content = path.read_text()
|
|
74
|
+
fmt = format_file_arg(args, path)
|
|
75
|
+
title = args.title or path.name
|
|
76
|
+
# A journaled operation_id is reused across reruns of THIS command, so a
|
|
77
|
+
# crashed-then-rerun publish replays instead of double-creating. Cleared
|
|
78
|
+
# only after every local success step has completed.
|
|
79
|
+
parts = [title, content, fmt, args.description or "",
|
|
80
|
+
scope.workspace_id, scope.parent_id]
|
|
81
|
+
op = command_journal.operation_id_for("publish", parts)
|
|
82
|
+
result = _client().publish(title, content, fmt, args.description,
|
|
83
|
+
scope.workspace_id, scope.parent_id,
|
|
84
|
+
operation_id=op)
|
|
85
|
+
mapping = write_mapping(result, path)
|
|
86
|
+
print(mapping["url"])
|
|
87
|
+
command_journal.forget("publish", parts)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cmd_open(args: argparse.Namespace) -> None:
|
|
91
|
+
print(f"{_server_url()}{public_document_path(args.document_id)}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def cmd_status(args: argparse.Namespace) -> None:
|
|
95
|
+
payload = _client().status(args.document_id)
|
|
96
|
+
if args.json:
|
|
97
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
98
|
+
return
|
|
99
|
+
for key, value in payload.items():
|
|
100
|
+
if key in ACCESS_KEYS:
|
|
101
|
+
continue
|
|
102
|
+
print(f"{key}: {value}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def cmd_server_info(args: argparse.Namespace) -> None:
|
|
106
|
+
client = _client()
|
|
107
|
+
payload = {"health": client.health,
|
|
108
|
+
"ready": client.ready,
|
|
109
|
+
"version": client.version_info}[args.kind]()
|
|
110
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def cmd_workspaces(args: argparse.Namespace) -> None:
|
|
114
|
+
payload = _client().workspaces()
|
|
115
|
+
if args.json:
|
|
116
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
117
|
+
return
|
|
118
|
+
for ws in payload.get("workspaces", []):
|
|
119
|
+
print(f"{ws.get('workspace_id')}\t{ws.get('workspace_name')}\t"
|
|
120
|
+
f"[{ws.get('workspace_type')}]\t{ws.get('role')}")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def cmd_documents(args: argparse.Namespace) -> None:
|
|
124
|
+
payload = _client().dashboard(args.workspace_id or "")
|
|
125
|
+
if args.json:
|
|
126
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
127
|
+
return
|
|
128
|
+
for doc in payload.get("reviews", []):
|
|
129
|
+
status = doc.get("lifecycle_status") or ""
|
|
130
|
+
print(f"{doc.get('document_id')}\t{doc.get('title')}\t"
|
|
131
|
+
f"[{status}]\t{doc.get('unresolved_count')} open")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def cmd_search(args: argparse.Namespace) -> None:
|
|
135
|
+
try:
|
|
136
|
+
payload = search_documents(
|
|
137
|
+
_client(), args.query, workspace_id=args.workspace_id or "",
|
|
138
|
+
limit=args.limit, archived=args.archived,
|
|
139
|
+
include_content=args.include_content)
|
|
140
|
+
except ValidationError as exc:
|
|
141
|
+
raise SystemExit(str(exc)) from exc
|
|
142
|
+
if args.json:
|
|
143
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
144
|
+
return
|
|
145
|
+
for doc in payload.get("matches", []):
|
|
146
|
+
print(f"{doc.get('document_id')}\t{doc.get('title')}\t"
|
|
147
|
+
f"[{doc.get('match_field')}]\t{doc.get('snippet')}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def cmd_doctor(args: argparse.Namespace) -> None:
|
|
151
|
+
cfg = _load_config()
|
|
152
|
+
report = doctor_report(
|
|
153
|
+
_client(), _server_url(), _automation_key(cfg),
|
|
154
|
+
os.environ.get("REVISO_WORKSPACE_ID", cfg.get("workspace_id", "")))
|
|
155
|
+
if args.json:
|
|
156
|
+
print(json.dumps(report, indent=2, sort_keys=True))
|
|
157
|
+
return
|
|
158
|
+
print_doctor(report)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def cmd_packet(args: argparse.Namespace) -> None:
|
|
162
|
+
v = "2" if args.v2 else "3"
|
|
163
|
+
packet = _client().packet(args.document_id, args.thread_id, version=v)
|
|
164
|
+
pid = packet.get("document_id", args.document_id)
|
|
165
|
+
path = packet_path(f"pkt_{pid}", "json")
|
|
166
|
+
write_json(path, packet)
|
|
167
|
+
print(str(path))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _workspace_scope(explicit: str = "", parent_id: str = ""):
|
|
171
|
+
cfg = _load_config()
|
|
172
|
+
key = _automation_key(cfg)
|
|
173
|
+
return resolve_agent_create_scope(
|
|
174
|
+
explicit_workspace_id=explicit, explicit_parent_id=parent_id,
|
|
175
|
+
config=cfg, has_automation_key=key.startswith("rak_"))
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _add_basic(sub: argparse._SubParsersAction) -> None:
|
|
179
|
+
auth = sub.add_parser("auth")
|
|
180
|
+
auth.add_argument("action", nargs="?", default="login")
|
|
181
|
+
auth.add_argument("--server")
|
|
182
|
+
auth.add_argument("--key")
|
|
183
|
+
auth.set_defaults(func=cmd_auth)
|
|
184
|
+
pub = sub.add_parser("publish")
|
|
185
|
+
pub.add_argument("file")
|
|
186
|
+
pub.add_argument("--title")
|
|
187
|
+
pub.add_argument("--format", choices=["markdown", "md", "html"])
|
|
188
|
+
pub.add_argument("--description", default="Initial draft")
|
|
189
|
+
pub.add_argument("--workspace-id", default="")
|
|
190
|
+
pub.add_argument("--parent-id", default="")
|
|
191
|
+
pub.set_defaults(func=cmd_publish)
|
|
192
|
+
opn = sub.add_parser("open")
|
|
193
|
+
opn.add_argument("document_id")
|
|
194
|
+
opn.set_defaults(func=cmd_open)
|
|
195
|
+
st = sub.add_parser("status")
|
|
196
|
+
st.add_argument("document_id")
|
|
197
|
+
st.add_argument("--json", action="store_true")
|
|
198
|
+
st.set_defaults(func=cmd_status)
|
|
199
|
+
wss = sub.add_parser("workspaces")
|
|
200
|
+
wss.add_argument("--json", action="store_true")
|
|
201
|
+
wss.set_defaults(func=cmd_workspaces)
|
|
202
|
+
docs = sub.add_parser("documents")
|
|
203
|
+
docs.add_argument("--workspace-id", default="")
|
|
204
|
+
docs.add_argument("--json", action="store_true")
|
|
205
|
+
docs.set_defaults(func=cmd_documents)
|
|
206
|
+
search = sub.add_parser("search")
|
|
207
|
+
search.add_argument("query")
|
|
208
|
+
search.add_argument("--workspace-id", default="")
|
|
209
|
+
search.add_argument("--limit", type=int, default=10)
|
|
210
|
+
search.add_argument("--archived", action="store_true")
|
|
211
|
+
search.add_argument("--include-content", action="store_true")
|
|
212
|
+
search.add_argument("--json", action="store_true")
|
|
213
|
+
search.set_defaults(func=cmd_search)
|
|
214
|
+
doctor = sub.add_parser("doctor")
|
|
215
|
+
doctor.add_argument("--json", action="store_true")
|
|
216
|
+
doctor.set_defaults(func=cmd_doctor)
|
|
217
|
+
for name in ("health", "ready", "version"):
|
|
218
|
+
srv = sub.add_parser(name)
|
|
219
|
+
srv.set_defaults(func=cmd_server_info, kind=name)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _add_packet(sub: argparse._SubParsersAction) -> None:
|
|
223
|
+
pkt = sub.add_parser("packet")
|
|
224
|
+
pkt.add_argument("document_id")
|
|
225
|
+
pkt.add_argument("--format", choices=["json"], default="json")
|
|
226
|
+
pkt.add_argument("--thread-id", action="append")
|
|
227
|
+
pkt.add_argument("--v2", action="store_true",
|
|
228
|
+
help="Use legacy packet v2 with sections")
|
|
229
|
+
pkt.set_defaults(func=cmd_packet)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _add_review_io(sub: argparse._SubParsersAction) -> None:
|
|
233
|
+
exp = sub.add_parser("export")
|
|
234
|
+
exp.add_argument("document_id")
|
|
235
|
+
exp.add_argument("--output", default="")
|
|
236
|
+
exp.set_defaults(func=cmd_export)
|
|
237
|
+
imp = sub.add_parser("import")
|
|
238
|
+
imp.add_argument("file")
|
|
239
|
+
imp.add_argument("--workspace-id", default="")
|
|
240
|
+
imp.add_argument("--parent-id", default="")
|
|
241
|
+
imp.set_defaults(func=cmd_import)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def cmd_export(args: argparse.Namespace) -> None:
|
|
245
|
+
bundle = _client().export_review(args.document_id)
|
|
246
|
+
if args.output:
|
|
247
|
+
write_json(Path(args.output), bundle)
|
|
248
|
+
print(args.output)
|
|
249
|
+
return
|
|
250
|
+
print(json.dumps(bundle, indent=2, sort_keys=True))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def cmd_import(args: argparse.Namespace) -> None:
|
|
254
|
+
bundle = read_json(Path(args.file))
|
|
255
|
+
scope = _workspace_scope(args.workspace_id or "", args.parent_id or "")
|
|
256
|
+
result = _client().import_review(
|
|
257
|
+
bundle, scope.workspace_id, scope.parent_id)
|
|
258
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
262
|
+
parser = argparse.ArgumentParser(prog="reviso")
|
|
263
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
264
|
+
_add_basic(sub)
|
|
265
|
+
_add_packet(sub)
|
|
266
|
+
_add_review_io(sub)
|
|
267
|
+
add_guest_comment_commands(sub, _client)
|
|
268
|
+
add_invite_commands(sub, _client)
|
|
269
|
+
add_version_commands(sub, _client)
|
|
270
|
+
return parser
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def main(argv: list[str] | None = None) -> None:
|
|
274
|
+
parser = build_parser()
|
|
275
|
+
args = parser.parse_args(argv)
|
|
276
|
+
args.func(args)
|
reviso_cli/cli/doctor.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""CLI environment diagnostics."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.request
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def doctor_report(client, server_url: str, token: str,
|
|
9
|
+
workspace_id: str = "") -> dict:
|
|
10
|
+
checks = [
|
|
11
|
+
_static("server_config", bool(server_url), server_url or "not set"),
|
|
12
|
+
_static("auth_token", bool(token), _token_message(token)),
|
|
13
|
+
_call("health", client.health),
|
|
14
|
+
_call("ready", client.ready),
|
|
15
|
+
_call("version", client.version_info),
|
|
16
|
+
_call("workspaces", client.workspaces),
|
|
17
|
+
_mcp_check(server_url),
|
|
18
|
+
]
|
|
19
|
+
if workspace_id:
|
|
20
|
+
checks.append(_workspace_check(checks[-2], workspace_id))
|
|
21
|
+
return {"ok": all(check["status"] != "fail" for check in checks),
|
|
22
|
+
"checks": checks}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def print_doctor(report: dict) -> None:
|
|
26
|
+
for check in report["checks"]:
|
|
27
|
+
print(f"{check['status'].upper()}\t{check['name']}\t{check['message']}")
|
|
28
|
+
print("OK" if report["ok"] else "FAILED")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _static(name: str, ok: bool, message: str) -> dict:
|
|
32
|
+
return {"name": name, "status": "ok" if ok else "warn",
|
|
33
|
+
"message": message}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _call(name: str, func) -> dict:
|
|
37
|
+
try:
|
|
38
|
+
payload = func()
|
|
39
|
+
except SystemExit as exc:
|
|
40
|
+
return {"name": name, "status": "fail", "message": str(exc)}
|
|
41
|
+
except Exception as exc: # noqa: BLE001 - diagnostic boundary.
|
|
42
|
+
return {"name": name, "status": "fail", "message": str(exc)}
|
|
43
|
+
return {"name": name, "status": "ok", "message": _summary(payload),
|
|
44
|
+
"payload": payload}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _mcp_check(server_url: str) -> dict:
|
|
48
|
+
if not server_url:
|
|
49
|
+
return {"name": "mcp_endpoint", "status": "warn",
|
|
50
|
+
"message": "server not set"}
|
|
51
|
+
req = urllib.request.Request(
|
|
52
|
+
f"{server_url.rstrip('/')}/mcp",
|
|
53
|
+
headers={"Accept": "application/json"},
|
|
54
|
+
method="GET",
|
|
55
|
+
)
|
|
56
|
+
try:
|
|
57
|
+
urllib.request.urlopen(req, timeout=5).read()
|
|
58
|
+
except urllib.error.HTTPError as exc:
|
|
59
|
+
if exc.code == 405:
|
|
60
|
+
return {"name": "mcp_endpoint", "status": "ok",
|
|
61
|
+
"message": "reachable; POST JSON-RPC expected"}
|
|
62
|
+
return {"name": "mcp_endpoint", "status": "fail",
|
|
63
|
+
"message": f"HTTP {exc.code}"}
|
|
64
|
+
except Exception as exc: # noqa: BLE001 - diagnostic boundary.
|
|
65
|
+
return {"name": "mcp_endpoint", "status": "fail",
|
|
66
|
+
"message": str(exc)}
|
|
67
|
+
return {"name": "mcp_endpoint", "status": "ok", "message": "reachable"}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _workspace_check(workspaces: dict, workspace_id: str) -> dict:
|
|
71
|
+
rows = (workspaces.get("payload") or {}).get("workspaces") or []
|
|
72
|
+
found = any(row.get("workspace_id") == workspace_id for row in rows)
|
|
73
|
+
return {"name": "workspace_scope", "status": "ok" if found else "fail",
|
|
74
|
+
"message": workspace_id if found else f"{workspace_id} not visible"}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _token_message(token: str) -> str:
|
|
78
|
+
return "set" if token else "missing REVISO_AUTOMATION_KEY"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _summary(payload: dict) -> str:
|
|
82
|
+
if "workspaces" in payload:
|
|
83
|
+
return f"{len(payload['workspaces'])} workspaces"
|
|
84
|
+
if "version" in payload:
|
|
85
|
+
return str(payload["version"])
|
|
86
|
+
if "status" in payload:
|
|
87
|
+
return str(payload["status"])
|
|
88
|
+
return "ok"
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""CLI commands for guest comment workflows."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def add_guest_comment_commands(sub: argparse._SubParsersAction,
|
|
10
|
+
client_factory: Callable) -> None:
|
|
11
|
+
guest = sub.add_parser("guest")
|
|
12
|
+
guest.add_argument("document_id"); guest.add_argument("--name", required=True)
|
|
13
|
+
guest.add_argument("--email")
|
|
14
|
+
guest.set_defaults(func=lambda a: cmd_guest(a, client_factory))
|
|
15
|
+
comment = sub.add_parser("comment")
|
|
16
|
+
comment.add_argument("document_id")
|
|
17
|
+
comment.add_argument("--guest-id", default="")
|
|
18
|
+
comment.add_argument("--guest-token", default="")
|
|
19
|
+
comment.add_argument("--body", required=True)
|
|
20
|
+
comment.add_argument("--anchor-type", choices=["block", "selection"],
|
|
21
|
+
default="block")
|
|
22
|
+
comment.add_argument("--block-id", default="")
|
|
23
|
+
comment.add_argument("--quote", default="")
|
|
24
|
+
comment.set_defaults(func=lambda a: cmd_comment(a, client_factory))
|
|
25
|
+
reply = sub.add_parser("reply")
|
|
26
|
+
reply.add_argument("thread_id")
|
|
27
|
+
reply.add_argument("--guest-id", default="")
|
|
28
|
+
reply.add_argument("--guest-token", default="")
|
|
29
|
+
reply.add_argument("--body", required=True)
|
|
30
|
+
reply.set_defaults(func=lambda a: cmd_reply(a, client_factory))
|
|
31
|
+
resolve = sub.add_parser("resolve")
|
|
32
|
+
resolve.add_argument("thread_id")
|
|
33
|
+
resolve.add_argument("--guest-id", default="")
|
|
34
|
+
resolve.add_argument("--guest-token", default="")
|
|
35
|
+
resolve.add_argument("--reason", required=True)
|
|
36
|
+
resolve.set_defaults(func=lambda a: cmd_resolve(a, client_factory))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_guest(args: argparse.Namespace, client_factory: Callable) -> None:
|
|
40
|
+
result = client_factory().guest(args.document_id, args.name, args.email or None)
|
|
41
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cmd_comment(args: argparse.Namespace, client_factory: Callable) -> None:
|
|
45
|
+
anchor = {"type": args.anchor_type, "block_id": args.block_id,
|
|
46
|
+
"quote": args.quote}
|
|
47
|
+
result = client_factory().comment(args.document_id, args.guest_id,
|
|
48
|
+
args.guest_token, args.body, anchor)
|
|
49
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def cmd_reply(args: argparse.Namespace, client_factory: Callable) -> None:
|
|
53
|
+
result = client_factory().reply(args.thread_id, args.guest_id,
|
|
54
|
+
args.guest_token, args.body)
|
|
55
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cmd_resolve(args: argparse.Namespace, client_factory: Callable) -> None:
|
|
59
|
+
result = client_factory().resolve(args.thread_id, args.guest_id,
|
|
60
|
+
args.guest_token, args.reason)
|
|
61
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""CLI invite commands.
|
|
2
|
+
|
|
3
|
+
Anonymous share links are deliberately not part of this client. A share link
|
|
4
|
+
carries a bearer token in its URL, and creating one is a browser action where the
|
|
5
|
+
person handing it out can see who it is going to. Invites by email are here.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
from typing import Callable
|
|
12
|
+
|
|
13
|
+
from ..client import RevisoClient
|
|
14
|
+
from ..access import invite_access_from_body, invite_for_public, invite_list_for_public
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_invite_commands(sub: argparse._SubParsersAction,
|
|
18
|
+
client_factory: Callable[[], RevisoClient]) -> None:
|
|
19
|
+
invite = sub.add_parser("invite")
|
|
20
|
+
invite_sub = invite.add_subparsers(dest="invite_cmd", required=True)
|
|
21
|
+
create = invite_sub.add_parser("create")
|
|
22
|
+
create.add_argument("document_id")
|
|
23
|
+
create.add_argument("--email", required=True)
|
|
24
|
+
create.add_argument("--access", default="",
|
|
25
|
+
choices=["view", "comment", "edit"])
|
|
26
|
+
create.add_argument("--json", action="store_true")
|
|
27
|
+
create.set_defaults(func=lambda a: _create(a, client_factory))
|
|
28
|
+
list_cmd = invite_sub.add_parser("list")
|
|
29
|
+
list_cmd.add_argument("document_id")
|
|
30
|
+
list_cmd.add_argument("--json", action="store_true")
|
|
31
|
+
list_cmd.set_defaults(func=lambda a: _list(a, client_factory))
|
|
32
|
+
revoke = invite_sub.add_parser("revoke")
|
|
33
|
+
revoke.add_argument("invite_id")
|
|
34
|
+
revoke.add_argument("--json", action="store_true")
|
|
35
|
+
revoke.set_defaults(func=lambda a: _revoke(a, client_factory))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _create(args: argparse.Namespace,
|
|
39
|
+
client_factory: Callable[[], RevisoClient]) -> None:
|
|
40
|
+
body = {}
|
|
41
|
+
if args.access:
|
|
42
|
+
body["access"] = args.access
|
|
43
|
+
access = invite_access_from_body(body)
|
|
44
|
+
result = invite_for_public(
|
|
45
|
+
client_factory().create_invite(
|
|
46
|
+
args.document_id, args.email, access))
|
|
47
|
+
if args.json:
|
|
48
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
49
|
+
return
|
|
50
|
+
if result.get("existing"):
|
|
51
|
+
print(f"pending invite already exists: {result['invite_id']}")
|
|
52
|
+
else:
|
|
53
|
+
print(result.get("invite_url") or result["invite_id"])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _list(args: argparse.Namespace,
|
|
57
|
+
client_factory: Callable[[], RevisoClient]) -> None:
|
|
58
|
+
result = invite_list_for_public(client_factory().list_invites(args.document_id))
|
|
59
|
+
if args.json:
|
|
60
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
61
|
+
return
|
|
62
|
+
for item in result.get("invites", []):
|
|
63
|
+
print(f"{item['id']}\t{item['status']}\t{item['access']}\t{item['email']}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _revoke(args: argparse.Namespace,
|
|
67
|
+
client_factory: Callable[[], RevisoClient]) -> None:
|
|
68
|
+
result = client_factory().revoke_invite(args.invite_id)
|
|
69
|
+
if args.json:
|
|
70
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
71
|
+
else:
|
|
72
|
+
print(f"{result['id']}: {result['status']}")
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""V1.0 version/edit CLI commands."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .. import command_journal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def cmd_update(args: argparse.Namespace) -> None:
|
|
12
|
+
path = Path(args.file)
|
|
13
|
+
client = args.client_factory()
|
|
14
|
+
content = path.read_text()
|
|
15
|
+
fmt = format_file_arg(args, path)
|
|
16
|
+
# Journal key is USER inputs only -- NOT the dynamically resolved latest, so
|
|
17
|
+
# a rerun after a lost response does not drift onto the version the first run
|
|
18
|
+
# just committed. The concrete base resolved on first run is replayed via the
|
|
19
|
+
# stored request, keeping operation_id + base_version_id in lockstep (P1c).
|
|
20
|
+
parts = [args.document_id, content, args.description or "", fmt,
|
|
21
|
+
args.agent_name or "", args.base_version_id or ""]
|
|
22
|
+
|
|
23
|
+
def resolve() -> dict:
|
|
24
|
+
base = (args.base_version_id
|
|
25
|
+
or client.latest_content(args.document_id)["version_id"])
|
|
26
|
+
return {"base_version_id": base}
|
|
27
|
+
|
|
28
|
+
entry = command_journal.reserve("update", parts, resolve=resolve)
|
|
29
|
+
op = entry["operation_id"]
|
|
30
|
+
base = entry["request"]["base_version_id"]
|
|
31
|
+
result = client.update(args.document_id, content, base,
|
|
32
|
+
args.description, fmt,
|
|
33
|
+
args.agent_name, operation_id=op)
|
|
34
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
35
|
+
# Forget only after the local output step succeeded (see cmd_publish).
|
|
36
|
+
command_journal.forget("update", parts)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def cmd_versions(args: argparse.Namespace) -> None:
|
|
40
|
+
print(json.dumps(args.client_factory().versions(args.document_id),
|
|
41
|
+
indent=2, sort_keys=True))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cmd_diff(args: argparse.Namespace) -> None:
|
|
45
|
+
result = args.client_factory().diff(args.document_id, args.from_version_id,
|
|
46
|
+
args.to_version_id)
|
|
47
|
+
if args.json:
|
|
48
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
49
|
+
else:
|
|
50
|
+
print("\n".join(result.get("unified_diff", [])))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def cmd_rollback(args: argparse.Namespace) -> None:
|
|
54
|
+
client = args.client_factory()
|
|
55
|
+
base = args.base_version_id or client.latest_content(args.document_id)["version_id"]
|
|
56
|
+
result = client.rollback(args.document_id, args.target_version_id, base,
|
|
57
|
+
args.description)
|
|
58
|
+
print(json.dumps(result, indent=2, sort_keys=True))
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def add_version_commands(sub: argparse._SubParsersAction, client_factory) -> None:
|
|
62
|
+
upd = sub.add_parser("update")
|
|
63
|
+
upd.add_argument("document_id"); upd.add_argument("file")
|
|
64
|
+
upd.add_argument("--description", required=True)
|
|
65
|
+
upd.add_argument("--base-version-id", help="Version to base the update on (CAS). Omit to auto-base on the latest version (last-writer-wins); pass it for documents others are editing.")
|
|
66
|
+
upd.add_argument("--format", choices=["markdown", "md", "html"])
|
|
67
|
+
upd.add_argument("--agent-name", default="Agent")
|
|
68
|
+
upd.set_defaults(func=cmd_update, client_factory=client_factory)
|
|
69
|
+
vers = sub.add_parser("versions")
|
|
70
|
+
vers.add_argument("document_id")
|
|
71
|
+
vers.set_defaults(func=cmd_versions, client_factory=client_factory)
|
|
72
|
+
diff = sub.add_parser("diff")
|
|
73
|
+
diff.add_argument("document_id"); diff.add_argument("from_version_id")
|
|
74
|
+
diff.add_argument("to_version_id"); diff.add_argument("--json", action="store_true")
|
|
75
|
+
diff.set_defaults(func=cmd_diff, client_factory=client_factory)
|
|
76
|
+
rb = sub.add_parser("rollback")
|
|
77
|
+
rb.add_argument("document_id"); rb.add_argument("target_version_id")
|
|
78
|
+
rb.add_argument("--description", required=True)
|
|
79
|
+
rb.add_argument("--base-version-id")
|
|
80
|
+
rb.set_defaults(func=cmd_rollback, client_factory=client_factory)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def format_file_arg(args: argparse.Namespace, path: Path) -> str:
|
|
84
|
+
if getattr(args, "format", None):
|
|
85
|
+
return "html" if args.format == "html" else "markdown"
|
|
86
|
+
return "html" if path.suffix.lower() in (".html", ".htm") else "markdown"
|