# Task ID: 18

**Title:** Implement Users CLI Commands

**Status:** done

**Dependencies:** 8, 11, 17

**Priority:** high

**Description:** Create the users command group with list, show, and search commands.

**Details:**

Create `src/teams_phone/cli/users.py`:
```python
users_app = typer.Typer(name="users", help="User voice configuration management")

@users_app.command("list")
def list_users(
    ctx: typer.Context,
    licensed: Optional[bool] = typer.Option(None, "--licensed/--unlicensed"),
    assigned: Optional[bool] = typer.Option(None, "--assigned/--unassigned"),
    account_type: Optional[str] = typer.Option(None, "--type"),
    limit: int = typer.Option(100, "--limit"),
):
    """List users with voice configuration."""
    # Display: displayName, UPN, phoneNumber, isVoiceEnabled, accountType
    ...

@users_app.command()
def show(ctx: typer.Context, user: str = typer.Argument(..., help="User UPN, name, or ID")):
    """Show detailed voice configuration for a user."""
    # Display: all user fields + policies with assignment source
    ...

@users_app.command()
def search(ctx: typer.Context, query: str = typer.Argument(...)):
    """Search users by name, UPN, or phone number."""
    ...
```

**Test Strategy:**

CLI tests with mocked UserService: verify list filters, show displays policy sources, search returns matches, JSON output is valid.

---

## Implementation Notes

### Session: 2026-01-29 19:15 - Agent: claude-opus-4-5-20251101

**Status:** Implemented

#### What Was Done
- Created `src/teams_phone/cli/users.py` with full users command group implementation
- Implemented `list` command with filters: `--licensed/--unlicensed`, `--assigned/--unassigned`, `--account-type`, `--limit`, `--all`
- Implemented `show` command with user identifier resolution (UPN, display name, or object ID)
- Implemented `search` command with `--field` (name/upn/phone) and `--limit` options
- Updated `src/teams_phone/cli/main.py` to import `users_app` from users.py (removed placeholder)
- Created `tests/unit/test_cli_users.py` with 33 comprehensive unit tests
- All filters passed through to UserService correctly
- Mutually exclusive options validated (--limit/--all)
- JSON output support for all commands

#### Key Decisions Made
- **Async bridging pattern**: Used `asyncio.run()` with async context manager for GraphClient. Created `_run_async()` helper that handles the full async lifecycle.
- **Default limit of 50**: CLI defaults to 50 users for list command (vs. 100 in the task spec) to match CLI spec document.
- **SearchField enum**: Created enum for `--field` option values (name, upn, phone) for type safety.
- **Phone number display formatting**: Uses `—` (em dash) for missing phone numbers, formats account types as human-readable strings.

#### Patterns Discovered
- **Async service pattern**: See `users.py:42-60` (`_run_async()`) - creates GraphClient as async context manager, instantiates UserService inside, returns tuple of (result, formatter)
- **Boolean flag pairs**: Typer's `--licensed/--unlicensed` syntax automatically handles the pair as Optional[bool] with three states (True, False, None)
- **Rich table ANSI escapes**: When testing Rich tables, use `--no-color` flag or check for partial matches to avoid ANSI escape code issues

#### Open Items for Next Agent
- [ ] The `--field` option for search doesn't fully constrain the search - UserService's auto-detection logic still applies. May need to enhance UserService if strict field filtering is required.
- [ ] Phone number search with `--field phone` prepends `+` for numeric queries, but non-numeric phone patterns (like `*5678`) aren't handled.

#### Gotchas and Warnings
- **Import UserService directly**: Must use `from teams_phone.services.user_service import UserService` NOT `from teams_phone.services import UserService` to avoid circular import.
- **ANSI escapes in tests**: Rich library adds color codes even in tests. Use `--no-color` flag or check for partial string matches.
- **Pre-existing test failure**: `test_output_formatter.py::TestSuccessMessage::test_success_includes_checkmark` fails due to Windows encoding - not related to this task.

---
