pymobiledevice3 6.2.0__py3-none-any.whl → 7.0.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.
Files changed (61) hide show
  1. pymobiledevice3/__main__.py +136 -44
  2. pymobiledevice3/_version.py +2 -2
  3. pymobiledevice3/bonjour.py +19 -20
  4. pymobiledevice3/cli/activation.py +24 -22
  5. pymobiledevice3/cli/afc.py +49 -41
  6. pymobiledevice3/cli/amfi.py +13 -18
  7. pymobiledevice3/cli/apps.py +71 -65
  8. pymobiledevice3/cli/backup.py +134 -93
  9. pymobiledevice3/cli/bonjour.py +31 -29
  10. pymobiledevice3/cli/cli_common.py +179 -232
  11. pymobiledevice3/cli/companion_proxy.py +12 -12
  12. pymobiledevice3/cli/crash.py +95 -52
  13. pymobiledevice3/cli/developer/__init__.py +62 -0
  14. pymobiledevice3/cli/developer/accessibility/__init__.py +65 -0
  15. pymobiledevice3/cli/developer/accessibility/settings.py +43 -0
  16. pymobiledevice3/cli/developer/arbitration.py +50 -0
  17. pymobiledevice3/cli/developer/condition.py +33 -0
  18. pymobiledevice3/cli/developer/core_device.py +294 -0
  19. pymobiledevice3/cli/developer/debugserver.py +244 -0
  20. pymobiledevice3/cli/developer/dvt/__init__.py +387 -0
  21. pymobiledevice3/cli/developer/dvt/core_profile_session.py +295 -0
  22. pymobiledevice3/cli/developer/dvt/simulate_location.py +56 -0
  23. pymobiledevice3/cli/developer/dvt/sysmon/__init__.py +69 -0
  24. pymobiledevice3/cli/developer/dvt/sysmon/process.py +188 -0
  25. pymobiledevice3/cli/developer/fetch_symbols.py +108 -0
  26. pymobiledevice3/cli/developer/simulate_location.py +51 -0
  27. pymobiledevice3/cli/diagnostics/__init__.py +75 -0
  28. pymobiledevice3/cli/diagnostics/battery.py +47 -0
  29. pymobiledevice3/cli/idam.py +18 -22
  30. pymobiledevice3/cli/lockdown.py +70 -75
  31. pymobiledevice3/cli/mounter.py +99 -57
  32. pymobiledevice3/cli/notification.py +38 -26
  33. pymobiledevice3/cli/pcap.py +36 -20
  34. pymobiledevice3/cli/power_assertion.py +15 -16
  35. pymobiledevice3/cli/processes.py +11 -17
  36. pymobiledevice3/cli/profile.py +120 -75
  37. pymobiledevice3/cli/provision.py +27 -26
  38. pymobiledevice3/cli/remote.py +108 -99
  39. pymobiledevice3/cli/restore.py +134 -129
  40. pymobiledevice3/cli/springboard.py +50 -50
  41. pymobiledevice3/cli/syslog.py +138 -74
  42. pymobiledevice3/cli/usbmux.py +66 -27
  43. pymobiledevice3/cli/version.py +2 -5
  44. pymobiledevice3/cli/webinspector.py +149 -103
  45. pymobiledevice3/remote/remote_service_discovery.py +11 -10
  46. pymobiledevice3/restore/device.py +28 -4
  47. pymobiledevice3/service_connection.py +1 -1
  48. pymobiledevice3/services/mobilebackup2.py +4 -1
  49. pymobiledevice3/services/screenshot.py +2 -2
  50. pymobiledevice3/services/web_protocol/automation_session.py +4 -2
  51. pymobiledevice3/services/web_protocol/cdp_screencast.py +2 -1
  52. pymobiledevice3/services/web_protocol/element.py +3 -3
  53. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/METADATA +3 -2
  54. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/RECORD +58 -45
  55. pymobiledevice3/cli/completions.py +0 -50
  56. pymobiledevice3/cli/developer.py +0 -1645
  57. pymobiledevice3/cli/diagnostics.py +0 -110
  58. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/WHEEL +0 -0
  59. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/entry_points.txt +0 -0
  60. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/licenses/LICENSE +0 -0
  61. {pymobiledevice3-6.2.0.dist-info → pymobiledevice3-7.0.0.dist-info}/top_level.txt +0 -0
@@ -1,11 +1,11 @@
1
- from typing import IO
1
+ from pathlib import Path
2
+ from typing import Annotated, Literal
2
3
 
3
- import click
4
4
  import IPython
5
+ import typer
6
+ from typer_injector import InjectingTyper
5
7
 
6
- from pymobiledevice3.cli.cli_common import Command, print_json
7
- from pymobiledevice3.lockdown import LockdownClient
8
- from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
8
+ from pymobiledevice3.cli.cli_common import ServiceProviderDep, print_json
9
9
  from pymobiledevice3.services.springboard import SpringBoardServicesService
10
10
 
11
11
  SHELL_USAGE = """
@@ -13,32 +13,28 @@ Use `service` to access the service features
13
13
  """
14
14
 
15
15
 
16
- @click.group()
17
- def cli():
18
- pass
16
+ cli = InjectingTyper(
17
+ name="springboard",
18
+ help="Interact with SpringBoard UI (icons, wallpapers, orientation, shell).",
19
+ no_args_is_help=True,
20
+ )
21
+ state_cli = InjectingTyper(
22
+ name="state",
23
+ help="Icon state operations.",
24
+ no_args_is_help=True,
25
+ )
26
+ cli.add_typer(state_cli)
19
27
 
20
28
 
21
- @cli.group()
22
- def springboard():
23
- """Access device UI"""
24
- pass
25
-
26
-
27
- @springboard.group()
28
- def state():
29
- """icons state options"""
30
- pass
31
-
32
-
33
- @state.command("get", cls=Command)
34
- def state_get(service_provider: LockdownClient):
35
- """get icon state"""
29
+ @state_cli.command("get")
30
+ def state_get(service_provider: ServiceProviderDep) -> None:
31
+ """Fetch the current icon layout/state."""
36
32
  print_json(SpringBoardServicesService(lockdown=service_provider).get_icon_state())
37
33
 
38
34
 
39
- @springboard.command("shell", cls=Command)
40
- def springboard_shell(service_provider: LockdownClient):
41
- """open a shell to communicate with SpringBoardServicesService"""
35
+ @cli.command("shell")
36
+ def springboard_shell(service_provider: ServiceProviderDep) -> None:
37
+ """Open an IPython shell bound to SpringBoardServicesService."""
42
38
  service = SpringBoardServicesService(lockdown=service_provider)
43
39
  IPython.embed(
44
40
  header=SHELL_USAGE,
@@ -48,43 +44,47 @@ def springboard_shell(service_provider: LockdownClient):
48
44
  )
49
45
 
50
46
 
51
- @springboard.command("icon", cls=Command)
52
- @click.argument("bundle_id")
53
- @click.argument("out", type=click.File("wb"))
54
- def springboard_icon(service_provider: LockdownClient, bundle_id, out):
55
- """get application's icon"""
56
- out.write(SpringBoardServicesService(lockdown=service_provider).get_icon_pngdata(bundle_id))
47
+ @cli.command("icon")
48
+ def springboard_icon(service_provider: ServiceProviderDep, bundle_id: str, out: Path) -> None:
49
+ """Save an app's icon PNG to the given path."""
50
+ out.write_bytes(SpringBoardServicesService(lockdown=service_provider).get_icon_pngdata(bundle_id))
57
51
 
58
52
 
59
- @springboard.command("orientation", cls=Command)
60
- def springboard_orientation(service_provider: LockdownClient):
61
- """get screen orientation"""
53
+ @cli.command("orientation")
54
+ def springboard_orientation(service_provider: ServiceProviderDep) -> None:
55
+ """Print current screen orientation."""
62
56
  print(SpringBoardServicesService(lockdown=service_provider).get_interface_orientation())
63
57
 
64
58
 
65
- @springboard.command("wallpaper-home-screen", cls=Command)
66
- @click.argument("out", type=click.File("wb"))
67
- def springboard_wallpaper_home_screen(service_provider: LockdownClient, out: IO) -> None:
68
- """get homescreen wallpaper"""
69
- out.write(SpringBoardServicesService(lockdown=service_provider).get_wallpaper_pngdata())
59
+ @cli.command("wallpaper-home-screen")
60
+ def springboard_wallpaper_home_screen(service_provider: ServiceProviderDep, out: Path) -> None:
61
+ """Save the homescreen wallpaper PNG to the given path."""
62
+ out.write_bytes(SpringBoardServicesService(lockdown=service_provider).get_wallpaper_pngdata())
70
63
 
71
64
 
72
- @springboard.command("wallpaper-preview-image", cls=Command)
73
- @click.argument("wallpaper-name", type=click.Choice(["homescreen", "lockscreen"]))
74
- @click.argument("out", type=click.File("wb"))
75
- @click.option("-r", "--reload", is_flag=True, help="reload icon state before fetching image")
65
+ @cli.command("wallpaper-preview-image")
76
66
  def springboard_wallpaper_preview_image(
77
- service_provider: LockdownClient, wallpaper_name: str, out: IO, reload: bool
67
+ service_provider: ServiceProviderDep,
68
+ wallpaper_name: Literal["homescreen", "lockscreen"],
69
+ out: Path,
70
+ reload: Annotated[
71
+ bool,
72
+ typer.Option(
73
+ "--reload",
74
+ "-r",
75
+ help="reload icon state before fetching image",
76
+ ),
77
+ ] = False,
78
78
  ) -> None:
79
- """get the preview image of either the homescreen or the lockscreen"""
79
+ """Save the preview image for the homescreen or lockscreen wallpaper (optionally reload state first)."""
80
80
  with SpringBoardServicesService(lockdown=service_provider) as springboard_service:
81
81
  if reload:
82
82
  springboard_service.reload_icon_state()
83
- out.write(springboard_service.get_wallpaper_preview_image(wallpaper_name))
83
+ out.write_bytes(springboard_service.get_wallpaper_preview_image(wallpaper_name))
84
84
 
85
85
 
86
- @springboard.command("homescreen-icon-metrics", cls=Command)
87
- def springboard_homescreen_icon_metrics(service_provider: LockdownServiceProvider) -> None:
88
- """Get homescreen icon metrics"""
86
+ @cli.command("homescreen-icon-metrics")
87
+ def springboard_homescreen_icon_metrics(service_provider: ServiceProviderDep) -> None:
88
+ """Print homescreen icon spacing/metrics."""
89
89
  with SpringBoardServicesService(lockdown=service_provider) as springboard_service:
90
90
  print_json(springboard_service.get_homescreen_icon_metrics())
@@ -2,38 +2,41 @@ import logging
2
2
  import os
3
3
  import posixpath
4
4
  import re
5
- from typing import Optional, TextIO
6
-
7
- import click
8
-
9
- from pymobiledevice3.cli.cli_common import Command, get_last_used_terminal_formatting, user_requested_colored_output
10
- from pymobiledevice3.lockdown import LockdownClient
5
+ from contextlib import nullcontext
6
+ from pathlib import Path
7
+ from typing import Annotated, Optional, TextIO
8
+
9
+ import typer
10
+ from typer_injector import InjectingTyper
11
+
12
+ from pymobiledevice3.cli.cli_common import (
13
+ ServiceProviderDep,
14
+ get_last_used_terminal_formatting,
15
+ user_requested_colored_output,
16
+ )
11
17
  from pymobiledevice3.lockdown_service_provider import LockdownServiceProvider
12
- from pymobiledevice3.services.os_trace import OsTraceService, SyslogLogLevel
18
+ from pymobiledevice3.services.os_trace import OsTraceService, SyslogEntry, SyslogLogLevel
13
19
  from pymobiledevice3.services.syslog import SyslogService
14
20
 
15
21
  logger = logging.getLogger(__name__)
16
22
 
17
-
18
- @click.group()
19
- def cli() -> None:
20
- pass
21
-
22
-
23
- @cli.group()
24
- def syslog() -> None:
25
- """Watch syslog messages"""
26
- pass
23
+ cli = InjectingTyper(
24
+ name="syslog",
25
+ help="Watch syslog messages",
26
+ no_args_is_help=True,
27
+ )
27
28
 
28
29
 
29
- @syslog.command("live-old", cls=Command)
30
- def syslog_live_old(service_provider: LockdownClient):
30
+ @cli.command("live-old")
31
+ def syslog_live_old(service_provider: ServiceProviderDep) -> None:
31
32
  """view live syslog lines in raw bytes form from old relay"""
32
33
  for line in SyslogService(service_provider=service_provider).watch():
33
34
  print(line)
34
35
 
35
36
 
36
- def format_line(color, pid, syslog_entry, include_label: bool, image_offset: bool = False) -> Optional[str]:
37
+ def format_line(
38
+ color: bool, pid: int, syslog_entry: SyslogEntry, include_label: bool, image_offset: bool = False
39
+ ) -> Optional[str]:
37
40
  log_level_colors = {
38
41
  SyslogLogLevel.NOTICE.name: "white",
39
42
  SyslogLogLevel.INFO.name: "white",
@@ -60,17 +63,17 @@ def format_line(color, pid, syslog_entry, include_label: bool, image_offset: boo
60
63
  label = f"[{syslog_entry.label.subsystem}][{syslog_entry.label.category}]"
61
64
 
62
65
  if color:
63
- timestamp = click.style(str(timestamp), "green")
64
- process_name = click.style(process_name, "magenta")
66
+ timestamp = typer.style(str(timestamp), "green")
67
+ process_name = typer.style(process_name, "magenta")
65
68
  if len(image_name) > 0:
66
- image_name = click.style(image_name, "magenta")
69
+ image_name = typer.style(image_name, "magenta")
67
70
  if image_offset:
68
- image_offset_str = click.style(image_offset_str, "blue")
69
- syslog_pid = click.style(syslog_pid, "cyan")
71
+ image_offset_str = typer.style(image_offset_str, "blue")
72
+ syslog_pid = typer.style(syslog_pid, "cyan")
70
73
  log_level_color = log_level_colors[level]
71
- level = click.style(level, log_level_color)
72
- label = click.style(label, "cyan")
73
- message = click.style(message, log_level_color)
74
+ level = typer.style(level, log_level_color)
75
+ label = typer.style(label, "cyan")
76
+ message = typer.style(message, log_level_color)
74
77
 
75
78
  line_format = "{timestamp} {process_name}{{{image_name}{image_offset_str}}}[{pid}] <{level}>: {message}"
76
79
 
@@ -93,7 +96,7 @@ def format_line(color, pid, syslog_entry, include_label: bool, image_offset: boo
93
96
  def syslog_live(
94
97
  service_provider: LockdownServiceProvider,
95
98
  out: Optional[TextIO],
96
- pid: Optional[int],
99
+ pid: int,
97
100
  process_name: Optional[str],
98
101
  match: list[str],
99
102
  match_insensitive: list[str],
@@ -106,9 +109,9 @@ def syslog_live(
106
109
  match_regex += [re.compile(f".*({r}).*", re.IGNORECASE | re.DOTALL) for r in insensitive_regex]
107
110
 
108
111
  def replace(m):
109
- if len(m.groups()):
110
- return line.replace(m.group(1), click.style(m.group(1), bold=True, underline=True))
111
- return None
112
+ if len(m.groups()) and line:
113
+ return line.replace(m.group(1), typer.style(m.group(1), bold=True, underline=True))
114
+ return ""
112
115
 
113
116
  for syslog_entry in OsTraceService(lockdown=service_provider).syslog(pid=pid):
114
117
  if process_name and posixpath.basename(syslog_entry.filename) != process_name:
@@ -117,6 +120,9 @@ def syslog_live(
117
120
  line_no_style = format_line(False, pid, syslog_entry, include_label, image_offset)
118
121
  line = format_line(user_requested_colored_output(), pid, syslog_entry, include_label, image_offset)
119
122
 
123
+ if line_no_style is None or line is None:
124
+ continue
125
+
120
126
  skip = False
121
127
 
122
128
  if match is not None:
@@ -127,7 +133,7 @@ def syslog_live(
127
133
  break
128
134
  else:
129
135
  if user_requested_colored_output():
130
- match_line = match_line.replace(m, click.style(m, bold=True, underline=True))
136
+ match_line = match_line.replace(m, typer.style(m, bold=True, underline=True))
131
137
  line = match_line
132
138
 
133
139
  if match_insensitive is not None:
@@ -143,7 +149,7 @@ def syslog_live(
143
149
  last_color_formatting = get_last_used_terminal_formatting(line[:start])
144
150
  line = (
145
151
  line[:start]
146
- + click.style(line[start:end], bold=True, underline=True)
152
+ + typer.style(line[start:end], bold=True, underline=True)
147
153
  + last_color_formatting
148
154
  + line[end:]
149
155
  )
@@ -168,50 +174,108 @@ def syslog_live(
168
174
  print(line_no_style, file=out, flush=True)
169
175
 
170
176
 
171
- @syslog.command("live", cls=Command)
172
- @click.option("-o", "--out", type=click.File("wt"), help="log file")
173
- @click.option("--pid", type=click.INT, default=-1, help="pid to filter. -1 for all")
174
- @click.option("-pn", "--process-name", help="process name to filter")
175
- @click.option("-m", "--match", multiple=True, help="match expression")
176
- @click.option("-mi", "--match-insensitive", multiple=True, help="insensitive match expression")
177
- @click.option("include_label", "--label", is_flag=True, help="should include label")
178
- @click.option("-e", "--regex", multiple=True, help="filter only lines matching given regex")
179
- @click.option("-ei", "--insensitive-regex", multiple=True, help="filter only lines matching given regex (insensitive)")
180
- @click.option("-io", "--image-offset", is_flag=True, help="Include image offset in log line")
177
+ @cli.command("live")
181
178
  def cli_syslog_live(
182
- service_provider: LockdownServiceProvider,
183
- out: Optional[TextIO],
184
- pid: Optional[int],
185
- process_name: Optional[str],
186
- match: list[str],
187
- match_insensitive: list[str],
188
- include_label: bool,
189
- regex: list[str],
190
- insensitive_regex: list[str],
191
- image_offset: bool,
179
+ service_provider: ServiceProviderDep,
180
+ out: Annotated[
181
+ Optional[Path],
182
+ typer.Option(
183
+ "--out",
184
+ "-o",
185
+ help="log file",
186
+ ),
187
+ ] = None,
188
+ pid: Annotated[
189
+ int,
190
+ typer.Option(help="pid to filter. -1 for all"),
191
+ ] = -1,
192
+ process_name: Annotated[
193
+ Optional[str],
194
+ typer.Option(
195
+ "--process-name",
196
+ "-pn",
197
+ help="process name to filter",
198
+ ),
199
+ ] = None,
200
+ match: Annotated[
201
+ Optional[list[str]],
202
+ typer.Option(
203
+ "--match",
204
+ "-m",
205
+ help="match expression",
206
+ ),
207
+ ] = None,
208
+ match_insensitive: Annotated[
209
+ Optional[list[str]],
210
+ typer.Option(
211
+ "--match-insensitive",
212
+ "-mi",
213
+ help="case-insensitive match expression",
214
+ ),
215
+ ] = None,
216
+ include_label: Annotated[
217
+ bool,
218
+ typer.Option(
219
+ "--label",
220
+ help="should include label",
221
+ ),
222
+ ] = False,
223
+ regex: Annotated[
224
+ Optional[list[str]],
225
+ typer.Option(
226
+ "--regex",
227
+ "-e",
228
+ help="filter only lines matching given regex",
229
+ ),
230
+ ] = None,
231
+ insensitive_regex: Annotated[
232
+ Optional[list[str]],
233
+ typer.Option(
234
+ "--insensitive-regex",
235
+ "-ei",
236
+ help="filter only lines matching given regex (insensitive)",
237
+ ),
238
+ ] = None,
239
+ image_offset: Annotated[
240
+ bool,
241
+ typer.Option(
242
+ "--image-offset",
243
+ "-io",
244
+ help="Include image offset in log line",
245
+ ),
246
+ ] = False,
192
247
  ) -> None:
193
248
  """view live syslog lines"""
194
249
 
195
- syslog_live(
196
- service_provider,
197
- out,
198
- pid,
199
- process_name,
200
- match,
201
- match_insensitive,
202
- include_label,
203
- regex,
204
- insensitive_regex,
205
- image_offset,
206
- )
250
+ with out.open("wt") if out else nullcontext() as out_file:
251
+ syslog_live(
252
+ service_provider,
253
+ out_file,
254
+ pid,
255
+ process_name,
256
+ match or [],
257
+ match_insensitive or [],
258
+ include_label,
259
+ regex or [],
260
+ insensitive_regex or [],
261
+ )
207
262
 
208
263
 
209
- @syslog.command("collect", cls=Command)
210
- @click.argument("out", type=click.Path(exists=False, dir_okay=True, file_okay=False))
211
- @click.option("--size-limit", type=click.INT)
212
- @click.option("--age-limit", type=click.INT)
213
- @click.option("--start-time", type=click.INT)
214
- def syslog_collect(service_provider: LockdownClient, out, size_limit, age_limit, start_time):
264
+ @cli.command("collect")
265
+ def syslog_collect(
266
+ service_provider: ServiceProviderDep,
267
+ out: Annotated[
268
+ Path,
269
+ typer.Argument(
270
+ exists=False,
271
+ dir_okay=True,
272
+ file_okay=False,
273
+ ),
274
+ ],
275
+ size_limit: int,
276
+ age_limit: int,
277
+ start_time: int,
278
+ ) -> None:
215
279
  """
216
280
  Collect the system logs into a .logarchive that can be viewed later with tools such as log or Console.
217
281
  If the filename doesn't exist, system_logs.logarchive will be created in the given directory.
@@ -219,12 +283,12 @@ def syslog_collect(service_provider: LockdownClient, out, size_limit, age_limit,
219
283
  if not os.path.exists(out):
220
284
  os.makedirs(out)
221
285
 
222
- if not out.endswith(".logarchive"):
286
+ if out.suffix != ".logarchive":
223
287
  logger.warning(
224
288
  "given out path doesn't end with a .logarchive - consider renaming to be able to view "
225
289
  "the file with the likes of the Console.app and the `log show` utilities"
226
290
  )
227
291
 
228
292
  OsTraceService(lockdown=service_provider).collect(
229
- out, size_limit=size_limit, age_limit=age_limit, start_time=start_time
293
+ str(out), size_limit=size_limit, age_limit=age_limit, start_time=start_time
230
294
  )
@@ -1,35 +1,53 @@
1
1
  import logging
2
2
  import tempfile
3
+ from typing import Annotated, Optional
3
4
 
4
- import click
5
+ import typer
6
+ from typer_injector import InjectingTyper
5
7
 
6
8
  from pymobiledevice3 import usbmux
7
- from pymobiledevice3.cli.cli_common import USBMUX_OPTION_HELP, BaseCommand, print_json
9
+ from pymobiledevice3.cli.cli_common import USBMUX_OPTION_HELP, print_json
8
10
  from pymobiledevice3.lockdown import create_using_usbmux
9
11
  from pymobiledevice3.tcp_forwarder import UsbmuxTcpForwarder
10
12
 
11
13
  logger = logging.getLogger(__name__)
12
14
 
13
15
 
14
- @click.group()
15
- def cli() -> None:
16
- pass
17
-
18
-
19
- @cli.group("usbmux")
20
- def usbmux_cli() -> None:
21
- """List devices or forward a TCP port"""
22
- pass
23
-
24
-
25
- @usbmux_cli.command("forward", cls=BaseCommand)
26
- @click.option("usbmux_address", "--usbmux", help=USBMUX_OPTION_HELP)
27
- @click.argument("src_port", type=click.IntRange(1, 0xFFFF))
28
- @click.argument("dst_port", type=click.IntRange(1, 0xFFFF))
29
- @click.option("--serial", help="device serial number")
30
- @click.option("-d", "--daemonize", is_flag=True)
31
- def usbmux_forward(usbmux_address: str, src_port: int, dst_port: int, serial: str, daemonize: bool):
32
- """forward tcp port"""
16
+ cli = InjectingTyper(
17
+ name="usbmux",
18
+ help="Inspect usbmuxd-connected devices and forward TCP ports to them.",
19
+ no_args_is_help=True,
20
+ )
21
+
22
+
23
+ @cli.command("forward")
24
+ def usbmux_forward(
25
+ src_port: Annotated[
26
+ int,
27
+ typer.Argument(min=1, max=0xFFFF),
28
+ ],
29
+ dst_port: Annotated[
30
+ int,
31
+ typer.Argument(min=1, max=0xFFFF),
32
+ ],
33
+ *,
34
+ usbmux_address: Annotated[
35
+ Optional[str],
36
+ typer.Option(
37
+ "--usbmux",
38
+ help=USBMUX_OPTION_HELP,
39
+ ),
40
+ ] = None,
41
+ serial: Annotated[
42
+ str,
43
+ typer.Option(help="Device serial/UDID to forward traffic to."),
44
+ ],
45
+ daemonize: Annotated[
46
+ bool,
47
+ typer.Option("--daemonize", "-d", help="Run the forwarder in the background."),
48
+ ] = False,
49
+ ) -> None:
50
+ """Forward a local TCP port to the device via usbmuxd."""
33
51
  forwarder = UsbmuxTcpForwarder(serial, dst_port, src_port, usbmux_address=usbmux_address)
34
52
 
35
53
  if daemonize:
@@ -45,12 +63,33 @@ def usbmux_forward(usbmux_address: str, src_port: int, dst_port: int, serial: st
45
63
  forwarder.start()
46
64
 
47
65
 
48
- @usbmux_cli.command("list", cls=BaseCommand)
49
- @click.option("usbmux_address", "--usbmux", help=USBMUX_OPTION_HELP)
50
- @click.option("-u", "--usb", is_flag=True, help="show only usb devices")
51
- @click.option("-n", "--network", is_flag=True, help="show only network devices")
52
- def usbmux_list(usbmux_address: str, usb: bool, network: bool) -> None:
53
- """list connected devices"""
66
+ @cli.command("list")
67
+ def usbmux_list(
68
+ usbmux_address: Annotated[
69
+ Optional[str],
70
+ typer.Option(
71
+ "--usbmux",
72
+ help=USBMUX_OPTION_HELP,
73
+ ),
74
+ ] = None,
75
+ usb: Annotated[
76
+ bool,
77
+ typer.Option(
78
+ "--usb",
79
+ "-u",
80
+ help="show only USB devices",
81
+ ),
82
+ ] = False,
83
+ network: Annotated[
84
+ bool,
85
+ typer.Option(
86
+ "--network",
87
+ "-n",
88
+ help="show only network devices",
89
+ ),
90
+ ] = False,
91
+ ) -> None:
92
+ """List devices known to usbmuxd (USB and Wi-Fi)."""
54
93
  connected_devices = []
55
94
  for device in usbmux.list_devices(usbmux_address=usbmux_address):
56
95
  udid = device.serial
@@ -1,9 +1,6 @@
1
- import click
1
+ from typer import Typer
2
2
 
3
-
4
- @click.group()
5
- def cli() -> None:
6
- pass
3
+ cli = Typer()
7
4
 
8
5
 
9
6
  @cli.command()