Skip to content

feat(cli): enhance welcome flow, enforce auth, and customize help output#95

Merged
omsherikar merged 4 commits intomainfrom
feat/cli-enhancements-v2
Jan 28, 2026
Merged

feat(cli): enhance welcome flow, enforce auth, and customize help output#95
omsherikar merged 4 commits intomainfrom
feat/cli-enhancements-v2

Conversation

@omsherikar
Copy link
Contributor

@omsherikar omsherikar commented Jan 28, 2026

Summary by CodeRabbit

  • New Features

    • Device-based login flow with API key verification, auth commands (login, status, logout), improved CLI help, and richer startup/dashboard UI with themed banners and panels
    • Persistent local credential storage and background token polling for device authorization
  • Tests

    • End-to-end login flow tests plus new auth fixtures for CLI tests
  • Chores

    • Version, changelog, and metadata updated; default log format changed to text

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 28, 2026

Warning

Rate limit exceeded

@omsherikar has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 17 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds device-code authentication, local credential storage, API key validation, expanded CLI commands/UI (login, auth status/logout, custom help), device authorization/token polling modules, tests for login flows, and version/metadata updates. Includes CLI theming and startup/dashboard UX changes.

Changes

Cohort / File(s) Summary
Authentication Core
refactron/core/device_auth.py
New device-authorization and token polling: DeviceAuthorization, TokenResponse, start_device_authorization(), poll_for_token() with HTTP helpers and error handling.
Credentials Persistence
refactron/core/credentials.py
New RefactronCredentials dataclass and JSON persistence: to_dict()/from_dict(), credentials_path(), save_credentials(), load_credentials(), delete_credentials() (writes ~0600).
CLI & UX
refactron/cli.py
Large CLI overhaul: login, logout, auth group (status, logout), API key validation (_validate_api_key / ApiKeyValidationResult), _auth_banner, CustomHelpGroup, themed messages, startup/dashboard UI, and main(ctx) signature change.
CLI Tests (new)
tests/test_cli_login.py
End-to-end tests for device-code login flow, token polling, credential persistence, API key validation, logout, and auth status; extensive mocking.
Global Tests Fixture
tests/test_cli.py, tests/test_backup.py
Added autouse mock_auth pytest fixture to mock credentials in CLI tests.
Integration Tests (unchanged)
tests/test_cli_integration.py
No functional changes.
Version & Metadata
pyproject.toml, refactron/__init__.py, CHANGELOG.md
Version bumped to 1.0.12, author email and site/docs URLs updated, changelog populated.
Config Default
refactron/core/config.py
Default log_format changed from "json" to "text".

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant CLI
    participant API as "Refactron API"
    participant Storage as "Local Storage"

    User->>CLI: refactron login
    CLI->>API: POST /oauth/device
    API-->>CLI: DeviceAuthorization (device_code, user_code, verification_uri)
    CLI->>User: display verification_uri and user_code
    User->>API: visit verification_uri and enter user_code
    loop poll until success or expiry
        CLI->>API: POST /oauth/token (poll with device_code)
        API-->>CLI: authorization_pending / slow_down / error / success
        CLI->>CLI: adjust interval on slow_down
    end
    API-->>CLI: TokenResponse (access_token, email, plan, api_key?)
    CLI->>User: prompt for API key (optional) / validate
    CLI->>Storage: save_credentials(RefactronCredentials)
    Storage-->>CLI: persisted (~/.refactron/credentials.json)
    CLI->>User: display success (email, plan)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

security, refactoring

Poem

🐰
I hopped to fetch a device code bright and keen,
Polling through loops where tokens convene.
JSON snug under my paw so tight,
Banners gleam and prompts delight.
Login saved — the rabbit hums with glee.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main changes: enhanced welcome/CLI flow, authentication enforcement, and custom help output for the CLI module.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added the enhancement New feature or request label Jan 28, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@refactron/cli.py`:
- Around line 738-776: The global auth gate currently exempts only "login" and
"logout" via the exempt_commands list, which blocks the "auth" command group (so
subcommands like "auth status" can't run); update the exempt_commands
declaration (used where ctx.invoked_subcommand is checked) to include "auth" so
that the auth command group and its subcommands (e.g., status/logout) are
allowed to run without prior authentication; no other logic changes to
load_credentials, ctx.invoke(login,...), or the post-login re-check are needed.
- Around line 835-841: Replace the hard-coded app URL construction assigned to
login_url with the server-provided device verification URI: use
auth.verification_uri (and if you want a direct link, append the code as a query
param or use auth.verification_uri_complete if available) instead of building
f"https://app.refactron.dev/…"; update the displayed URL and keep the displayed
verification code using auth.user_code so the Text() instructions (variable
instructions) correctly reflect the server-supplied verification URI for
non-production API bases.

In `@refactron/core/credentials.py`:
- Around line 43-48: The parsing of expires_at in load_credentials currently
calls datetime.fromisoformat(expires_at_raw) which raises ValueError on
malformed ISO strings; update the block handling expires_at_raw to catch
exceptions (ValueError) around datetime.fromisoformat so that on any parse error
expires_at stays None (preserving the “returns None if invalid” contract), and
keep the existing timezone fix-up (replace tzinfo=timezone.utc) only when
parsing succeeds; reference symbols: expires_at_raw, expires_at,
load_credentials, datetime.fromisoformat.

In `@tests/test_cli_integration.py`:
- Around line 1-2: Remove the stray blank lines in the test file so it is not
entirely empty—either delete the file if unused or replace the blank-only
contents with the minimal valid file (e.g., an empty Python file containing a
single newline) and re-run the formatters (Black / end-of-file-fixer) to ensure
formatting hooks pass; target the test_cli_integration.py test module for this
change.

In `@tests/test_cli_login.py`:
- Around line 19-27: The test asserts the API base URL inside
_mock_start_device_authorization but the CLI call on lines ~75 omits
--api-base-url and the expected output is hard-coded; update the test invocation
that calls the login CLI to pass --api-base-url "http://0.0.0.0:3001" and change
the output assertion to assert against the verification_uri from the mocked
DeviceAuthorization (the "verification_uri" value returned by
_mock_start_device_authorization) instead of a hard-coded host string so the
test aligns with the mock.
- Around line 136-140: The test assertions expect the old success message and
plan but the CLI now prints a fast-path message "Already authenticated" and only
includes the user email; update the test in tests/test_cli_login.py (the block
invoking runner.invoke(main, ["login", "--no-browser"]) and checking
result.output) to assert that result.exit_code == 0, assert "Already
authenticated" (or allow either "Logged in" OR "Already authenticated") is in
result.output, assert "existing@example.com" is in result.output, and remove (or
make optional) the assertion that "pro" is in result.output so the test matches
the current fast-path output from main.

Comment on lines +835 to +841
login_url = f"https://app.refactron.dev/login?{urlencode({'code': auth.user_code})}"

instructions = Text()
instructions.append("Please visit the following URL to authenticate:\n\n", style="dim")
instructions.append(f" {login_url}\n\n", style="underline bold #5f5fff")
instructions.append("Verification Code:\n", style="dim")
instructions.append(f" {auth.user_code}\n", style="bold white")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use the device authorization verification_uri instead of a hard-coded app URL.

Line 835 builds a fixed login URL, which breaks when --api-base-url points to staging/local and ignores the server-provided verification URI. Use auth.verification_uri (optionally appending the code) so the flow works across environments.

🔧 Proposed fix
-    login_url = f"https://app.refactron.dev/login?{urlencode({'code': auth.user_code})}"
+    login_url = auth.verification_uri
+    if auth.user_code:
+        sep = "&" if "?" in login_url else "?"
+        login_url = f"{login_url}{sep}{urlencode({'code': auth.user_code})}"
🤖 Prompt for AI Agents
In `@refactron/cli.py` around lines 835 - 841, Replace the hard-coded app URL
construction assigned to login_url with the server-provided device verification
URI: use auth.verification_uri (and if you want a direct link, append the code
as a query param or use auth.verification_uri_complete if available) instead of
building f"https://app.refactron.dev/…"; update the displayed URL and keep the
displayed verification code using auth.user_code so the Text() instructions
(variable instructions) correctly reflect the server-supplied verification URI
for non-production API bases.

Comment on lines +43 to +48
expires_at_raw = data.get("expires_at")
expires_at: Optional[datetime] = None
if isinstance(expires_at_raw, str) and expires_at_raw.strip():
expires_at = datetime.fromisoformat(expires_at_raw)
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard expires_at parsing against invalid ISO strings.

Line 46 can raise ValueError on malformed timestamps, which bubbles up and contradicts the “returns None if invalid” contract in load_credentials. Treat invalid values as None so the CLI doesn’t crash on a corrupted credentials file.

🔧 Proposed fix
-        if isinstance(expires_at_raw, str) and expires_at_raw.strip():
-            expires_at = datetime.fromisoformat(expires_at_raw)
-            if expires_at.tzinfo is None:
-                expires_at = expires_at.replace(tzinfo=timezone.utc)
+        if isinstance(expires_at_raw, str) and expires_at_raw.strip():
+            try:
+                expires_at = datetime.fromisoformat(expires_at_raw)
+            except ValueError:
+                expires_at = None
+            else:
+                if expires_at.tzinfo is None:
+                    expires_at = expires_at.replace(tzinfo=timezone.utc)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
expires_at_raw = data.get("expires_at")
expires_at: Optional[datetime] = None
if isinstance(expires_at_raw, str) and expires_at_raw.strip():
expires_at = datetime.fromisoformat(expires_at_raw)
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
expires_at_raw = data.get("expires_at")
expires_at: Optional[datetime] = None
if isinstance(expires_at_raw, str) and expires_at_raw.strip():
try:
expires_at = datetime.fromisoformat(expires_at_raw)
except ValueError:
expires_at = None
else:
if expires_at.tzinfo is None:
expires_at = expires_at.replace(tzinfo=timezone.utc)
🤖 Prompt for AI Agents
In `@refactron/core/credentials.py` around lines 43 - 48, The parsing of
expires_at in load_credentials currently calls
datetime.fromisoformat(expires_at_raw) which raises ValueError on malformed ISO
strings; update the block handling expires_at_raw to catch exceptions
(ValueError) around datetime.fromisoformat so that on any parse error expires_at
stays None (preserving the “returns None if invalid” contract), and keep the
existing timezone fix-up (replace tzinfo=timezone.utc) only when parsing
succeeds; reference symbols: expires_at_raw, expires_at, load_credentials,
datetime.fromisoformat.

Comment on lines +19 to +27
def _mock_start_device_authorization(api_base_url: str, timeout_seconds: int = 10):
assert api_base_url == "http://0.0.0.0:3001"
return DeviceAuthorization(
device_code="devcode-123",
user_code="ABCD-EFGH",
verification_uri="https://refactron.dev/auth/device",
expires_in=900,
interval=1,
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pass --api-base-url and align the login URL with the mock response.

Line 19 asserts a local base URL, but Line 75 invokes login without --api-base-url, so it defaults to the production value. Also, the output assertion should track the verification URL returned by the mock to avoid hard-coded host coupling.

🔧 Proposed fix
-        return DeviceAuthorization(
+        return DeviceAuthorization(
             device_code="devcode-123",
             user_code="ABCD-EFGH",
-            verification_uri="https://refactron.dev/auth/device",
+            verification_uri="http://localhost:3000/login",
             expires_in=900,
             interval=1,
         )
@@
-    result = runner.invoke(main, ["login", "--no-browser"])
+    result = runner.invoke(
+        main, ["login", "--no-browser", "--api-base-url", "http://0.0.0.0:3001"]
+    )
@@
-    assert "localhost:3000/login" in result.output
+    assert "localhost:3000/login" in result.output

Also applies to: 75-81

🧰 Tools
🪛 Ruff (0.14.14)

19-19: Unused function argument: timeout_seconds

(ARG001)

🤖 Prompt for AI Agents
In `@tests/test_cli_login.py` around lines 19 - 27, The test asserts the API base
URL inside _mock_start_device_authorization but the CLI call on lines ~75 omits
--api-base-url and the expected output is hard-coded; update the test invocation
that calls the login CLI to pass --api-base-url "http://0.0.0.0:3001" and change
the output assertion to assert against the verification_uri from the mocked
DeviceAuthorization (the "verification_uri" value returned by
_mock_start_device_authorization) instead of a hard-coded host string so the
test aligns with the mock.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a device-code login experience to the Refactron CLI, persists credentials locally, enforces authentication before most commands, and replaces the default Click help with a custom Rich-rendered help/welcome flow.

Changes:

  • Introduces device authorization + token polling helpers and a local credentials store.
  • Adds CLI login/logout/auth status and a global auth gate, plus a new Rich-themed welcome/help UX.
  • Adds pytest coverage for login/logout/auth-status flows (with one placeholder integration test file).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 15 comments.

Show a summary per file
File Description
refactron/cli.py Adds auth gating, login/logout/auth commands, API key verification, and new Rich UI/help/welcome behavior
refactron/core/device_auth.py Implements device-code style auth requests (/oauth/device, /oauth/token)
refactron/core/credentials.py Implements JSON credential persistence with best-effort permission tightening
tests/test_cli_login.py Adds tests for login/logout/auth status behavior
tests/test_cli_integration.py Currently empty placeholder

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

from urllib.parse import urlencode

import click
import requests # type: ignore
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

requests is imported here but it is not declared in the project dependencies (pyproject.toml / requirements.txt). This will cause a runtime ModuleNotFoundError for users installing the package.

Consider either adding requests to the core dependencies (and updating requirements.txt), or rewriting _validate_api_key to use the existing stdlib HTTP approach used in refactron.core.device_auth to avoid a new dependency.

Copilot uses AI. Check for mistakes.
refactron/cli.py Outdated
Comment on lines 738 to 739
# Check authentication for all commands except login/logout
exempt_commands = ["login", "logout"]
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global auth gate runs for any ctx.invoked_subcommand not in ['login','logout'], which includes auth status. That makes refactron auth status unusable when logged out (and conflicts with auth_status() explicitly handling the logged-out case).

Please exempt auth status (or the whole auth group) from the auth gate, or move auth enforcement to only the commands that truly require it.

Suggested change
# Check authentication for all commands except login/logout
exempt_commands = ["login", "logout"]
# Check authentication for all commands except login/logout/auth
exempt_commands = ["login", "logout", "auth"]

Copilot uses AI. Check for mistakes.
Comment on lines +756 to +760
console.print(Align.center(Text("\nAuthentication Required", style="bold")))

if Prompt.ask("\nLog in to continue?", choices=["y", "n"], default="y") == "y":
try:
ctx.invoke(
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This interactive prompt (Prompt.ask) will block or raise EOFError in non-interactive contexts (CI, scripts, CliRunner tests) when credentials are missing.

Consider detecting non-TTY input (or a --non-interactive/--yes flag) and failing fast with a clear message instead of prompting, so CLI commands behave predictably in automation.

Copilot uses AI. Check for mistakes.
Comment on lines +778 to +780
if ctx.invoked_subcommand is None:
_run_startup_animation()
_run_minimal_loop(ctx)
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running the CLI with no subcommand now triggers an animation and then enters an infinite interactive loop, which is a breaking change from typical CLI behavior (it will hang in scripts/CI when refactron is invoked without args).

Consider only entering this mode when stdin/stdout are TTYs, and otherwise default to showing help and exiting non-interactively.

Copilot uses AI. Check for mistakes.
Comment on lines +19 to +21
def _mock_start_device_authorization(api_base_url: str, timeout_seconds: int = 10):
assert api_base_url == "http://0.0.0.0:3001"
return DeviceAuthorization(
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mock asserts api_base_url == 'http://0.0.0.0:3001', but refactron login defaults --api-base-url to DEFAULT_API_BASE_URL (https://api.refactron.dev). As written, the test will fail unless it passes --api-base-url http://0.0.0.0:3001 (or the CLI default is changed).

Update the test invocation to include --api-base-url or relax the assertion to match the current CLI behavior.

Copilot uses AI. Check for mistakes.
creds = load_credentials()
if creds and creds.access_token:
is_authenticated = True
except SystemExit:
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except SystemExit:
except SystemExit:
# User aborted the login flow (e.g. via Ctrl+C or an explicit exit);
# treat this as "not authenticated" and fall through to the normal exit path.

Copilot uses AI. Check for mistakes.
# Best-effort permissions tightening (Windows may ignore chmod semantics).
try:
os.chmod(target, 0o600)
except OSError:
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except OSError:
except OSError:
# Intentionally ignore permission errors: some platforms or filesystems
# may not support chmod semantics, but this should not block saving creds.

Copilot uses AI. Check for mistakes.
data = json.loads(raw) if raw else {}
if isinstance(data, dict):
return data
except Exception:
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'except' clause does nothing but pass and there is no explanatory comment.

Suggested change
except Exception:
except Exception:
# If the error body cannot be parsed as JSON for any reason,
# ignore the parsing failure and fall back to a generic error.

Copilot uses AI. Check for mistakes.
if ctx.invoked_subcommand is None:
_run_startup_animation()
_run_minimal_loop(ctx)
pass
Copy link

Copilot AI Jan 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unnecessary 'pass' statement.

Suggested change
pass

Copilot uses AI. Check for mistakes.
@github-actions github-actions bot added dependencies documentation Improvements or additions to documentation labels Jan 28, 2026
omsherikar and others added 2 commits January 29, 2026 03:07
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@omsherikar omsherikar merged commit 171eccf into main Jan 28, 2026
14 of 15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies documentation Improvements or additions to documentation enhancement New feature or request size: x-large testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant