Skip to content

Commit 64623e6

Browse files
Kevin Allioliclaude
andcommitted
release: v1.2.0 — setup UX polish (completion install + creds validation)
Features - orca completion install [shell]: auto-install shell completion (bash/zsh: append idempotent eval line to ~/.bashrc or ~/.zshrc; fish: write ~/.config/fish/completions/orca.fish via 'orca' subprocess). Auto-detects shell from $SHELL when omitted. Legacy 'orca completion <shell>' still prints instructions for backward compatibility. - orca setup: at the end of the wizard, offer to auto-install shell completion for the detected shell; then validate the just-saved profile by hitting Keystone so typos / wrong auth URLs surface immediately. Both steps are skipped in non-TTY contexts (CI, piped input, tests). - profile list: new 'Auth' column (password / app-cred) and 'User / Credential' column that shows the AC id for application-credential profiles (previously empty); project cell shows '(pre-scoped)' for AC. Refactor - Shell completion helpers moved from commands/setup.py to core/shell_completion.py and shared by both 'setup' and 'completion'. Tests - 2245 passing, coverage 85.59% (gate 85%). Co-Authored-By: Claude Opus 4.7 <[email protected]>
1 parent f784564 commit 64623e6

8 files changed

Lines changed: 519 additions & 15 deletions

File tree

orca_cli/commands/completion.py

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
"""``orca completion`` — generate shell auto-completion scripts."""
1+
"""``orca completion`` — install shell auto-completion or print instructions."""
22

33
from __future__ import annotations
44

55
import click
66
from rich.console import Console
77

8+
from orca_cli.core.shell_completion import (
9+
SUPPORTED_SHELLS,
10+
detect_shell,
11+
install_completion,
12+
)
13+
814
console = Console()
915

1016
INSTRUCTIONS = {
@@ -23,14 +29,87 @@
2329
}
2430

2531

26-
@click.command()
27-
@click.argument("shell", type=click.Choice(["bash", "zsh", "fish"], case_sensitive=False))
28-
def completion(shell: str) -> None:
29-
"""Generate shell completion script and display installation instructions.
30-
31-
Supported shells: bash, zsh, fish.
32-
"""
33-
shell = shell.lower()
32+
def _print_instructions(shell: str) -> None:
3433
console.print(f"\n[bold cyan]Shell completion for {shell}[/bold cyan]\n")
3534
console.print(INSTRUCTIONS[shell])
36-
console.print()
35+
console.print(
36+
f"\n[dim]Or run 'orca completion install {shell}' "
37+
f"to install automatically.[/dim]\n"
38+
)
39+
40+
41+
class _CompletionGroup(click.Group):
42+
"""Group that accepts a bare shell name as shorthand for ``show <shell>``.
43+
44+
Lets users keep the legacy ``orca completion bash`` UX while also exposing
45+
the newer ``orca completion install <shell>`` subcommand.
46+
"""
47+
48+
def resolve_command(self, ctx, args):
49+
if args and args[0].lower() in SUPPORTED_SHELLS:
50+
return super().resolve_command(ctx, ["show", *args])
51+
return super().resolve_command(ctx, args)
52+
53+
54+
@click.group(cls=_CompletionGroup, invoke_without_command=True)
55+
@click.pass_context
56+
def completion(ctx: click.Context) -> None:
57+
"""Shell completion: print instructions or install automatically.
58+
59+
\b
60+
Examples:
61+
orca completion # auto-detect shell, print instructions
62+
orca completion bash # print instructions for bash (legacy)
63+
orca completion show zsh # same, explicit
64+
orca completion install # auto-detect shell, install
65+
orca completion install fish # install for fish
66+
"""
67+
if ctx.invoked_subcommand is not None:
68+
return
69+
# Bare ``orca completion`` → auto-detect + print
70+
detected = detect_shell()
71+
if not detected:
72+
console.print(
73+
"[yellow]Could not auto-detect your shell.[/yellow] "
74+
"Pass one explicitly: [cyan]orca completion <bash|zsh|fish>[/cyan]."
75+
)
76+
ctx.exit(1)
77+
_print_instructions(detected)
78+
79+
80+
@completion.command("show")
81+
@click.argument("shell", required=False,
82+
type=click.Choice(list(SUPPORTED_SHELLS), case_sensitive=False))
83+
@click.pass_context
84+
def completion_show(ctx: click.Context, shell: str | None) -> None:
85+
"""Print manual installation instructions for the given shell."""
86+
resolved = shell.lower() if shell else detect_shell()
87+
if not resolved:
88+
console.print(
89+
"[yellow]Could not auto-detect your shell.[/yellow] "
90+
"Pass one explicitly: [cyan]orca completion show <bash|zsh|fish>[/cyan]."
91+
)
92+
ctx.exit(1)
93+
_print_instructions(resolved)
94+
95+
96+
@completion.command("install")
97+
@click.argument("shell", required=False,
98+
type=click.Choice(list(SUPPORTED_SHELLS), case_sensitive=False))
99+
@click.pass_context
100+
def completion_install(ctx: click.Context, shell: str | None) -> None:
101+
"""Install orca shell completion (auto-detects shell when omitted).
102+
103+
\b
104+
- bash/zsh: appends an eval line to ~/.bashrc / ~/.zshrc (idempotent).
105+
- fish: writes ~/.config/fish/completions/orca.fish.
106+
"""
107+
resolved = shell.lower() if shell else detect_shell()
108+
if not resolved:
109+
console.print(
110+
"[red]Could not auto-detect your shell.[/red] "
111+
"Pass one explicitly: [cyan]orca completion install <bash|zsh|fish>[/cyan]."
112+
)
113+
ctx.exit(1)
114+
msg = install_completion(resolved)
115+
console.print(f"[green]✓ {msg}[/green]")

orca_cli/commands/profile.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ def profile_list() -> None:
8787
"""List all profiles."""
8888
from rich.table import Table
8989

90+
from orca_cli.core.config import _is_app_cred
91+
9092
profiles = list_profiles()
9193
active = get_active_profile_name()
9294

@@ -97,8 +99,9 @@ def profile_list() -> None:
9799
table = Table(title="Profiles", show_lines=False)
98100
table.add_column("", no_wrap=True)
99101
table.add_column("Name", style="bold")
102+
table.add_column("Auth")
100103
table.add_column("Auth URL")
101-
table.add_column("Username")
104+
table.add_column("User / Credential")
102105
table.add_column("Project")
103106
table.add_column("Color")
104107

@@ -107,12 +110,27 @@ def profile_list() -> None:
107110
marker = "[green bold]●[/green bold]" if name == active else " "
108111
display_name = f"[{color}]{name}[/{color}]" if color else name
109112
color_preview = f"[{color}]●[/{color}] {color}" if color else "—"
110-
project = cfg.get("project_name") or cfg.get("project_id") or "—"
113+
114+
if _is_app_cred(cfg):
115+
auth_label = "[magenta]app-cred[/magenta]"
116+
user_cell = (
117+
cfg.get("application_credential_id")
118+
or cfg.get("application_credential_name")
119+
or "—"
120+
)
121+
# AC is pre-scoped — project lives in the token, not the config.
122+
project = "[dim](pre-scoped)[/dim]"
123+
else:
124+
auth_label = "[cyan]password[/cyan]"
125+
user_cell = cfg.get("username") or "—"
126+
project = cfg.get("project_name") or cfg.get("project_id") or "—"
127+
111128
table.add_row(
112129
marker,
113130
display_name,
131+
auth_label,
114132
cfg.get("auth_url", "—"),
115-
cfg.get("username", "—"),
133+
user_cell,
116134
project,
117135
color_preview,
118136
)

orca_cli/commands/setup.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
import sys
6+
57
import click
68
from rich.console import Console
79

@@ -13,9 +15,74 @@
1315
set_active_profile,
1416
)
1517
from orca_cli.core.context import OrcaContext
18+
from orca_cli.core.shell_completion import detect_shell, install_completion
1619

1720
console = Console()
1821

22+
23+
def _maybe_install_completion() -> None:
24+
"""Offer to install shell completion for the detected shell.
25+
26+
Skipped when stdin isn't a TTY (CI, piped input, test harness) so the
27+
wizard stays non-destructive outside interactive sessions.
28+
"""
29+
if not sys.stdin.isatty():
30+
return
31+
shell = detect_shell()
32+
if not shell:
33+
console.print(
34+
"[dim]Could not auto-detect your shell. "
35+
"Run 'orca completion install <bash|zsh|fish>' if you want completion.[/dim]"
36+
)
37+
return
38+
console.print(
39+
f"\n[bold]Shell completion[/bold] (detected: [cyan]{shell}[/cyan])"
40+
)
41+
if not click.confirm(" Install orca completion now?", default=True):
42+
console.print(
43+
f"[dim]Skipped. Run 'orca completion install {shell}' later if you change your mind.[/dim]"
44+
)
45+
return
46+
msg = install_completion(shell)
47+
console.print(f"[green]✓ {msg}[/green]")
48+
49+
50+
def _maybe_validate_credentials(name: str) -> None:
51+
"""Attempt a fresh Keystone authentication with the just-saved profile.
52+
53+
Skipped when stdin isn't a TTY (test harness, piped input). This is a
54+
read-only check: it authenticates, prints success/failure, then closes.
55+
"""
56+
if not sys.stdin.isatty():
57+
return
58+
console.print(f"\n[bold]Validating credentials for '{name}'...[/bold]")
59+
from orca_cli.core.client import OrcaClient
60+
from orca_cli.core.exceptions import APIError, AuthenticationError, OrcaCLIError
61+
cfg = load_config(profile_name=name)
62+
client = None
63+
try:
64+
client = OrcaClient(cfg)
65+
# Force a fresh auth to actually hit Keystone (bypasses any cached
66+
# token that might belong to a prior profile with the same key).
67+
client._authenticate()
68+
console.print(f"[green]✓ Authentication successful[/green] "
69+
f"(catalog: {len(client._catalog)} services)")
70+
except AuthenticationError as exc:
71+
console.print(f"[red]✗ Authentication failed:[/red] {exc.message}")
72+
console.print("[dim]Re-run 'orca setup' to fix the credentials.[/dim]")
73+
except APIError as exc:
74+
console.print(f"[red]✗ Keystone error ({exc.status_code}):[/red] {exc.message}")
75+
except OrcaCLIError as exc:
76+
console.print(f"[yellow]⚠ {exc.message}[/yellow]")
77+
except Exception as exc: # pragma: no cover — safety net
78+
console.print(f"[yellow]⚠ Could not validate: {exc}[/yellow]")
79+
finally:
80+
if client is not None:
81+
try:
82+
client.close()
83+
except Exception:
84+
pass
85+
1986
_PASSWORD_FIELDS = [
2087
("auth_url", "Auth URL (Keystone)", "https://keystone.example.com:5000"),
2188
("username", "Username", ""),
@@ -125,3 +192,6 @@ def setup(ctx: click.Context, profile_name: str | None) -> None:
125192
console.print(f"[green]Switched to '{name}'.[/green]\n")
126193
else:
127194
console.print()
195+
196+
_maybe_install_completion()
197+
_maybe_validate_credentials(name)

orca_cli/core/shell_completion.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
"""Shell completion install helpers.
2+
3+
Used by both ``orca setup`` (offers auto-install at the end of the wizard)
4+
and ``orca completion`` (dedicated command to install or print instructions).
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import os
10+
import shutil
11+
import subprocess
12+
from pathlib import Path
13+
14+
_COMPLETION_EVAL = {
15+
"bash": 'eval "$(_ORCA_COMPLETE=bash_source orca)"',
16+
"zsh": 'eval "$(_ORCA_COMPLETE=zsh_source orca)"',
17+
}
18+
_COMPLETION_MARKER = "_ORCA_COMPLETE=" # presence = already installed
19+
20+
_RC_FILE = {
21+
"bash": Path("~/.bashrc"),
22+
"zsh": Path("~/.zshrc"),
23+
}
24+
_FISH_COMPLETION_FILE = Path("~/.config/fish/completions/orca.fish")
25+
26+
SUPPORTED_SHELLS = ("bash", "zsh", "fish")
27+
28+
29+
def detect_shell() -> str | None:
30+
"""Return ``bash``/``zsh``/``fish`` based on ``$SHELL``, or ``None``."""
31+
shell_env = os.environ.get("SHELL", "")
32+
if not shell_env:
33+
return None
34+
name = Path(shell_env).name
35+
return name if name in SUPPORTED_SHELLS else None
36+
37+
38+
def install_completion_bashzsh(shell: str) -> str:
39+
"""Append the completion eval line to the shell rc file (idempotent).
40+
41+
Returns a human-readable status message.
42+
"""
43+
line = _COMPLETION_EVAL[shell]
44+
rc = _RC_FILE[shell].expanduser()
45+
if rc.exists() and _COMPLETION_MARKER in rc.read_text():
46+
return f"Already present in {rc}."
47+
rc.parent.mkdir(parents=True, exist_ok=True)
48+
with rc.open("a") as fh:
49+
fh.write(f"\n# orca-cli shell completion\n{line}\n")
50+
return f"Appended to {rc}. Open a new shell or run: source {rc}"
51+
52+
53+
def install_completion_fish() -> str:
54+
"""Generate the fish completion script via ``orca`` and write it to
55+
``~/.config/fish/completions/orca.fish``.
56+
"""
57+
target = _FISH_COMPLETION_FILE.expanduser()
58+
if target.exists() and _COMPLETION_MARKER in target.read_text():
59+
return f"Already present at {target}."
60+
exe = shutil.which("orca")
61+
if not exe:
62+
return "orca not on PATH — run 'orca completion fish' for manual install."
63+
target.parent.mkdir(parents=True, exist_ok=True)
64+
env = os.environ.copy()
65+
env["_ORCA_COMPLETE"] = "fish_source"
66+
try:
67+
result = subprocess.run(
68+
[exe], env=env, capture_output=True, text=True, timeout=10, check=False,
69+
)
70+
except (OSError, subprocess.SubprocessError) as exc:
71+
return f"Failed to generate fish completion: {exc}"
72+
if result.returncode != 0 or not result.stdout.strip():
73+
return f"orca completion generation failed: {result.stderr.strip() or 'empty output'}"
74+
target.write_text(result.stdout)
75+
return f"Wrote {target}. Start a new fish session."
76+
77+
78+
def install_completion(shell: str) -> str:
79+
"""Install completion for ``shell``. Dispatches to the right helper."""
80+
if shell == "fish":
81+
return install_completion_fish()
82+
if shell in ("bash", "zsh"):
83+
return install_completion_bashzsh(shell)
84+
raise ValueError(f"Unsupported shell: {shell}")

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
44

55
[tool.poetry]
66
name = "orca-openstackclient"
7-
version = "1.1.0"
7+
version = "1.2.0"
88
description = "orca — OpenStack Rich Command-line Alternative"
99
authors = ["Kevin Allioli"]
1010
license = "Apache-2.0"

0 commit comments

Comments
 (0)