# CLI Specification: Teams Phone CLI MVP

**Version:** 1.4
**Date:** 2026-01-27
**Status:** Draft (Aligned with Graph API Contracts v1.7 - includes locationId enforcement by number type)

---

## Overview

This document defines the complete CLI functionality for the Teams Phone CLI MVP. The CLI follows a consistent `<noun> <verb> [args] [--flags]` command structure and provides six core command groups.

## Architecture: Hybrid Graph API + CSV Export

The CLI uses a **hybrid architecture** based on Microsoft Graph API availability:

### Graph API (Real-time)
The following operations use Microsoft Graph API beta endpoints for real-time data:
- **Users**: All operations (`list`, `show`, `search`) via `/admin/teams/userConfigurations`
- **Numbers**: All operations (`list`, `show`, `search`, `assign`, `unassign`, `update`) via `/admin/teams/telephoneNumberManagement/numberAssignments`
- **Policies**: Assignment operations (`assign`, `bulk-assign`) via `/admin/teams/policy/userAssignments/assign`
- **Policies**: User policy viewing via `userConfigurations.effectivePolicyAssignments`

### PowerShell CSV Export (Reference Data)
The following operations **require PowerShell CSV export** because Graph API endpoints do not exist:
- **Locations**: All operations (`list`, `show`, `search`) - no Graph API available
- **Policies**: List available policies (`policies list`) - no Graph API endpoint for listing policy definitions

### CSV Export Requirement

Users must run the PowerShell export script periodically to generate reference data:

```powershell
./Export-TeamsPhoneData.ps1 `
  -TenantId "your-tenant-id" `
  -ApplicationId "your-app-id" `
  -CertificateThumbprint "your-cert-thumbprint" `
  -OutputPath "~/.teams-phone/cache/"
```

**Generated Files:**
- `~/.teams-phone/cache/locations.csv` - Emergency locations
- `~/.teams-phone/cache/policies.csv` - Available policies by type
- `~/.teams-phone/cache/export-metadata.json` - Export timestamp and counts

**Degraded Mode Behavior:**
- If CSV files are missing, commands display a warning but may continue with reduced functionality
- Location validation during number assignment will warn if location ID cannot be verified
- Policy tab completion and validation will be unavailable
- CSV files older than 30 days trigger a staleness warning

**See Also:** `graph-api-contracts.md` Section 7 for detailed CSV schemas and PowerShell cmdlet documentation.

---

## Global Options

All commands support these global options:

| Option | Short | Description |
|--------|-------|-------------|
| `--tenant <name>` | `-t` | Override default tenant for this command |
| `--json` | `-j` | Output in JSON format for automation |
| `--verbose` | `-v` | Show detailed output and debug information |
| `--no-cache` | | Force fresh data fetch, ignore cache |
| `--debug-log <path>` | | Write sanitized JSON diagnostic logs to a file |
| `--help` | `-h` | Show help for command |
| `--version` | | Show application version |

---

## Authentication Behavior

Commands that require authentication (all commands except `auth login`, `auth status`, `tenants list`, `tenants add`, and `--help`/`--version`) will automatically trigger authentication if no valid token exists:

1. **Auto-login flow:** If no cached token exists or token is expired and cannot be refreshed, the CLI automatically initiates the tenant's configured auth method (default: certificate).
2. **Non-interactive auth:** Certificate or client-secret authentication proceeds automatically without user interaction.
3. **Auth failure:** If authentication fails (missing certificate, invalid secret, etc.), the command fails with exit code 2 and guidance to check credentials or run `auth login` first.
4. **Explicit control:** Use `auth login` to authenticate before running commands, especially in scripts.

**Example behavior:**
```bash
# First run - certificate configured
$ teams-phone users list
Authenticating with certificate... ✓ Authenticated

# Command proceeds after successful auth
┌─────────────┬─────────────────────────┬──────────────┐
│ Display Name│ UPN                     │ Phone Number │
...

# First run - no credentials configured
$ teams-phone users list
Error: No valid authentication credentials found.

  Configure authentication using one of these methods:
  - Certificate: teams-phone auth login --method certificate --cert-path /path/to/cert.pem
  - Client secret: teams-phone auth login --method client-secret --secret <secret>

  Or set credentials in config file (~/.teams-phone/config.toml)
```

---

## Command Group 1: Users

Manage and query user voice configurations.

### `users list`

List users with voice-related configuration fields and filtering (includes licensed/unlicensed and assigned/unassigned).

```
teams-phone users list [OPTIONS]
```

**Options:**
| Option | Description |
|--------|-------------|
| `--licensed` | Show only users with Teams Phone license |
| `--unlicensed` | Show only users without Teams Phone license |
| `--assigned` | Show only users with phone numbers assigned |
| `--unassigned` | Show only users without phone numbers |
| `--account-type <type>` | Filter by account type: `user`, `resource-account`, `guest` |
| `--limit <n>` | Limit results to n users (default: 50) |
| `--all` | Fetch all users (may be slow for large tenants) |

**Validation:**
- If both `--licensed` and `--unlicensed` are provided → error: "Cannot specify both --licensed and --unlicensed"
- If both `--assigned` and `--unassigned` are provided → error: "Cannot specify both --assigned and --unassigned"
- If both `--limit` and `--all` are provided → error: "Cannot specify both --limit and --all"

**Output Fields:**
- Display Name
- UPN (User Principal Name)
- Account Type (User/Resource Account/Guest)
- Phone Number (or "—" if none)
- License Status (Licensed/Unlicensed)
- Voice Enabled (Yes/No)
- Location

**Example:**
```bash
# List first 50 users (default limit)
teams-phone users list

# List all licensed users without phone numbers
teams-phone users list --licensed --unassigned --all

# Output as JSON for scripting
teams-phone users list --json
```

---

### `users show <user>`

Show detailed voice configuration for a specific user.

```
teams-phone users show <user> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<user>` | User identifier: UPN, display name, or object ID |

**Resolution Behavior:**
- UPN (contains @): Exact match required
- Object ID (UUID format): Exact match required
- Display name: Partial match; if multiple matches found, error with candidate list

**Output Sections:**
1. **Identity**: Display name, UPN, Object ID, Account Type, Department, Location
2. **Licensing**: Teams Phone license status, license SKU
3. **Phone Number**: Assigned number, number type, assignment date
4. **Voice Policies**:
   - Calling Policy
   - Voicemail Policy
   - Emergency Calling Policy
   - Emergency Call Routing Policy
   - Caller ID Policy
   - Dial Plan
   - Voice Routing Policy (if Direct Routing)
5. **Voice Status**: Enterprise Voice enabled, Hosted Voicemail enabled

**Example:**
```bash
# Show user by UPN
teams-phone users show john.doe@contoso.com

# Show user by display name
teams-phone users show "John Doe"

# Show user with verbose API details
teams-phone users show john.doe@contoso.com --verbose
```

---

### `users search <query>`

Search for users by name, UPN, or phone number.

```
teams-phone users search <query> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<query>` | Search term (partial match supported) |

**Options:**
| Option | Description |
|--------|-------------|
| `--field <field>` | Limit search to specific field: `name`, `upn`, `phone` |
| `--limit <n>` | Maximum results (default: 25) |

**Example:**
```bash
# Search by partial name
teams-phone users search "john"

# Search by phone number ending
teams-phone users search "5678" --field phone

# Search by UPN domain
teams-phone users search "@contoso.com" --field upn
```

---

## Command Group 2: Numbers

Manage phone number inventory.

### `numbers list`

List all phone numbers in the tenant inventory.

```
teams-phone numbers list [OPTIONS]
```

**Options:**
| Option | Description |
|--------|-------------|
| `--status <status>` | Filter by status: `assigned`, `unassigned`, `reserved` |
| `--type <type>` | Filter by number type: `calling-plan`, `direct-routing`, `operator-connect` |
| `--city <city>` | Filter by city/location |
| `--assigned-to <user>` | Filter by assigned user (UPN, display name, or object ID) |
| `--limit <n>` | Limit results (default: 100) |
| `--all` | Fetch all numbers (may be slow for large inventories) |

**Validation:**
- If both `--limit` and `--all` are provided → error: "Cannot specify both --limit and --all"

**Output Fields:**
- Phone Number (E.164 format)
- Status (Assigned/Unassigned/Reserved)
- Type (Calling Plan/Direct Routing/Operator Connect)
- Assigned To (user or "—")
- City/Location
- Capabilities (User/Service/Conference)

**Example:**
```bash
# List all phone numbers
teams-phone numbers list

# List only unassigned Calling Plan numbers
teams-phone numbers list --status unassigned --type calling-plan

# List all numbers in Seattle
teams-phone numbers list --city Seattle --all
```

---

### `numbers show <number>`

Show detailed information for a specific phone number.

```
teams-phone numbers show <number> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<number>` | Phone number (E.164 or partial) |

**Output Sections:**
1. **Number Details**: Full E.164 number, formatted display, operator ID
2. **Assignment**: Status, assigned user (if any), assignment date, assignment category (primary/private/alternate)
3. **Activation**: Activation state (activated/assignmentPending/assignmentFailed/updatePending/updateFailed)
4. **Source**: Number type, acquisition date, source details (online/onPremises)
5. **Port Status**: Port-in status (if applicable): completed, firmOrderCommitmentAccepted, or N/A
6. **Capabilities**: User assignment, service assignment, conferencing
7. **Location**: City, state/region, country, emergency location ID (if assigned)
8. **Network**: Network site ID (if assigned, for Location-Based Routing)

**Notes:**
- If multiple numbers match a partial input, return an error and list the matches.

**Example:**
```bash
# Show number details
teams-phone numbers show +12065551234

# Show by partial number (last 4 digits)
teams-phone numbers show 1234
```

---

### `numbers search <pattern>`

Search phone numbers by pattern.

```
teams-phone numbers search <pattern> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<pattern>` | Search pattern (supports partial matching, wildcards) |

**Options:**
| Option | Description |
|--------|-------------|
| `--status <status>` | Filter results by status |
| `--assigned-to <user>` | Limit results to numbers assigned to a specific user |
| `--limit <n>` | Maximum results (default: 25) |

**Search Patterns:**
- Last N digits: `1234` matches `+12065551234`
- Area code: `206*` matches all 206 numbers
- Full number: `+12065551234` exact match
- Wildcard: `*555*` matches numbers containing 555

**Example:**
```bash
# Search by last 4 digits
teams-phone numbers search 5678

# Search by area code, unassigned only
teams-phone numbers search "206*" --status unassigned

# Search for pattern
teams-phone numbers search "*555*"
```

---

### `numbers assign <user> <number> [location]`

Assign a phone number to a user with emergency location.

```
teams-phone numbers assign <user> <number> [location] [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<user>` | User identifier (UPN, display name, or object ID) |
| `<number>` | Phone number to assign (E.164 format: +12065551234) |
| `[location]` | Emergency location (ID or name). **Required for Calling Plan numbers** (E911 regulations). Optional for Direct Routing (can be set later via `numbers update`). Required unless default configured. |

**Options:**
| Option | Description |
|--------|-------------|
| `--location <loc>` | Emergency location (alternative to positional argument) |
| `--dry-run` | Preview the assignment without making changes |
| `--force` | Skip confirmation prompt |
| `--wait` | Wait for operation to complete (default: true) |
| `--no-wait` | Return immediately after submitting (async mode) |
| `--timeout <seconds>` | Maximum wait time (default: 60) |

**Async Operation Behavior:**

This command uses the Graph API `assignNumber` endpoint which returns `202 Accepted` and operates asynchronously:

1. **Request submitted** → API returns operation ID
2. **Polling** → CLI polls `/operations/{id}` endpoint every 2-5 seconds
3. **Terminal states** → `succeeded`, `failed`, or `skipped`
4. **Default behavior** → CLI waits for completion (up to 60s timeout)
5. **Non-blocking mode** → Use `--no-wait` to return immediately with operation ID

**Progress Display:**
- Shows spinner/progress indicator while waiting
- Displays final status: ✓ Success or ✗ Failed with error details
- With `--verbose`: shows polling attempts and operation state transitions

**Phone Number Format:**
- Input accepts E.164 format: `+12065551234`
- API requires digits only (no `+`): `12065551234`
- CLI automatically normalizes format before API call

**Validations:**
- User must have Teams Phone license
- Number must be unassigned and have `userAssignment` capability
- Number must be in tenant inventory
- Emergency location must exist (validated against CSV cache if available)

**Number Type Specific Requirements:**
- **Calling Plan (US/Canada)**: Emergency location is REQUIRED (E911 regulatory requirement). API call will fail without locationId.
- **Calling Plan (EMEA)**: Emergency location is REQUIRED at number acquisition/porting time.
- **Direct Routing**: Emergency location is OPTIONAL. Can be assigned without location and set later via `numbers update`.
- **Operator Connect**: Emergency location requirement varies by provider (typically required).

**Note on API vs Enforcement:** The Graph API schema marks `locationId` as optional, but backend validation enforces it for Calling Plan numbers due to E911 regulations. The CLI detects number type and enforces location requirement accordingly.

**Location Resolution:**
1. If both `[location]` and `--location` are provided and differ → error: "Conflicting locations specified"
2. If both are provided and match → use the provided value
3. If only one is provided → use it
4. Else if `default_location` configured for tenant → use it
5. Else if number type is Calling Plan or Operator Connect → error: "Emergency location required for Calling Plan numbers (E911 regulations)" with list of available locations (exit code 5)
6. Else (Direct Routing) → proceed without location (can be set later via `numbers update`)

**Location Lookup:**
- Location can be specified by ID (UUID) or name (case-insensitive partial match)
- If name matches multiple locations → error with candidate list
- If location not found → error: "Location not found: <name>" with suggestions from `locations search`
- If location exists but is not validated → warning displayed, assignment proceeds

**Example:**
```bash
# Assign number with location
teams-phone numbers assign john.doe@contoso.com +12065551234 "Seattle-HQ"

# Assign using --location flag
teams-phone numbers assign john.doe@contoso.com +12065551234 --location "Seattle-HQ"

# Assign using default location from config
teams-phone numbers assign john.doe@contoso.com +12065551234

# Preview assignment
teams-phone numbers assign john.doe@contoso.com +12065551234 "Seattle-HQ" --dry-run

# Assign without confirmation prompt
teams-phone numbers assign john.doe@contoso.com +12065551234 "Seattle-HQ" --force
```

---

### `numbers unassign <number>`

Remove phone number assignment from a user.

```
teams-phone numbers unassign <number> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<number>` | Phone number to unassign (E.164 format: +12065551234) |

**Options:**
| Option | Description |
|--------|-------------|
| `--dry-run` | Preview the unassignment without making changes |
| `--force` | Skip confirmation prompt |
| `--wait` | Wait for operation to complete (default: true) |
| `--no-wait` | Return immediately after submitting (async mode) |
| `--timeout <seconds>` | Maximum wait time (default: 60) |

**Async Operation Behavior:**

This command uses the Graph API `unassignNumber` endpoint which returns `202 Accepted` and operates asynchronously:

1. **Request submitted** → API returns operation ID
2. **Polling** → CLI polls `/operations/{id}` endpoint every 2-5 seconds
3. **Terminal states** → `succeeded`, `failed`, or `skipped`
4. **Default behavior** → CLI waits for completion (up to 60s timeout)

**Phone Number Format:**
- Input accepts E.164 format: `+12065551234`
- API requires digits only (no `+`): `12065551234`
- CLI automatically normalizes format before API call

**Validations:**
- Number must be currently assigned to a user
- Does NOT require user ID (API determines from number)

**Example:**
```bash
# Unassign number (waits for completion)
teams-phone numbers unassign +12065551234

# Preview unassignment
teams-phone numbers unassign +12065551234 --dry-run

# Unassign without waiting
teams-phone numbers unassign +12065551234 --no-wait
```

---

### `numbers update <number>`

Update phone number settings (location, network site).

```
teams-phone numbers update <number> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<number>` | Phone number to update (E.164 format: +12065551234) |

**Options:**
| Option | Description |
|--------|-------------|
| `--location <loc>` | Update emergency location (ID or name) |
| `--clear-location` | Remove emergency location assignment |
| `--network-site <site>` | Update network site ID (for Location-Based Routing) |
| `--clear-network-site` | Remove network site assignment |
| `--dry-run` | Preview the update without making changes |
| `--force` | Skip confirmation prompt |

**Operation Behavior:**

This command uses the Graph API `updateNumber` endpoint which is **synchronous** (unlike assign/unassign):

1. **Request submitted** → API returns `200 OK` immediately
2. **No polling required** → Changes are applied synchronously
3. **Instant feedback** → Success or error returned immediately

**Phone Number Format:**
- Input accepts E.164 format: `+12065551234`
- API requires E.164 format with `+` prefix (unlike assign/unassign)
- CLI automatically normalizes format before API call

**Clear vs Update Behavior:**
- `--location <loc>` → Sets location to specified value
- `--clear-location` → Sets location to empty string (removes assignment)
- Omit both → Location remains unchanged
- Same logic applies to network site

**Validations:**
- Number must exist in tenant inventory
- Number must be assigned to a user (cannot update unassigned numbers)
- Emergency location must exist (validated against CSV cache if available)

**Example:**
```bash
# Update emergency location
teams-phone numbers update +12065551234 --location "Seattle-HQ"

# Remove location assignment
teams-phone numbers update +12065551234 --clear-location

# Update network site (for Location-Based Routing)
teams-phone numbers update +12065551234 --network-site "site-seattle-01"

# Preview update
teams-phone numbers update +12065551234 --location "Seattle-HQ" --dry-run

# Update both location and network site
teams-phone numbers update +12065551234 --location "Seattle-HQ" --network-site "site-seattle-01"
```

**Notes:**
- Network sites are used for Location-Based Routing (LBR) scenarios
- If you don't use LBR, you can ignore the network site options
- This is faster than unassigning and reassigning a number to change location

---

### `numbers bulk-assign <csv-file>`

Bulk assign phone numbers from a CSV file.

```
teams-phone numbers bulk-assign <csv-file> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<csv-file>` | Path to CSV file with assignments |

**Options:**
| Option | Description |
|--------|-------------|
| `--dry-run` | Validate and preview all assignments without executing |
| `--continue-on-error` | Continue processing after failures |
| `--retry-failed <file>` | Retry only failed assignments from previous run |
| `--output <file>` | Write results to file (CSV or JSON based on extension) |
| `--default-location <loc>` | Default location for rows without location column |

**CSV Format:**
```csv
user,phone_number,location
john.doe@contoso.com,+12065551234,Seattle-HQ
jane.smith@contoso.com,+12065555678,Portland-Office
bob.wilson@contoso.com,+14255551234,Seattle-HQ
```

**Location Resolution (per row):**
1. If `location` column present and non-empty → use it
2. Else if `--default-location` flag provided → use it
3. Else if `default_location` configured for tenant → use it
4. Else → row fails validation

**Progress Display:**
- Live progress bar showing completion percentage
- Real-time success/failure count
- Per-line status updates
- Final summary with success/failure breakdown

**Example:**
```bash
# Preview bulk assignment
teams-phone numbers bulk-assign assignments.csv --dry-run

# Execute bulk assignment
teams-phone numbers bulk-assign assignments.csv

# Execute with explicit default location for rows missing location
teams-phone numbers bulk-assign assignments.csv --default-location "Seattle-HQ"

# Continue on errors and save results
teams-phone numbers bulk-assign assignments.csv --continue-on-error --output results.csv

# Retry only failed assignments
teams-phone numbers bulk-assign assignments.csv --retry-failed failed.csv
```

---

## Command Group 3: Policies

Manage voice policy assignments.

### `policies list [type]`

List available policies, optionally filtered by type.

```
teams-phone policies list [type] [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `[type]` | Policy type (optional): `calling`, `voicemail`, `emergency`, `emergency-routing`, `caller-id`, `dial-plan`, `voice-routing` |

**Options:**
| Option | Description |
|--------|-------------|
| `--show-assignments` | Include user count per policy |

**Data Source: PowerShell CSV Export**

⚠️ This command reads from `~/.teams-phone/cache/policies.csv` generated by PowerShell export script.

**Graph API Limitation:** No Graph API endpoint exists for listing policy definitions. The `effectivePolicyAssignments` field in `userConfigurations` shows which policies are assigned to users, but there's no endpoint to list all available policies by type.

**Policy Types Supported:**
- `TeamsCallingPolicy` → CLI type: `calling`
- `TeamsVoicemailPolicy` → CLI type: `voicemail`
- `CallingLineIdentity` → CLI type: `caller-id`
- `TenantDialPlan` → CLI type: `dial-plan`
- `OnlineVoiceRoutingPolicy` → CLI type: `voice-routing`
- `TeamsEmergencyCallingPolicy` → CLI type: `emergency`
- `TeamsEmergencyCallRoutingPolicy` → CLI type: `emergency-routing`

**⚠️ Known Limitation - Voicemail Policy:**
According to Graph API documentation, `TeamsVoicemailPolicy` is not supported in the batch policy assignment endpoint. However, it may work via the individual assignment endpoint. If voicemail policy assignment fails, users should use PowerShell: `Grant-CsOnlineVoicemailPolicy -Identity <user> -PolicyName <policy>`.

**Status:** Requires testing to confirm Graph API support for individual assignment.

**Degraded Mode:** If CSV file is missing or stale, command displays warning and may not show all available policies.

**Output Fields:**
- Policy Name
- Policy Type
- Description (if available)
- Is Global (Yes/No)
- Assigned Users (if --show-assignments, requires cross-referencing with user data)

**Example:**
```bash
# List all policy types
teams-phone policies list

# List only calling policies
teams-phone policies list calling

# List voicemail policies with assignment counts
teams-phone policies list voicemail --show-assignments
```

---

### `policies show <user>`

Show all policy assignments for a user.

```
teams-phone policies show <user> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<user>` | User identifier (UPN, display name, or object ID) |

**Output:**
Table showing all policy types and the user's assigned policy for each, including assignment source:
- Calling Policy (with assignment source: Direct or via Group)
- Voicemail Policy (with assignment source)
- Emergency Calling Policy (with assignment source)
- Emergency Call Routing Policy (with assignment source)
- Caller ID Policy (with assignment source)
- Dial Plan (with assignment source)
- Voice Routing Policy (with assignment source)

**Assignment Source:**
- **Direct**: Policy assigned directly to the user
- **Group**: Policy assigned via group membership (shows group name/ID)
- **Global**: User has no specific assignment (using tenant default)

**Note:** Understanding assignment source helps troubleshoot policy conflicts and manage group-based assignments.

**Example:**
```bash
# Show all policies for user
teams-phone policies show john.doe@contoso.com
```

---

### `policies assign <user> <policy-type> <policy-name>`

Assign a policy to a user.

```
teams-phone policies assign <user> <policy-type> <policy-name> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<user>` | User identifier |
| `<policy-type>` | Type of policy: `calling`, `voicemail`, `emergency`, `emergency-routing`, `caller-id`, `dial-plan`, `voice-routing` |
| `<policy-name>` | Name of the policy to assign (use "Global" for default) |

**Options:**
| Option | Description |
|--------|-------------|
| `--dry-run` | Preview assignment without making changes |
| `--force` | Skip confirmation prompt |

**Example:**
```bash
# Assign calling policy
teams-phone policies assign john.doe@contoso.com calling "AllowCalling"

# Assign voicemail policy with preview
teams-phone policies assign john.doe@contoso.com voicemail "TranscriptionEnabled" --dry-run

# Reset to global/default policy
teams-phone policies assign john.doe@contoso.com calling "Global"
```

---

### `policies bulk-assign <csv-file>`

Bulk assign policies from a CSV file.

```
teams-phone policies bulk-assign <csv-file> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<csv-file>` | Path to CSV file with policy assignments |

**Options:**
| Option | Description |
|--------|-------------|
| `--dry-run` | Validate and preview all assignments |
| `--continue-on-error` | Continue processing after failures |
| `--output <file>` | Write results to file |

**CSV Format:**
```csv
user,policy_type,policy_name
john.doe@contoso.com,calling,AllowCalling
jane.smith@contoso.com,voicemail,TranscriptionEnabled
```

**Example:**
```bash
# Preview bulk policy assignment
teams-phone policies bulk-assign policies.csv --dry-run

# Execute bulk assignment
teams-phone policies bulk-assign policies.csv --continue-on-error
```

---

## Command Group 4: Tenants

Manage multi-tenant configuration.

### `tenants list`

List all configured tenant profiles.

```
teams-phone tenants list [OPTIONS]
```

**Output Fields:**
- Profile Name
- Display Name
- Tenant ID
- Status (Current/Available)
- Last Used

**Example:**
```bash
teams-phone tenants list
```

---

### `tenants current`

Show the currently active tenant.

```
teams-phone tenants current [OPTIONS]
```

**Output:**
- Profile name
- Display name
- Tenant ID
- Connection status

**Example:**
```bash
teams-phone tenants current
```

---

### `tenants switch <name>`

Switch to a different tenant profile.

```
teams-phone tenants switch <name> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<name>` | Tenant profile name |

**Example:**
```bash
# Switch to different tenant
teams-phone tenants switch fabrikam

# Switch and verify
teams-phone tenants switch contoso && teams-phone tenants current
```

---

### `tenants add`

Add a new tenant profile (interactive wizard).

```
teams-phone tenants add [OPTIONS]
```

**Options:**
| Option | Description |
|--------|-------------|
| `--name <name>` | Profile name (skip prompt) |
| `--tenant-id <id>` | Tenant ID (skip prompt) |
| `--client-id <id>` | Client/Application ID (skip prompt) |
| `--display-name <name>` | Display name (skip prompt) |
| `--auth-method <method>` | Auth method: `certificate` (recommended), `client-secret` |

**Behavior:**
- **Full interactive:** If no flags provided, prompts for all required values
- **Partial flags:** Prompts only for missing required values (name, tenant-id, client-id)
- **Full non-interactive:** If all required flags provided, skips prompts entirely
- **Validation:** Always validates tenant-id and client-id are valid UUIDs before saving

**Interactive Prompts (when needed):**
1. Profile name (short identifier for CLI)
2. Tenant ID (GUID from Azure AD)
3. Client ID (App Registration ID)
4. Display name (friendly name, defaults to profile name if skipped)
5. Authentication method selection (defaults to certificate)
6. Test connection (optional, prompted in interactive mode)

**Example:**
```bash
# Interactive wizard
teams-phone tenants add

# Non-interactive with all options (certificate)
teams-phone tenants add \
  --name contoso \
  --tenant-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
  --client-id "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy" \
  --display-name "Contoso Ltd" \
  --auth-method certificate
```

---

### `tenants remove <name>`

Remove a tenant profile.

```
teams-phone tenants remove <name> [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<name>` | Tenant profile name to remove |

**Options:**
| Option | Description |
|--------|-------------|
| `--force` | Skip confirmation prompt |

**Example:**
```bash
# Remove with confirmation
teams-phone tenants remove oldtenant

# Remove without confirmation
teams-phone tenants remove oldtenant --force
```

---

### `tenants test [name]`

Test connection to a tenant.

```
teams-phone tenants test [name] [OPTIONS]
```

**Arguments:**
| Argument | Description |
|----------|-------------|
| `[name]` | Tenant profile name (default: current tenant) |

**Tests Performed:**
1. Authentication (token acquisition)
2. Graph API connectivity
3. Required permissions validation
4. Teams Phone Graph endpoint access (beta where required)

**Example:**
```bash
# Test current tenant
teams-phone tenants test

# Test specific tenant
teams-phone tenants test fabrikam
```

---

## Command Group 5: Auth

Authentication management commands.

### `auth login`

Authenticate with the current tenant.

```
teams-phone auth login [OPTIONS]
```

**Options:**
| Option | Description |
|--------|-------------|
| `--method <method>` | Auth method: `certificate` (recommended), `client-secret` |
| `--force` | Force new authentication even if token exists |
| `--secret` | Client secret (for `client-secret` method; can also use `TEAMS_PHONE_CLIENT_SECRET` env var) |
| `--cert-path <path>` | Path to certificate file (for `certificate` method) |
| `--cert-thumbprint <hex>` | Certificate thumbprint (for `certificate` method with system store) |

**Method-Specific Requirements:**
- `certificate` (recommended): Requires either `--cert-path` (PEM/PFX file) or `--cert-thumbprint` (system certificate store). Most secure option.
- `client-secret`: Requires `--secret` or `TEAMS_PHONE_CLIENT_SECRET` environment variable. Secrets should be rotated regularly.

**Example:**
```bash
# Login with certificate file (recommended)
teams-phone auth login --method certificate --cert-path /path/to/cert.pem

# Login with certificate from Windows certificate store
teams-phone auth login --method certificate --cert-thumbprint "ABC123DEF456..."

# Login with client secret (from environment)
export TEAMS_PHONE_CLIENT_SECRET="your-secret"
teams-phone auth login --method client-secret

# Login with client secret (inline - less secure, avoid in scripts)
teams-phone auth login --method client-secret --secret "your-secret"
```

---

### `auth status`

Show current authentication status.

```
teams-phone auth status [OPTIONS]
```

**Output:**
- Authenticated: Yes/No
- Tenant
- Token expiry
- Authentication method

**Example:**
```bash
teams-phone auth status
```

---

### `auth logout`

Clear cached authentication tokens.

```
teams-phone auth logout [OPTIONS]
```

**Options:**
| Option | Description |
|--------|-------------|
| `--all` | Clear tokens for all tenants |

**Example:**
```bash
# Logout current tenant
teams-phone auth logout

# Logout all tenants
teams-phone auth logout --all
```

---

### `auth refresh`

Refresh cached authentication tokens for the current tenant.

```
teams-phone auth refresh [OPTIONS]
```

**Options:**
| Option | Description |
|--------|-------------|
| `--force` | Force refresh even if token is still valid |

**Example:**
```bash
# Refresh token for current tenant
teams-phone auth refresh

# Force refresh
teams-phone auth refresh --force
```
---

## Command Group 6: Locations

Manage emergency locations for phone number assignments.

**⚠️ Important: All location commands read from PowerShell CSV export**

Microsoft Graph API does **not provide endpoints** for emergency location management. All location operations (`list`, `show`, `search`) read from `~/.teams-phone/cache/locations.csv` generated by the PowerShell export script.

**Graph API Limitation:** The `numberAssignment` resource contains `locationId` and `civicAddressId` fields, but there is no corresponding Graph API to query location details. Emergency locations can only be managed via:
- Teams Admin Center (web UI)
- PowerShell cmdlets: `Get-CsOnlineLisCivicAddress`, `Get-CsOnlineLisLocation`

**Setup Required:** Run `Export-TeamsPhoneData.ps1` to generate location cache. See "Architecture: Hybrid Graph API + CSV Export" section above.

### `locations list`

List all emergency locations in the tenant.

```
teams-phone locations list [OPTIONS]
```

**Data Source:** Reads from `~/.teams-phone/cache/locations.csv`

**Options:**
| Option | Description |
|--------|-------------|
| `--city <city>` | Filter by city |
| `--validated` | Show only validated addresses |
| `--limit <n>` | Limit results (default: 100) |
| `--all` | Fetch all locations |

**Validation:**
- If both `--limit` and `--all` are provided → error: "Cannot specify both --limit and --all"

**Output Fields:**
- Location ID (UUID)
- Name/Description
- Address (street, city, state, postal, country)
- Company Name
- Validation Status (based on export data)
- Default Location (Yes/No)
- Assigned Numbers Count (if available in export)

**Example:**
```bash
# List all emergency locations
teams-phone locations list

# List locations in Seattle
teams-phone locations list --city Seattle

# Output as JSON
teams-phone locations list --json
```

---

### `locations show <location>`

Show detailed information for a specific emergency location.

```
teams-phone locations show <location> [OPTIONS]
```

**Data Source:** Reads from `~/.teams-phone/cache/locations.csv`

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<location>` | Location identifier: ID (UUID), name, or partial match |

**Resolution Behavior:**
- Location ID (UUID format): Exact match required
- Location name: Partial match; if multiple matches found, error with candidate list

**Output Sections:**
1. **Identity**: Location ID (UUID), Name/Description, Civic Address ID
2. **Company**: Company Name
3. **Address**: Street Number, Street Name, City, State/Province, Postal Code, Country
4. **Coordinates**: Latitude/Longitude (if available in export)
5. **Default Status**: Whether this is the default location for the civic address
6. **Export Info**: Last exported timestamp

**Example:**
```bash
# Show location by name
teams-phone locations show "Seattle-HQ"

# Show location by ID
teams-phone locations show loc_abc123

# Show with verbose details
teams-phone locations show "Seattle-HQ" --verbose
```

---

### `locations search <query>`

Search emergency locations by name or address.

```
teams-phone locations search <query> [OPTIONS]
```

**Data Source:** Reads from `~/.teams-phone/cache/locations.csv`

**Arguments:**
| Argument | Description |
|----------|-------------|
| `<query>` | Search term (partial match supported, case-insensitive) |

**Options:**
| Option | Description |
|--------|-------------|
| `--field <field>` | Limit search to: `name`, `address`, `city`, `company` |
| `--limit <n>` | Maximum results (default: 25) |

**Search Behavior:**
- Searches across name/description, full address, city, and company name
- Case-insensitive partial matching
- With `--field`: limits search to specific field only

**Example:**
```bash
# Search by name
teams-phone locations search "HQ"

# Search by city
teams-phone locations search "Seattle" --field city

# Search by street address
teams-phone locations search "Main St" --field address
```

---

## Output Formats

### Default (Rich Tables)

Human-readable output with:
- Colored status indicators (green=good, yellow=warning, red=error)
- Formatted tables with borders
- Progress bars for long operations
- Summary statistics

### JSON Output (`--json`)

Machine-readable JSON for automation:
- Consistent structure across commands
- Includes metadata (timestamp, tenant, command)
- Array of objects for list commands
- Single object for show commands

**Example JSON output:**
```json
{
  "metadata": {
    "command": "users list",
    "tenant": "contoso",
    "timestamp": "2026-01-24T10:30:00Z",
    "count": 2,
    "correlation_id": "a1b2c3d4"
  },
  "data": [
    {
      "id": "xxx-xxx-xxx",
      "displayName": "John Doe",
      "userPrincipalName": "john.doe@contoso.com",
      "phoneNumber": "+12065551234",
      "licensed": true,
      "voiceEnabled": true
    },
    {
      "id": "yyy-yyy-yyy",
      "displayName": "Jane Smith",
      "userPrincipalName": "jane.smith@contoso.com",
      "phoneNumber": null,
      "licensed": true,
      "voiceEnabled": false
    }
  ]
}
```

---

## Logging & Diagnostics

- Human-readable logs are written to stderr by default
- `--verbose` shows expanded diagnostic details
- `--debug-log <path>` writes sanitized JSON lines for troubleshooting (no secrets)
- Verbose/debug logs include a correlation ID per command invocation
- PII (e.g., UPNs) is masked or truncated in logs; full identifiers appear only in primary output

### Debug Log Format (`--debug-log`)

When `--debug-log <path>` is specified, the CLI writes newline-delimited JSON (JSON Lines) to the specified file. Each line is a self-contained JSON object:

```json
{"ts":"2026-01-24T10:30:00.123Z","correlation_id":"a1b2c3d4","level":"INFO","event":"command_start","command":"users list","tenant":"contoso"}
{"ts":"2026-01-24T10:30:00.456Z","correlation_id":"a1b2c3d4","level":"DEBUG","event":"cache_miss","cache_key":"contoso:users:list"}
{"ts":"2026-01-24T10:30:00.789Z","correlation_id":"a1b2c3d4","level":"DEBUG","event":"graph_request","method":"GET","endpoint":"/users","duration_ms":234}
{"ts":"2026-01-24T10:30:01.012Z","correlation_id":"a1b2c3d4","level":"DEBUG","event":"graph_response","status":200,"items_count":50}
{"ts":"2026-01-24T10:30:01.345Z","correlation_id":"a1b2c3d4","level":"INFO","event":"command_complete","exit_code":0,"duration_ms":1222}
```

**Log Fields:**
| Field | Description |
|-------|-------------|
| `ts` | ISO 8601 timestamp with milliseconds |
| `correlation_id` | UUID linking all log entries for a single command invocation |
| `level` | Log level: `DEBUG`, `INFO`, `WARNING`, `ERROR` |
| `event` | Event type: `command_start`, `command_complete`, `graph_request`, `graph_response`, `cache_hit`, `cache_miss`, `retry`, `error` |
| `...` | Additional context fields vary by event type |

**Security:** Debug logs never contain tokens, secrets, or full UPNs. User identifiers are truncated (e.g., `j***@contoso.com`).

---

## Error Handling

### Exit Codes

| Code | Meaning |
|------|---------|
| 0 | Success |
| 1 | General error |
| 2 | Authentication error |
| 3 | Authorization error (missing permissions) |
| 4 | Not found (user, number, policy) |
| 5 | Validation error (invalid input) |
| 6 | Rate limit exceeded |
| 7 | Configuration error |

### Error Messages

All errors include:
- Clear description of what went wrong
- Suggested remediation steps
- `--verbose` flag hint for more details

**Example:**
```
Error: User not found: john.doe@contoso.com

  The specified user was not found in the tenant directory.

  Suggestions:
  - Verify the UPN is correct
  - Check if the user exists: teams-phone users search "john"
  - Ensure you're connected to the correct tenant

  Use --verbose for detailed error information.
```

### Graph API Error Mapping

The CLI maps Graph API HTTP status codes to CLI exit codes as follows:

| Graph API Status | CLI Exit Code | Error Type | Notes |
|------------------|---------------|------------|-------|
| `200`, `202`, `204` | 0 | Success | Operation completed successfully |
| `400` | 5 | Validation error | Invalid input parameters |
| `401` | 2 | Authentication error | Token missing, expired, or invalid |
| `403` | 3 | Authorization error | Missing required permissions |
| `404` | 4 | Not found | User, number, policy, or location not found |
| `405` | 1 | General error | Method not allowed (e.g., beta endpoint called via v1.0) |
| `409` | 5 | Validation error | Conflict (e.g., number already assigned) |
| `424` | 1 | General error | Failed dependency (batch operation) |
| `429` | 6 | Rate limit exceeded | Throttling limit exceeded |
| `500`, `502`, `503`, `504` | 1 | General error | Server error (will retry with backoff) |
| Network errors | 1 | General error | Connection timeout, DNS failure, etc. |

**Retry Strategy:**
- `429` (Rate limit): Retry with exponential backoff (3 attempts)
- `503` (Service unavailable): Retry with exponential backoff (3 attempts)
- All other errors: No automatic retry

### Cache Fallback (Read-only)

- If Graph is unavailable or throttled after retries (3 retries with exponential backoff), read-only commands may return cached data with a visible stale indicator and age
- Fallback data must be less than 1 hour old (regardless of normal TTL settings: users 5min, numbers 5min, policies 10min)
- If no valid fallback data exists, command fails with exit code 6 and retry guidance
- `--no-cache` disables fallback and surfaces the underlying error immediately
- Write commands (assign, unassign, reserve, policy assign, etc.) never fall back to cached data

---

## Configuration File

Location: `~/.teams-phone/config.toml`

```toml
[default]
tenant = "contoso"
output_format = "table"  # table, json
color = true

[cache]
users_ttl = 300  # seconds
numbers_ttl = 300
policies_ttl = 600
tenant_ttl = 3600

[tenants.contoso]
tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
display_name = "Contoso Ltd"
auth_method = "certificate"  # certificate (recommended), client-secret
cert_path = "/path/to/contoso-cert.pem"  # or use cert_thumbprint on Windows
default_location = "Seattle-HQ"  # Default emergency location for number assignments

[tenants.fabrikam]
tenant_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
client_id = "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy"
display_name = "Fabrikam Inc"
auth_method = "client-secret"
# secret stored in TEAMS_PHONE_CLIENT_SECRET env var (do not store secrets in config)
default_location = "NYC-Main"
```

---

## Bulk Operations Implementation Strategy

### Graph API Batch Endpoint Usage

The CLI uses the Microsoft Graph batch endpoint (`POST /beta/$batch`) for certain bulk operations to improve performance.

**Batch-Enabled Operations:**
- `users list --all` - Fetch multiple user pages in parallel (when result set > 1000 users)
- `numbers list --all` - Fetch multiple number pages in parallel (when result set > 1000 numbers)
- Internal lookups - Resolve multiple user IDs/phone numbers simultaneously

**Sequential Operations:**
- `numbers bulk-assign` - Sequential calls with progress reporting (better UX, easier error handling)
- `policies bulk-assign` - Sequential calls with progress reporting
- Any operation requiring real-time progress updates

**Batch Constraints:**
- Maximum 20 requests per batch
- Cannot mix v1.0 and beta endpoints
- Each request still counts against rate limits
- Responses may arrive out of order (use request ID correlation)

**Why Sequential for Bulk Assign:**
1. **Progress reporting** - Live progress bar requires sequential processing
2. **Error handling** - Easier to identify which specific row failed
3. **Async operations** - Number assign/unassign return 202 and require polling per request
4. **User experience** - Real-time feedback more important than raw speed for bulk operations

**Future Optimization:**
Post-MVP, we may add `--fast` flag to bulk operations that uses batching for speed at the cost of reduced progress visibility.

---

## Command Summary

| Command | Description | Data Source |
|---------|-------------|-------------|
| **Users** | | |
| `users list` | List users with voice-related configuration | Graph API |
| `users show <user>` | Show user voice configuration | Graph API |
| `users search <query>` | Search users by name/UPN/phone | Graph API |
| **Numbers** | | |
| `numbers list` | List phone number inventory | Graph API |
| `numbers show <number>` | Show phone number details | Graph API |
| `numbers search <pattern>` | Search phone numbers | Graph API |
| `numbers assign <user> <number> [location]` | Assign number to user (async operation) | Graph API |
| `numbers unassign <number>` | Remove number assignment (async operation) | Graph API |
| `numbers update <number>` | Update number location/network site (sync operation) | Graph API |
| `numbers bulk-assign <csv>` | Bulk assign from CSV with locations | Graph API |
| **Policies** | | |
| `policies list [type]` | List available policies by type | CSV Export |
| `policies show <user>` | Show user's policy assignments | Graph API |
| `policies assign <user> <type> <name>` | Assign policy to user | Graph API |
| `policies bulk-assign <csv>` | Bulk assign policies from CSV | Graph API |
| **Locations** | | |
| `locations list` | List emergency locations | CSV Export |
| `locations show <location>` | Show location details | CSV Export |
| `locations search <query>` | Search locations by name/address | CSV Export |
| **Tenants** | | |
| `tenants list` | List configured tenants | Local Config |
| `tenants current` | Show current tenant | Local Config |
| `tenants switch <name>` | Switch active tenant | Local Config |
| `tenants add` | Add new tenant profile | Local Config |
| `tenants remove <name>` | Remove tenant profile | Local Config |
| `tenants test [name]` | Test tenant connection | Graph API |
| **Auth** | | |
| `auth login` | Authenticate with tenant (certificate or client-secret) | MSAL |
| `auth status` | Show auth status | MSAL |
| `auth logout` | Clear auth tokens | MSAL |
| `auth refresh` | Refresh authentication tokens | MSAL |

---

## MVP Checklist

### Core Commands (Must Have)

**Users (Graph API)**
- [ ] `users list` with filtering (licensed/unlicensed, assigned/unassigned)
- [ ] Account type filtering and display (user/resource-account/guest)
- [ ] `users show` with full voice config and all policy assignments
- [ ] `users search` with partial matching by name/UPN/phone

**Numbers (Graph API)**
- [ ] `numbers list` with filtering by status/type/city
- [ ] `numbers show` with full details
- [ ] Enhanced number details (activation state, port status, assignment source)
- [ ] `numbers search` with pattern matching (last 4 digits, wildcards)
- [ ] `numbers assign` with emergency location, async polling, dry-run
- [ ] `numbers unassign` with async polling, dry-run
- [ ] `numbers update` with location/network-site updates, dry-run
- [ ] `numbers bulk-assign` with CSV import, location column support, progress bar

**Policies (Graph API + CSV)**
- [ ] `policies list` by type (reads from CSV export)
- [ ] `policies show` for user (Graph API effectivePolicyAssignments)
- [ ] Enhanced policy display with assignment source (direct/group/global)
- [ ] `policies assign` with validation and dry-run
- [ ] `policies bulk-assign` from CSV

**Locations (CSV Export Only)**
- [ ] `locations list` with filtering by city (reads from CSV)
- [ ] `locations show` with full address details (reads from CSV)
- [ ] `locations search` with partial matching (reads from CSV)
- [ ] CSV staleness warning (>30 days old)
- [ ] Degraded mode handling when CSV missing

**Tenants (Local Config)**
- [ ] `tenants list` showing all configured profiles
- [ ] `tenants current` with connection status
- [ ] `tenants switch` with profile validation
- [ ] `tenants add` interactive wizard with UUID validation
- [ ] `tenants remove` with confirmation
- [ ] `tenants test` validating auth + permissions

**Auth (MSAL)**
- [ ] `auth login` with certificate (recommended) and client-secret methods
- [ ] `auth status` showing token expiry
- [ ] `auth logout` clearing cached tokens
- [ ] `auth refresh` forcing token renewal

### Global Features (Must Have)
- [ ] `--json` output for all commands
- [ ] `--verbose` mode with detailed API call logging
- [ ] `--tenant` override for multi-tenant operations
- [ ] `--no-cache` option forcing fresh data fetch
- [ ] `--help` for all commands with examples
- [ ] `--version` flag
- [ ] Rich table output with color-coded status
- [ ] Progress bars for bulk operations and async polling
- [ ] Consistent error messages with exit codes (0-7)
- [ ] Phone number format normalization (E.164 ↔ digits-only)

### Infrastructure (Must Have)
- [ ] PowerShell export script (`Export-TeamsPhoneData.ps1`)
  - [ ] Service principal authentication (certificate/secret)
  - [ ] Export locations to CSV
  - [ ] Export policies to CSV
  - [ ] Generate export-metadata.json
  - [ ] Error handling and logging
- [ ] CSV cache management
  - [ ] Read locations.csv with schema validation
  - [ ] Read policies.csv with schema validation
  - [ ] Staleness detection (>30 days)
  - [ ] Missing file warnings
- [ ] Async operation polling
  - [ ] Poll `/operations/{id}` endpoint
  - [ ] Exponential backoff (2-5 seconds)
  - [ ] Terminal state detection
  - [ ] Timeout handling (default 60s)
  - [ ] `--wait` / `--no-wait` modes
- [ ] Graph API error mapping (HTTP status → exit codes)
- [ ] Bulk operation strategy (batch vs sequential with progress)

### Nice to Have (Post-MVP)
- [ ] Shell completion (bash, zsh, fish, PowerShell)
- [ ] `--output <file>` for all list commands
- [ ] Automatic CSV refresh reminder based on age
