pipecatcloud 0.2.11__tar.gz → 0.2.12__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 (56) hide show
  1. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/CHANGELOG.md +47 -0
  2. {pipecatcloud-0.2.11/src/pipecatcloud.egg-info → pipecatcloud-0.2.12}/PKG-INFO +1 -1
  3. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/_utils/deploy_utils.py +3 -0
  4. pipecatcloud-0.2.12/src/pipecatcloud/_utils/regions.py +70 -0
  5. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/api.py +46 -5
  6. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/__init__.py +1 -1
  7. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/agent.py +20 -1
  8. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/deploy.py +29 -1
  9. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/docker.py +73 -9
  10. pipecatcloud-0.2.12/src/pipecatcloud/cli/commands/regions.py +40 -0
  11. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/secrets.py +75 -4
  12. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/entry_point.py +2 -0
  13. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/config.py +1 -0
  14. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/constants.py +7 -0
  15. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12/src/pipecatcloud.egg-info}/PKG-INFO +1 -1
  16. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud.egg-info/SOURCES.txt +5 -1
  17. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/tests/test_agent_sessions.py +25 -21
  18. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/tests/test_agent_stop.py +18 -14
  19. pipecatcloud-0.2.12/tests/test_api_client.py +272 -0
  20. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/tests/test_docker_commands.py +66 -5
  21. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/tests/test_krisp_viva.py +14 -9
  22. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/tests/test_managed_keys.py +11 -6
  23. pipecatcloud-0.2.12/tests/test_regions.py +219 -0
  24. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/.github/workflows/publish-pypi.yml +0 -0
  25. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/.github/workflows/publish-test.yml +0 -0
  26. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/.github/workflows/tests.yml +0 -0
  27. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/.gitignore +0 -0
  28. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/LICENSE +0 -0
  29. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/README.md +0 -0
  30. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/pipecat-cloud.png +0 -0
  31. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/pyproject.toml +0 -0
  32. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/pytest.ini +0 -0
  33. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/setup.cfg +0 -0
  34. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/__init__.py +0 -0
  35. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/__main__.py +0 -0
  36. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/__version__.py +0 -0
  37. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/_utils/__init__.py +0 -0
  38. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/_utils/async_utils.py +0 -0
  39. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/_utils/auth_utils.py +0 -0
  40. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/_utils/console_utils.py +0 -0
  41. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/agent.py +0 -0
  42. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/api.py +0 -0
  43. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/__init__.py +0 -0
  44. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/auth.py +0 -0
  45. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/commands/organizations.py +0 -0
  46. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/cli/config.py +0 -0
  47. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/exception.py +0 -0
  48. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/py.typed +0 -0
  49. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/session.py +0 -0
  50. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud/smallwebrtc/session_manager.py +0 -0
  51. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud.egg-info/dependency_links.txt +0 -0
  52. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud.egg-info/entry_points.txt +0 -0
  53. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud.egg-info/requires.txt +0 -0
  54. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/src/pipecatcloud.egg-info/top_level.txt +0 -0
  55. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/tests/__init__.py +0 -0
  56. {pipecatcloud-0.2.11 → pipecatcloud-0.2.12}/uv.lock +0 -0
@@ -5,6 +5,53 @@ All notable changes to **Pipecat Cloud** will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.12] - 2025-11-26
9
+
10
+ ### Added
11
+
12
+ - Added regional support for secrets and services/agents. Services and secrets
13
+ must be in the same region. Available regions are fetched dynamically from
14
+ the API and can be viewed with `pcc regions list`.
15
+
16
+ - Added `pcc regions list` command to display available regions with their
17
+ display names (e.g., "US West (Oregon)", "Europe (Frankfurt)",
18
+ "Asia Pacific (Mumbai)").
19
+
20
+ - Added `--region` / `-r` option to the `secrets set` command to specify the
21
+ region when creating or updating secret sets.
22
+
23
+ - Added `--region` / `-r` filter option to the `secrets list` command to filter
24
+ secret sets by region. The Region column is now displayed in the output
25
+ table.
26
+
27
+ - Added `--region` / `-r` option to the `secrets image-pull-secret` command to
28
+ specify the region for image pull secrets.
29
+
30
+ - Added `--region` / `-r` option to the `deploy` command to specify the region
31
+ for service deployments. Region can also be configured via `pcc-deploy.toml`
32
+ by adding the `region` attribute. The region is displayed in the deployment
33
+ review panel.
34
+
35
+ - Added `--region` / `-r` filter option to the `agent list` command to filter
36
+ agents by region. The Region column is now displayed in the output table.
37
+
38
+ - Added region validation that checks user-provided region codes against the
39
+ live API region list, with helpful error messages showing available regions.
40
+
41
+ ### Changed
42
+
43
+ - When the `--region` option is not specified for create/update operations
44
+ (secrets, deployments), the CLI now defaults to `us-west` with a deprecation
45
+ warning. Users are encouraged to explicitly specify the region as this
46
+ default will be required in a future version.
47
+
48
+ - All error messages reference the Pipecat CLI (e.g. `pipecat cloud`) in place
49
+ of `pipecatcloud` or `pcc`.
50
+
51
+ ### Fixed
52
+
53
+ - Improved error handling when building a docker image on an x86 machine.
54
+
8
55
  ## [0.2.11] - 2025-11-12
9
56
 
10
57
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pipecatcloud
3
- Version: 0.2.11
3
+ Version: 0.2.12
4
4
  Summary: Cloud hosting for Pipecat AI applications
5
5
  License: BSD 2-Clause License
6
6
  Project-URL: Website, https://docs.pipecat.daily.co/
@@ -80,6 +80,7 @@ class DeployConfigParams:
80
80
  image: Optional[str] = None
81
81
  image_credentials: Optional[str] = None
82
82
  secret_set: Optional[str] = None
83
+ region: Optional[str] = None
83
84
  scaling: ScalingParams = ScalingParams()
84
85
  enable_krisp: bool = False
85
86
  enable_managed_keys: bool = False
@@ -97,6 +98,7 @@ class DeployConfigParams:
97
98
  "image": self.image,
98
99
  "image_credentials": self.image_credentials,
99
100
  "secret_set": self.secret_set,
101
+ "region": self.region,
100
102
  "scaling": self.scaling.to_dict() if self.scaling else None,
101
103
  "enable_krisp": self.enable_krisp,
102
104
  "enable_managed_keys": self.enable_managed_keys,
@@ -144,6 +146,7 @@ def load_deploy_config_file() -> Optional[DeployConfigParams]:
144
146
  "image",
145
147
  "image_credentials",
146
148
  "secret_set",
149
+ "region",
147
150
  "scaling",
148
151
  "enable_krisp",
149
152
  "enable_managed_keys",
@@ -0,0 +1,70 @@
1
+ #
2
+ # Copyright (c) 2025, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ """
8
+ Region utilities for Pipecat Cloud CLI.
9
+
10
+ Provides functions to fetch and cache available regions from the API.
11
+ """
12
+
13
+ from typing import Dict, List, Optional
14
+
15
+ from pipecatcloud.cli.api import API
16
+ from pipecatcloud.cli.config import config
17
+
18
+ # Module-level cache for regions
19
+ _regions_cache: Optional[List[Dict[str, str]]] = None
20
+
21
+
22
+ async def get_regions() -> List[Dict[str, str]]:
23
+ """
24
+ Fetch available regions from the API with caching.
25
+
26
+ This function is cached so the API is only called once per CLI session.
27
+
28
+ Returns:
29
+ List of region objects with 'code' and 'display_name' fields
30
+ Example: [
31
+ {"code": "us-west", "display_name": "US West"},
32
+ {"code": "eu-central", "display_name": "EU Central"}
33
+ ]
34
+ """
35
+ global _regions_cache
36
+
37
+ if _regions_cache is None:
38
+ org = config.get("org")
39
+ # API returns (data, error) tuple
40
+ data, error = await API.regions(org=org)
41
+ if error or not data:
42
+ return []
43
+ _regions_cache = data
44
+
45
+ return _regions_cache
46
+
47
+
48
+ async def get_region_codes() -> List[str]:
49
+ """
50
+ Get list of region codes only.
51
+
52
+ Returns:
53
+ List of region codes (e.g., ['us-west', 'eu-central', 'ap-south'])
54
+ """
55
+ regions = await get_regions()
56
+ return [r["code"] for r in regions]
57
+
58
+
59
+ async def validate_region(region: str) -> bool:
60
+ """
61
+ Validate that a region code is supported.
62
+
63
+ Args:
64
+ region: Region code to validate
65
+
66
+ Returns:
67
+ True if region is valid, False otherwise
68
+ """
69
+ valid_codes = await get_region_codes()
70
+ return region in valid_codes
@@ -236,13 +236,16 @@ class _API:
236
236
 
237
237
  # Secret
238
238
 
239
- async def _secrets_list(self, org: str, secret_set: Optional[str] = None) -> dict | None:
239
+ async def _secrets_list(self, org: str, secret_set: Optional[str] = None, region: Optional[str] = None) -> dict | None:
240
240
  if secret_set:
241
241
  url = f"{self.construct_api_url('secrets_path').format(org=org)}/{secret_set}"
242
242
  else:
243
243
  url = f"{self.construct_api_url('secrets_path').format(org=org)}"
244
244
 
245
- result = await self._base_request("GET", url, not_found_is_empty=True) or {}
245
+ # Build query params if region filter is specified
246
+ params = {"region": region} if region else None
247
+
248
+ result = await self._base_request("GET", url, params=params, not_found_is_empty=True) or {}
246
249
 
247
250
  if "sets" in result:
248
251
  return result["sets"]
@@ -258,12 +261,17 @@ class _API:
258
261
  Args:
259
262
  org: Organization ID,
260
263
  secret_set: (optional) name of secret set to lookup
264
+ region: (optional) filter by region
261
265
 
262
266
  """
263
267
  return self.create_api_method(self._secrets_list)
264
268
 
265
- async def _secrets_upsert(self, data: dict, set_name: str, org: str) -> dict:
269
+ async def _secrets_upsert(self, data: dict, set_name: str, org: str, region: str) -> dict:
266
270
  url = f"{self.construct_api_url('secrets_path').format(org=org)}/{set_name}"
271
+
272
+ # Add region to data payload
273
+ data = {**data, "region": region}
274
+
267
275
  return await self._base_request("PUT", url, json=data) or {}
268
276
 
269
277
  @property
@@ -273,6 +281,7 @@ class _API:
273
281
  data: key and value of secret to add (or credentials for image pull secrets)
274
282
  set_name: name of set to create or update
275
283
  org: Organization ID
284
+ region: region for secret set
276
285
  """
277
286
  return self.create_api_method(self._secrets_upsert)
278
287
 
@@ -316,6 +325,7 @@ class _API:
316
325
  "image": deploy_config.image,
317
326
  "imagePullSecretSet": deploy_config.image_credentials,
318
327
  "secretSet": deploy_config.secret_set,
328
+ "region": deploy_config.region,
319
329
  "autoScaling": {
320
330
  "minAgents": deploy_config.scaling.min_agents,
321
331
  "maxAgents": deploy_config.scaling.max_agents,
@@ -373,9 +383,13 @@ class _API:
373
383
  """
374
384
  return self.create_api_method(self._agent)
375
385
 
376
- async def _agents(self, org: str) -> List[dict] | None:
386
+ async def _agents(self, org: str, region: Optional[str] = None) -> List[dict] | None:
377
387
  url = f"{self.construct_api_url('services_path').format(org=org)}"
378
- result = await self._base_request("GET", url) or {}
388
+
389
+ # Build query params if region filter is specified
390
+ params = {"region": region} if region else None
391
+
392
+ result = await self._base_request("GET", url, params=params) or {}
379
393
 
380
394
  if "services" in result:
381
395
  return result["services"]
@@ -384,6 +398,11 @@ class _API:
384
398
 
385
399
  @property
386
400
  def agents(self):
401
+ """List agents/services
402
+ Args:
403
+ org: Organization ID
404
+ region: (optional) filter by region
405
+ """
387
406
  return self.create_api_method(self._agents)
388
407
 
389
408
  async def _start_agent(
@@ -465,3 +484,25 @@ class _API:
465
484
  org: Organization ID
466
485
  """
467
486
  return self.create_api_method(self._agent_session_terminate)
487
+
488
+ # Regions
489
+
490
+ async def _regions(self, org: str) -> list[dict] | None:
491
+ url = self.construct_api_url("regions_path").format(org=org)
492
+ result = await self._base_request("GET", url, not_found_is_empty=True)
493
+
494
+ if result and "regions" in result:
495
+ # Return full region objects with code and display_name
496
+ return result["regions"]
497
+
498
+ return None
499
+
500
+ @property
501
+ def regions(self):
502
+ """List available regions
503
+ Args:
504
+ org: Organization ID
505
+ Returns:
506
+ List of region objects with 'code' and 'display_name' fields
507
+ """
508
+ return self.create_api_method(self._regions)
@@ -4,7 +4,7 @@
4
4
  # SPDX-License-Identifier: BSD 2-Clause License
5
5
  #
6
6
 
7
- PIPECAT_CLI_NAME = "pcc"
7
+ PIPECAT_CLI_NAME = "pipecat cloud"
8
8
  PIPECAT_CREDENTIALS_PATH = "~/.config/pipecatcloud/pipecatcloud.toml"
9
9
  PIPECAT_DEPLOY_CONFIG_PATH = "pcc-deploy.toml"
10
10
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  import json
8
8
  from enum import Enum
9
+ from typing import Optional
9
10
 
10
11
  import aiohttp
11
12
  import questionary
@@ -28,9 +29,11 @@ from pipecatcloud._utils.console_utils import (
28
29
  format_timestamp,
29
30
  )
30
31
  from pipecatcloud._utils.deploy_utils import DeployConfigParams, with_deploy_config
32
+ from pipecatcloud._utils.regions import get_region_codes, validate_region
31
33
  from pipecatcloud.cli import PIPECAT_CLI_NAME
32
34
  from pipecatcloud.cli.api import API
33
35
  from pipecatcloud.cli.config import config
36
+ from pipecatcloud.constants import Region
34
37
 
35
38
  agent_cli = typer.Typer(name="agent", help="Agent management", no_args_is_help=True)
36
39
 
@@ -45,13 +48,27 @@ async def list(
45
48
  organization: str = typer.Option(
46
49
  None, "--organization", "-o", help="Organization to list agents for"
47
50
  ),
51
+ region: Optional[Region] = typer.Option(
52
+ None,
53
+ "--region",
54
+ "-r",
55
+ help="Filter by region",
56
+ ),
48
57
  ):
49
58
  org = organization or config.get("org")
50
59
 
60
+ # Validate region if provided
61
+ if region and not await validate_region(region):
62
+ valid_regions = await get_region_codes()
63
+ console.print(
64
+ f"[red]Invalid region '{region}'. Valid regions are: {', '.join(valid_regions)}[/red]"
65
+ )
66
+ return typer.Exit(1)
67
+
51
68
  with console.status(
52
69
  f"[dim]Fetching agents for organization: [bold]'{org}'[/bold][/dim]", spinner="dots"
53
70
  ):
54
- data, error = await API.agents(org=org)
71
+ data, error = await API.agents(org=org, region=region)
55
72
 
56
73
  if error:
57
74
  return typer.Exit()
@@ -66,6 +83,7 @@ async def list(
66
83
  else:
67
84
  table = Table(show_header=True, show_lines=True, border_style="dim", box=box.SIMPLE)
68
85
  table.add_column("Name")
86
+ table.add_column("Region")
69
87
  table.add_column("Agent ID")
70
88
  table.add_column("Active Deployment ID")
71
89
  table.add_column("Created At")
@@ -74,6 +92,7 @@ async def list(
74
92
  for service in data:
75
93
  table.add_row(
76
94
  f"[bold]{service['name']}[/bold]",
95
+ service["region"],
77
96
  service["id"],
78
97
  service["activeDeploymentId"],
79
98
  service["createdAt"],
@@ -5,6 +5,7 @@
5
5
  #
6
6
 
7
7
  import asyncio
8
+ from typing import Optional
8
9
 
9
10
  import typer
10
11
  from loguru import logger
@@ -23,7 +24,8 @@ from pipecatcloud._utils.deploy_utils import (
23
24
  ScalingParams,
24
25
  with_deploy_config,
25
26
  )
26
- from pipecatcloud.constants import KRISP_VIVA_MODELS
27
+ from pipecatcloud._utils.regions import get_region_codes, validate_region
28
+ from pipecatcloud.constants import KRISP_VIVA_MODELS, Region
27
29
  from pipecatcloud.cli import PIPECAT_CLI_NAME
28
30
  from pipecatcloud.cli.api import API
29
31
  from pipecatcloud.cli.config import config
@@ -300,6 +302,13 @@ def create_deploy_command(app: typer.Typer):
300
302
  help="Agent profile to use for deployment",
301
303
  rich_help_panel="Deployment Configuration",
302
304
  ),
305
+ region: Optional[Region] = typer.Option(
306
+ None,
307
+ "--region",
308
+ "-r",
309
+ help="Region for service deployment",
310
+ rich_help_panel="Deployment Configuration",
311
+ ),
303
312
  skip_confirm: bool = typer.Option(
304
313
  False,
305
314
  "--force",
@@ -372,6 +381,24 @@ def create_deploy_command(app: typer.Typer):
372
381
  partial_config.enable_managed_keys = managed_keys or partial_config.enable_managed_keys
373
382
  partial_config.agent_profile = profile or partial_config.agent_profile
374
383
 
384
+ # Handle region with warning if not specified
385
+ deploy_region = region or partial_config.region
386
+ if not deploy_region:
387
+ logger.warning(
388
+ "Region not specified, defaulting to 'us-west'. Please use --region to specify explicitly. "
389
+ "This default will be required in a future version."
390
+ )
391
+ deploy_region = "us-west"
392
+ partial_config.region = deploy_region
393
+
394
+ # Validate region against API
395
+ if not await validate_region(deploy_region):
396
+ valid_regions = await get_region_codes()
397
+ console.error(
398
+ f"Invalid region '{deploy_region}'. Valid regions are: {', '.join(valid_regions)}"
399
+ )
400
+ return typer.Exit(1)
401
+
375
402
  # Handle Krisp VIVA configuration
376
403
  if krisp_viva_audio_filter is not None:
377
404
  partial_config.krisp_viva = KrispVivaConfig(audio_filter=krisp_viva_audio_filter)
@@ -409,6 +436,7 @@ def create_deploy_command(app: typer.Typer):
409
436
  (f"[bold white]Agent name:[/bold white] [green]{partial_config.agent_name}[/green]"),
410
437
  (f"[bold white]Image:[/bold white] [green]{partial_config.image}[/green]"),
411
438
  (f"[bold white]Organization:[/bold white] [green]{org}[/green]"),
439
+ (f"[bold white]Region:[/bold white] [green]{partial_config.region}[/green]"),
412
440
  (f"[bold white]Secret set:[/bold white] {'[dim]None[/dim]' if not partial_config.secret_set else '[green] '+ partial_config.secret_set + '[/green]'}"),
413
441
  (f"[bold white]Image pull secret:[/bold white] {'[dim]None[/dim]' if not partial_config.image_credentials else '[green]' + partial_config.image_credentials + '[/green]'}"),
414
442
  (f"[bold white]Agent profile:[/bold white] {'[dim]None[/dim]' if not partial_config.agent_profile else '[green]' + partial_config.agent_profile + '[/green]'}"),
@@ -14,6 +14,7 @@ from rich.panel import Panel
14
14
  from pipecatcloud._utils.async_utils import synchronizer
15
15
  from pipecatcloud._utils.console_utils import console
16
16
  from pipecatcloud._utils.deploy_utils import DeployConfigParams, with_deploy_config
17
+ from pipecatcloud.cli import PIPECAT_CLI_NAME
17
18
 
18
19
  docker_cli = typer.Typer(
19
20
  name="docker", help="Docker build and push utilities", no_args_is_help=True
@@ -25,9 +26,61 @@ class RegistryType(str, Enum):
25
26
  CUSTOM = "custom"
26
27
 
27
28
 
28
- def _provide_error_hints(stderr: str, command: list[str], registry_info: dict = None):
29
+ def _suggest_and_install_binfmt():
30
+ """Suggest and optionally install binfmt_misc support for cross-platform builds."""
31
+ console.print("\n[yellow]⚠️ Cross-platform build support missing[/yellow]")
32
+ console.print(
33
+ "[yellow] Building ARM64 images on x86_64 requires QEMU and binfmt_misc support.[/yellow]"
34
+ )
35
+ console.print(
36
+ "\n[yellow] This is a one-time setup that persists across Docker restarts.[/yellow]"
37
+ )
38
+ console.print(
39
+ "\n[dim] The following command will be run (requires Docker --privileged flag):[/dim]"
40
+ )
41
+ console.print("[dim] docker run --privileged --rm tonistiigi/binfmt --install all[/dim]")
42
+
43
+ if typer.confirm("\nWould you like to install binfmt support now?", default=True):
44
+ console.print("\n[cyan]Installing binfmt support...[/cyan]")
45
+ install_cmd = [
46
+ "docker",
47
+ "run",
48
+ "--privileged",
49
+ "--rm",
50
+ "tonistiigi/binfmt",
51
+ "--install",
52
+ "all",
53
+ ]
54
+ if run_docker_command(
55
+ install_cmd, "Setting up cross-platform build support", stream_output=False
56
+ ):
57
+ console.success("✓ binfmt support installed! You can now retry your build.")
58
+ else:
59
+ console.error("Failed to install binfmt support.")
60
+ console.print("\n[yellow]You can install it manually by running:[/yellow]")
61
+ console.print(
62
+ "[yellow] docker run --privileged --rm tonistiigi/binfmt --install all[/yellow]"
63
+ )
64
+ else:
65
+ console.print("\n[yellow]Skipping installation. To install manually, run:[/yellow]")
66
+ console.print(
67
+ "[yellow] docker run --privileged --rm tonistiigi/binfmt --install all[/yellow]"
68
+ )
69
+
70
+
71
+ def _provide_error_hints(stderr: str, stdout: str, command: list[str], registry_info: dict = None):
29
72
  """Provide helpful error hints only when Docker's message isn't clear enough."""
30
73
  stderr_lower = stderr.lower()
74
+ stdout_lower = stdout.lower()
75
+ combined_output = f"{stderr_lower} {stdout_lower}"
76
+
77
+ # Check for exec format error (cross-platform build issue)
78
+ if (
79
+ "exec format error" in combined_output
80
+ or "exec /bin/sh: exec format error" in combined_output
81
+ ):
82
+ _suggest_and_install_binfmt()
83
+ return
31
84
 
32
85
  # Only add hints for authentication errors where the solution isn't obvious
33
86
  if _is_auth_error(stderr_lower) and "push" in " ".join(command):
@@ -59,6 +112,8 @@ def run_docker_command(
59
112
 
60
113
  if stream_output:
61
114
  # Stream output in real-time for build commands
115
+ # Collect output for error analysis
116
+ output_lines = []
62
117
  process = subprocess.Popen(
63
118
  command,
64
119
  stdout=subprocess.PIPE,
@@ -70,11 +125,17 @@ def run_docker_command(
70
125
 
71
126
  # Stream output line by line
72
127
  for line in process.stdout:
128
+ output_lines.append(line)
73
129
  console.print(f"[dim]{line.rstrip()}[/dim]")
74
130
 
75
131
  process.wait()
76
132
  if process.returncode != 0:
77
- raise subprocess.CalledProcessError(process.returncode, command)
133
+ # Create a pseudo-exception with captured output
134
+ error = subprocess.CalledProcessError(process.returncode, command)
135
+ error.output = "".join(output_lines)
136
+ error.stdout = error.output
137
+ error.stderr = ""
138
+ raise error
78
139
  else:
79
140
  # Capture output for push commands (less verbose)
80
141
  result = subprocess.run(command, capture_output=True, text=True, check=True)
@@ -84,13 +145,16 @@ def run_docker_command(
84
145
  return True
85
146
  except subprocess.CalledProcessError as e:
86
147
  console.error(f"Docker command failed: {' '.join(command)}")
87
- if e.stdout:
88
- console.print(f"[red]stdout: {e.stdout}[/red]")
89
- if e.stderr:
90
- console.print(f"[red]stderr: {e.stderr}[/red]")
148
+ stdout = getattr(e, "stdout", "") or getattr(e, "output", "") or ""
149
+ stderr = getattr(e, "stderr", "") or ""
150
+
151
+ if stdout:
152
+ console.print(f"[red]stdout: {stdout}[/red]")
153
+ if stderr:
154
+ console.print(f"[red]stderr: {stderr}[/red]")
91
155
 
92
- # Provide helpful hints for common errors
93
- _provide_error_hints(e.stderr, command, registry_info)
156
+ # Provide helpful hints for common errors
157
+ _provide_error_hints(stderr, stdout, command, registry_info)
94
158
  return False
95
159
  except FileNotFoundError:
96
160
  console.error("Docker not found. Please ensure Docker is installed and in your PATH.")
@@ -336,7 +400,7 @@ async def build_push(
336
400
  console.print("\n[dim]To deploy this image, update your pcc-deploy.toml:[/dim]")
337
401
  console.print(f' [bold]image = "{version_tag}"[/bold]')
338
402
  console.print("\n[dim]Then run:[/dim]")
339
- console.print(" [bold]pipecat cloud deploy[/bold]")
403
+ console.print(f" [bold]{PIPECAT_CLI_NAME} deploy[/bold]")
340
404
 
341
405
 
342
406
  def create_docker_command(app: typer.Typer):
@@ -0,0 +1,40 @@
1
+ #
2
+ # Copyright (c) 2025, Daily
3
+ #
4
+ # SPDX-License-Identifier: BSD 2-Clause License
5
+ #
6
+
7
+ import typer
8
+ from rich.table import Table
9
+
10
+ from pipecatcloud._utils.async_utils import synchronizer
11
+ from pipecatcloud._utils.auth_utils import requires_login
12
+ from pipecatcloud._utils.console_utils import console
13
+ from pipecatcloud._utils.regions import get_regions
14
+
15
+ regions_cli = typer.Typer(name="regions", help="Region management", no_args_is_help=True)
16
+
17
+
18
+ @regions_cli.command(name="list", help="List available regions")
19
+ @synchronizer.create_blocking
20
+ @requires_login
21
+ async def list_regions():
22
+ """List all available regions with their display names."""
23
+
24
+ with console.status("[dim]Fetching available regions...[/dim]", spinner="dots"):
25
+ regions = await get_regions()
26
+
27
+ if not regions:
28
+ console.print("[yellow]No regions available[/yellow]")
29
+ return
30
+
31
+ # Create table
32
+ table = Table(show_header=True, header_style="bold")
33
+ table.add_column("Code")
34
+ table.add_column("Name")
35
+
36
+ # Add rows
37
+ for region in regions:
38
+ table.add_row(region["code"], region["display_name"])
39
+
40
+ console.print(table)