We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
bug: confusing error message when running `meltano cloud project use` with no projects
### Meltano Version

cloud branch

### Python Version

NA

### Bug scope

CLI (options, error messages, logging, etc.)

### Operating System

N/A

### Description

Current behaviour:

```
(meltano310) will ~/meltano/meltano (cloud) $ meltano cloud project use
Need help fixing this problem? Visit http://melta.no/ for troubleshooting steps, or to
join our friendly Slack community.

A list of choices needs to be provided.
```

Expected behaviour:

```
(meltano310) will ~/meltano/meltano (cloud) $ meltano cloud project use
Need help fixing this problem? Visit http://melta.no/ for troubleshooting steps, or to
join our friendly Slack community.

No Meltano Cloud projects available to use. Please create a project before running 'meltano cloud project use'.
```

### Code

_No response_

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/cloud-cli/meltano/cloud/cli/project.py`
Content:
```
1 """Meltano Cloud `project` command."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 import json
7 import logging
8 import platform
9 import sys
10 import typing as t
11 
12 import click
13 import questionary
14 import tabulate
15 
16 from meltano.cloud.api.client import MeltanoCloudClient
17 from meltano.cloud.cli.base import pass_context, run_async
18 
19 if t.TYPE_CHECKING:
20     from meltano.cloud.api.config import MeltanoCloudConfig
21     from meltano.cloud.api.types import CloudProject
22     from meltano.cloud.cli.base import MeltanoCloudCLIContext
23 
24 DEFAULT_GET_PROJECTS_LIMIT = 125
25 MAX_PAGE_SIZE = 250
26 
27 logger = logging.getLogger()
28 
29 
30 class ProjectsCloudClient(MeltanoCloudClient):
31     """A Meltano Cloud client with extensions for projects."""
32 
33     async def get_projects(
34         self,
35         *,
36         project_id: str | None = None,
37         project_name: str | None = None,
38         page_size: int | None = None,
39         page_token: str | None = None,
40     ):
41         """Use GET to get Meltano Cloud project projects.
42 
43         Args:
44             project_id: The Meltano Cloud ID for the project.
45             project_name: The name of the project.
46             page_size: The number of items to request per page.
47             page_token: The page token.
48         """
49         async with self.authenticated():
50             return await self._json_request(
51                 "GET",
52                 f"/projects/v1/{self.config.tenant_resource_key}",
53                 params=self.clean_params(
54                     {
55                         "project_id": project_id,
56                         "project_name": project_name,
57                         "page_size": page_size,
58                         "page_token": page_token,
59                     },
60                 ),
61             )
62 
63 
64 @click.group("project")
65 def project_group() -> None:
66     """Interact with Meltano Cloud projects."""
67 
68 
69 def _safe_get_internal_project_id(config: MeltanoCloudConfig) -> str | None:
70     """Get the internal project ID, or `None` if it could not be obtained."""
71     try:
72         return config.internal_project_id
73     except Exception:
74         logger.debug(
75             "Could not get internal project ID from config; using `None` instead.",
76         )
77         return None
78 
79 
80 async def _get_projects(
81     config: MeltanoCloudConfig,
82     *,
83     project_id: str | None = None,
84     project_name: str | None = None,
85     limit: int = DEFAULT_GET_PROJECTS_LIMIT,
86 ) -> list[CloudProject]:
87     page_token = None
88     page_size = min(limit, MAX_PAGE_SIZE)
89     results: list[CloudProject] = []
90 
91     async with ProjectsCloudClient(config=config) as client:
92         while True:
93             response = await client.get_projects(
94                 project_id=project_id,
95                 project_name=project_name,
96                 page_size=page_size,
97                 page_token=page_token,
98             )
99 
100             results.extend(response["results"])
101 
102             if response["pagination"] and len(results) < limit:
103                 page_token = response["pagination"]["next_page_token"]
104             else:
105                 break
106 
107     return [
108         {
109             **x,  # type: ignore[misc]
110             "default": x["project_id"] == _safe_get_internal_project_id(config),
111         }
112         for x in results[:limit]
113     ]
114 
115 
116 def _process_table_row(project: CloudProject) -> tuple[str, ...]:
117     return (
118         "X" if project["default"] else "",
119         project["project_name"],
120         project["git_repository"],
121     )
122 
123 
124 def _format_projects_table(projects: list[CloudProject], table_format: str) -> str:
125     """Format the projects as a table.
126 
127     Args:
128         projects: The projects to format.
129 
130     Returns:
131         The formatted projects.
132     """
133     return tabulate.tabulate(
134         [_process_table_row(project) for project in projects],
135         headers=(
136             "Default",
137             "Name",
138             "Git Repository",
139         ),
140         tablefmt=table_format,
141         # To avoid a tabulate bug (IndexError), only set colalign if there are projects
142         colalign=("center", "left", "left") if projects else (),
143     )
144 
145 
146 private_project_attributes = {"tenant_resource_key", "project_id"}
147 
148 
149 def _remove_private_project_attributes(project: CloudProject) -> dict[str, t.Any]:
150     return {k: v for k, v in project.items() if k not in private_project_attributes}
151 
152 
153 project_list_formatters = {
154     "json": lambda x: json.dumps(x, indent=2),
155     "markdown": lambda x: _format_projects_table(x, table_format="github"),
156     "terminal": lambda x: _format_projects_table(x, table_format="rounded_outline"),
157 }
158 
159 
160 @project_group.command("list")
161 @click.option(
162     "--limit",
163     required=False,
164     type=int,
165     default=DEFAULT_GET_PROJECTS_LIMIT,
166     help="The maximum number of projects to display.",
167 )
168 @click.option(
169     "--format",
170     "output_format",
171     required=False,
172     default="terminal",
173     type=click.Choice(("terminal", "markdown", "json")),
174     help="The output format to use.",
175 )
176 @pass_context
177 @run_async
178 async def list_projects(
179     context: MeltanoCloudCLIContext,
180     output_format: str,
181     limit: int,
182 ) -> None:
183     """List Meltano Cloud projects."""
184     click.echo(
185         project_list_formatters[output_format](
186             [
187                 _remove_private_project_attributes(x)
188                 for x in (await _get_projects(config=context.config, limit=limit))
189             ],
190         ),
191     )
192 
193 
194 def _check_for_duplicate_project_names(projects: list[CloudProject]) -> None:
195     project_names = [x["project_name"] for x in projects]
196     if len(set(project_names)) != len(project_names):
197         click.secho(
198             "Error: Multiple Meltano Cloud projects have the same name. "
199             "Please specify the project using the `--id` option with its "
200             "internal ID, shown below. Note that these IDs may change at any "
201             "time. To avoid this issue, please use unique project names.",
202             fg="red",
203         )
204         for project in projects:
205             click.echo(
206                 f"{project['project_id']}: {project['project_name']} "
207                 f"({project['git_repository']!r})",
208             )
209         sys.exit(1)
210 
211 
212 class ProjectChoicesQuestionaryOption(click.Option):
213     """Click option that provides an interactive prompt for Cloud Project names."""
214 
215     def prompt_for_value(self, ctx: click.Context) -> t.Any:
216         """Prompt the user to interactively select a Meltano Cloud project by name.
217 
218         Args:
219             ctx: The Click context.
220 
221         Returns:
222             The name of the selected project, or `None` if the project was
223             selected using the `--id` option.
224         """
225         if "project_id" in ctx.params:
226             # The project has been specified by ID - don't prompt for a name
227             return None
228 
229         if platform.system() == "Windows":
230             asyncio.set_event_loop_policy(
231                 asyncio.WindowsSelectorEventLoopPolicy(),  # type: ignore[attr-defined]
232             )
233 
234         context: MeltanoCloudCLIContext = ctx.obj
235         context.projects = asyncio.run(_get_projects(context.config))
236         _check_for_duplicate_project_names(context.projects)
237         default_project_name = next(
238             (
239                 x
240                 for x in context.projects
241                 if x["project_id"] == context.config.default_project_id
242             ),
243             {"project_name": None},
244         )["project_name"]
245         return questionary.select(
246             message="",
247             qmark="Use Meltano Cloud project",
248             choices=[x["project_name"] for x in context.projects],
249             default=default_project_name,
250         ).unsafe_ask()  # Use Click's Ctrl-C handling instead of Questionary's
251 
252 
253 @project_group.command("use")
254 @click.option(
255     "--name",
256     "project_name",
257     cls=ProjectChoicesQuestionaryOption,
258     help=(
259         "The name of a Meltano Cloud project - "
260         "see `meltano cloud project list` for the available options."
261     ),
262     prompt=True,
263 )
264 @click.option(
265     "--id",
266     "project_id",
267     help=(
268         "The internal ID of a Meltano Cloud project - this ID is unstable and "
269         "should only be used if necessary to disambiguate when multiple "
270         "projects share a name."
271     ),
272     default=None,
273 )
274 @pass_context
275 @run_async
276 async def use_project(
277     context: MeltanoCloudCLIContext,
278     project_name: str | None,
279     project_id: str | None,
280 ) -> None:
281     """Set a project as the default to use for Meltano Cloud CLI commands."""
282     if project_id is not None and project_name is not None:
283         raise click.UsageError("The '--name' and '--id' options are mutually exclusive")
284     if project_id is not None:
285         context.config.internal_project_id = project_id
286         click.secho(
287             (
288                 f"Set the project with ID {project_id!r} as the default "
289                 "Meltano Cloud project for future commands"
290             ),
291             fg="green",
292         )
293         return
294 
295     if context.projects is None:  # Interactive config was not used
296         context.projects = await _get_projects(context.config)
297         _check_for_duplicate_project_names(context.projects)
298         if project_name not in {x["project_name"] for x in context.projects}:
299             raise click.ClickException(
300                 f"Unable to use project named {project_name!r} - no available "
301                 "project matches name.",
302             )
303     context.config.internal_project_id = next(
304         x for x in context.projects if x["project_name"] == project_name
305     )["project_id"]
306     click.secho(
307         (
308             f"Set {project_name!r} as the default Meltano Cloud project for "
309             "future commands"
310         ),
311         fg="green",
312     )
313
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/cloud-cli/meltano/cloud/cli/project.py b/src/cloud-cli/meltano/cloud/cli/project.py
--- a/src/cloud-cli/meltano/cloud/cli/project.py
+++ b/src/cloud-cli/meltano/cloud/cli/project.py
@@ -242,6 +242,11 @@
             ),
             {"project_name": None},
         )["project_name"]
+        if not context.projects:
+            raise click.ClickException(
+                "No Meltano Cloud projects available to use. Please create a "
+                "project before running 'meltano cloud project use'.",
+            )
         return questionary.select(
             message="",
             qmark="Use Meltano Cloud project",

{"golden_diff": "diff --git a/src/cloud-cli/meltano/cloud/cli/project.py b/src/cloud-cli/meltano/cloud/cli/project.py\n--- a/src/cloud-cli/meltano/cloud/cli/project.py\n+++ b/src/cloud-cli/meltano/cloud/cli/project.py\n@@ -242,6 +242,11 @@\n             ),\n             {\"project_name\": None},\n         )[\"project_name\"]\n+        if not context.projects:\n+            raise click.ClickException(\n+                \"No Meltano Cloud projects available to use. Please create a \"\n+                \"project before running 'meltano cloud project use'.\",\n+            )\n         return questionary.select(\n             message=\"\",\n             qmark=\"Use Meltano Cloud project\",\n", "issue": "bug: confusing error message when running `meltano cloud project use` with no projects\n### Meltano Version\n\ncloud branch\n\n### Python Version\n\nNA\n\n### Bug scope\n\nCLI (options, error messages, logging, etc.)\n\n### Operating System\n\nN/A\n\n### Description\n\nCurrent behaviour:\r\n\r\n```\r\n(meltano310) will ~/meltano/meltano (cloud) $ meltano cloud project use\r\nNeed help fixing this problem? Visit http://melta.no/ for troubleshooting steps, or to\r\njoin our friendly Slack community.\r\n\r\nA list of choices needs to be provided.\r\n```\r\n\r\nExpected behaviour:\r\n\r\n```\r\n(meltano310) will ~/meltano/meltano (cloud) $ meltano cloud project use\r\nNeed help fixing this problem? Visit http://melta.no/ for troubleshooting steps, or to\r\njoin our friendly Slack community.\r\n\r\nNo Meltano Cloud projects available to use. Please create a project before running 'meltano cloud project use'.\r\n```\n\n### Code\n\n_No response_\n", "before_files": [{"content": "\"\"\"Meltano Cloud `project` command.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport platform\nimport sys\nimport typing as t\n\nimport click\nimport questionary\nimport tabulate\n\nfrom meltano.cloud.api.client import MeltanoCloudClient\nfrom meltano.cloud.cli.base import pass_context, run_async\n\nif t.TYPE_CHECKING:\n    from meltano.cloud.api.config import MeltanoCloudConfig\n    from meltano.cloud.api.types import CloudProject\n    from meltano.cloud.cli.base import MeltanoCloudCLIContext\n\nDEFAULT_GET_PROJECTS_LIMIT = 125\nMAX_PAGE_SIZE = 250\n\nlogger = logging.getLogger()\n\n\nclass ProjectsCloudClient(MeltanoCloudClient):\n    \"\"\"A Meltano Cloud client with extensions for projects.\"\"\"\n\n    async def get_projects(\n        self,\n        *,\n        project_id: str | None = None,\n        project_name: str | None = None,\n        page_size: int | None = None,\n        page_token: str | None = None,\n    ):\n        \"\"\"Use GET to get Meltano Cloud project projects.\n\n        Args:\n            project_id: The Meltano Cloud ID for the project.\n            project_name: The name of the project.\n            page_size: The number of items to request per page.\n            page_token: The page token.\n        \"\"\"\n        async with self.authenticated():\n            return await self._json_request(\n                \"GET\",\n                f\"/projects/v1/{self.config.tenant_resource_key}\",\n                params=self.clean_params(\n                    {\n                        \"project_id\": project_id,\n                        \"project_name\": project_name,\n                        \"page_size\": page_size,\n                        \"page_token\": page_token,\n                    },\n                ),\n            )\n\n\n@click.group(\"project\")\ndef project_group() -> None:\n    \"\"\"Interact with Meltano Cloud projects.\"\"\"\n\n\ndef _safe_get_internal_project_id(config: MeltanoCloudConfig) -> str | None:\n    \"\"\"Get the internal project ID, or `None` if it could not be obtained.\"\"\"\n    try:\n        return config.internal_project_id\n    except Exception:\n        logger.debug(\n            \"Could not get internal project ID from config; using `None` instead.\",\n        )\n        return None\n\n\nasync def _get_projects(\n    config: MeltanoCloudConfig,\n    *,\n    project_id: str | None = None,\n    project_name: str | None = None,\n    limit: int = DEFAULT_GET_PROJECTS_LIMIT,\n) -> list[CloudProject]:\n    page_token = None\n    page_size = min(limit, MAX_PAGE_SIZE)\n    results: list[CloudProject] = []\n\n    async with ProjectsCloudClient(config=config) as client:\n        while True:\n            response = await client.get_projects(\n                project_id=project_id,\n                project_name=project_name,\n                page_size=page_size,\n                page_token=page_token,\n            )\n\n            results.extend(response[\"results\"])\n\n            if response[\"pagination\"] and len(results) < limit:\n                page_token = response[\"pagination\"][\"next_page_token\"]\n            else:\n                break\n\n    return [\n        {\n            **x,  # type: ignore[misc]\n            \"default\": x[\"project_id\"] == _safe_get_internal_project_id(config),\n        }\n        for x in results[:limit]\n    ]\n\n\ndef _process_table_row(project: CloudProject) -> tuple[str, ...]:\n    return (\n        \"X\" if project[\"default\"] else \"\",\n        project[\"project_name\"],\n        project[\"git_repository\"],\n    )\n\n\ndef _format_projects_table(projects: list[CloudProject], table_format: str) -> str:\n    \"\"\"Format the projects as a table.\n\n    Args:\n        projects: The projects to format.\n\n    Returns:\n        The formatted projects.\n    \"\"\"\n    return tabulate.tabulate(\n        [_process_table_row(project) for project in projects],\n        headers=(\n            \"Default\",\n            \"Name\",\n            \"Git Repository\",\n        ),\n        tablefmt=table_format,\n        # To avoid a tabulate bug (IndexError), only set colalign if there are projects\n        colalign=(\"center\", \"left\", \"left\") if projects else (),\n    )\n\n\nprivate_project_attributes = {\"tenant_resource_key\", \"project_id\"}\n\n\ndef _remove_private_project_attributes(project: CloudProject) -> dict[str, t.Any]:\n    return {k: v for k, v in project.items() if k not in private_project_attributes}\n\n\nproject_list_formatters = {\n    \"json\": lambda x: json.dumps(x, indent=2),\n    \"markdown\": lambda x: _format_projects_table(x, table_format=\"github\"),\n    \"terminal\": lambda x: _format_projects_table(x, table_format=\"rounded_outline\"),\n}\n\n\n@project_group.command(\"list\")\n@click.option(\n    \"--limit\",\n    required=False,\n    type=int,\n    default=DEFAULT_GET_PROJECTS_LIMIT,\n    help=\"The maximum number of projects to display.\",\n)\n@click.option(\n    \"--format\",\n    \"output_format\",\n    required=False,\n    default=\"terminal\",\n    type=click.Choice((\"terminal\", \"markdown\", \"json\")),\n    help=\"The output format to use.\",\n)\n@pass_context\n@run_async\nasync def list_projects(\n    context: MeltanoCloudCLIContext,\n    output_format: str,\n    limit: int,\n) -> None:\n    \"\"\"List Meltano Cloud projects.\"\"\"\n    click.echo(\n        project_list_formatters[output_format](\n            [\n                _remove_private_project_attributes(x)\n                for x in (await _get_projects(config=context.config, limit=limit))\n            ],\n        ),\n    )\n\n\ndef _check_for_duplicate_project_names(projects: list[CloudProject]) -> None:\n    project_names = [x[\"project_name\"] for x in projects]\n    if len(set(project_names)) != len(project_names):\n        click.secho(\n            \"Error: Multiple Meltano Cloud projects have the same name. \"\n            \"Please specify the project using the `--id` option with its \"\n            \"internal ID, shown below. Note that these IDs may change at any \"\n            \"time. To avoid this issue, please use unique project names.\",\n            fg=\"red\",\n        )\n        for project in projects:\n            click.echo(\n                f\"{project['project_id']}: {project['project_name']} \"\n                f\"({project['git_repository']!r})\",\n            )\n        sys.exit(1)\n\n\nclass ProjectChoicesQuestionaryOption(click.Option):\n    \"\"\"Click option that provides an interactive prompt for Cloud Project names.\"\"\"\n\n    def prompt_for_value(self, ctx: click.Context) -> t.Any:\n        \"\"\"Prompt the user to interactively select a Meltano Cloud project by name.\n\n        Args:\n            ctx: The Click context.\n\n        Returns:\n            The name of the selected project, or `None` if the project was\n            selected using the `--id` option.\n        \"\"\"\n        if \"project_id\" in ctx.params:\n            # The project has been specified by ID - don't prompt for a name\n            return None\n\n        if platform.system() == \"Windows\":\n            asyncio.set_event_loop_policy(\n                asyncio.WindowsSelectorEventLoopPolicy(),  # type: ignore[attr-defined]\n            )\n\n        context: MeltanoCloudCLIContext = ctx.obj\n        context.projects = asyncio.run(_get_projects(context.config))\n        _check_for_duplicate_project_names(context.projects)\n        default_project_name = next(\n            (\n                x\n                for x in context.projects\n                if x[\"project_id\"] == context.config.default_project_id\n            ),\n            {\"project_name\": None},\n        )[\"project_name\"]\n        return questionary.select(\n            message=\"\",\n            qmark=\"Use Meltano Cloud project\",\n            choices=[x[\"project_name\"] for x in context.projects],\n            default=default_project_name,\n        ).unsafe_ask()  # Use Click's Ctrl-C handling instead of Questionary's\n\n\n@project_group.command(\"use\")\n@click.option(\n    \"--name\",\n    \"project_name\",\n    cls=ProjectChoicesQuestionaryOption,\n    help=(\n        \"The name of a Meltano Cloud project - \"\n        \"see `meltano cloud project list` for the available options.\"\n    ),\n    prompt=True,\n)\n@click.option(\n    \"--id\",\n    \"project_id\",\n    help=(\n        \"The internal ID of a Meltano Cloud project - this ID is unstable and \"\n        \"should only be used if necessary to disambiguate when multiple \"\n        \"projects share a name.\"\n    ),\n    default=None,\n)\n@pass_context\n@run_async\nasync def use_project(\n    context: MeltanoCloudCLIContext,\n    project_name: str | None,\n    project_id: str | None,\n) -> None:\n    \"\"\"Set a project as the default to use for Meltano Cloud CLI commands.\"\"\"\n    if project_id is not None and project_name is not None:\n        raise click.UsageError(\"The '--name' and '--id' options are mutually exclusive\")\n    if project_id is not None:\n        context.config.internal_project_id = project_id\n        click.secho(\n            (\n                f\"Set the project with ID {project_id!r} as the default \"\n                \"Meltano Cloud project for future commands\"\n            ),\n            fg=\"green\",\n        )\n        return\n\n    if context.projects is None:  # Interactive config was not used\n        context.projects = await _get_projects(context.config)\n        _check_for_duplicate_project_names(context.projects)\n        if project_name not in {x[\"project_name\"] for x in context.projects}:\n            raise click.ClickException(\n                f\"Unable to use project named {project_name!r} - no available \"\n                \"project matches name.\",\n            )\n    context.config.internal_project_id = next(\n        x for x in context.projects if x[\"project_name\"] == project_name\n    )[\"project_id\"]\n    click.secho(\n        (\n            f\"Set {project_name!r} as the default Meltano Cloud project for \"\n            \"future commands\"\n        ),\n        fg=\"green\",\n    )\n", "path": "src/cloud-cli/meltano/cloud/cli/project.py"}], "after_files": [{"content": "\"\"\"Meltano Cloud `project` command.\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport json\nimport logging\nimport platform\nimport sys\nimport typing as t\n\nimport click\nimport questionary\nimport tabulate\n\nfrom meltano.cloud.api.client import MeltanoCloudClient\nfrom meltano.cloud.cli.base import pass_context, run_async\n\nif t.TYPE_CHECKING:\n    from meltano.cloud.api.config import MeltanoCloudConfig\n    from meltano.cloud.api.types import CloudProject\n    from meltano.cloud.cli.base import MeltanoCloudCLIContext\n\nDEFAULT_GET_PROJECTS_LIMIT = 125\nMAX_PAGE_SIZE = 250\n\nlogger = logging.getLogger()\n\n\nclass ProjectsCloudClient(MeltanoCloudClient):\n    \"\"\"A Meltano Cloud client with extensions for projects.\"\"\"\n\n    async def get_projects(\n        self,\n        *,\n        project_id: str | None = None,\n        project_name: str | None = None,\n        page_size: int | None = None,\n        page_token: str | None = None,\n    ):\n        \"\"\"Use GET to get Meltano Cloud project projects.\n\n        Args:\n            project_id: The Meltano Cloud ID for the project.\n            project_name: The name of the project.\n            page_size: The number of items to request per page.\n            page_token: The page token.\n        \"\"\"\n        async with self.authenticated():\n            return await self._json_request(\n                \"GET\",\n                f\"/projects/v1/{self.config.tenant_resource_key}\",\n                params=self.clean_params(\n                    {\n                        \"project_id\": project_id,\n                        \"project_name\": project_name,\n                        \"page_size\": page_size,\n                        \"page_token\": page_token,\n                    },\n                ),\n            )\n\n\n@click.group(\"project\")\ndef project_group() -> None:\n    \"\"\"Interact with Meltano Cloud projects.\"\"\"\n\n\ndef _safe_get_internal_project_id(config: MeltanoCloudConfig) -> str | None:\n    \"\"\"Get the internal project ID, or `None` if it could not be obtained.\"\"\"\n    try:\n        return config.internal_project_id\n    except Exception:\n        logger.debug(\n            \"Could not get internal project ID from config; using `None` instead.\",\n        )\n        return None\n\n\nasync def _get_projects(\n    config: MeltanoCloudConfig,\n    *,\n    project_id: str | None = None,\n    project_name: str | None = None,\n    limit: int = DEFAULT_GET_PROJECTS_LIMIT,\n) -> list[CloudProject]:\n    page_token = None\n    page_size = min(limit, MAX_PAGE_SIZE)\n    results: list[CloudProject] = []\n\n    async with ProjectsCloudClient(config=config) as client:\n        while True:\n            response = await client.get_projects(\n                project_id=project_id,\n                project_name=project_name,\n                page_size=page_size,\n                page_token=page_token,\n            )\n\n            results.extend(response[\"results\"])\n\n            if response[\"pagination\"] and len(results) < limit:\n                page_token = response[\"pagination\"][\"next_page_token\"]\n            else:\n                break\n\n    return [\n        {\n            **x,  # type: ignore[misc]\n            \"default\": x[\"project_id\"] == _safe_get_internal_project_id(config),\n        }\n        for x in results[:limit]\n    ]\n\n\ndef _process_table_row(project: CloudProject) -> tuple[str, ...]:\n    return (\n        \"X\" if project[\"default\"] else \"\",\n        project[\"project_name\"],\n        project[\"git_repository\"],\n    )\n\n\ndef _format_projects_table(projects: list[CloudProject], table_format: str) -> str:\n    \"\"\"Format the projects as a table.\n\n    Args:\n        projects: The projects to format.\n\n    Returns:\n        The formatted projects.\n    \"\"\"\n    return tabulate.tabulate(\n        [_process_table_row(project) for project in projects],\n        headers=(\n            \"Default\",\n            \"Name\",\n            \"Git Repository\",\n        ),\n        tablefmt=table_format,\n        # To avoid a tabulate bug (IndexError), only set colalign if there are projects\n        colalign=(\"center\", \"left\", \"left\") if projects else (),\n    )\n\n\nprivate_project_attributes = {\"tenant_resource_key\", \"project_id\"}\n\n\ndef _remove_private_project_attributes(project: CloudProject) -> dict[str, t.Any]:\n    return {k: v for k, v in project.items() if k not in private_project_attributes}\n\n\nproject_list_formatters = {\n    \"json\": lambda x: json.dumps(x, indent=2),\n    \"markdown\": lambda x: _format_projects_table(x, table_format=\"github\"),\n    \"terminal\": lambda x: _format_projects_table(x, table_format=\"rounded_outline\"),\n}\n\n\n@project_group.command(\"list\")\n@click.option(\n    \"--limit\",\n    required=False,\n    type=int,\n    default=DEFAULT_GET_PROJECTS_LIMIT,\n    help=\"The maximum number of projects to display.\",\n)\n@click.option(\n    \"--format\",\n    \"output_format\",\n    required=False,\n    default=\"terminal\",\n    type=click.Choice((\"terminal\", \"markdown\", \"json\")),\n    help=\"The output format to use.\",\n)\n@pass_context\n@run_async\nasync def list_projects(\n    context: MeltanoCloudCLIContext,\n    output_format: str,\n    limit: int,\n) -> None:\n    \"\"\"List Meltano Cloud projects.\"\"\"\n    click.echo(\n        project_list_formatters[output_format](\n            [\n                _remove_private_project_attributes(x)\n                for x in (await _get_projects(config=context.config, limit=limit))\n            ],\n        ),\n    )\n\n\ndef _check_for_duplicate_project_names(projects: list[CloudProject]) -> None:\n    project_names = [x[\"project_name\"] for x in projects]\n    if len(set(project_names)) != len(project_names):\n        click.secho(\n            \"Error: Multiple Meltano Cloud projects have the same name. \"\n            \"Please specify the project using the `--id` option with its \"\n            \"internal ID, shown below. Note that these IDs may change at any \"\n            \"time. To avoid this issue, please use unique project names.\",\n            fg=\"red\",\n        )\n        for project in projects:\n            click.echo(\n                f\"{project['project_id']}: {project['project_name']} \"\n                f\"({project['git_repository']!r})\",\n            )\n        sys.exit(1)\n\n\nclass ProjectChoicesQuestionaryOption(click.Option):\n    \"\"\"Click option that provides an interactive prompt for Cloud Project names.\"\"\"\n\n    def prompt_for_value(self, ctx: click.Context) -> t.Any:\n        \"\"\"Prompt the user to interactively select a Meltano Cloud project by name.\n\n        Args:\n            ctx: The Click context.\n\n        Returns:\n            The name of the selected project, or `None` if the project was\n            selected using the `--id` option.\n        \"\"\"\n        if \"project_id\" in ctx.params:\n            # The project has been specified by ID - don't prompt for a name\n            return None\n\n        if platform.system() == \"Windows\":\n            asyncio.set_event_loop_policy(\n                asyncio.WindowsSelectorEventLoopPolicy(),  # type: ignore[attr-defined]\n            )\n\n        context: MeltanoCloudCLIContext = ctx.obj\n        context.projects = asyncio.run(_get_projects(context.config))\n        _check_for_duplicate_project_names(context.projects)\n        default_project_name = next(\n            (\n                x\n                for x in context.projects\n                if x[\"project_id\"] == context.config.default_project_id\n            ),\n            {\"project_name\": None},\n        )[\"project_name\"]\n        if not context.projects:\n            raise click.ClickException(\n                \"No Meltano Cloud projects available to use. Please create a \"\n                \"project before running 'meltano cloud project use'.\",\n            )\n        return questionary.select(\n            message=\"\",\n            qmark=\"Use Meltano Cloud project\",\n            choices=[x[\"project_name\"] for x in context.projects],\n            default=default_project_name,\n        ).unsafe_ask()  # Use Click's Ctrl-C handling instead of Questionary's\n\n\n@project_group.command(\"use\")\n@click.option(\n    \"--name\",\n    \"project_name\",\n    cls=ProjectChoicesQuestionaryOption,\n    help=(\n        \"The name of a Meltano Cloud project - \"\n        \"see `meltano cloud project list` for the available options.\"\n    ),\n    prompt=True,\n)\n@click.option(\n    \"--id\",\n    \"project_id\",\n    help=(\n        \"The internal ID of a Meltano Cloud project - this ID is unstable and \"\n        \"should only be used if necessary to disambiguate when multiple \"\n        \"projects share a name.\"\n    ),\n    default=None,\n)\n@pass_context\n@run_async\nasync def use_project(\n    context: MeltanoCloudCLIContext,\n    project_name: str | None,\n    project_id: str | None,\n) -> None:\n    \"\"\"Set a project as the default to use for Meltano Cloud CLI commands.\"\"\"\n    if project_id is not None and project_name is not None:\n        raise click.UsageError(\"The '--name' and '--id' options are mutually exclusive\")\n    if project_id is not None:\n        context.config.internal_project_id = project_id\n        click.secho(\n            (\n                f\"Set the project with ID {project_id!r} as the default \"\n                \"Meltano Cloud project for future commands\"\n            ),\n            fg=\"green\",\n        )\n        return\n\n    if context.projects is None:  # Interactive config was not used\n        context.projects = await _get_projects(context.config)\n        _check_for_duplicate_project_names(context.projects)\n        if project_name not in {x[\"project_name\"] for x in context.projects}:\n            raise click.ClickException(\n                f\"Unable to use project named {project_name!r} - no available \"\n                \"project matches name.\",\n            )\n    context.config.internal_project_id = next(\n        x for x in context.projects if x[\"project_name\"] == project_name\n    )[\"project_id\"]\n    click.secho(\n        (\n            f\"Set {project_name!r} as the default Meltano Cloud project for \"\n            \"future commands\"\n        ),\n        fg=\"green\",\n    )\n", "path": "src/cloud-cli/meltano/cloud/cli/project.py"}]}