reviso-cli 0.1.0__tar.gz
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-0.1.0/LICENSE +25 -0
- reviso_cli-0.1.0/PKG-INFO +146 -0
- reviso_cli-0.1.0/README.md +100 -0
- reviso_cli-0.1.0/pyproject.toml +39 -0
- reviso_cli-0.1.0/setup.cfg +4 -0
- reviso_cli-0.1.0/src/reviso_cli/__init__.py +8 -0
- reviso_cli-0.1.0/src/reviso_cli/__main__.py +6 -0
- reviso_cli-0.1.0/src/reviso_cli/access.py +49 -0
- reviso_cli-0.1.0/src/reviso_cli/cli/__init__.py +276 -0
- reviso_cli-0.1.0/src/reviso_cli/cli/doctor.py +88 -0
- reviso_cli-0.1.0/src/reviso_cli/cli/guest_comments.py +61 -0
- reviso_cli-0.1.0/src/reviso_cli/cli/invites.py +72 -0
- reviso_cli-0.1.0/src/reviso_cli/cli/versions.py +86 -0
- reviso_cli-0.1.0/src/reviso_cli/client/__init__.py +231 -0
- reviso_cli-0.1.0/src/reviso_cli/client/collab.py +39 -0
- reviso_cli-0.1.0/src/reviso_cli/client/errors.py +28 -0
- reviso_cli-0.1.0/src/reviso_cli/client/http.py +59 -0
- reviso_cli-0.1.0/src/reviso_cli/client/participation.py +120 -0
- reviso_cli-0.1.0/src/reviso_cli/command_journal.py +142 -0
- reviso_cli-0.1.0/src/reviso_cli/document_route_keys.py +35 -0
- reviso_cli-0.1.0/src/reviso_cli/document_search.py +82 -0
- reviso_cli-0.1.0/src/reviso_cli/errors.py +6 -0
- reviso_cli-0.1.0/src/reviso_cli/ids.py +17 -0
- reviso_cli-0.1.0/src/reviso_cli/local.py +53 -0
- reviso_cli-0.1.0/src/reviso_cli/origin.py +35 -0
- reviso_cli-0.1.0/src/reviso_cli/paths.py +67 -0
- reviso_cli-0.1.0/src/reviso_cli/scope.py +65 -0
- reviso_cli-0.1.0/src/reviso_cli.egg-info/PKG-INFO +146 -0
- reviso_cli-0.1.0/src/reviso_cli.egg-info/SOURCES.txt +32 -0
- reviso_cli-0.1.0/src/reviso_cli.egg-info/dependency_links.txt +1 -0
- reviso_cli-0.1.0/src/reviso_cli.egg-info/entry_points.txt +2 -0
- reviso_cli-0.1.0/src/reviso_cli.egg-info/requires.txt +3 -0
- reviso_cli-0.1.0/src/reviso_cli.egg-info/top_level.txt +1 -0
- reviso_cli-0.1.0/tests/test_client.py +98 -0
reviso_cli-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Reviso
|
|
4
|
+
|
|
5
|
+
This license applies to the contents of this repository: the Reviso command line
|
|
6
|
+
client. It does not apply to the Reviso product, its service, or its server
|
|
7
|
+
software, none of which are open source and none of which are licensed here.
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: reviso-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command line client for Reviso: publish drafts for human review, read reviewer comments back, and publish revisions safely.
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Reviso
|
|
8
|
+
|
|
9
|
+
This license applies to the contents of this repository: the Reviso command line
|
|
10
|
+
client. It does not apply to the Reviso product, its service, or its server
|
|
11
|
+
software, none of which are open source and none of which are licensed here.
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
|
|
31
|
+
Keywords: reviso,review,collaboration,documents,cli,agent
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Programming Language :: Python :: 3
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Topic :: Text Processing :: Markup
|
|
40
|
+
Requires-Python: >=3.11
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
License-File: LICENSE
|
|
43
|
+
Provides-Extra: test
|
|
44
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
# reviso-cli
|
|
48
|
+
|
|
49
|
+
Command line client for [Reviso](https://reviso.work), a content collaboration
|
|
50
|
+
system where agents publish Markdown and HTML drafts and a human team reviews
|
|
51
|
+
them.
|
|
52
|
+
|
|
53
|
+
This is the client half of Reviso. It talks to a Reviso server over HTTP and
|
|
54
|
+
contains no server code. It has **no runtime dependencies**: the transport is
|
|
55
|
+
`urllib`, so publishing a Markdown file does not pull a dependency graph into an
|
|
56
|
+
agent's environment.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
Not on PyPI yet, so install from source:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install git+https://github.com/reviso-si/reviso-cli
|
|
64
|
+
# or run it without installing
|
|
65
|
+
uvx --from git+https://github.com/reviso-si/reviso-cli reviso --help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Installs the `reviso` command. Once the package is published this becomes
|
|
69
|
+
`pip install reviso-cli`.
|
|
70
|
+
|
|
71
|
+
## Quick start
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
reviso auth login --server https://reviso.work
|
|
75
|
+
# set REVISO_AUTOMATION_KEY in the environment first, or pass --key
|
|
76
|
+
reviso doctor --json
|
|
77
|
+
|
|
78
|
+
reviso publish draft.md --title "Q3 launch plan" --description "Initial draft"
|
|
79
|
+
# prints the document URL
|
|
80
|
+
|
|
81
|
+
reviso documents --json # find the document id
|
|
82
|
+
reviso packet doc_abc123 --format json # reviewer comments and version metadata
|
|
83
|
+
reviso versions doc_abc123 # read the current version id
|
|
84
|
+
|
|
85
|
+
reviso update doc_abc123 revised.md \
|
|
86
|
+
--description "Address reviewer feedback" \
|
|
87
|
+
--base-version-id ver_xyz
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## The rule that matters
|
|
91
|
+
|
|
92
|
+
**Always pass `--base-version-id` when you revise a document.**
|
|
93
|
+
|
|
94
|
+
Omitting it is not a safe default. Without it the command re-bases onto whatever
|
|
95
|
+
version is newest and lets your write win, which silently discards a human's
|
|
96
|
+
concurrent edit. The safe shape is always two steps:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
reviso versions <document_id> # take the newest version_id
|
|
100
|
+
reviso update <document_id> file --description "..." --base-version-id <ver_...>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If the write is rejected as a conflict, re-read and re-apply your change on top of
|
|
104
|
+
the human's edit. Do not retry with the flag removed.
|
|
105
|
+
|
|
106
|
+
## Command groups
|
|
107
|
+
|
|
108
|
+
| Group | Commands |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| Connection | `auth`, `doctor`, `health`, `ready`, `version` |
|
|
111
|
+
| Documents | `publish`, `open`, `status`, `documents`, `search`, `packet` |
|
|
112
|
+
| Revisions | `versions`, `diff`, `update`, `rollback` |
|
|
113
|
+
| Reviewers | `invite`, `guest`, `comment`, `reply`, `resolve` |
|
|
114
|
+
| Data | `export`, `import`, `workspaces` |
|
|
115
|
+
|
|
116
|
+
## Deliberately not in this client
|
|
117
|
+
|
|
118
|
+
These are boundaries, not gaps to be filled later:
|
|
119
|
+
|
|
120
|
+
- **No server commands.** Starting a Reviso server is not part of the client.
|
|
121
|
+
- **No share-link creation.** A share link carries a bearer token in its URL, and
|
|
122
|
+
creating one is a browser action where the person handing it out can see who it
|
|
123
|
+
is going to. Invites by email are here instead.
|
|
124
|
+
- **No owner or operator surfaces.** Metrics, debug replay, and internal replay
|
|
125
|
+
endpoints are not exposed.
|
|
126
|
+
|
|
127
|
+
If you need MCP tools rather than a shell, the hosted connector is the better
|
|
128
|
+
path in most hosts. See [reviso-agent](https://github.com/reviso-si/reviso-agent)
|
|
129
|
+
for the agent skill.
|
|
130
|
+
|
|
131
|
+
## Known limitations
|
|
132
|
+
|
|
133
|
+
- `publish` prints only the document URL. It does not return the document id, so
|
|
134
|
+
use `reviso documents --json` to continue from the id.
|
|
135
|
+
- `update` and `rollback` accept an omitted base version and fall back to
|
|
136
|
+
last-writer-wins. Always pass the flag. A future release should make the safe
|
|
137
|
+
behaviour the default.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
|
|
141
|
+
MIT. See [LICENSE](LICENSE).
|
|
142
|
+
|
|
143
|
+
The Reviso product itself is **not** open source. This repository covers the
|
|
144
|
+
client only; it grants no rights to the service or its server software. See
|
|
145
|
+
[VISIBILITY.md](VISIBILITY.md) for the boundary this repository holds, and
|
|
146
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# reviso-cli
|
|
2
|
+
|
|
3
|
+
Command line client for [Reviso](https://reviso.work), a content collaboration
|
|
4
|
+
system where agents publish Markdown and HTML drafts and a human team reviews
|
|
5
|
+
them.
|
|
6
|
+
|
|
7
|
+
This is the client half of Reviso. It talks to a Reviso server over HTTP and
|
|
8
|
+
contains no server code. It has **no runtime dependencies**: the transport is
|
|
9
|
+
`urllib`, so publishing a Markdown file does not pull a dependency graph into an
|
|
10
|
+
agent's environment.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
Not on PyPI yet, so install from source:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install git+https://github.com/reviso-si/reviso-cli
|
|
18
|
+
# or run it without installing
|
|
19
|
+
uvx --from git+https://github.com/reviso-si/reviso-cli reviso --help
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Installs the `reviso` command. Once the package is published this becomes
|
|
23
|
+
`pip install reviso-cli`.
|
|
24
|
+
|
|
25
|
+
## Quick start
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
reviso auth login --server https://reviso.work
|
|
29
|
+
# set REVISO_AUTOMATION_KEY in the environment first, or pass --key
|
|
30
|
+
reviso doctor --json
|
|
31
|
+
|
|
32
|
+
reviso publish draft.md --title "Q3 launch plan" --description "Initial draft"
|
|
33
|
+
# prints the document URL
|
|
34
|
+
|
|
35
|
+
reviso documents --json # find the document id
|
|
36
|
+
reviso packet doc_abc123 --format json # reviewer comments and version metadata
|
|
37
|
+
reviso versions doc_abc123 # read the current version id
|
|
38
|
+
|
|
39
|
+
reviso update doc_abc123 revised.md \
|
|
40
|
+
--description "Address reviewer feedback" \
|
|
41
|
+
--base-version-id ver_xyz
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## The rule that matters
|
|
45
|
+
|
|
46
|
+
**Always pass `--base-version-id` when you revise a document.**
|
|
47
|
+
|
|
48
|
+
Omitting it is not a safe default. Without it the command re-bases onto whatever
|
|
49
|
+
version is newest and lets your write win, which silently discards a human's
|
|
50
|
+
concurrent edit. The safe shape is always two steps:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
reviso versions <document_id> # take the newest version_id
|
|
54
|
+
reviso update <document_id> file --description "..." --base-version-id <ver_...>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
If the write is rejected as a conflict, re-read and re-apply your change on top of
|
|
58
|
+
the human's edit. Do not retry with the flag removed.
|
|
59
|
+
|
|
60
|
+
## Command groups
|
|
61
|
+
|
|
62
|
+
| Group | Commands |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Connection | `auth`, `doctor`, `health`, `ready`, `version` |
|
|
65
|
+
| Documents | `publish`, `open`, `status`, `documents`, `search`, `packet` |
|
|
66
|
+
| Revisions | `versions`, `diff`, `update`, `rollback` |
|
|
67
|
+
| Reviewers | `invite`, `guest`, `comment`, `reply`, `resolve` |
|
|
68
|
+
| Data | `export`, `import`, `workspaces` |
|
|
69
|
+
|
|
70
|
+
## Deliberately not in this client
|
|
71
|
+
|
|
72
|
+
These are boundaries, not gaps to be filled later:
|
|
73
|
+
|
|
74
|
+
- **No server commands.** Starting a Reviso server is not part of the client.
|
|
75
|
+
- **No share-link creation.** A share link carries a bearer token in its URL, and
|
|
76
|
+
creating one is a browser action where the person handing it out can see who it
|
|
77
|
+
is going to. Invites by email are here instead.
|
|
78
|
+
- **No owner or operator surfaces.** Metrics, debug replay, and internal replay
|
|
79
|
+
endpoints are not exposed.
|
|
80
|
+
|
|
81
|
+
If you need MCP tools rather than a shell, the hosted connector is the better
|
|
82
|
+
path in most hosts. See [reviso-agent](https://github.com/reviso-si/reviso-agent)
|
|
83
|
+
for the agent skill.
|
|
84
|
+
|
|
85
|
+
## Known limitations
|
|
86
|
+
|
|
87
|
+
- `publish` prints only the document URL. It does not return the document id, so
|
|
88
|
+
use `reviso documents --json` to continue from the id.
|
|
89
|
+
- `update` and `rollback` accept an omitted base version and fall back to
|
|
90
|
+
last-writer-wins. Always pass the flag. A future release should make the safe
|
|
91
|
+
behaviour the default.
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
MIT. See [LICENSE](LICENSE).
|
|
96
|
+
|
|
97
|
+
The Reviso product itself is **not** open source. This repository covers the
|
|
98
|
+
client only; it grants no rights to the service or its server software. See
|
|
99
|
+
[VISIBILITY.md](VISIBILITY.md) for the boundary this repository holds, and
|
|
100
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "reviso-cli"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Command line client for Reviso: publish drafts for human review, read reviewer comments back, and publish revisions safely."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
keywords = ["reviso", "review", "collaboration", "documents", "cli", "agent"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Text Processing :: Markup",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# The client is stdlib-only on purpose. Installing it must not pull a dependency
|
|
25
|
+
# graph into an agent's environment to publish a Markdown file.
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = ["pytest>=8.0"]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
reviso = "reviso_cli.cli:main"
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
pythonpath = ["src"]
|
|
@@ -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)
|