remotivelabs-cli 0.0.42__tar.gz → 0.1.0__tar.gz

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.
Files changed (66) hide show
  1. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/PKG-INFO +6 -4
  2. remotivelabs_cli-0.1.0/cli/.DS_Store +0 -0
  3. remotivelabs_cli-0.1.0/cli/api/cloud/tokens.py +62 -0
  4. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/brokers.py +0 -1
  5. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/export.py +4 -4
  6. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/lib/broker.py +9 -13
  7. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/license_flows.py +1 -1
  8. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/scripting.py +2 -1
  9. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/signals.py +9 -10
  10. remotivelabs_cli-0.1.0/cli/cloud/auth/cmd.py +91 -0
  11. remotivelabs_cli-0.1.0/cli/cloud/auth/login.py +316 -0
  12. remotivelabs_cli-0.1.0/cli/cloud/auth_tokens.py +340 -0
  13. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/brokers.py +3 -4
  14. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/cloud_cli.py +5 -5
  15. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/configs.py +1 -2
  16. remotivelabs_cli-0.1.0/cli/cloud/organisations.py +112 -0
  17. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/projects.py +5 -6
  18. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/recordings.py +9 -16
  19. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/recordings_playback.py +6 -8
  20. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/sample_recordings.py +2 -3
  21. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/service_account_tokens.py +21 -5
  22. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/service_accounts.py +32 -4
  23. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/storage/cmd.py +1 -1
  24. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/storage/copy.py +3 -4
  25. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/connect/connect.py +1 -1
  26. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/connect/protopie/protopie.py +12 -14
  27. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/errors.py +6 -1
  28. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/remotive.py +30 -6
  29. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/settings/__init__.py +1 -2
  30. remotivelabs_cli-0.1.0/cli/settings/config_file.py +92 -0
  31. remotivelabs_cli-0.1.0/cli/settings/core.py +404 -0
  32. remotivelabs_cli-0.1.0/cli/settings/migrate_all_token_files.py +74 -0
  33. remotivelabs_cli-0.1.0/cli/settings/migrate_token_file.py +52 -0
  34. remotivelabs_cli-0.1.0/cli/settings/token_file.py +87 -0
  35. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/tools/can/can.py +2 -2
  36. remotivelabs_cli-0.1.0/cli/typer/typer_utils.py +25 -0
  37. remotivelabs_cli-0.1.0/cli/utils/__init__.py +0 -0
  38. {remotivelabs_cli-0.0.42/cli/cloud → remotivelabs_cli-0.1.0/cli/utils}/rest_helper.py +114 -39
  39. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/pyproject.toml +23 -38
  40. remotivelabs_cli-0.0.42/cli/cloud/auth/cmd.py +0 -67
  41. remotivelabs_cli-0.0.42/cli/cloud/auth/login.py +0 -62
  42. remotivelabs_cli-0.0.42/cli/cloud/auth_tokens.py +0 -33
  43. remotivelabs_cli-0.0.42/cli/cloud/organisations.py +0 -13
  44. remotivelabs_cli-0.0.42/cli/settings/cmd.py +0 -72
  45. remotivelabs_cli-0.0.42/cli/settings/core.py +0 -261
  46. remotivelabs_cli-0.0.42/cli/settings/token_file.py +0 -22
  47. remotivelabs_cli-0.0.42/cli/typer/typer_utils.py +0 -8
  48. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/LICENSE +0 -0
  49. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/README.md +0 -0
  50. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/__init__.py +0 -0
  51. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/files.py +0 -0
  52. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/lib/__about__.py +0 -0
  53. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/licenses.py +0 -0
  54. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/playback.py +0 -0
  55. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/broker/record.py +0 -0
  56. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/__init__.py +0 -0
  57. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/auth/__init__.py +0 -0
  58. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/resumable_upload.py +0 -0
  59. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/storage/__init__.py +0 -0
  60. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/storage/uri_or_path.py +0 -0
  61. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/cloud/uri.py +0 -0
  62. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/connect/__init__.py +0 -0
  63. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/tools/__init__.py +0 -0
  64. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/tools/can/__init__.py +0 -0
  65. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/tools/tools.py +0 -0
  66. {remotivelabs_cli-0.0.42 → remotivelabs_cli-0.1.0}/cli/typer/__init__.py +0 -0
@@ -1,16 +1,18 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: remotivelabs-cli
3
- Version: 0.0.42
3
+ Version: 0.1.0
4
4
  Summary: CLI for operating RemotiveCloud and RemotiveBroker
5
5
  Author: Johan Rask
6
6
  Author-email: johan.rask@remotivelabs.com
7
- Requires-Python: >=3.8,<4
7
+ Requires-Python: >=3.9,<4
8
8
  Classifier: Programming Language :: Python :: 3
9
- Classifier: Programming Language :: Python :: 3.8
10
9
  Classifier: Programming Language :: Python :: 3.9
11
10
  Classifier: Programming Language :: Python :: 3.10
12
11
  Classifier: Programming Language :: Python :: 3.11
13
12
  Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Requires-Dist: click (<8.2.0)
15
+ Requires-Dist: dacite (>=1.9.2,<2.0.0)
14
16
  Requires-Dist: grpc-stubs (>=1.53.0.5)
15
17
  Requires-Dist: mypy-protobuf (>=3.0.0)
16
18
  Requires-Dist: plotext (>=5.2,<6.0)
Binary file
@@ -0,0 +1,62 @@
1
+ import dataclasses
2
+ from typing import Any
3
+
4
+ import requests
5
+
6
+ from cli.utils.rest_helper import RestHelper as Rest
7
+
8
+ #
9
+ # Attempt to start cloud-api.
10
+ # Might be better to generate somehow instead.
11
+ # Lets discuss further
12
+ #
13
+
14
+
15
+ @dataclasses.dataclass
16
+ class EmptyResponse:
17
+ is_success: bool
18
+ status_code: int
19
+
20
+
21
+ @dataclasses.dataclass
22
+ class JsonResponse:
23
+ is_success: bool
24
+ status_code: int
25
+ __response: requests.Response
26
+
27
+ def json(self) -> Any:
28
+ return self.__response.json()
29
+
30
+ def text(self) -> str:
31
+ return self.__response.text
32
+
33
+
34
+ def __delete_response_from_response(response: requests.Response) -> EmptyResponse:
35
+ if 200 > response.status_code > 299:
36
+ ok = False
37
+ else:
38
+ ok = True
39
+ return EmptyResponse(is_success=ok, status_code=response.status_code)
40
+
41
+
42
+ def __json_response_from_response(response: requests.Response) -> JsonResponse:
43
+ if 200 > response.status_code > 299:
44
+ ok = False
45
+ else:
46
+ ok = True
47
+ return JsonResponse(is_success=ok, status_code=response.status_code, __response=response)
48
+
49
+
50
+ def create() -> JsonResponse:
51
+ response = Rest.handle_post(url="/api/me/keys", return_response=True)
52
+ return __json_response_from_response(response)
53
+
54
+
55
+ def revoke(name: str) -> EmptyResponse:
56
+ res_revoke = Rest.handle_patch(f"/api/me/keys/{name}/revoke", quiet=True, allow_status_codes=[404, 401])
57
+ return __delete_response_from_response(res_revoke)
58
+
59
+
60
+ def delete(name: str) -> EmptyResponse:
61
+ res_delete = Rest.handle_delete(f"/api/me/keys/{name}", quiet=True, allow_status_codes=[404, 401])
62
+ return __delete_response_from_response(res_delete)
@@ -26,7 +26,6 @@ def main(
26
26
  if url is not None:
27
27
  os.environ["REMOTIVE_BROKER_URL"] = url
28
28
  # Do other global stuff, handle other global options here
29
- return
30
29
 
31
30
 
32
31
  @app.command(help="Discover brokers on this network")
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- import os
4
3
  import signal as os_signal
4
+ import sys
5
5
  from typing import Any, List
6
6
 
7
7
  import grpc
@@ -57,7 +57,7 @@ def influxdb(
57
57
  def exit_on_ctrlc(_sig: Any, _frame: Any) -> None:
58
58
  if output is not None:
59
59
  f.close()
60
- os._exit(0)
60
+ sys.exit(0)
61
61
 
62
62
  def per_frame_influx_line_protocol(x: Any) -> None:
63
63
  signals = list(x)
@@ -67,7 +67,7 @@ def influxdb(
67
67
  frame = signals[0]["name"].rsplit(".", 1)[0]
68
68
  # frame_name = signals[0]["name"].split(".")[1]
69
69
  namespace = signals[0]["namespace"]
70
- signals_str = ",".join(list(map(lambda s: f'{sig}={s["value"]}', signals)))
70
+ signals_str = ",".join(list(map(lambda s: f"{sig}={s['value']}", signals)))
71
71
  influx_lp = f"{frame},namespace={namespace} {signals_str} {round(signals[0]['timestamp_us'] * 1000)}"
72
72
  if output is not None:
73
73
  f.write(f"{influx_lp}\n")
@@ -112,7 +112,7 @@ def influxdb(
112
112
  arr = sig.split(":")
113
113
  if len(arr) != 2:
114
114
  ErrorPrinter.print_hint(f"--signal must have format namespace:signal ({sig})")
115
- exit(1)
115
+ sys.exit(1)
116
116
 
117
117
  return SubscribableSignal(namespace=arr[0], name=arr[1])
118
118
 
@@ -6,6 +6,7 @@ import os
6
6
  import posixpath
7
7
  import queue
8
8
  import signal as os_signal
9
+ import sys
9
10
  import tempfile
10
11
  import time
11
12
  import zipfile
@@ -59,9 +60,7 @@ class Broker:
59
60
  os.environ["ACCESS_TOKEN"] = "false"
60
61
  self.intercept_channel = br.create_channel(url, None, None)
61
62
  else:
62
- err_console.print(
63
- "Option --api-key will is deprecated and will be removed. " "Use access access tokens by logging in with cli."
64
- )
63
+ err_console.print("Option --api-key will is deprecated and will be removed. Use access access tokens by logging in with cli.")
65
64
  os.environ["ACCESS_TOKEN"] = "false"
66
65
  self.intercept_channel = br.create_channel(url, api_key, None)
67
66
 
@@ -303,11 +302,10 @@ class Broker:
303
302
 
304
303
  for r in response:
305
304
  if r["data"]:
306
- print(f'Successfully received traffic on {r["namespace"]}')
305
+ print(f"Successfully received traffic on {r['namespace']}")
307
306
  keep_running = False
308
- else:
309
- if not wait_for_traffic or (not keep_running and not keep_running_during_recording):
310
- print(f'Namespace {r["namespace"]} did not receive any traffic')
307
+ elif not wait_for_traffic or (not keep_running and not keep_running_during_recording):
308
+ print(f"Namespace {r['namespace']} did not receive any traffic")
311
309
 
312
310
  def upload(self, file: str, dest: str) -> None:
313
311
  try:
@@ -456,10 +454,8 @@ class Broker:
456
454
  if ns not in existing_ns:
457
455
  ns_not_matching.append(ns)
458
456
  if len(ns_not_matching) > 0:
459
- ErrorPrinter.print_hint(
460
- f"Namespace(s) {ns_not_matching} does not exist on broker. " f"Namespaces found on broker: {existing_ns}"
461
- )
462
- exit(1)
457
+ ErrorPrinter.print_hint(f"Namespace(s) {ns_not_matching} does not exist on broker. Namespaces found on broker: {existing_ns}")
458
+ sys.exit(1)
463
459
 
464
460
  available_signals = list(filter(verify_namespace, existing_signals)) # type: ignore
465
461
  signals_to_subscribe_to = list(filter(find_subscribed_signal, available_signals)) # type: ignore
@@ -468,8 +464,8 @@ class Broker:
468
464
  signals_subscribed_to_but_does_not_exist = set(subscribed_signals) - set(map(lambda s: s["signal"], signals_to_subscribe_to))
469
465
 
470
466
  if len(signals_subscribed_to_but_does_not_exist) > 0:
471
- ErrorPrinter.print_hint(f"One or more signals you subscribed to does not exist " f"{signals_subscribed_to_but_does_not_exist}")
472
- exit(1)
467
+ ErrorPrinter.print_hint(f"One or more signals you subscribed to does not exist {signals_subscribed_to_but_does_not_exist}")
468
+ sys.exit(1)
473
469
 
474
470
  return list(map(lambda s: SubscribableSignal(s["signal"], s["namespace"]), signals_to_subscribe_to))
475
471
 
@@ -11,7 +11,7 @@ from rich.console import Console
11
11
  from rich.progress import Progress, SpinnerColumn, TextColumn
12
12
 
13
13
  from cli.broker.lib.broker import Broker, LicenseInfo
14
- from cli.cloud.rest_helper import RestHelper as Rest
14
+ from cli.utils.rest_helper import RestHelper as Rest
15
15
 
16
16
  console = Console(stderr=True)
17
17
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  from string import Template
4
5
  from typing import List
5
6
 
@@ -35,7 +36,7 @@ def new_script(
35
36
  arr = sig.split(":")
36
37
  if len(arr) != 2:
37
38
  ErrorPrinter.print_hint(f"--input-signal must have format namespace:signal ({sig})")
38
- exit(1)
39
+ sys.exit(1)
39
40
  return arr[0], arr[1]
40
41
 
41
42
  signals_to_subscribe_to = list(map(to_subscribable_signal, input_signal))
@@ -4,6 +4,7 @@ import json
4
4
  import numbers
5
5
  import os
6
6
  import signal as os_signal
7
+ import sys
7
8
  from datetime import datetime
8
9
  from pathlib import Path
9
10
  from typing import Any, Dict, Iterable, List, TypedDict, Union
@@ -60,11 +61,11 @@ def read_scripted_code_file(file_path: Path) -> bytes:
60
61
  return file.read()
61
62
  except FileNotFoundError:
62
63
  print("File not found. Please check your file path.")
63
- exit(1)
64
+ sys.exit(1)
64
65
 
65
66
 
66
67
  @app.command()
67
- def subscribe( # noqa: C901
68
+ def subscribe( # noqa: C901, PLR0913, PLR0915
68
69
  url: str = typer.Option(DEFAULT_GRPC_URL, help="Broker URL", envvar="REMOTIVE_BROKER_URL"),
69
70
  api_key: str = typer.Option(None, help="Cloud Broker API-KEY or access token", envvar="REMOTIVE_BROKER_API_KEY"),
70
71
  signal: List[str] = typer.Option([], help="Signal names to subscribe to, mandatory when not using script"),
@@ -79,7 +80,7 @@ def subscribe( # noqa: C901
79
80
  help="Supply a path to Lua script that to use for signal transformation",
80
81
  ),
81
82
  on_change_only: bool = typer.Option(default=False, help="Only get signal if value is changed"),
82
- x_plot: bool = typer.Option(default=False, help="Experimental: Plot the signal in terminal. Note graphs are not " "aligned by time"),
83
+ x_plot: bool = typer.Option(default=False, help="Experimental: Plot the signal in terminal. Note graphs are not aligned by time"),
83
84
  x_plot_size: int = typer.Option(default=100, help="Experimental: how many points show for each plot"),
84
85
  # samples: int = typer.Option(default=0, he)
85
86
  ) -> None:
@@ -100,12 +101,12 @@ def subscribe( # noqa: C901
100
101
  if script is None:
101
102
  if len(signal) == 0:
102
103
  ErrorPrinter.print_generic_error("You must use --signal or use --script when subscribing")
103
- exit(1)
104
+ sys.exit(1)
104
105
 
105
106
  if script is not None:
106
107
  if len(signal) > 0:
107
108
  ErrorPrinter.print_generic_error("You must must not specify --signal when using --script")
108
- exit(1)
109
+ sys.exit(1)
109
110
 
110
111
  plt.title("Signals")
111
112
 
@@ -113,8 +114,6 @@ def subscribe( # noqa: C901
113
114
  os._exit(0)
114
115
 
115
116
  def on_frame_plot(x: Iterable[Any]) -> None:
116
- global signal_values
117
-
118
117
  plt.clt() # to clear the terminal
119
118
  plt.cld() # to clear the data only
120
119
  frames = list(x)
@@ -146,9 +145,9 @@ def subscribe( # noqa: C901
146
145
  signal_values[f"ts_{name}"] = signal_values[f"ts_{name}"][len(signal_values[f"ts_{name}"]) - x_plot_size :]
147
146
 
148
147
  cnt = 1
149
- for key in signal_values:
148
+ for key, value in signal_values.items():
150
149
  if not key.startswith("ts_"):
151
- plt.subplot(cnt, 1).plot(signal_values[f"ts_{key}"], signal_values[key], label=key, color=cnt)
150
+ plt.subplot(cnt, 1).plot(signal_values[f"ts_{key}"], value, label=key, color=cnt)
152
151
  cnt = cnt + 1
153
152
  plt.sleep(0.001) # to add
154
153
  plt.show()
@@ -174,7 +173,7 @@ def subscribe( # noqa: C901
174
173
  arr = sig.split(":")
175
174
  if len(arr) != 2:
176
175
  ErrorPrinter.print_hint(f"--signal must have format namespace:signal ({sig})")
177
- exit(1)
176
+ sys.exit(1)
178
177
  return SubscribableSignal(namespace=arr[0], name=arr[1])
179
178
 
180
179
  signals_to_subscribe_to = list(map(to_subscribable_signal, signal))
@@ -0,0 +1,91 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+
5
+ import typer
6
+
7
+ from cli.cloud.auth.login import login as do_login
8
+ from cli.errors import ErrorPrinter
9
+ from cli.settings import TokenNotFoundError, settings
10
+ from cli.typer import typer_utils
11
+ from cli.utils.rest_helper import RestHelper as Rest
12
+
13
+ from .. import auth_tokens
14
+
15
+ HELP = """
16
+ Manage how you authenticate with our cloud platform
17
+ """
18
+ app = typer_utils.create_typer(help=HELP)
19
+ # app.add_typer(auth_tokens.app, name="credentials", help="Manage account credentials")
20
+
21
+
22
+ @app.command(name="login")
23
+ def login(browser: bool = typer.Option(default=True, help="Does not automatically open browser, instead shows a link")) -> None:
24
+ """
25
+ Login to the cli using browser
26
+
27
+ If not able to open a browser it will show fallback to headless login and show a link that
28
+ users can copy into any browser when this is unsupported where running the cli - such as in docker,
29
+ virtual machine or ssh sessions.
30
+
31
+ This will be used as the current access token in all subsequent requests. This would
32
+ be the same as activating a personal access key or service-account access key.
33
+ """
34
+ do_login(headless=not browser)
35
+
36
+
37
+ @app.command()
38
+ def whoami() -> None:
39
+ """
40
+ Validates authentication and fetches your account information
41
+ """
42
+ try:
43
+ Rest.handle_get("/api/whoami")
44
+ except TokenNotFoundError as e:
45
+ ErrorPrinter.print_hint(str(e))
46
+ sys.exit(1)
47
+
48
+
49
+ @app.command()
50
+ def print_access_token(
51
+ account: str = typer.Option(None, help="Email of the account you want to print access token for, defaults to active"),
52
+ ) -> None:
53
+ """
54
+ Print current active access token or the token for the specified account
55
+ """
56
+ if account is None:
57
+ try:
58
+ print(settings.get_active_token())
59
+ except TokenNotFoundError:
60
+ ErrorPrinter.print_generic_error("You have no active account", exit_code=1)
61
+ else:
62
+ config = settings.get_cli_config()
63
+ if account in config.accounts:
64
+ token_name = config.accounts[account].credentials_name
65
+ try:
66
+ print(settings.get_token_file(token_name).token)
67
+ except TokenNotFoundError:
68
+ ErrorPrinter.print_generic_error(f"Token file for {account} could not be found", exit_code=1)
69
+ else:
70
+ ErrorPrinter.print_generic_error(f"No account for {account} was found", exit_code=1)
71
+
72
+
73
+ def print_access_token_file() -> None:
74
+ """
75
+ Print current active token and its metadata
76
+ """
77
+ try:
78
+ print(settings.get_active_token_file())
79
+ except TokenNotFoundError as e:
80
+ ErrorPrinter.print_hint(str(e))
81
+ sys.exit(1)
82
+
83
+
84
+ # @app.command(help="Clears active credentials")
85
+ def logout() -> None:
86
+ settings.clear_active_token()
87
+ print("Access token removed")
88
+
89
+
90
+ app.command("activate")(auth_tokens.select_personal_token)
91
+ app.command("list")(auth_tokens.list_pats_files)