eeroctl 1.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- eeroctl/__init__.py +19 -0
- eeroctl/commands/__init__.py +32 -0
- eeroctl/commands/activity.py +237 -0
- eeroctl/commands/auth.py +471 -0
- eeroctl/commands/completion.py +142 -0
- eeroctl/commands/device.py +492 -0
- eeroctl/commands/eero/__init__.py +12 -0
- eeroctl/commands/eero/base.py +224 -0
- eeroctl/commands/eero/led.py +154 -0
- eeroctl/commands/eero/nightlight.py +235 -0
- eeroctl/commands/eero/updates.py +82 -0
- eeroctl/commands/network/__init__.py +18 -0
- eeroctl/commands/network/advanced.py +191 -0
- eeroctl/commands/network/backup.py +162 -0
- eeroctl/commands/network/base.py +331 -0
- eeroctl/commands/network/dhcp.py +118 -0
- eeroctl/commands/network/dns.py +197 -0
- eeroctl/commands/network/forwards.py +115 -0
- eeroctl/commands/network/guest.py +162 -0
- eeroctl/commands/network/security.py +162 -0
- eeroctl/commands/network/speedtest.py +99 -0
- eeroctl/commands/network/sqm.py +194 -0
- eeroctl/commands/profile.py +671 -0
- eeroctl/commands/troubleshoot.py +317 -0
- eeroctl/context.py +254 -0
- eeroctl/errors.py +156 -0
- eeroctl/exit_codes.py +68 -0
- eeroctl/formatting/__init__.py +90 -0
- eeroctl/formatting/base.py +181 -0
- eeroctl/formatting/device.py +430 -0
- eeroctl/formatting/eero.py +591 -0
- eeroctl/formatting/misc.py +87 -0
- eeroctl/formatting/network.py +659 -0
- eeroctl/formatting/profile.py +443 -0
- eeroctl/main.py +161 -0
- eeroctl/options.py +429 -0
- eeroctl/output.py +739 -0
- eeroctl/safety.py +259 -0
- eeroctl/utils.py +181 -0
- eeroctl-1.7.1.dist-info/METADATA +115 -0
- eeroctl-1.7.1.dist-info/RECORD +45 -0
- eeroctl-1.7.1.dist-info/WHEEL +5 -0
- eeroctl-1.7.1.dist-info/entry_points.txt +3 -0
- eeroctl-1.7.1.dist-info/licenses/LICENSE +21 -0
- eeroctl-1.7.1.dist-info/top_level.txt +1 -0
eeroctl/commands/auth.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""Authentication commands for the Eero CLI.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
- eero auth login: Start authentication flow
|
|
5
|
+
- eero auth logout: End current session
|
|
6
|
+
- eero auth clear: Clear all stored credentials
|
|
7
|
+
- eero auth status: Show authentication status
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from typing import TypedDict
|
|
16
|
+
|
|
17
|
+
import click
|
|
18
|
+
from eero import EeroClient
|
|
19
|
+
from eero.exceptions import EeroAuthenticationException, EeroException
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.prompt import Confirm, Prompt
|
|
22
|
+
from rich.table import Table
|
|
23
|
+
|
|
24
|
+
from ..context import EeroCliContext, ensure_cli_context, get_cli_context
|
|
25
|
+
from ..exit_codes import ExitCode
|
|
26
|
+
from ..output import OutputFormat
|
|
27
|
+
from ..utils import get_cookie_file
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class _UserData(TypedDict):
|
|
33
|
+
"""Type definition for user data in account info."""
|
|
34
|
+
|
|
35
|
+
id: str | None
|
|
36
|
+
name: str | None
|
|
37
|
+
email: str | None
|
|
38
|
+
phone: str | None
|
|
39
|
+
role: str | None
|
|
40
|
+
created_at: str | None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class _AccountData(TypedDict):
|
|
44
|
+
"""Type definition for account data."""
|
|
45
|
+
|
|
46
|
+
id: str | None
|
|
47
|
+
name: str | None
|
|
48
|
+
premium_status: str | None
|
|
49
|
+
premium_expiry: str | None
|
|
50
|
+
created_at: str | None
|
|
51
|
+
users: list[_UserData]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@click.group(name="auth")
|
|
55
|
+
@click.pass_context
|
|
56
|
+
def auth_group(ctx: click.Context) -> None:
|
|
57
|
+
"""Manage authentication.
|
|
58
|
+
|
|
59
|
+
\b
|
|
60
|
+
Commands:
|
|
61
|
+
login - Authenticate with your Eero account
|
|
62
|
+
logout - End current session
|
|
63
|
+
clear - Clear all stored credentials
|
|
64
|
+
status - Show authentication status
|
|
65
|
+
|
|
66
|
+
\b
|
|
67
|
+
Examples:
|
|
68
|
+
eero auth login # Start login flow
|
|
69
|
+
eero auth status # Check if authenticated
|
|
70
|
+
eero auth logout # End session
|
|
71
|
+
"""
|
|
72
|
+
ensure_cli_context(ctx)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@auth_group.command(name="login")
|
|
76
|
+
@click.option("--force", is_flag=True, help="Force new login even if already authenticated")
|
|
77
|
+
@click.option("--no-keyring", is_flag=True, help="Don't use keyring for secure token storage")
|
|
78
|
+
@click.pass_context
|
|
79
|
+
def auth_login(ctx: click.Context, force: bool, no_keyring: bool) -> None:
|
|
80
|
+
"""Login to your Eero account.
|
|
81
|
+
|
|
82
|
+
Starts an interactive authentication flow. A verification code
|
|
83
|
+
will be sent to your email or phone number.
|
|
84
|
+
|
|
85
|
+
\b
|
|
86
|
+
Examples:
|
|
87
|
+
eero auth login # Start login flow
|
|
88
|
+
eero auth login --force # Force new login
|
|
89
|
+
"""
|
|
90
|
+
cli_ctx = get_cli_context(ctx)
|
|
91
|
+
console = cli_ctx.console
|
|
92
|
+
|
|
93
|
+
async def run() -> None:
|
|
94
|
+
async with EeroClient(
|
|
95
|
+
cookie_file=str(get_cookie_file()),
|
|
96
|
+
use_keyring=not no_keyring,
|
|
97
|
+
) as client:
|
|
98
|
+
if client.is_authenticated and not force:
|
|
99
|
+
# Validate session is actually working, not just locally present
|
|
100
|
+
try:
|
|
101
|
+
await client.get_networks()
|
|
102
|
+
console.print(
|
|
103
|
+
"[bold yellow]Already authenticated.[/bold yellow] "
|
|
104
|
+
"Use --force to login again."
|
|
105
|
+
)
|
|
106
|
+
return
|
|
107
|
+
except EeroAuthenticationException:
|
|
108
|
+
# Session expired, continue with login flow
|
|
109
|
+
console.print("[yellow]Session expired. Starting new login...[/yellow]")
|
|
110
|
+
|
|
111
|
+
await _interactive_login(client, force, console, cli_ctx)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
asyncio.run(run())
|
|
115
|
+
except EeroAuthenticationException as e:
|
|
116
|
+
cli_ctx.renderer.render_error(str(e))
|
|
117
|
+
sys.exit(ExitCode.AUTH_REQUIRED)
|
|
118
|
+
except EeroException as e:
|
|
119
|
+
cli_ctx.renderer.render_error(str(e))
|
|
120
|
+
sys.exit(ExitCode.GENERIC_ERROR)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
async def _interactive_login(
|
|
124
|
+
client: EeroClient, force: bool, console, cli_ctx: EeroCliContext
|
|
125
|
+
) -> bool:
|
|
126
|
+
"""Interactive login process."""
|
|
127
|
+
cookie_file = get_cookie_file()
|
|
128
|
+
|
|
129
|
+
# Check for existing session
|
|
130
|
+
if os.path.exists(cookie_file) and not force:
|
|
131
|
+
try:
|
|
132
|
+
with open(cookie_file, "r") as f:
|
|
133
|
+
cookies = json.load(f)
|
|
134
|
+
if cookies.get("user_token") and cookies.get("session_id"):
|
|
135
|
+
console.print(
|
|
136
|
+
Panel.fit(
|
|
137
|
+
"An existing authentication session was found.",
|
|
138
|
+
title="Eero Login",
|
|
139
|
+
border_style="blue",
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
reuse = Confirm.ask("Do you want to reuse the existing session?")
|
|
143
|
+
|
|
144
|
+
if reuse:
|
|
145
|
+
with cli_ctx.status("Testing existing session..."):
|
|
146
|
+
try:
|
|
147
|
+
networks = await client.get_networks()
|
|
148
|
+
console.print(
|
|
149
|
+
f"[bold green]Session valid! "
|
|
150
|
+
f"Found {len(networks)} network(s).[/bold green]"
|
|
151
|
+
)
|
|
152
|
+
return True
|
|
153
|
+
except Exception as ex:
|
|
154
|
+
logger.debug("Session validation failed: %s", ex)
|
|
155
|
+
console.print("[yellow]Existing session invalid.[/yellow]")
|
|
156
|
+
except Exception as ex:
|
|
157
|
+
logger.debug("Failed to check existing session: %s", ex)
|
|
158
|
+
|
|
159
|
+
# Clear existing auth data
|
|
160
|
+
await client._api.auth.clear_auth_data()
|
|
161
|
+
|
|
162
|
+
# Start fresh login
|
|
163
|
+
console.print(
|
|
164
|
+
Panel.fit(
|
|
165
|
+
"Please login to your Eero account.\nA verification code will be sent to you.",
|
|
166
|
+
title="Eero Login",
|
|
167
|
+
border_style="blue",
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
user_identifier = Prompt.ask("Email or phone number")
|
|
172
|
+
|
|
173
|
+
with cli_ctx.status("Requesting verification code..."):
|
|
174
|
+
try:
|
|
175
|
+
result = await client.login(user_identifier)
|
|
176
|
+
if not result:
|
|
177
|
+
console.print("[bold red]Failed to request verification code[/bold red]")
|
|
178
|
+
return False
|
|
179
|
+
console.print("[bold green]Verification code sent![/bold green]")
|
|
180
|
+
except EeroException as ex:
|
|
181
|
+
console.print(f"[bold red]Error:[/bold red] {ex}")
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
# Verification loop
|
|
185
|
+
max_attempts = 3
|
|
186
|
+
for attempt in range(max_attempts):
|
|
187
|
+
verification_code = Prompt.ask("Verification code (check your email/phone)")
|
|
188
|
+
|
|
189
|
+
with cli_ctx.status("Verifying..."):
|
|
190
|
+
try:
|
|
191
|
+
result = await client.verify(verification_code)
|
|
192
|
+
if result:
|
|
193
|
+
console.print("[bold green]Login successful![/bold green]")
|
|
194
|
+
return True
|
|
195
|
+
except EeroException as ex:
|
|
196
|
+
console.print(f"[bold red]Error:[/bold red] {ex}")
|
|
197
|
+
|
|
198
|
+
if attempt < max_attempts - 1:
|
|
199
|
+
resend = Confirm.ask("Resend verification code?")
|
|
200
|
+
if resend:
|
|
201
|
+
with cli_ctx.status("Resending..."):
|
|
202
|
+
await client._api.auth.resend_verification_code()
|
|
203
|
+
console.print("[green]Code resent![/green]")
|
|
204
|
+
|
|
205
|
+
console.print("[bold red]Too many failed attempts[/bold red]")
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@auth_group.command(name="logout")
|
|
210
|
+
@click.pass_context
|
|
211
|
+
def auth_logout(ctx: click.Context) -> None:
|
|
212
|
+
"""Logout from your Eero account.
|
|
213
|
+
|
|
214
|
+
Ends the current session and clears the session token.
|
|
215
|
+
Credentials are preserved for easy re-authentication.
|
|
216
|
+
"""
|
|
217
|
+
cli_ctx = get_cli_context(ctx)
|
|
218
|
+
console = cli_ctx.console
|
|
219
|
+
|
|
220
|
+
async def run() -> None:
|
|
221
|
+
async with EeroClient(cookie_file=str(get_cookie_file())) as client:
|
|
222
|
+
if not client.is_authenticated:
|
|
223
|
+
console.print("[yellow]Not logged in[/yellow]")
|
|
224
|
+
return
|
|
225
|
+
|
|
226
|
+
with cli_ctx.status("Logging out..."):
|
|
227
|
+
try:
|
|
228
|
+
result = await client.logout()
|
|
229
|
+
if result:
|
|
230
|
+
console.print("[bold green]Logged out successfully[/bold green]")
|
|
231
|
+
else:
|
|
232
|
+
console.print("[bold red]Failed to logout[/bold red]")
|
|
233
|
+
except EeroException as ex:
|
|
234
|
+
console.print(f"[bold red]Error:[/bold red] {ex}")
|
|
235
|
+
sys.exit(ExitCode.GENERIC_ERROR)
|
|
236
|
+
|
|
237
|
+
asyncio.run(run())
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@auth_group.command(name="clear")
|
|
241
|
+
@click.option("--force", "-f", is_flag=True, help="Skip confirmation")
|
|
242
|
+
@click.pass_context
|
|
243
|
+
def auth_clear(ctx: click.Context, force: bool) -> None:
|
|
244
|
+
"""Clear all stored authentication data.
|
|
245
|
+
|
|
246
|
+
Removes all stored credentials including tokens and session data.
|
|
247
|
+
You will need to login again after this.
|
|
248
|
+
"""
|
|
249
|
+
cli_ctx = get_cli_context(ctx)
|
|
250
|
+
console = cli_ctx.console
|
|
251
|
+
|
|
252
|
+
if not force and not cli_ctx.non_interactive:
|
|
253
|
+
confirmed = Confirm.ask(
|
|
254
|
+
"This will clear all authentication data. Continue?",
|
|
255
|
+
default=False,
|
|
256
|
+
)
|
|
257
|
+
if not confirmed:
|
|
258
|
+
console.print("[yellow]Cancelled[/yellow]")
|
|
259
|
+
return
|
|
260
|
+
elif cli_ctx.non_interactive and not force:
|
|
261
|
+
cli_ctx.renderer.render_error(
|
|
262
|
+
"Clearing auth data requires confirmation. Use --force in non-interactive mode."
|
|
263
|
+
)
|
|
264
|
+
sys.exit(ExitCode.SAFETY_RAIL)
|
|
265
|
+
|
|
266
|
+
async def run() -> None:
|
|
267
|
+
async with EeroClient(cookie_file=str(get_cookie_file())) as client:
|
|
268
|
+
await client._api.auth.clear_auth_data()
|
|
269
|
+
console.print("[bold green]Authentication data cleared[/bold green]")
|
|
270
|
+
|
|
271
|
+
asyncio.run(run())
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _get_session_info() -> dict:
|
|
275
|
+
"""Read session info from cookie file."""
|
|
276
|
+
from datetime import datetime
|
|
277
|
+
|
|
278
|
+
cookie_file = get_cookie_file()
|
|
279
|
+
session_info = {
|
|
280
|
+
"cookie_file": str(cookie_file),
|
|
281
|
+
"cookie_exists": cookie_file.exists(),
|
|
282
|
+
"session_expiry": None,
|
|
283
|
+
"session_expired": True, # Default to expired
|
|
284
|
+
"has_token": False,
|
|
285
|
+
"preferred_network_id": None,
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if cookie_file.exists():
|
|
289
|
+
try:
|
|
290
|
+
with open(cookie_file, "r") as f:
|
|
291
|
+
data = json.load(f)
|
|
292
|
+
session_info["session_expiry"] = data.get("session_expiry")
|
|
293
|
+
session_info["preferred_network_id"] = data.get("preferred_network_id")
|
|
294
|
+
session_info["has_token"] = bool(data.get("user_token"))
|
|
295
|
+
|
|
296
|
+
# Check if session is expired based on expiry date
|
|
297
|
+
expiry_str = data.get("session_expiry")
|
|
298
|
+
if expiry_str:
|
|
299
|
+
try:
|
|
300
|
+
expiry = datetime.fromisoformat(expiry_str)
|
|
301
|
+
session_info["session_expired"] = datetime.now() > expiry
|
|
302
|
+
except ValueError:
|
|
303
|
+
logger.debug("Failed to parse session expiry date: %s", expiry_str)
|
|
304
|
+
except Exception as ex:
|
|
305
|
+
logger.debug("Failed to read cookie file: %s", ex)
|
|
306
|
+
|
|
307
|
+
return session_info
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _check_keyring_available() -> bool:
|
|
311
|
+
"""Check if keyring is available and has eero credentials."""
|
|
312
|
+
try:
|
|
313
|
+
import keyring
|
|
314
|
+
|
|
315
|
+
token = keyring.get_password("eero", "user_token")
|
|
316
|
+
return token is not None
|
|
317
|
+
except Exception:
|
|
318
|
+
return False
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@auth_group.command(name="status")
|
|
322
|
+
@click.pass_context
|
|
323
|
+
def auth_status(ctx: click.Context) -> None:
|
|
324
|
+
"""Show current authentication status.
|
|
325
|
+
|
|
326
|
+
Displays session info, authentication method, and account details.
|
|
327
|
+
"""
|
|
328
|
+
cli_ctx = get_cli_context(ctx)
|
|
329
|
+
console = cli_ctx.console
|
|
330
|
+
|
|
331
|
+
async def run() -> None:
|
|
332
|
+
cookie_file = get_cookie_file()
|
|
333
|
+
session_info = _get_session_info()
|
|
334
|
+
keyring_available = _check_keyring_available()
|
|
335
|
+
|
|
336
|
+
async with EeroClient(cookie_file=str(cookie_file)) as client:
|
|
337
|
+
is_auth = client.is_authenticated
|
|
338
|
+
account_data: _AccountData | None = None
|
|
339
|
+
|
|
340
|
+
# Determine session validity based on expiry date, not API call
|
|
341
|
+
# (API call may fail due to network issues, not expired session)
|
|
342
|
+
session_valid = (
|
|
343
|
+
is_auth and session_info["has_token"] and not session_info["session_expired"]
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Try to get account info if we have a valid session
|
|
347
|
+
if session_valid:
|
|
348
|
+
try:
|
|
349
|
+
with cli_ctx.status("Getting account info..."):
|
|
350
|
+
account = await client.get_account()
|
|
351
|
+
users_list: list[_UserData] = [
|
|
352
|
+
_UserData(
|
|
353
|
+
id=u.id,
|
|
354
|
+
name=u.name,
|
|
355
|
+
email=u.email,
|
|
356
|
+
phone=u.phone,
|
|
357
|
+
role=u.role,
|
|
358
|
+
created_at=str(u.created_at) if u.created_at else None,
|
|
359
|
+
)
|
|
360
|
+
for u in (account.users or [])
|
|
361
|
+
]
|
|
362
|
+
account_data = _AccountData(
|
|
363
|
+
id=account.id,
|
|
364
|
+
name=account.name,
|
|
365
|
+
premium_status=account.premium_status,
|
|
366
|
+
premium_expiry=(
|
|
367
|
+
str(account.premium_expiry) if account.premium_expiry else None
|
|
368
|
+
),
|
|
369
|
+
created_at=str(account.created_at) if account.created_at else None,
|
|
370
|
+
users=users_list,
|
|
371
|
+
)
|
|
372
|
+
except Exception as ex:
|
|
373
|
+
# API call failed but session may still be valid per expiry date
|
|
374
|
+
logger.debug("Session verification API call failed: %s", ex)
|
|
375
|
+
|
|
376
|
+
# Determine auth method
|
|
377
|
+
auth_method = "keyring" if keyring_available else "cookie"
|
|
378
|
+
|
|
379
|
+
if cli_ctx.is_structured_output():
|
|
380
|
+
data = {
|
|
381
|
+
"authenticated": is_auth,
|
|
382
|
+
"session_valid": session_valid,
|
|
383
|
+
"auth_method": auth_method,
|
|
384
|
+
"session": {
|
|
385
|
+
"cookie_file": session_info["cookie_file"],
|
|
386
|
+
"expiry": session_info["session_expiry"],
|
|
387
|
+
"preferred_network_id": session_info["preferred_network_id"],
|
|
388
|
+
},
|
|
389
|
+
"keyring_available": keyring_available,
|
|
390
|
+
"account": account_data,
|
|
391
|
+
}
|
|
392
|
+
cli_ctx.render_structured(data, "eero.auth.status/v1")
|
|
393
|
+
|
|
394
|
+
elif cli_ctx.output_format == OutputFormat.LIST:
|
|
395
|
+
# List format - parseable key-value rows
|
|
396
|
+
status = (
|
|
397
|
+
"valid" if session_valid else ("expired" if is_auth else "not_authenticated")
|
|
398
|
+
)
|
|
399
|
+
print(f"status {status}")
|
|
400
|
+
print(f"auth_method {auth_method}")
|
|
401
|
+
print(f"cookie_file {session_info['cookie_file']}")
|
|
402
|
+
print(f"session_expiry {session_info['session_expiry'] or 'N/A'}")
|
|
403
|
+
print(f"keyring_available {keyring_available}")
|
|
404
|
+
if account_data:
|
|
405
|
+
print(f"account_id {account_data['id']}")
|
|
406
|
+
print(f"account_name {account_data['name'] or 'N/A'}")
|
|
407
|
+
print(f"premium_status {account_data['premium_status'] or 'N/A'}")
|
|
408
|
+
print(f"premium_expiry {account_data['premium_expiry'] or 'N/A'}")
|
|
409
|
+
for u in account_data.get("users", []):
|
|
410
|
+
print(f"user {u['email']} {u['role']} {u['name'] or ''}")
|
|
411
|
+
|
|
412
|
+
else:
|
|
413
|
+
# Table format - Rich tables
|
|
414
|
+
# Session info table
|
|
415
|
+
session_table = Table(title="Session Information")
|
|
416
|
+
session_table.add_column("Property", style="cyan")
|
|
417
|
+
session_table.add_column("Value")
|
|
418
|
+
|
|
419
|
+
if session_valid:
|
|
420
|
+
status_display = "[green]Valid[/green]"
|
|
421
|
+
elif is_auth and session_info["session_expired"]:
|
|
422
|
+
status_display = "[yellow]Expired[/yellow]"
|
|
423
|
+
else:
|
|
424
|
+
status_display = "[red]Not Authenticated[/red]"
|
|
425
|
+
|
|
426
|
+
session_table.add_row("Status", status_display)
|
|
427
|
+
session_table.add_row("Auth Method", f"[blue]{auth_method}[/blue]")
|
|
428
|
+
session_table.add_row("Cookie File", session_info["cookie_file"])
|
|
429
|
+
session_table.add_row("Session Expiry", session_info["session_expiry"] or "N/A")
|
|
430
|
+
session_table.add_row(
|
|
431
|
+
"Keyring Available",
|
|
432
|
+
"[green]Yes[/green]" if keyring_available else "[dim]No[/dim]",
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
console.print(session_table)
|
|
436
|
+
|
|
437
|
+
# Account info table (only if we got account data)
|
|
438
|
+
if account_data:
|
|
439
|
+
console.print()
|
|
440
|
+
account_table = Table(title="Account Information")
|
|
441
|
+
account_table.add_column("Property", style="cyan")
|
|
442
|
+
account_table.add_column("Value")
|
|
443
|
+
|
|
444
|
+
account_table.add_row("Account ID", account_data["id"])
|
|
445
|
+
account_table.add_row("Account Name", account_data["name"] or "N/A")
|
|
446
|
+
premium = account_data["premium_status"] or "N/A"
|
|
447
|
+
if premium and "active" in premium.lower():
|
|
448
|
+
premium = f"[green]{premium}[/green]"
|
|
449
|
+
account_table.add_row("Premium Status", premium)
|
|
450
|
+
account_table.add_row("Premium Expiry", account_data["premium_expiry"] or "N/A")
|
|
451
|
+
account_table.add_row("Created", account_data["created_at"] or "N/A")
|
|
452
|
+
|
|
453
|
+
console.print(account_table)
|
|
454
|
+
|
|
455
|
+
# Users table
|
|
456
|
+
if account_data.get("users"):
|
|
457
|
+
console.print()
|
|
458
|
+
users_table = Table(title="Account Users")
|
|
459
|
+
users_table.add_column("Email", style="cyan")
|
|
460
|
+
users_table.add_column("Name")
|
|
461
|
+
users_table.add_column("Role", style="magenta")
|
|
462
|
+
|
|
463
|
+
for u in account_data["users"]:
|
|
464
|
+
users_table.add_row(u["email"], u["name"] or "", u["role"])
|
|
465
|
+
|
|
466
|
+
console.print(users_table)
|
|
467
|
+
elif not session_valid:
|
|
468
|
+
console.print()
|
|
469
|
+
console.print("[yellow]Run `eero auth login` to authenticate.[/yellow]")
|
|
470
|
+
|
|
471
|
+
asyncio.run(run())
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Shell completion commands for the Eero CLI.
|
|
2
|
+
|
|
3
|
+
Commands:
|
|
4
|
+
- eero completion bash: Generate bash completion
|
|
5
|
+
- eero completion zsh: Generate zsh completion
|
|
6
|
+
- eero completion fish: Generate fish completion
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ..context import ensure_cli_context
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@click.group(name="completion")
|
|
15
|
+
@click.pass_context
|
|
16
|
+
def completion_group(ctx: click.Context) -> None:
|
|
17
|
+
"""Generate shell completion scripts.
|
|
18
|
+
|
|
19
|
+
\b
|
|
20
|
+
Supported shells:
|
|
21
|
+
bash - Bash completion
|
|
22
|
+
zsh - Zsh completion
|
|
23
|
+
fish - Fish completion
|
|
24
|
+
|
|
25
|
+
\b
|
|
26
|
+
Installation:
|
|
27
|
+
# Bash (add to ~/.bashrc)
|
|
28
|
+
eval "$(eero completion bash)"
|
|
29
|
+
|
|
30
|
+
# Zsh (add to ~/.zshrc)
|
|
31
|
+
eval "$(eero completion zsh)"
|
|
32
|
+
|
|
33
|
+
# Fish (add to ~/.config/fish/completions/eero.fish)
|
|
34
|
+
eero completion fish > ~/.config/fish/completions/eero.fish
|
|
35
|
+
"""
|
|
36
|
+
ensure_cli_context(ctx)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@completion_group.command(name="bash")
|
|
40
|
+
@click.pass_context
|
|
41
|
+
def completion_bash(ctx: click.Context) -> None:
|
|
42
|
+
"""Generate bash completion script.
|
|
43
|
+
|
|
44
|
+
\b
|
|
45
|
+
Usage:
|
|
46
|
+
# Add to ~/.bashrc:
|
|
47
|
+
eval "$(eero completion bash)"
|
|
48
|
+
|
|
49
|
+
# Or source from file:
|
|
50
|
+
eero completion bash > /etc/bash_completion.d/eero
|
|
51
|
+
"""
|
|
52
|
+
# Click provides shell completion via _EERO_COMPLETE
|
|
53
|
+
script = """
|
|
54
|
+
_eero_completion() {
|
|
55
|
+
local IFS=$'\\n'
|
|
56
|
+
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \\
|
|
57
|
+
COMP_CWORD=$COMP_CWORD \\
|
|
58
|
+
_EERO_COMPLETE=complete_bash $1 ) )
|
|
59
|
+
return 0
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
complete -F _eero_completion -o default eero
|
|
63
|
+
"""
|
|
64
|
+
click.echo(script)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@completion_group.command(name="zsh")
|
|
68
|
+
@click.pass_context
|
|
69
|
+
def completion_zsh(ctx: click.Context) -> None:
|
|
70
|
+
"""Generate zsh completion script.
|
|
71
|
+
|
|
72
|
+
\b
|
|
73
|
+
Usage:
|
|
74
|
+
# Add to ~/.zshrc:
|
|
75
|
+
eval "$(eero completion zsh)"
|
|
76
|
+
|
|
77
|
+
# Or add to fpath:
|
|
78
|
+
eero completion zsh > ~/.zsh/completions/_eero
|
|
79
|
+
"""
|
|
80
|
+
script = """
|
|
81
|
+
#compdef eero
|
|
82
|
+
|
|
83
|
+
_eero() {
|
|
84
|
+
local -a completions
|
|
85
|
+
local -a completions_with_descriptions
|
|
86
|
+
local -a response
|
|
87
|
+
(( ! $+commands[eero] )) && return 1
|
|
88
|
+
|
|
89
|
+
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) _EERO_COMPLETE=complete_zsh eero)}")
|
|
90
|
+
|
|
91
|
+
for key descr in ${(kv)response}; do
|
|
92
|
+
if [[ "$descr" == "_" ]]; then
|
|
93
|
+
completions+=("$key")
|
|
94
|
+
else
|
|
95
|
+
completions_with_descriptions+=("$key":"$descr")
|
|
96
|
+
fi
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
if [ -n "$completions_with_descriptions" ]; then
|
|
100
|
+
_describe -V unsorted completions_with_descriptions -U
|
|
101
|
+
fi
|
|
102
|
+
|
|
103
|
+
if [ -n "$completions" ]; then
|
|
104
|
+
compadd -U -V unsorted -a completions
|
|
105
|
+
fi
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
compdef _eero eero
|
|
109
|
+
"""
|
|
110
|
+
click.echo(script)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@completion_group.command(name="fish")
|
|
114
|
+
@click.pass_context
|
|
115
|
+
def completion_fish(ctx: click.Context) -> None:
|
|
116
|
+
"""Generate fish completion script.
|
|
117
|
+
|
|
118
|
+
\b
|
|
119
|
+
Usage:
|
|
120
|
+
# Save to fish completions directory:
|
|
121
|
+
eero completion fish > ~/.config/fish/completions/eero.fish
|
|
122
|
+
"""
|
|
123
|
+
script = """
|
|
124
|
+
function _eero_completion
|
|
125
|
+
set -l response (env _EERO_COMPLETE=complete_fish COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) eero)
|
|
126
|
+
|
|
127
|
+
for completion in $response
|
|
128
|
+
set -l metadata (string split "," -- $completion)
|
|
129
|
+
|
|
130
|
+
if [ $metadata[1] = "plain" ]
|
|
131
|
+
echo $metadata[2]
|
|
132
|
+
else if [ $metadata[1] = "dir" ]
|
|
133
|
+
__fish_complete_directories $metadata[2]
|
|
134
|
+
else if [ $metadata[1] = "file" ]
|
|
135
|
+
__fish_complete_path $metadata[2]
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
complete --no-files --command eero --arguments "(_eero_completion)"
|
|
141
|
+
"""
|
|
142
|
+
click.echo(script)
|