Skip to content

Commit 9ee05f0

Browse files
yeldarbyclaude
andcommitted
fix(cli): whoami honors --api-key, normalize error JSON schema
- whoami/auth status now validates against the API when --api-key is explicitly provided, instead of silently showing saved config - Error JSON output is now always {"error": {"message": "...", ...}} instead of sometimes a string, sometimes an object — consistent schema for AI agents and programmatic consumers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2558170 commit 9ee05f0

File tree

5 files changed

+30
-20
lines changed

5 files changed

+30
-20
lines changed

roboflow/cli/_output.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,17 +85,24 @@ def output_error(
8585
parsed, human_message = _parse_error_message(message)
8686

8787
if getattr(args, "json", False):
88-
# If the raw message was JSON containing an "error" key, unwrap it
89-
# so we emit {"error": {message details}} not {"error": {"error": ...}}.
88+
# Normalise error to always be {"error": {"message": "..."}} so
89+
# consumers see a consistent schema regardless of error source.
9090
if parsed is not None and "error" in parsed:
91-
error_value: Any = parsed["error"]
91+
inner: Any = parsed["error"]
9292
elif parsed is not None:
93-
error_value = parsed
93+
inner = parsed
9494
else:
95-
error_value = message
96-
payload: dict[str, Any] = {"error": error_value}
95+
inner = None
96+
97+
if isinstance(inner, dict):
98+
error_obj: dict[str, Any] = dict(inner)
99+
error_obj.setdefault("message", human_message)
100+
else:
101+
error_obj = {"message": human_message}
102+
97103
if hint:
98-
payload["hint"] = hint
104+
error_obj.setdefault("hint", hint)
105+
payload: dict[str, Any] = {"error": error_obj}
99106
print(json.dumps(payload), file=sys.stderr)
100107
else:
101108
msg = f"Error: {human_message}"

roboflow/cli/handlers/auth.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,19 +188,18 @@ def _status(args: argparse.Namespace) -> None:
188188
workspaces = config.get("workspaces", {})
189189
default_ws_url = config.get("RF_WORKSPACE")
190190

191-
# Fall back to --api-key flag or ROBOFLOW_API_KEY env var
192-
api_key = getattr(args, "api_key", None) or os.getenv("ROBOFLOW_API_KEY")
191+
# Explicit --api-key flag takes priority, then env var
192+
explicit_api_key = getattr(args, "api_key", None)
193+
api_key = explicit_api_key or os.getenv("ROBOFLOW_API_KEY")
193194

194-
if not workspaces and not default_ws_url and not api_key:
195-
output_error(args, "Not logged in.", hint="Run 'roboflow auth login' to authenticate.", exit_code=2)
196-
return # unreachable, but helps mypy
197-
198-
if api_key and not default_ws_url:
199-
# No config file, but we have an API key — fetch workspace from API
195+
# When an explicit --api-key is provided, always validate it against the
196+
# API rather than showing saved config — the user wants to check *this* key.
197+
if explicit_api_key or (api_key and not default_ws_url):
200198
import requests
201199

202200
from roboflow.config import API_URL
203201

202+
assert api_key is not None # guaranteed by the condition above
204203
resp = requests.post(API_URL + "/?api_key=" + api_key)
205204
if resp.status_code == 200:
206205
ws_url = resp.json().get("workspace", "unknown")
@@ -216,6 +215,10 @@ def _status(args: argparse.Namespace) -> None:
216215
output_error(args, "API key is invalid or expired.", exit_code=2)
217216
return
218217

218+
if not workspaces and not default_ws_url and not api_key:
219+
output_error(args, "Not logged in.", hint="Run 'roboflow auth login' to authenticate.", exit_code=2)
220+
return # unreachable, but helps mypy
221+
219222
if not default_ws_url:
220223
output_error(args, "No default workspace configured.", hint="Run 'roboflow auth set-workspace <id>'.")
221224
return # unreachable, but helps mypy
@@ -224,7 +227,7 @@ def _status(args: argparse.Namespace) -> None:
224227
default_ws = workspaces_by_url.get(default_ws_url)
225228

226229
if default_ws:
227-
# Use stored API key, or fall back to flag/env
230+
# Use stored API key, or fall back to env var
228231
display_key = api_key or default_ws.get("apiKey", "")
229232
masked = dict(default_ws)
230233
masked["apiKey"] = _mask_key(display_key)

tests/cli/test_annotation_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def test_stub_json_mode(self):
9999
sys.stderr = old
100100

101101
result = json.loads(buf.getvalue())
102-
self.assertIn("not yet implemented", result["error"])
102+
self.assertIn("not yet implemented", result["error"]["message"])
103103

104104

105105
if __name__ == "__main__":

tests/cli/test_deployment_handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def _fake_handler(args: object) -> None:
101101
err_output = stderr.getvalue().strip()
102102
parsed = json.loads(err_output)
103103
self.assertIn("error", parsed)
104-
self.assertIn("401", parsed["error"])
104+
self.assertIn("401", parsed["error"]["message"])
105105

106106
def test_wrapped_success_prints_output(self) -> None:
107107
"""On success, wrapped func should replay captured stdout."""

tests/cli/test_output.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@ def test_output_error_json_mode(self) -> None:
6868
sys.stderr = old_stderr
6969
self.assertEqual(ctx.exception.code, 1)
7070
result = json.loads(buf.getvalue())
71-
self.assertEqual(result["error"], "something broke")
72-
self.assertEqual(result["hint"], "try again")
71+
self.assertEqual(result["error"]["message"], "something broke")
72+
self.assertEqual(result["error"]["hint"], "try again")
7373

7474
def test_output_error_text_mode(self) -> None:
7575
from roboflow.cli._output import output_error

0 commit comments

Comments
 (0)