Repository: dbt-labs/dbt-core

Issue:
[Bug] Running `dbt init` without any adapters installed throws an error
### Is this a new bug in dbt-core?

- [X] I believe this is a new bug in dbt-core
- [X] I have searched the existing issues, and I could not find an existing issue for this bug

### Current Behavior

When I run `dbt init` without and adapter installed I get the following prompt 

```
Which database would you like to use?


(Don't see the one you want? https://docs.getdbt.com/docs/available-adapters)

Enter a number: 
```
There are no options to choose from. If i enter a dummy option, like 1, I get an error. 

```
18:01:10  Traceback (most recent call last):
  File "/Users/jordanstein/jaffle-metrics/venv/lib/python3.11/site-packages/dbt/cli/requires.py", line 86, in wrapper
    result, success = func(*args, **kwargs)
                      ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jordanstein/jaffle-metrics/venv/lib/python3.11/site-packages/dbt/cli/requires.py", line 71, in wrapper
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jordanstein/jaffle-metrics/venv/lib/python3.11/site-packages/dbt/cli/main.py", line 459, in init
    results = task.run()
              ^^^^^^^^^^
  File "/Users/jordanstein/jaffle-metrics/venv/lib/python3.11/site-packages/dbt/task/init.py", line 276, in run
    adapter = self.ask_for_adapter_choice()
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/jordanstein/jaffle-metrics/venv/lib/python3.11/site-packages/dbt/task/init.py", line 231, in ask_for_adapter_choice
    return available_adapters[numeric_choice - 1]
           ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
IndexError: list index out of range
```

### Expected Behavior

I would expect a prompt to tell me to install an adapter, and gracefully exit the interactive prompt without an  error. For example:

```
There are no data platform adapters installed! 
Please install the adapter compatible with your data platform using pip install dbt-[adapter]. 

For example: `pip install dbt-postgres`. 

Read our adapter documentation for further details https://docs.getdbt.com/docs/available-adapters.  
```

### Steps To Reproduce

1. Create a clean venv and install dbt-core
2. Create a minimal dbt_project.yml
```
name: jaffle-metrics

config-version: 2
version: 1.0.0

profile: dbt-jaffle-metrics
```
3. Run `dbt init`

### Relevant log output

_No response_

### Environment

```markdown
- OS: mac
- Python: 3.11
- dbt:1.5
```


### Which database adapter are you using with dbt?

_No response_

### Additional Context

We're using the `_get_adapter_plugin_name` function to yeild the available plugin name. If the adapter spec is none - we should return a prompt to install an adapter. 

```
--dbt/version.py
def _get_adapter_plugin_names() -> Iterator[str]:
    spec = importlib.util.find_spec("dbt.adapters")
    # If None, then nothing provides an importable 'dbt.adapters', so we will
    # not be reporting plugin versions today
    if spec is None or spec.submodule_search_locations is None:
        return

    for adapters_path in spec.submodule_search_locations:
        version_glob = os.path.join(adapters_path, "*", "__version__.py")
        for version_path in glob.glob(version_glob):
            # the path is like .../dbt/adapters/{plugin_name}/__version__.py
            # except it could be \\ on windows!
            plugin_root, _ = os.path.split(version_path)
            _, plugin_name = os.path.split(plugin_root)
            yield plugin_name
```


Discussion:

Resolving patch:
diff --git a/core/dbt/exceptions.py b/core/dbt/exceptions.py
--- a/core/dbt/exceptions.py
+++ b/core/dbt/exceptions.py
@@ -699,6 +699,15 @@ def get_message(self) -> str:
         return msg
 
 
+class NoAdaptersAvailableError(DbtRuntimeError):
+    def __init__(self):
+        super().__init__(msg=self.get_message())
+
+    def get_message(self) -> str:
+        msg = "No adapters available. Learn how to install an adapter by going to https://docs.getdbt.com/docs/supported-data-platforms#adapter-installation"
+        return msg
+
+
 class BadSpecError(DbtInternalError):
     def __init__(self, repo, revision, error):
         self.repo = repo
diff --git a/core/dbt/task/init.py b/core/dbt/task/init.py
--- a/core/dbt/task/init.py
+++ b/core/dbt/task/init.py
@@ -3,7 +3,6 @@
 from pathlib import Path
 import re
 import shutil
-import sys
 from typing import Optional
 
 import yaml
@@ -145,17 +144,17 @@ def write_profile(self, profile: dict, profile_name: str):
         This will overwrite any profile with a matching name."""
         # Create the profile directory if it doesn't exist
         profiles_filepath = Path(get_flags().PROFILES_DIR) / Path("profiles.yml")
+
+        profiles = {profile_name: profile}
+
         if profiles_filepath.exists():
-            with open(profiles_filepath, "r+") as f:
+            with open(profiles_filepath, "r") as f:
                 profiles = yaml.safe_load(f) or {}
                 profiles[profile_name] = profile
-                f.seek(0)
-                yaml.dump(profiles, f)
-                f.truncate()
-        else:
-            profiles = {profile_name: profile}
-            with open(profiles_filepath, "w") as f:
-                yaml.dump(profiles, f)
+
+        # Write the profiles dictionary to a brand-new or pre-existing file
+        with open(profiles_filepath, "w") as f:
+            yaml.dump(profiles, f)
 
     def create_profile_from_profile_template(self, profile_template: dict, profile_name: str):
         """Create and write a profile using the supplied profile_template."""
@@ -221,6 +220,10 @@ def create_profile_using_project_profile_template(self, profile_name):
     def ask_for_adapter_choice(self) -> str:
         """Ask the user which adapter (database) they'd like to use."""
         available_adapters = list(_get_adapter_plugin_names())
+
+        if not available_adapters:
+            raise dbt.exceptions.NoAdaptersAvailableError()
+
         prompt_msg = (
             "Which database would you like to use?\n"
             + "\n".join([f"[{n+1}] {v}" for n, v in enumerate(available_adapters)])
@@ -244,6 +247,21 @@ def get_valid_project_name(self) -> str:
 
         return name
 
+    def create_new_project(self, project_name: str):
+        self.copy_starter_repo(project_name)
+        os.chdir(project_name)
+        with open("dbt_project.yml", "r") as f:
+            content = f"{f.read()}".format(project_name=project_name, profile_name=project_name)
+        with open("dbt_project.yml", "w") as f:
+            f.write(content)
+        fire_event(
+            ProjectCreated(
+                project_name=project_name,
+                docs_url=DOCS_URL,
+                slack_url=SLACK_URL,
+            )
+        )
+
     def run(self):
         """Entry point for the init task."""
         profiles_dir = get_flags().PROFILES_DIR
@@ -258,8 +276,21 @@ def run(self):
         if in_project:
             # When dbt init is run inside an existing project,
             # just setup the user's profile.
-            fire_event(SettingUpProfile())
             profile_name = self.get_profile_name_from_current_project()
+        else:
+            # When dbt init is run outside of an existing project,
+            # create a new project and set up the user's profile.
+            project_name = self.get_valid_project_name()
+            project_path = Path(project_name)
+            if project_path.exists():
+                fire_event(ProjectNameAlreadyExists(name=project_name))
+                return
+            self.create_new_project(project_name)
+            profile_name = project_name
+
+        # Ask for adapter only if skip_profile_setup flag is not provided.
+        if not self.args.skip_profile_setup:
+            fire_event(SettingUpProfile())
             if not self.check_if_can_write_profile(profile_name=profile_name):
                 return
             # If a profile_template.yml exists in the project root, that effectively
@@ -275,38 +306,3 @@ def run(self):
                     fire_event(InvalidProfileTemplateYAML())
             adapter = self.ask_for_adapter_choice()
             self.create_profile_from_target(adapter, profile_name=profile_name)
-            return
-
-        # When dbt init is run outside of an existing project,
-        # create a new project and set up the user's profile.
-        available_adapters = list(_get_adapter_plugin_names())
-        if not len(available_adapters):
-            print("No adapters available. Go to https://docs.getdbt.com/docs/available-adapters")
-            sys.exit(1)
-        project_name = self.get_valid_project_name()
-        project_path = Path(project_name)
-        if project_path.exists():
-            fire_event(ProjectNameAlreadyExists(name=project_name))
-            return
-
-        self.copy_starter_repo(project_name)
-        os.chdir(project_name)
-        with open("dbt_project.yml", "r+") as f:
-            content = f"{f.read()}".format(project_name=project_name, profile_name=project_name)
-            f.seek(0)
-            f.write(content)
-            f.truncate()
-
-        # Ask for adapter only if skip_profile_setup flag is not provided.
-        if not self.args.skip_profile_setup:
-            if not self.check_if_can_write_profile(profile_name=project_name):
-                return
-            adapter = self.ask_for_adapter_choice()
-            self.create_profile_from_target(adapter, profile_name=project_name)
-            fire_event(
-                ProjectCreated(
-                    project_name=project_name,
-                    docs_url=DOCS_URL,
-                    slack_url=SLACK_URL,
-                )
-            )


Test patch:
diff --git a/tests/functional/init/test_init.py b/tests/functional/init/test_init.py
--- a/tests/functional/init/test_init.py
+++ b/tests/functional/init/test_init.py
@@ -387,6 +387,12 @@ def test_init_task_in_project_with_invalid_profile_template(
             )
 
 
+class TestInitInsideOfProjectBase:
+    @pytest.fixture(scope="class")
+    def project_name(self, unique_schema):
+        return f"my_project_{unique_schema}"
+
+
 class TestInitOutsideOfProjectBase:
     @pytest.fixture(scope="class")
     def project_name(self, unique_schema):
@@ -583,7 +589,7 @@ def test_init_invalid_project_name_cli(
         manager.prompt.side_effect = [valid_name]
         mock_get_adapter.return_value = [project.adapter.type()]
 
-        run_dbt(["init", invalid_name, "-s"])
+        run_dbt(["init", invalid_name, "--skip-profile-setup"])
         manager.assert_has_calls(
             [
                 call.prompt("Enter a name for your project (letters, digits, underscore)"),
@@ -607,7 +613,7 @@ def test_init_invalid_project_name_prompt(
         manager.prompt.side_effect = [invalid_name, valid_name]
         mock_get_adapter.return_value = [project.adapter.type()]
 
-        run_dbt(["init", "-s"])
+        run_dbt(["init", "--skip-profile-setup"])
         manager.assert_has_calls(
             [
                 call.prompt("Enter a name for your project (letters, digits, underscore)"),
@@ -639,8 +645,8 @@ def test_init_provided_project_name_and_skip_profile_setup(
         mock_get.return_value = [project.adapter.type()]
 
         # provide project name through the init command
-        run_dbt(["init", project_name, "-s"])
-        manager.assert_not_called()
+        run_dbt(["init", project_name, "--skip-profile-setup"])
+        assert len(manager.mock_calls) == 0
 
         with open(os.path.join(project.project_root, project_name, "dbt_project.yml"), "r") as f:
             assert (
@@ -684,3 +690,21 @@ def test_init_provided_project_name_and_skip_profile_setup(
       +materialized: view
 """
             )
+
+
+class TestInitInsideProjectAndSkipProfileSetup(TestInitInsideOfProjectBase):
+    @mock.patch("dbt.task.init._get_adapter_plugin_names")
+    @mock.patch("click.confirm")
+    @mock.patch("click.prompt")
+    def test_init_inside_project_and_skip_profile_setup(
+        self, mock_prompt, mock_confirm, mock_get, project, project_name
+    ):
+        manager = mock.Mock()
+        manager.attach_mock(mock_prompt, "prompt")
+        manager.attach_mock(mock_confirm, "confirm")
+
+        assert Path("dbt_project.yml").exists()
+
+        # skip interactive profile setup
+        run_dbt(["init", "--skip-profile-setup"])
+        assert len(manager.mock_calls) == 0