nxplora 0.16.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.
- nxplora-0.16.0/.gitignore +7 -0
- nxplora-0.16.0/PKG-INFO +56 -0
- nxplora-0.16.0/README.md +46 -0
- nxplora-0.16.0/pyproject.toml +22 -0
- nxplora-0.16.0/scripts/check_wheel.py +52 -0
- nxplora-0.16.0/scripts/pty_proof.py +154 -0
- nxplora-0.16.0/src/nxplora_cli/__init__.py +1 -0
- nxplora-0.16.0/src/nxplora_cli/__main__.py +44 -0
- nxplora-0.16.0/src/nxplora_cli/app.py +422 -0
- nxplora-0.16.0/src/nxplora_cli/auth.py +151 -0
- nxplora-0.16.0/src/nxplora_cli/events.py +75 -0
- nxplora-0.16.0/src/nxplora_cli/opening.py +194 -0
- nxplora-0.16.0/src/nxplora_cli/transport.py +221 -0
- nxplora-0.16.0/tests/conftest.py +44 -0
- nxplora-0.16.0/tests/stub_server.py +124 -0
- nxplora-0.16.0/tests/test_opening_git_hardening.py +429 -0
- nxplora-0.16.0/tests/test_smoke.py +51 -0
- nxplora-0.16.0/tests/test_thin_client.py +332 -0
nxplora-0.16.0/PKG-INFO
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nxplora
|
|
3
|
+
Version: 0.16.0
|
|
4
|
+
Summary: NX in your terminal, on your Nexplora account.
|
|
5
|
+
License: Proprietary
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: certifi
|
|
8
|
+
Requires-Dist: textual<2,>=1.0
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# nx-cli
|
|
12
|
+
|
|
13
|
+
**Private source. Proprietary. Published to PyPI as `nxplora` only when ready.**
|
|
14
|
+
|
|
15
|
+
The NX terminal client: a thin client of a person's Nexplora account. Each message is a real run on the same engine the
|
|
16
|
+
chat uses, with the account's approvals and billing. It shows what really came back and nothing else.
|
|
17
|
+
|
|
18
|
+
## Charter
|
|
19
|
+
|
|
20
|
+
| nx-cli OWNS | nx-cli NEVER contains |
|
|
21
|
+
|---|---|
|
|
22
|
+
| Sign-in / enrollment with nx-cloud | Brain logic: routing, prompts, CodeChat harness, council, cognition router, brain core (these stay in private `Nexploraai/NX`) |
|
|
23
|
+
| Terminal UI, streaming, rendering | Model IDs, provider URLs or provider keys |
|
|
24
|
+
| Actions that must run on the operator's own machine (host, computer, phone) — only under nx-cloud-issued, approval-gated commands | Global Nexplora / NX secrets |
|
|
25
|
+
| Its own update channel | Any upload / publish path to a package index other than the gated release job |
|
|
26
|
+
|
|
27
|
+
Every published build must pass a check that none of the private NX modules are present in the wheel.
|
|
28
|
+
|
|
29
|
+
## What it does
|
|
30
|
+
|
|
31
|
+
- **In a Nexplora Code session's terminal** it is already signed in: the app writes a short-lived session ticket (bound to the
|
|
32
|
+
person, the Operation and that session, one hour, usable only for sending a message there) to a private file, and renews it
|
|
33
|
+
while the page holds the terminal. The person's login is never on that computer. The ticket can't approve anything.
|
|
34
|
+
- **On any other computer** `nx` shows a short link and a code. Approve it in the browser and it's done. The sign-in stays in
|
|
35
|
+
`~/.nx-cli/credentials.json` (0600). `nx logout` ends it. Work runs on the person's Nexplora cloud computer, not this one.
|
|
36
|
+
- **Approvals** are given in the chat. `nx` says what is waiting and where, then follows the run as it goes on.
|
|
37
|
+
- **Colours** are the terminal's own, with no painted background, so it matches a light or dark panel.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
python -m pip install .
|
|
41
|
+
nx
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Checks
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python -m pytest # includes a real local HTTP server (tests/stub_server.py)
|
|
48
|
+
python -m build --wheel && python scripts/check_wheel.py dist/*.whl # the charter's check; a release must pass it
|
|
49
|
+
python scripts/pty_proof.py "$(command -v nx)" /tmp/nx-proof # real pty, reads the screen, fails on any painted background
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## PyPI
|
|
53
|
+
|
|
54
|
+
The `nxplora` project name is owned and all historical releases are yanked (2026-09-29). Existing installs
|
|
55
|
+
run `nx upgrade` → `pip install -U nxplora`, so the first nx-cli release published under `nxplora` becomes
|
|
56
|
+
the migration path for those users. Never delete the PyPI project (name-takeover risk).
|
nxplora-0.16.0/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# nx-cli
|
|
2
|
+
|
|
3
|
+
**Private source. Proprietary. Published to PyPI as `nxplora` only when ready.**
|
|
4
|
+
|
|
5
|
+
The NX terminal client: a thin client of a person's Nexplora account. Each message is a real run on the same engine the
|
|
6
|
+
chat uses, with the account's approvals and billing. It shows what really came back and nothing else.
|
|
7
|
+
|
|
8
|
+
## Charter
|
|
9
|
+
|
|
10
|
+
| nx-cli OWNS | nx-cli NEVER contains |
|
|
11
|
+
|---|---|
|
|
12
|
+
| Sign-in / enrollment with nx-cloud | Brain logic: routing, prompts, CodeChat harness, council, cognition router, brain core (these stay in private `Nexploraai/NX`) |
|
|
13
|
+
| Terminal UI, streaming, rendering | Model IDs, provider URLs or provider keys |
|
|
14
|
+
| Actions that must run on the operator's own machine (host, computer, phone) — only under nx-cloud-issued, approval-gated commands | Global Nexplora / NX secrets |
|
|
15
|
+
| Its own update channel | Any upload / publish path to a package index other than the gated release job |
|
|
16
|
+
|
|
17
|
+
Every published build must pass a check that none of the private NX modules are present in the wheel.
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
- **In a Nexplora Code session's terminal** it is already signed in: the app writes a short-lived session ticket (bound to the
|
|
22
|
+
person, the Operation and that session, one hour, usable only for sending a message there) to a private file, and renews it
|
|
23
|
+
while the page holds the terminal. The person's login is never on that computer. The ticket can't approve anything.
|
|
24
|
+
- **On any other computer** `nx` shows a short link and a code. Approve it in the browser and it's done. The sign-in stays in
|
|
25
|
+
`~/.nx-cli/credentials.json` (0600). `nx logout` ends it. Work runs on the person's Nexplora cloud computer, not this one.
|
|
26
|
+
- **Approvals** are given in the chat. `nx` says what is waiting and where, then follows the run as it goes on.
|
|
27
|
+
- **Colours** are the terminal's own, with no painted background, so it matches a light or dark panel.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
python -m pip install .
|
|
31
|
+
nx
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Checks
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
python -m pytest # includes a real local HTTP server (tests/stub_server.py)
|
|
38
|
+
python -m build --wheel && python scripts/check_wheel.py dist/*.whl # the charter's check; a release must pass it
|
|
39
|
+
python scripts/pty_proof.py "$(command -v nx)" /tmp/nx-proof # real pty, reads the screen, fails on any painted background
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## PyPI
|
|
43
|
+
|
|
44
|
+
The `nxplora` project name is owned and all historical releases are yanked (2026-09-29). Existing installs
|
|
45
|
+
run `nx upgrade` → `pip install -U nxplora`, so the first nx-cli release published under `nxplora` becomes
|
|
46
|
+
the migration path for those users. Never delete the PyPI project (name-takeover risk).
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nxplora"
|
|
7
|
+
version = "0.16.0"
|
|
8
|
+
description = "NX in your terminal, on your Nexplora account."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "Proprietary" }
|
|
12
|
+
dependencies = ["textual>=1.0,<2", "certifi"]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
nx = "nxplora_cli.__main__:main"
|
|
16
|
+
nxplora = "nxplora_cli.__main__:main"
|
|
17
|
+
|
|
18
|
+
[tool.hatch.build.targets.wheel]
|
|
19
|
+
packages = ["src/nxplora_cli"]
|
|
20
|
+
|
|
21
|
+
[tool.pytest.ini_options]
|
|
22
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""The charter's check: a published wheel holds this client and nothing of NX. Exits non-zero, naming what it found.
|
|
3
|
+
|
|
4
|
+
Usage: python scripts/check_wheel.py dist/nxplora-*.whl
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import zipfile
|
|
11
|
+
|
|
12
|
+
ALLOWED_MODULES = {"__init__.py", "__main__.py", "app.py", "auth.py", "events.py", "opening.py", "transport.py"}
|
|
13
|
+
# Never in a client: brain modules, model ids, provider addresses, secret names.
|
|
14
|
+
FORBIDDEN = re.compile(
|
|
15
|
+
r"nx_brain|nx_agent|nx_channel|chat_ui|autonomy_loop|nx_capability|from nx\b|import nx\b"
|
|
16
|
+
r"|claude-[a-z0-9]|gpt-[0-9]|gemini-[0-9]|qwen|deepseek|kimi|moonshot"
|
|
17
|
+
r"|api\.anthropic\.com|api\.openai\.com|generativelanguage\.googleapis|deepinfra|openrouter"
|
|
18
|
+
r"|SUPABASE_|sk-ant-|sk-proj-|BEGIN (?:RSA |EC )?PRIVATE KEY",
|
|
19
|
+
re.IGNORECASE,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def check(path: str) -> list[str]:
|
|
24
|
+
problems: list[str] = []
|
|
25
|
+
with zipfile.ZipFile(path) as wheel:
|
|
26
|
+
for name in wheel.namelist():
|
|
27
|
+
top = name.split("/", 1)[0]
|
|
28
|
+
if top.endswith(".dist-info"):
|
|
29
|
+
continue
|
|
30
|
+
if top != "nxplora_cli" or name.split("/", 1)[1] not in ALLOWED_MODULES:
|
|
31
|
+
problems.append(f"{name}: not part of this client")
|
|
32
|
+
continue
|
|
33
|
+
found = FORBIDDEN.search(wheel.read(name).decode("utf-8", "replace"))
|
|
34
|
+
if found:
|
|
35
|
+
problems.append(f"{name}: contains {found.group(0)!r}")
|
|
36
|
+
return problems
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def main() -> int:
|
|
40
|
+
if len(sys.argv) != 2:
|
|
41
|
+
print(__doc__, file=sys.stderr)
|
|
42
|
+
return 2
|
|
43
|
+
problems = check(sys.argv[1])
|
|
44
|
+
for problem in problems:
|
|
45
|
+
print(problem, file=sys.stderr)
|
|
46
|
+
if not problems:
|
|
47
|
+
print(f"{sys.argv[1]}: clean")
|
|
48
|
+
return 1 if problems else 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run a built `nx` in a real pty against the local stub (tests/stub_server.py), read the screen with a terminal emulator,
|
|
3
|
+
and write it out as text and as a picture in the terminal panel's real palettes. Never touches production.
|
|
4
|
+
|
|
5
|
+
Usage: python scripts/pty_proof.py NX_BINARY OUT_DIR [--term xterm-16color] [--no-send]
|
|
6
|
+
Exits 1 if the app painted any background, or the ticket reached the screen.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import fcntl
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import pty
|
|
15
|
+
import select
|
|
16
|
+
import struct
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
import termios
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import pyte
|
|
24
|
+
from PIL import Image, ImageDraw, ImageFont
|
|
25
|
+
|
|
26
|
+
sys.path.insert(0, str(Path(__file__).parent.parent / "tests"))
|
|
27
|
+
from stub_server import SESSION, Stub # noqa: E402
|
|
28
|
+
|
|
29
|
+
COLS, ROWS = 96, 26
|
|
30
|
+
# The panel's xterm themes, from lib/code/pty-bridge.ts LIVING_INK_XTERM, and a plain dark one.
|
|
31
|
+
INK = {"default_bg": "#efe9dc", "default": "#2b2620", "black": "#d9d3c6", "red": "#a63a2a", "green": "#4f6b34", "brown": "#9a7413", "blue": "#3f5d7a", "magenta": "#7a466a", "cyan": "#3f6b68", "white": "#4a4438",
|
|
32
|
+
"brightblack": "#6f675a", "brightred": "#c04a34", "brightgreen": "#5f7f40", "brightbrown": "#b68a1a", "brightblue": "#4d6f92", "brightmagenta": "#93567f", "brightcyan": "#4f807c", "brightwhite": "#2b2620"}
|
|
33
|
+
DARK = {"default_bg": "#14161a", "default": "#d7d9dc", "black": "#14161a", "red": "#e06c60", "green": "#8fbf6a", "brown": "#d9b45b", "blue": "#7aa2d6", "magenta": "#c78bd0", "cyan": "#6fc0ba", "white": "#c8cacc",
|
|
34
|
+
"brightblack": "#8a8f96", "brightred": "#ef8378", "brightgreen": "#a5d180", "brightbrown": "#e8c673", "brightblue": "#92b6e6", "brightmagenta": "#d7a2df", "brightcyan": "#86d3cd", "brightwhite": "#ffffff"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def colour(name: str, palette: dict[str, str]) -> str:
|
|
38
|
+
if name == "default":
|
|
39
|
+
return palette["default"]
|
|
40
|
+
if name in palette:
|
|
41
|
+
return palette[name]
|
|
42
|
+
return f"#{name}" if len(name) == 6 else palette["default"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def picture(screen: pyte.Screen, palette: dict[str, str], path: Path) -> None:
|
|
46
|
+
font = None
|
|
47
|
+
for candidate in ("/System/Library/Fonts/Menlo.ttc", "/System/Library/Fonts/Monaco.ttf"):
|
|
48
|
+
if os.path.exists(candidate):
|
|
49
|
+
font = ImageFont.truetype(candidate, 15)
|
|
50
|
+
break
|
|
51
|
+
font = font or ImageFont.load_default()
|
|
52
|
+
w, h = 9, 19
|
|
53
|
+
image = Image.new("RGB", (COLS * w, ROWS * h), palette["default_bg"])
|
|
54
|
+
draw = ImageDraw.Draw(image)
|
|
55
|
+
for y in range(ROWS):
|
|
56
|
+
for x in range(COLS):
|
|
57
|
+
cell = screen.buffer[y][x]
|
|
58
|
+
fg, bg = colour(cell.fg, palette), palette["default_bg"] if cell.bg == "default" else colour(cell.bg, palette)
|
|
59
|
+
if cell.reverse:
|
|
60
|
+
fg, bg = bg, fg
|
|
61
|
+
if bg != palette["default_bg"]:
|
|
62
|
+
draw.rectangle([x * w, y * h, (x + 1) * w, (y + 1) * h], fill=bg)
|
|
63
|
+
if cell.data.strip():
|
|
64
|
+
draw.text((x * w, y * h), cell.data, font=font, fill=fg)
|
|
65
|
+
image.save(path)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main() -> int:
|
|
69
|
+
parser = argparse.ArgumentParser()
|
|
70
|
+
parser.add_argument("nx")
|
|
71
|
+
parser.add_argument("out")
|
|
72
|
+
parser.add_argument("--term", default="xterm-16color")
|
|
73
|
+
parser.add_argument("--no-send", action="store_true")
|
|
74
|
+
parser.add_argument("--name", default="nx")
|
|
75
|
+
args = parser.parse_args()
|
|
76
|
+
out = Path(args.out)
|
|
77
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
|
|
79
|
+
ticket = "nxst_stubpayload.stubsignature"
|
|
80
|
+
stub = Stub(ticket)
|
|
81
|
+
stub.run_scripts = [[
|
|
82
|
+
{"event": "tool_call", "tick": 0, "tool": "run_command", "args": {}, "summary": "Ran npm test"},
|
|
83
|
+
{"event": "assistant_text", "text": "All 12 tests pass."},
|
|
84
|
+
{"event": "approval_required", "id": "a1", "tick": 1, "tool": "git_push", "args": {}, "summary": "Push to main", "danger": "push"},
|
|
85
|
+
{"event": "done", "status": "awaiting_approval"},
|
|
86
|
+
]]
|
|
87
|
+
home = Path(tempfile.mkdtemp())
|
|
88
|
+
(home / ".nx-pty").mkdir()
|
|
89
|
+
(home / ".nx-cli").mkdir(mode=0o700)
|
|
90
|
+
session = home / ".nx-cli" / "session.json"
|
|
91
|
+
session.write_text(json.dumps({"v": 1, "origin": stub.origin, "session_id": SESSION, "ticket": ticket, "expires_at_ms": int(time.time() * 1000) + 3_600_000}))
|
|
92
|
+
os.chmod(session, 0o600)
|
|
93
|
+
|
|
94
|
+
screen = pyte.Screen(COLS, ROWS)
|
|
95
|
+
stream = pyte.ByteStream(screen)
|
|
96
|
+
raw = bytearray()
|
|
97
|
+
pid, fd = pty.fork()
|
|
98
|
+
if pid == 0:
|
|
99
|
+
env = {"HOME": str(home), "TERM": args.term, "PATH": os.environ["PATH"], "LANG": "C.UTF-8"}
|
|
100
|
+
os.chdir(home)
|
|
101
|
+
os.execve(args.nx, [args.nx], env)
|
|
102
|
+
fcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", ROWS, COLS, 0, 0))
|
|
103
|
+
|
|
104
|
+
def pump(seconds: float) -> None:
|
|
105
|
+
end = time.time() + seconds
|
|
106
|
+
while time.time() < end:
|
|
107
|
+
ready, _, _ = select.select([fd], [], [], 0.1)
|
|
108
|
+
if ready:
|
|
109
|
+
try:
|
|
110
|
+
data = os.read(fd, 65536)
|
|
111
|
+
except OSError:
|
|
112
|
+
return
|
|
113
|
+
raw.extend(data)
|
|
114
|
+
stream.feed(data)
|
|
115
|
+
|
|
116
|
+
pump(5)
|
|
117
|
+
first = out / f"{args.name}-open.txt"
|
|
118
|
+
first.write_text("\n".join(screen.display))
|
|
119
|
+
if not args.no_send:
|
|
120
|
+
for ch in "run the tests":
|
|
121
|
+
os.write(fd, ch.encode())
|
|
122
|
+
pump(0.03)
|
|
123
|
+
os.write(fd, b"\r")
|
|
124
|
+
pump(3)
|
|
125
|
+
(out / f"{args.name}-screen.txt").write_text("\n".join(screen.display))
|
|
126
|
+
picture(screen, INK, out / f"{args.name}-light-panel.png")
|
|
127
|
+
picture(screen, DARK, out / f"{args.name}-dark-panel.png")
|
|
128
|
+
|
|
129
|
+
painted: dict[str, int] = {}
|
|
130
|
+
fgs: dict[str, int] = {}
|
|
131
|
+
for y in range(ROWS):
|
|
132
|
+
for x in range(COLS):
|
|
133
|
+
cell = screen.buffer[y][x]
|
|
134
|
+
if cell.bg != "default":
|
|
135
|
+
painted[cell.bg] = painted.get(cell.bg, 0) + 1
|
|
136
|
+
fgs[cell.fg] = fgs.get(cell.fg, 0) + 1
|
|
137
|
+
os.write(fd, b"\x03")
|
|
138
|
+
pump(1)
|
|
139
|
+
report = {
|
|
140
|
+
"term": args.term,
|
|
141
|
+
"painted_background_cells": painted,
|
|
142
|
+
"foregrounds": fgs,
|
|
143
|
+
"ticket_on_screen": ticket.encode() in bytes(raw),
|
|
144
|
+
"requests": [{"path": r["path"], "body_fields": sorted(r["body"] or {})} for r in stub.requests],
|
|
145
|
+
}
|
|
146
|
+
(out / f"{args.name}-report.json").write_text(json.dumps(report, indent=1))
|
|
147
|
+
print(json.dumps(report))
|
|
148
|
+
print("\n".join(line.rstrip() for line in screen.display))
|
|
149
|
+
stub.close()
|
|
150
|
+
return 1 if painted or report["ticket_on_screen"] else 0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
if __name__ == "__main__":
|
|
154
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.16.0"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from . import __version__
|
|
7
|
+
from .auth import DEFAULT_ORIGIN, forget_device_login, load_device_login, safe_origin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
parser = argparse.ArgumentParser(prog="nx", description="NX in your terminal, on your Nexplora account")
|
|
12
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
13
|
+
parser.add_argument("--connect", metavar="URL", help="the Nexplora address to use (default: nexplora.ai)")
|
|
14
|
+
parser.add_argument("command", nargs="?", choices=["login", "logout"], help="sign in or out on this computer")
|
|
15
|
+
args = parser.parse_args()
|
|
16
|
+
origin = safe_origin(args.connect) if args.connect else None
|
|
17
|
+
if args.connect and not origin:
|
|
18
|
+
parser.error("that address isn't https, so nx won't send your sign-in to it")
|
|
19
|
+
if args.command == "logout":
|
|
20
|
+
_logout()
|
|
21
|
+
return
|
|
22
|
+
from .app import NxploraApp
|
|
23
|
+
|
|
24
|
+
app = NxploraApp(origin=origin or DEFAULT_ORIGIN, origin_chosen=bool(origin), force_login=args.command == "login")
|
|
25
|
+
app.run(mouse=False)
|
|
26
|
+
if app.exit_note:
|
|
27
|
+
print(app.exit_note)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _logout() -> None:
|
|
31
|
+
from .transport import Unreachable, revoke_device_session
|
|
32
|
+
|
|
33
|
+
login = load_device_login()
|
|
34
|
+
if login and login.device_session_id:
|
|
35
|
+
try:
|
|
36
|
+
revoke_device_session(login.origin, login.access_token, login.device_session_id)
|
|
37
|
+
except Unreachable:
|
|
38
|
+
print("Signed out here, but Nexplora couldn't be reached. You can end this sign-in in your account.", file=sys.stderr)
|
|
39
|
+
forget_device_login()
|
|
40
|
+
print("Signed out on this computer.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
if __name__ == "__main__":
|
|
44
|
+
main()
|