netconverter-cli 0.4.0__tar.gz → 0.4.1__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.
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/CHANGELOG.md +9 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/PKG-INFO +1 -1
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/pyproject.toml +1 -1
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/__init__.py +1 -1
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/clerk.py +5 -2
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/cli.py +3 -2
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/setup.py +3 -2
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/conftest.py +9 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_clerk.py +15 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_setup.py +1 -1
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_signin_default.py +20 -2
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/.gitignore +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/LICENSE +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/QUICK_START.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/README.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/SECURITY.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/COMMANDS.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/PRIVACY.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/RELEASING.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/VERIFICATION.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/README.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/chatgpt.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/claude-ai.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/claude-code.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/claude-desktop.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/getting-access.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/docs/connect/other-assistants.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/examples/README.md +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/activity.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/api.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/backend.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/branding.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/catalog.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/commands.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/connect.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/controller.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/credentials.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/guided.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/mcp_server.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/palette.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/presentation.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/privacy.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/providers.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/remote_mcp.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/tui.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/update.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/src/netconverter_cli/workspace.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_boundaries.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_clerk_host_binding.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_cli_020.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_connect_030.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_connect_031.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_download_analysis.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_inline.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_mcp.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_presentation.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_protocols.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_sessions.py +0 -0
- {netconverter_cli-0.4.0 → netconverter_cli-0.4.1}/tests/test_update.py +0 -0
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 — 2026-10-22
|
|
4
|
+
|
|
5
|
+
- **First-run setup honours the Clerk sign-in (#6).** `NETCONVERTER_API_KEY` in the
|
|
6
|
+
environment no longer skips the sign-in choice and is never copied into the OS keyring.
|
|
7
|
+
`whoami` reports `api_key_stored` for the keyring only and adds `api_key_in_environment`.
|
|
8
|
+
- **Sign-in errors show Clerk's full reason.** The OAuth error description was cut at
|
|
9
|
+
200 characters, hiding the cause sentence (e.g. a missing `device_code` grant, #684).
|
|
10
|
+
- **`whoami` shows one label per organization** (name, else slug) instead of the raw `org_...` id.
|
|
11
|
+
|
|
3
12
|
## 0.4.0 — 2026-10-21
|
|
4
13
|
|
|
5
14
|
- **On PyPI as `netconverter-cli`.** `uv tool install --python 3.12 netconverter-cli`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: netconverter-cli
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
4
4
|
Summary: NetConverter CLI — any-vendor network configuration migration, validation and optimization, and a local MCP bridge for your AI assistant
|
|
5
5
|
Project-URL: Homepage, https://netconverter.ai
|
|
6
6
|
Project-URL: Repository, https://github.com/valeronlabs/netconverter_terminals
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "netconverter-cli"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.1"
|
|
8
8
|
description = "NetConverter CLI — any-vendor network configuration migration, validation and optimization, and a local MCP bridge for your AI assistant"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -180,7 +180,8 @@ class OAuthError(ClerkError):
|
|
|
180
180
|
def __init__(self, error: str, body=None):
|
|
181
181
|
description = ""
|
|
182
182
|
if isinstance(body, dict):
|
|
183
|
-
description
|
|
183
|
+
# Clerk puts the cause in the last sentence; keep the whole description, not a cut-off head.
|
|
184
|
+
description = _safe_text(body.get("error_description"), 600)
|
|
184
185
|
super().__init__(
|
|
185
186
|
f"Sign-in refused: {error}" + (f" ({description})" if description else "")
|
|
186
187
|
)
|
|
@@ -567,9 +568,11 @@ def profile_summary(info: dict) -> dict:
|
|
|
567
568
|
if isinstance(info.get(key), str) and info[key]:
|
|
568
569
|
out[key] = _safe_text(info[key], 120)
|
|
569
570
|
orgs = []
|
|
570
|
-
for
|
|
571
|
+
# One label for the active organization: its name, else its slug; the raw id only as a last resort.
|
|
572
|
+
for key in ("org_name", "org_slug", "org_id"):
|
|
571
573
|
if isinstance(info.get(key), str) and info[key]:
|
|
572
574
|
orgs.append(_safe_text(info[key], 120))
|
|
575
|
+
break
|
|
573
576
|
for org in info.get("organizations") or info.get("orgs") or []:
|
|
574
577
|
if isinstance(org, dict):
|
|
575
578
|
label = org.get("name") or org.get("slug") or org.get("id")
|
|
@@ -8,7 +8,7 @@ import sys
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
from .api import Client, DEFAULT_BASE, APIError
|
|
10
10
|
from .backend import detect_profile, open_client
|
|
11
|
-
from .credentials import get_secret, keyring_available, save_secret
|
|
11
|
+
from .credentials import get_secret, keyring_available, load_secret, save_secret
|
|
12
12
|
from .controller import Controller
|
|
13
13
|
from .providers import Provider, ProviderError
|
|
14
14
|
from .commands import execute
|
|
@@ -139,7 +139,8 @@ def logout(args):
|
|
|
139
139
|
def whoami(args):
|
|
140
140
|
from .clerk import ClerkError, profile_summary
|
|
141
141
|
|
|
142
|
-
out = {"server": args.server, "api_key_stored": bool(
|
|
142
|
+
out = {"server": args.server, "api_key_stored": bool(load_secret(account(args.server))),
|
|
143
|
+
"api_key_in_environment": bool(os.environ.get("NETCONVERTER_API_KEY"))}
|
|
143
144
|
session = _clerk_session()
|
|
144
145
|
if session is None:
|
|
145
146
|
out["clerk"] = None
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from .api import Client, DEFAULT_BASE
|
|
6
6
|
from .backend import open_client
|
|
7
|
-
from .credentials import get_secret, save_secret
|
|
7
|
+
from .credentials import get_secret, load_secret, save_secret
|
|
8
8
|
from .providers import Provider, ProviderError
|
|
9
9
|
from .workspace import Workspace, WorkspaceError
|
|
10
10
|
|
|
@@ -85,7 +85,8 @@ class AccountSignIn:
|
|
|
85
85
|
|
|
86
86
|
def connect_credential(server, signin, ask, secret):
|
|
87
87
|
"""A stored key, the existing sign-in, or the user's choice of signing in or pasting a key."""
|
|
88
|
-
|
|
88
|
+
# Keyring only: a key from the environment must neither skip the sign-in nor be copied into the keyring.
|
|
89
|
+
saved = load_secret(account(server))
|
|
89
90
|
if not saved and signin.allowed(server):
|
|
90
91
|
session = signin.session()
|
|
91
92
|
if session is not None:
|
|
@@ -20,3 +20,12 @@ def _no_real_sign_in(request, monkeypatch):
|
|
|
20
20
|
monkeypatch.setattr(update, "cache_dir", lambda: cache)
|
|
21
21
|
if "vault" not in request.fixturenames:
|
|
22
22
|
monkeypatch.setattr(cli, "_clerk_session", lambda: None)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@pytest.fixture(autouse=True)
|
|
26
|
+
def _no_real_stored_key(request, monkeypatch):
|
|
27
|
+
"""First-run setup never sees an API key stored in the developer's real keyring."""
|
|
28
|
+
if "vault" not in request.fixturenames:
|
|
29
|
+
from netconverter_cli import setup
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr(setup, "load_secret", lambda name: "")
|
|
@@ -398,3 +398,18 @@ def test_every_token_request_names_the_mcp_resource():
|
|
|
398
398
|
|
|
399
399
|
src = inspect.getsource(clerk)
|
|
400
400
|
assert src.count('"resource": RESOURCE') >= 4
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_oauth_error_keeps_the_cause_sentence_of_a_long_description():
|
|
404
|
+
"""#684: Clerk's cause was cut off at 200 characters."""
|
|
405
|
+
from netconverter_cli.clerk import OAuthError
|
|
406
|
+
cause = "This OAuth application does not have the 'urn:ietf:params:oauth:grant-type:device_code' grant."
|
|
407
|
+
exc = OAuthError("unauthorized_client", {"error_description": "x" * 230 + " " + cause})
|
|
408
|
+
assert cause in str(exc)
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def test_profile_summary_shows_the_org_name_not_its_raw_id():
|
|
412
|
+
from netconverter_cli.clerk import profile_summary
|
|
413
|
+
info = {"org_id": "org_39VBTisVKmTSYHZamQtGhl8y9rS", "org_slug": "nexthopllc", "org_name": "NextHop LLC"}
|
|
414
|
+
assert profile_summary(info)["organizations"] == ["NextHop LLC"]
|
|
415
|
+
assert profile_summary({"org_id": "org_X"})["organizations"] == ["org_X"]
|
|
@@ -106,7 +106,7 @@ def test_deleted_working_folder_gives_a_clear_message(tmp_path, monkeypatch, cap
|
|
|
106
106
|
gone.mkdir()
|
|
107
107
|
monkeypatch.chdir(gone)
|
|
108
108
|
gone.rmdir()
|
|
109
|
-
monkeypatch.setattr(cli, '
|
|
109
|
+
monkeypatch.setattr(cli, 'load_secret', Mock(return_value=''))
|
|
110
110
|
monkeypatch.setattr(cli, '_clerk_session', Mock(return_value=None))
|
|
111
111
|
cli.main(['whoami']) # account commands need no workspace
|
|
112
112
|
assert '"api_key_stored": false' in capsys.readouterr().out
|
|
@@ -121,7 +121,7 @@ def _setup_mocks(monkeypatch):
|
|
|
121
121
|
client = Mock()
|
|
122
122
|
factory = Mock(return_value=(client, "external"))
|
|
123
123
|
monkeypatch.setattr(setup, "open_client", factory)
|
|
124
|
-
monkeypatch.setattr(setup, "
|
|
124
|
+
monkeypatch.setattr(setup, "load_secret", lambda name: "")
|
|
125
125
|
save = Mock(return_value=True)
|
|
126
126
|
monkeypatch.setattr(setup, "save_secret", save)
|
|
127
127
|
return client, factory, save
|
|
@@ -172,7 +172,7 @@ def test_setup_never_offers_sign_in_to_an_untrusted_server(tmp_path, monkeypatch
|
|
|
172
172
|
def test_plain_netconverter_in_a_new_folder_reaches_the_app_signed_in(tmp_path, monkeypatch):
|
|
173
173
|
session = FakeSession()
|
|
174
174
|
monkeypatch.setattr(cli, "_clerk_session", lambda: session)
|
|
175
|
-
monkeypatch.setattr(setup, "
|
|
175
|
+
monkeypatch.setattr(setup, "load_secret", lambda name: "")
|
|
176
176
|
monkeypatch.setattr(setup, "save_secret", Mock(return_value=True))
|
|
177
177
|
client = Mock()
|
|
178
178
|
factory = Mock(return_value=(client, "external"))
|
|
@@ -186,3 +186,21 @@ def test_plain_netconverter_in_a_new_folder_reaches_the_app_signed_in(tmp_path,
|
|
|
186
186
|
cli.main(["--workspace", str(tmp_path)])
|
|
187
187
|
factory.assert_called_once_with(session, PROD)
|
|
188
188
|
ran.assert_called_once()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_env_key_neither_skips_the_sign_in_nor_is_copied_to_the_keyring(monkeypatch):
|
|
192
|
+
"""#6: NETCONVERTER_API_KEY in the environment is not a stored key."""
|
|
193
|
+
session = FakeSession()
|
|
194
|
+
monkeypatch.setenv("NETCONVERTER_API_KEY", "nck_FROM_ENV")
|
|
195
|
+
monkeypatch.setattr(setup, "load_secret", lambda name: "")
|
|
196
|
+
monkeypatch.setattr(setup, "get_secret", lambda *a, **k: pytest.fail("env key must not be read"))
|
|
197
|
+
signin = Mock(allowed=Mock(return_value=True), session=Mock(return_value=session))
|
|
198
|
+
assert setup.connect_credential(PROD, signin, ask=Mock(), secret=Mock()) is session
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def test_blank_key_prompt_does_not_adopt_the_env_key(monkeypatch):
|
|
202
|
+
monkeypatch.setenv("NETCONVERTER_API_KEY", "nck_FROM_ENV")
|
|
203
|
+
monkeypatch.setattr(setup, "load_secret", lambda name: "")
|
|
204
|
+
signin = Mock(allowed=Mock(return_value=False))
|
|
205
|
+
with pytest.raises(ValueError):
|
|
206
|
+
setup.connect_credential(PROD, signin, ask=Mock(), secret=lambda prompt: "")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|