tescmd 0.3.1__py3-none-any.whl → 0.4.0__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.
- tescmd/__init__.py +1 -1
- tescmd/auth/oauth.py +10 -0
- tescmd/cli/auth.py +212 -145
- tescmd/cli/key.py +6 -7
- tescmd/cli/setup.py +134 -56
- tescmd/deploy/github_pages.py +13 -2
- tescmd/deploy/tailscale_serve.py +96 -8
- tescmd/openclaw/dispatcher.py +9 -2
- tescmd/openclaw/gateway.py +13 -0
- tescmd/telemetry/tailscale.py +78 -16
- tescmd-0.4.0.dist-info/METADATA +300 -0
- {tescmd-0.3.1.dist-info → tescmd-0.4.0.dist-info}/RECORD +15 -15
- tescmd-0.3.1.dist-info/METADATA +0 -543
- {tescmd-0.3.1.dist-info → tescmd-0.4.0.dist-info}/WHEEL +0 -0
- {tescmd-0.3.1.dist-info → tescmd-0.4.0.dist-info}/entry_points.txt +0 -0
- {tescmd-0.3.1.dist-info → tescmd-0.4.0.dist-info}/licenses/LICENSE +0 -0
tescmd/__init__.py
CHANGED
tescmd/auth/oauth.py
CHANGED
|
@@ -208,6 +208,14 @@ async def register_partner_account(
|
|
|
208
208
|
)
|
|
209
209
|
|
|
210
210
|
if resp.status_code >= 400:
|
|
211
|
+
# HTTP 422 with "already been taken" means the key or domain is
|
|
212
|
+
# already registered. Treat as success — registration is idempotent.
|
|
213
|
+
if resp.status_code == 422 and "already been taken" in resp.text:
|
|
214
|
+
logger.info(
|
|
215
|
+
"Partner already registered for %s region (key already taken)",
|
|
216
|
+
region,
|
|
217
|
+
)
|
|
218
|
+
return {"already_registered": True}, granted_scopes
|
|
211
219
|
raise AuthError(
|
|
212
220
|
f"Partner registration failed (HTTP {resp.status_code}): {resp.text}",
|
|
213
221
|
status_code=resp.status_code,
|
|
@@ -282,6 +290,8 @@ async def login_flow(
|
|
|
282
290
|
state,
|
|
283
291
|
force_consent=force_consent,
|
|
284
292
|
)
|
|
293
|
+
logger.info("Authorization URL: %s", url)
|
|
294
|
+
print(f"\n {url}\n")
|
|
285
295
|
webbrowser.open(url)
|
|
286
296
|
|
|
287
297
|
code, callback_state = server.wait_for_callback(timeout=120)
|
tescmd/cli/auth.py
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import sys
|
|
7
8
|
import time
|
|
9
|
+
import uuid
|
|
8
10
|
import webbrowser
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
from typing import TYPE_CHECKING
|
|
@@ -31,7 +33,9 @@ from tescmd.models.config import AppSettings
|
|
|
31
33
|
|
|
32
34
|
if TYPE_CHECKING:
|
|
33
35
|
from tescmd.cli.main import AppContext
|
|
36
|
+
from tescmd.deploy.tailscale_serve import KeyServer
|
|
34
37
|
from tescmd.output.formatter import OutputFormatter
|
|
38
|
+
from tescmd.telemetry.tailscale import TailscaleManager
|
|
35
39
|
|
|
36
40
|
DEVELOPER_PORTAL_URL = "https://developer.tesla.com/dashboard"
|
|
37
41
|
|
|
@@ -374,9 +378,10 @@ async def _cmd_register(app_ctx: AppContext) -> None:
|
|
|
374
378
|
region=region,
|
|
375
379
|
)
|
|
376
380
|
|
|
381
|
+
already = bool(_result.get("already_registered"))
|
|
377
382
|
if formatter.format == "json":
|
|
378
383
|
data: dict[str, object] = {
|
|
379
|
-
"status": "registered",
|
|
384
|
+
"status": "already_registered" if already else "registered",
|
|
380
385
|
"region": region,
|
|
381
386
|
"domain": domain,
|
|
382
387
|
}
|
|
@@ -384,7 +389,10 @@ async def _cmd_register(app_ctx: AppContext) -> None:
|
|
|
384
389
|
data["partner_scopes"] = partner_scopes
|
|
385
390
|
formatter.output(data, command="auth.register")
|
|
386
391
|
else:
|
|
387
|
-
|
|
392
|
+
if already:
|
|
393
|
+
formatter.rich.info("[green]Already registered — no action needed.[/green]")
|
|
394
|
+
else:
|
|
395
|
+
formatter.rich.info("[green]Registration successful.[/green]")
|
|
388
396
|
if partner_scopes:
|
|
389
397
|
formatter.rich.info(f"Partner scopes: {', '.join(partner_scopes)}")
|
|
390
398
|
missing = sorted(set(PARTNER_SCOPES) - set(partner_scopes))
|
|
@@ -448,7 +456,10 @@ async def _auto_register(
|
|
|
448
456
|
domain=domain,
|
|
449
457
|
region=region,
|
|
450
458
|
)
|
|
451
|
-
|
|
459
|
+
if _result.get("already_registered"):
|
|
460
|
+
formatter.rich.info("[green]Already registered — no action needed.[/green]")
|
|
461
|
+
else:
|
|
462
|
+
formatter.rich.info("[green]Registration successful.[/green]")
|
|
452
463
|
if partner_scopes:
|
|
453
464
|
missing = sorted(set(PARTNER_SCOPES) - set(partner_scopes))
|
|
454
465
|
if missing:
|
|
@@ -469,6 +480,8 @@ def _interactive_setup(
|
|
|
469
480
|
*,
|
|
470
481
|
domain: str = "",
|
|
471
482
|
tailscale_hostname: str = "",
|
|
483
|
+
full_tier: bool = False,
|
|
484
|
+
force: bool = False,
|
|
472
485
|
) -> tuple[str, str]:
|
|
473
486
|
"""Walk the user through first-time Tesla API credential setup.
|
|
474
487
|
|
|
@@ -476,9 +489,11 @@ def _interactive_setup(
|
|
|
476
489
|
portal instructions show ``https://{domain}`` as the Allowed Origin URL.
|
|
477
490
|
Tesla's Fleet API requires the origin to match the registration domain.
|
|
478
491
|
|
|
479
|
-
When *tailscale_hostname* is provided (or
|
|
480
|
-
offered the chance to start a Tailscale
|
|
481
|
-
origin URL when the portal app config
|
|
492
|
+
When *full_tier* is True and *tailscale_hostname* is provided (or
|
|
493
|
+
auto-detected), the user is offered the chance to start a Tailscale
|
|
494
|
+
Funnel so Tesla can verify the origin URL when the portal app config
|
|
495
|
+
is saved. The Tailscale prompt appears **before** the browser is
|
|
496
|
+
opened so the developer portal steps (1-6) are uninterrupted.
|
|
482
497
|
"""
|
|
483
498
|
info = formatter.rich.info
|
|
484
499
|
origin_url = f"https://{domain}" if domain else f"http://localhost:{port}"
|
|
@@ -492,170 +507,222 @@ def _interactive_setup(
|
|
|
492
507
|
)
|
|
493
508
|
info("")
|
|
494
509
|
|
|
495
|
-
#
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
510
|
+
# --- Tailscale detection + Funnel prompt (before browser opens) ----
|
|
511
|
+
# Only shown during the full setup wizard — the standalone ``auth
|
|
512
|
+
# setup`` command is credentials-only and defaults full_tier=False.
|
|
513
|
+
ts_hostname = tailscale_hostname if full_tier else ""
|
|
514
|
+
ts_funnel_started = False
|
|
515
|
+
_ts_manager: TailscaleManager | None = None
|
|
516
|
+
_key_server: KeyServer | None = None
|
|
501
517
|
|
|
502
|
-
if
|
|
503
|
-
|
|
504
|
-
|
|
518
|
+
if full_tier:
|
|
519
|
+
if not ts_hostname:
|
|
520
|
+
try:
|
|
521
|
+
from tescmd.deploy.tailscale_serve import is_tailscale_serve_ready
|
|
505
522
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
"Follow these steps to create a Fleet API application."
|
|
509
|
-
" If you already have one, skip to the credentials prompt below."
|
|
510
|
-
)
|
|
511
|
-
info("")
|
|
523
|
+
if run_async(is_tailscale_serve_ready()):
|
|
524
|
+
from tescmd.telemetry.tailscale import TailscaleManager
|
|
512
525
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
info("")
|
|
526
|
+
ts_hostname = run_async(TailscaleManager().get_hostname())
|
|
527
|
+
except Exception:
|
|
528
|
+
pass
|
|
517
529
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
info(" Application Name: [cyan]tescmd[/cyan] (or anything you like)")
|
|
521
|
-
info(" Description: [cyan]Personal CLI tool for vehicle status and control[/cyan]")
|
|
522
|
-
info(
|
|
523
|
-
" Purpose of Usage: [cyan]Query vehicle data and send commands from the terminal[/cyan]"
|
|
524
|
-
)
|
|
525
|
-
info(" Click Next.")
|
|
526
|
-
info("")
|
|
530
|
+
if ts_hostname:
|
|
531
|
+
ts_origin = f"https://{ts_hostname}"
|
|
527
532
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
533
|
+
info(f"[green]Tailscale detected:[/green] {ts_hostname}")
|
|
534
|
+
info("")
|
|
535
|
+
info(f" Adding [cyan]{ts_origin}[/cyan] as an Allowed Origin URL")
|
|
536
|
+
info(" will let [cyan]tescmd serve[/cyan] stream telemetry without")
|
|
537
|
+
info(" extra portal changes later.")
|
|
538
|
+
info("")
|
|
539
|
+
try:
|
|
540
|
+
answer = input(
|
|
541
|
+
"Start Tailscale Funnel so Tesla can verify the URL? [Y/n] "
|
|
542
|
+
).strip()
|
|
543
|
+
except (EOFError, KeyboardInterrupt):
|
|
544
|
+
answer = "n"
|
|
545
|
+
|
|
546
|
+
if answer.lower() != "n":
|
|
547
|
+
info("Starting Tailscale Funnel...")
|
|
548
|
+
try:
|
|
549
|
+
from tescmd.crypto.keys import (
|
|
550
|
+
generate_ec_key_pair,
|
|
551
|
+
has_key_pair,
|
|
552
|
+
load_public_key_pem,
|
|
553
|
+
)
|
|
554
|
+
from tescmd.deploy.tailscale_serve import KeyServer
|
|
555
|
+
from tescmd.telemetry.tailscale import TailscaleManager
|
|
556
|
+
|
|
557
|
+
key_dir = Path(AppSettings().config_dir).expanduser() / "keys"
|
|
558
|
+
|
|
559
|
+
if not has_key_pair(key_dir):
|
|
560
|
+
info("Generating EC P-256 key pair...")
|
|
561
|
+
generate_ec_key_pair(key_dir)
|
|
562
|
+
|
|
563
|
+
pem = load_public_key_pem(key_dir)
|
|
564
|
+
_key_server = KeyServer(pem, port=0)
|
|
565
|
+
_local_port = _key_server.server_address[1]
|
|
566
|
+
_key_server.start()
|
|
567
|
+
|
|
568
|
+
_ts_manager = TailscaleManager()
|
|
569
|
+
run_async(_ts_manager.start_funnel(_local_port))
|
|
570
|
+
|
|
571
|
+
ts_funnel_started = True
|
|
572
|
+
info(f"[green]Funnel active — {ts_origin} is reachable.[/green]")
|
|
573
|
+
except Exception as exc:
|
|
574
|
+
if _key_server is not None:
|
|
575
|
+
_key_server.stop()
|
|
576
|
+
_key_server = None
|
|
577
|
+
_ts_manager = None
|
|
578
|
+
info(f"[yellow]Could not start Funnel: {exc}[/yellow]")
|
|
579
|
+
info("[dim]You can add the origin URL manually later.[/dim]")
|
|
580
|
+
ts_hostname = ""
|
|
581
|
+
|
|
582
|
+
# --- Open browser + credential prompts (with guaranteed cleanup) ---
|
|
583
|
+
try:
|
|
532
584
|
try:
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
585
|
+
answer = input("Open the Tesla Developer Portal in your browser? [Y/n] ")
|
|
586
|
+
except (EOFError, KeyboardInterrupt):
|
|
587
|
+
info("")
|
|
588
|
+
return ("", "")
|
|
537
589
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
590
|
+
if answer.strip().lower() != "n":
|
|
591
|
+
webbrowser.open(DEVELOPER_PORTAL_URL)
|
|
592
|
+
info("[dim]Browser opened.[/dim]")
|
|
541
593
|
|
|
542
|
-
if ts_hostname:
|
|
543
|
-
ts_origin = f"https://{ts_hostname}"
|
|
544
594
|
info("")
|
|
545
|
-
info(
|
|
595
|
+
info(
|
|
596
|
+
"Follow these steps to create a Fleet API application."
|
|
597
|
+
" If you already have one, skip to the credentials prompt below."
|
|
598
|
+
)
|
|
546
599
|
info("")
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
info("
|
|
600
|
+
|
|
601
|
+
# Step 1 — Create New Application
|
|
602
|
+
info("[bold]Step 1 — Create New Application[/bold]")
|
|
603
|
+
info(" Select [cyan]Just for me[/cyan] and click Next.")
|
|
550
604
|
info("")
|
|
551
|
-
try:
|
|
552
|
-
answer = input("Start Tailscale Funnel so Tesla can verify the URL? [Y/n] ").strip()
|
|
553
|
-
except (EOFError, KeyboardInterrupt):
|
|
554
|
-
answer = "n"
|
|
555
605
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
ts_hostname = ""
|
|
568
|
-
|
|
569
|
-
# Step 3 — Client Details
|
|
570
|
-
info("[bold]Step 3 — Client Details[/bold]")
|
|
571
|
-
info(
|
|
572
|
-
" OAuth Grant Type: [cyan]Authorization Code and"
|
|
573
|
-
" Machine-to-Machine[/cyan] (the default)"
|
|
574
|
-
)
|
|
575
|
-
info(f" Allowed Origin URL: [cyan]{origin_url}[/cyan]")
|
|
576
|
-
if ts_hostname:
|
|
577
|
-
info(f" [bold]Also add:[/bold] [cyan]https://{ts_hostname}[/cyan]")
|
|
578
|
-
info(f" Allowed Redirect URI: [cyan]{redirect_uri}[/cyan]")
|
|
579
|
-
info(" Allowed Returned URL: (leave empty)")
|
|
580
|
-
info("")
|
|
581
|
-
if not ts_hostname:
|
|
606
|
+
# Step 2 — Application Details
|
|
607
|
+
#
|
|
608
|
+
# Generate a unique app name so the user doesn't collide with anyone
|
|
609
|
+
# else on the Tesla Developer Portal. Reuse a previously saved name
|
|
610
|
+
# if setup is being re-run.
|
|
611
|
+
saved_app_name = "" if force else os.environ.get("TESLA_APP_NAME", "")
|
|
612
|
+
app_name = saved_app_name or f"tescmd-{uuid.uuid4().hex[:8]}"
|
|
613
|
+
|
|
614
|
+
info("[bold]Step 2 — Application Details[/bold]")
|
|
615
|
+
info(f" Application Name: [cyan]{app_name}[/cyan]")
|
|
616
|
+
info(" Description: [cyan]Personal CLI tool for vehicle status and control[/cyan]")
|
|
582
617
|
info(
|
|
583
|
-
" [
|
|
584
|
-
"
|
|
618
|
+
" Purpose of Usage: [cyan]Query vehicle data and send commands"
|
|
619
|
+
" from the terminal[/cyan]"
|
|
585
620
|
)
|
|
586
|
-
info("
|
|
587
|
-
|
|
588
|
-
info("")
|
|
589
|
-
|
|
590
|
-
# Step 4 — API & Scopes
|
|
591
|
-
info("[bold]Step 4 — API & Scopes[/bold]")
|
|
592
|
-
info(" Under [bold]Fleet API[/bold], check at least:")
|
|
593
|
-
info(" [cyan]Vehicle Information[/cyan]")
|
|
594
|
-
info(" [cyan]Vehicle Location[/cyan]")
|
|
595
|
-
info(" [cyan]Vehicle Commands[/cyan]")
|
|
596
|
-
info(" [cyan]Vehicle Charging Management[/cyan]")
|
|
597
|
-
info(" Click Next.")
|
|
598
|
-
info("")
|
|
599
|
-
|
|
600
|
-
# Step 5 — Billing Details
|
|
601
|
-
info("[bold]Step 5 — Billing Details[/bold]")
|
|
602
|
-
info(" Click [cyan]Skip and Submit[/cyan] at the bottom of the page.")
|
|
603
|
-
info("")
|
|
604
|
-
|
|
605
|
-
# Post-creation
|
|
606
|
-
info("[bold]Step 6 — Copy your credentials[/bold]")
|
|
607
|
-
info(
|
|
608
|
-
" Open your dashboard:"
|
|
609
|
-
" [link=https://developer.tesla.com/en_US/dashboard]"
|
|
610
|
-
"developer.tesla.com/dashboard[/link]"
|
|
611
|
-
)
|
|
612
|
-
info(" Click [cyan]View Details[/cyan] on your app.")
|
|
613
|
-
info(" Under the [cyan]Credentials & APIs[/cyan] tab you'll see your")
|
|
614
|
-
info(" Client ID (copy icon) and Client Secret (eye icon to reveal).")
|
|
615
|
-
info("")
|
|
621
|
+
info(" Click Next.")
|
|
622
|
+
info("")
|
|
616
623
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
624
|
+
# Step 3 — Client Details
|
|
625
|
+
info("[bold]Step 3 — Client Details[/bold]")
|
|
626
|
+
info(
|
|
627
|
+
" OAuth Grant Type: [cyan]Authorization Code and"
|
|
628
|
+
" Machine-to-Machine[/cyan] (the default)"
|
|
629
|
+
)
|
|
630
|
+
info(f" Allowed Origin URL: [cyan]{origin_url}[/cyan]")
|
|
631
|
+
if ts_hostname:
|
|
632
|
+
info(f" [bold]Also add:[/bold] [cyan]https://{ts_hostname}[/cyan]")
|
|
633
|
+
info(f" Allowed Redirect URI: [cyan]{redirect_uri}[/cyan]")
|
|
634
|
+
info(" Allowed Returned URL: (leave empty)")
|
|
635
|
+
info("")
|
|
636
|
+
if not ts_hostname:
|
|
637
|
+
info(
|
|
638
|
+
" [dim]For telemetry streaming, add your Tailscale hostname"
|
|
639
|
+
" as an additional origin:[/dim]"
|
|
640
|
+
)
|
|
641
|
+
info(" [dim] https://<machine>.tailnet.ts.net[/dim]")
|
|
642
|
+
info(" Click Next.")
|
|
621
643
|
info("")
|
|
622
|
-
return ("", "")
|
|
623
644
|
|
|
624
|
-
|
|
625
|
-
info("[
|
|
626
|
-
|
|
645
|
+
# Step 4 — API & Scopes
|
|
646
|
+
info("[bold]Step 4 — API & Scopes[/bold]")
|
|
647
|
+
if full_tier:
|
|
648
|
+
info(" Under [bold]Fleet API[/bold], click [cyan]Select All[/cyan].")
|
|
649
|
+
else:
|
|
650
|
+
info(" Under [bold]Fleet API[/bold], check at least:")
|
|
651
|
+
info(" [cyan]Vehicle Information[/cyan]")
|
|
652
|
+
info(" [cyan]Vehicle Location[/cyan]")
|
|
653
|
+
info(" [cyan]Energy Information[/cyan]")
|
|
654
|
+
info(" [cyan]User Data[/cyan]")
|
|
655
|
+
info(" Click Next.")
|
|
656
|
+
info("")
|
|
627
657
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
except (EOFError, KeyboardInterrupt):
|
|
658
|
+
# Step 5 — Billing Details
|
|
659
|
+
info("[bold]Step 5 — Billing Details (Optional)[/bold]")
|
|
660
|
+
info(" Click [cyan]Skip and Submit[/cyan] at the bottom of the page.")
|
|
632
661
|
info("")
|
|
633
|
-
return ("", "")
|
|
634
662
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
663
|
+
# Post-creation
|
|
664
|
+
info("[bold]Step 6 — Copy your credentials[/bold]")
|
|
665
|
+
info(
|
|
666
|
+
" Open your dashboard:"
|
|
667
|
+
" [link=https://developer.tesla.com/en_US/dashboard]"
|
|
668
|
+
"developer.tesla.com/dashboard[/link]"
|
|
669
|
+
)
|
|
670
|
+
info(" Click [cyan]View Details[/cyan] on your app.")
|
|
671
|
+
info(" Under the [cyan]Credentials & APIs[/cyan] tab you'll see your")
|
|
672
|
+
info(" Client ID (copy icon) and Client Secret (eye icon to reveal).")
|
|
640
673
|
info("")
|
|
641
|
-
return (client_id, client_secret)
|
|
642
674
|
|
|
643
|
-
|
|
675
|
+
# Prompt for Client ID (required — retry on empty)
|
|
676
|
+
client_id = ""
|
|
677
|
+
for _ in range(3):
|
|
678
|
+
try:
|
|
679
|
+
client_id = input("Client ID: ").strip()
|
|
680
|
+
except (EOFError, KeyboardInterrupt):
|
|
681
|
+
info("")
|
|
682
|
+
return ("", "")
|
|
683
|
+
if client_id:
|
|
684
|
+
break
|
|
685
|
+
info("[yellow]Client ID is required.[/yellow]")
|
|
686
|
+
|
|
687
|
+
if not client_id:
|
|
688
|
+
info("[yellow]No Client ID provided. Setup cancelled.[/yellow]")
|
|
689
|
+
return ("", "")
|
|
690
|
+
|
|
691
|
+
# Prompt for Client Secret (required — retry on empty)
|
|
692
|
+
client_secret = ""
|
|
693
|
+
for _ in range(3):
|
|
694
|
+
try:
|
|
695
|
+
client_secret = input("Client Secret: ").strip()
|
|
696
|
+
except (EOFError, KeyboardInterrupt):
|
|
697
|
+
info("")
|
|
698
|
+
return ("", "")
|
|
699
|
+
if client_secret:
|
|
700
|
+
break
|
|
701
|
+
info("[yellow]Client Secret is required.[/yellow]")
|
|
702
|
+
|
|
703
|
+
if not client_secret:
|
|
704
|
+
info("[yellow]No Client Secret provided. Setup cancelled.[/yellow]")
|
|
705
|
+
return ("", "")
|
|
706
|
+
|
|
707
|
+
# Persist credentials to .env
|
|
708
|
+
info("")
|
|
644
709
|
_write_env_file(client_id, client_secret)
|
|
710
|
+
if not saved_app_name:
|
|
711
|
+
_write_env_value("TESLA_APP_NAME", app_name)
|
|
645
712
|
info("[green]Credentials saved to .env[/green]")
|
|
646
713
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
714
|
+
info("")
|
|
715
|
+
return (client_id, client_secret)
|
|
716
|
+
finally:
|
|
717
|
+
if ts_funnel_started:
|
|
718
|
+
if _key_server is not None:
|
|
719
|
+
_key_server.stop()
|
|
720
|
+
if _ts_manager is not None:
|
|
721
|
+
try:
|
|
722
|
+
run_async(_ts_manager.stop_funnel())
|
|
723
|
+
except Exception as exc:
|
|
724
|
+
info(f"[yellow]Warning: Failed to stop Tailscale Funnel: {exc}[/yellow]")
|
|
725
|
+
info("[dim]You may need to run 'tailscale funnel --bg off' manually.[/dim]")
|
|
659
726
|
|
|
660
727
|
|
|
661
728
|
def _prompt_for_domain(formatter: OutputFormatter) -> str:
|
tescmd/cli/key.py
CHANGED
|
@@ -585,13 +585,12 @@ async def _cmd_enroll(
|
|
|
585
585
|
formatter.rich.info("")
|
|
586
586
|
formatter.rich.info(f" Enrollment URL: [link={enroll_url}]{enroll_url}[/link]")
|
|
587
587
|
formatter.rich.info("")
|
|
588
|
-
formatter.rich.info(" 1.
|
|
589
|
-
formatter.rich.info(" 2.
|
|
590
|
-
formatter.rich.info(" 3.
|
|
591
|
-
formatter.rich.info(" 4. Approve it")
|
|
588
|
+
formatter.rich.info(" 1. Scan the QR code on the page above with your phone[/bold]")
|
|
589
|
+
formatter.rich.info(" 2. The Tesla app will show an [bold]Add Virtual Key[/bold] prompt")
|
|
590
|
+
formatter.rich.info(" 3. Select all Scopes (if shown) and approve it")
|
|
592
591
|
formatter.rich.info("")
|
|
593
592
|
formatter.rich.info(" [dim]If the prompt doesn't appear, force-quit the Tesla app,[/dim]")
|
|
594
|
-
formatter.rich.info(" [dim]
|
|
593
|
+
formatter.rich.info(" [dim]and scan the QR code again.[/dim]")
|
|
595
594
|
formatter.rich.info("━" * 55)
|
|
596
595
|
formatter.rich.info("")
|
|
597
596
|
|
|
@@ -601,11 +600,11 @@ async def _cmd_enroll(
|
|
|
601
600
|
formatter.rich.info("")
|
|
602
601
|
|
|
603
602
|
formatter.rich.info("After approving in the Tesla app, try a command:")
|
|
604
|
-
formatter.rich.info(" [cyan]tescmd
|
|
603
|
+
formatter.rich.info(" [cyan]tescmd vehicle list[/cyan]")
|
|
605
604
|
formatter.rich.info(" [cyan]tescmd charge status --wake[/cyan]")
|
|
606
605
|
formatter.rich.info("")
|
|
607
606
|
formatter.rich.info(
|
|
608
|
-
"[dim]Tip:
|
|
607
|
+
"[dim]Tip: The QR code must be scanned on your phone that has the Tesla app installed.[/dim]"
|
|
609
608
|
)
|
|
610
609
|
|
|
611
610
|
|