This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 263d3f80796 Improve PyCharm/IntelliJ IDEA setup with multi-module 
support and IDE auto-detection (#63508)
263d3f80796 is described below

commit 263d3f80796460d78ef98ef475a632d695dde232
Author: Jarek Potiuk <[email protected]>
AuthorDate: Sat Mar 14 18:03:58 2026 +0100

    Improve PyCharm/IntelliJ IDEA setup with multi-module support and IDE 
auto-detection (#63508)
    
    The `setup_idea.py` script now supports two modes: single-module (for 
PyCharm)
    and multi-module (for IntelliJ IDEA Ultimate), with automatic IDE detection.
    Key improvements:
    
    - Auto-detect installed IDE and pick appropriate module mode
    - Multi-module mode: each distribution gets its own .iml file for better
      per-module SDK control and cleaner project structure
    - Register Python SDKs in global jdk.table.xml so interpreters are
      available immediately without manual setup
    - Add --exclude flag to skip modules for faster indexing
    - Add --python flag to choose Python version for the virtualenv
    - Add --idea-path flag to target specific IDE installations
    - Configure project-wide exclusion patterns for build artifacts
    - Rename and expand PyCharm/IntelliJ quick-start documentation with
      detailed script options and mode explanations
---
 .gitignore                                         |    2 -
 contributing-docs/03_contributors_quick_start.rst  |    2 +-
 contributing-docs/06_development_environments.rst  |    2 +-
 contributing-docs/quick-start-ide/README.rst       |    2 +-
 .../contributors_quick_start_pycharm.rst           |  206 ----
 .../contributors_quick_start_pycharm_intellij.rst  |  330 ++++++
 dev/ide_setup/setup_idea.py                        | 1157 ++++++++++++++++++--
 7 files changed, 1423 insertions(+), 278 deletions(-)

diff --git a/.gitignore b/.gitignore
index fa1bc815406..91b96409bf8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,8 +43,6 @@ env/
 build/
 develop-eggs/
 dist/
-!providers/fab/**/www/static/dist
-!providers/edge3/**/www/dist
 downloads/
 eggs/
 .eggs/
diff --git a/contributing-docs/03_contributors_quick_start.rst 
b/contributing-docs/03_contributors_quick_start.rst
index 93ef708e627..b83c8593116 100644
--- a/contributing-docs/03_contributors_quick_start.rst
+++ b/contributing-docs/03_contributors_quick_start.rst
@@ -885,7 +885,7 @@ If you are familiar with Python development and use your 
favourite editors, Airf
 similarly to other projects of yours. However, if you need specific 
instructions for your IDE you
 will find more detailed instructions here:
 
-* `Pycharm/IntelliJ <quick-start-ide/contributors_quick_start_pycharm.rst>`_
+* `Pycharm/IntelliJ 
<quick-start-ide/contributors_quick_start_pycharm_intellij.rst>`_
 * `Visual Studio Code <quick-start-ide/contributors_quick_start_vscode.rst>`_
 
 
diff --git a/contributing-docs/06_development_environments.rst 
b/contributing-docs/06_development_environments.rst
index ea03ffa5b34..68d7fc56efc 100644
--- a/contributing-docs/06_development_environments.rst
+++ b/contributing-docs/06_development_environments.rst
@@ -66,7 +66,7 @@ Limitations:
 Typically you can connect your local virtualenv environments easily with your 
IDE
 and use it for development:
 
-- `PyCharm/IntelliJ <quick-start-ide/contributors_quick_start_pycharm.rst>`__ 
quick start instructions
+- `PyCharm/IntelliJ 
<quick-start-ide/contributors_quick_start_pycharm_intellij.rst>`__ quick start 
instructions
 - `VSCode <quick-start-ide/contributors_quick_start_vscode.rst>`__ quick start 
instructions
 
 Breeze Development Environment
diff --git a/contributing-docs/quick-start-ide/README.rst 
b/contributing-docs/quick-start-ide/README.rst
index 33386b9ab3d..3ea53255b88 100644
--- a/contributing-docs/quick-start-ide/README.rst
+++ b/contributing-docs/quick-start-ide/README.rst
@@ -24,7 +24,7 @@ This document describes how to set up your IDE to work with 
Airflow.
 Local development environments
 ------------------------------
 
-- `PyCharm/IntelliJ <contributors_quick_start_pycharm.rst>`__ quick start 
instructions
+- `PyCharm/IntelliJ <contributors_quick_start_pycharm_intellij.rst>`__ quick 
start instructions
 - `VSCode <contributors_quick_start_vscode.rst>`__ quick start instructions
 
 
diff --git 
a/contributing-docs/quick-start-ide/contributors_quick_start_pycharm.rst 
b/contributing-docs/quick-start-ide/contributors_quick_start_pycharm.rst
deleted file mode 100644
index 09e7599f7de..00000000000
--- a/contributing-docs/quick-start-ide/contributors_quick_start_pycharm.rst
+++ /dev/null
@@ -1,206 +0,0 @@
- .. Licensed to the Apache Software Foundation (ASF) under one
-    or more contributor license agreements.  See the NOTICE file
-    distributed with this work for additional information
-    regarding copyright ownership.  The ASF licenses this file
-    to you under the Apache License, Version 2.0 (the
-    "License"); you may not use this file except in compliance
-    with the License.  You may obtain a copy of the License at
-
- ..   http://www.apache.org/licenses/LICENSE-2.0
-
- .. Unless required by applicable law or agreed to in writing,
-    software distributed under the License is distributed on an
-    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-    KIND, either express or implied.  See the License for the
-    specific language governing permissions and limitations
-    under the License.
-
-.. contents:: Table of Contents
-   :depth: 2
-   :local:
-
-Setup your project
-##################
-
-1. Open your IDE or source code editor and select the option to clone the 
repository
-
-   .. raw:: html
-
-      <div align="center" style="padding-bottom:10px">
-        <img src="images/pycharm_clone.png"
-             alt="Cloning github fork to Pycharm">
-      </div>
-
-2. Paste the repository link in the URL field and submit.
-
-   .. raw:: html
-
-      <div align="center" style="padding-bottom:10px">
-        <img src="images/pycharm_click_on_clone.png"
-             alt="Cloning github fork to Pycharm">
-      </div>
-
-3. Synchronize local ``.venv`` virtualenv using uv
-
-    .. code-block:: bash
-
-      $ uv sync
-
-This will create ``.venv`` virtual environment in the project root directory 
and install all the dependencies of
-airflow core. If you plan to work on providers, at this time you can install 
dependencies for all providers:
-
-    .. code-block:: bash
-
-      $ uv sync --all-packages
-
-Or for specific provider and its cross-provider dependencies:
-
-    .. code-block:: bash
-
-      $ uv sync --packages apache-airflow-provider-amazon
-
-Next: Configure your IDEA project.
-
-3. The fastest way to add source roots is to configure the ``airflow.iml`` 
file under ``.idea`` directory and update the
-   ``module.xml`` file using the ``dev/ide_setup/setup_idea.py`` script:
-
-   To setup the source roots for all the modules that exist in the project, 
you can run the following command:
-   This needs to done on the Airflow repository root directory. It overwrites 
the existing ``.idea/airflow.iml`` and
-   ``.idea/modules.xml`` files if they exist.
-
-    .. code-block:: bash
-
-      $ uv run dev/ide_setup/setup_idea.py
-
-   Then Restart the PyCharm/IntelliJ IDEA.
-
-   .. raw:: html
-
-      <div align="center" style="padding-bottom:10px">
-        <img src="images/pycharm-airflow.iml.png"
-             alt="airflow.iml">
-      </div>
-
-   .. raw:: html
-
-        <div align="center" style="padding-bottom:10px">
-          <img src="images/pycharm-modules.xml.png"
-              alt="modules.xml">
-        </div>
-
-4. Alternatively, you can configure your project manually. Configure the 
source root directories well
-   as for ``airflow-core`` ``task-sdk``, ``airflow-ctl`` and ``devel-common``. 
You also have to set
-   "source" and "tests" root directories for each provider you want to develop 
(!).
-
-   In Airflow 3.0 we split ``airflow-core``, ``task-sdk``, ``airflow-ctl``, 
``devel-common``,
-   and each provider to be separate distribution - each with separate 
``pyproject.toml`` file,
-   so you need to separately add ``src`` and ``tests`` directories for each 
provider you develop
-   to be respectively "source roots" and "test roots".
-
-   .. raw:: html
-
-      <div align="center" style="padding-bottom:10px">
-        <img src="images/pycharm_add_provider_sources_and_tests.png"
-             alt="Adding Source Root directories to Pycharm">
-      </div>
-
-   You also need to add ``task-sdk`` sources (and ``devel-common`` in similar 
way).
-
-   .. raw:: html
-
-      <div align="center" style="padding-bottom:10px">
-        <img src="images/pycharm_add_task_sdk_sources.png"
-             alt="Adding Source Root directories to Pycharm">
-      </div>
-
-5. Once step 3 or 4 is done you should configure python interpreter for your 
PyCharm/IntelliJ to use
-   the virtualenv created by ``uv sync``.
-
-    .. raw:: html
-
-        <div align="center" style="padding-bottom:10px">
-          <img src="images/pycharm_add_interpreter.png"
-              alt="Configuring Python Interpreter">
-        </div>
-
-6. It is recommended to invalidate caches and restart PyCharm after setting up 
the project.
-
-   .. raw:: html
-
-      <div align="center" style="padding-bottom:10px">
-        <img src="images/pycharm_invalidate_caches.png"
-             alt="Invalidate caches and restart Pycharm">
-      </div>
-
-
-
-Setting up debugging
-####################
-
-It requires "airflow-env" virtual environment configured locally.
-
-1. Configuring Airflow database connection
-
-- Airflow is by default configured to use SQLite database. Configuration can 
be seen on local machine
-  ``~/airflow/airflow.cfg`` under ``sql_alchemy_conn``.
-
-- Installing required dependency for MySQL connection in ``airflow-env`` on 
local machine.
-
-  .. code-block:: bash
-
-    $ pyenv activate airflow-env
-    $ pip install PyMySQL
-
-- Now set ``sql_alchemy_conn = 
mysql+pymysql://root:@127.0.0.1:23306/airflow?charset=utf8mb4`` in file
-  ``~/airflow/airflow.cfg`` on local machine.
-
-2. Debugging an example Dag
-
-- Add Interpreter to PyCharm pointing interpreter path to 
``~/.pyenv/versions/airflow-env/bin/python``, which is virtual
-  environment ``airflow-env`` created with pyenv earlier. For adding an 
Interpreter go to ``File -> Setting -> Project:
-  airflow -> Python Interpreter``.
-
-  .. raw:: html
-
-    <div align="center" style="padding-bottom:10px">
-      <img src="images/pycharm_add_interpreter.png"
-           alt="Adding existing interpreter">
-    </div>
-
-- In PyCharm IDE open Airflow project, directory ``/files/dags`` of local 
machine is by default mounted to docker
-  machine when breeze Airflow is started. So any Dag file present in this 
directory will be picked automatically by
-  scheduler running in docker machine and same can be seen on 
``http://127.0.0.1:28080``.
-
-- Copy any example Dag present in the ``/airflow/example_dags`` directory to 
``/files/dags/``.
-
-- Add a ``__main__`` block at the end of your Dag file to make it runnable:
-
-  .. code-block:: python
-
-    if __name__ == "__main__":
-        dag.test()
-
-- Run the file.
-
-Creating a branch
-#################
-
-1. Click on the branch symbol in the status bar
-
-   .. raw:: html
-
-      <div align="center" style="padding-bottom:10px">
-        <img src="images/pycharm_creating_branch_1.png"
-             alt="Creating a new branch">
-      </div>
-
-2. Give a name to a branch and checkout
-
-   .. raw:: html
-
-      <div align="center" style="padding-bottom:10px">
-        <img src="images/pycharm_creating_branch_2.png"
-             alt="Giving a name to a branch">
-      </div>
-
-Follow the `Quick start 
<../03b_contributors_quick_start_seasoned_developers.rst>`_ for typical 
development tasks.
diff --git 
a/contributing-docs/quick-start-ide/contributors_quick_start_pycharm_intellij.rst
 
b/contributing-docs/quick-start-ide/contributors_quick_start_pycharm_intellij.rst
new file mode 100644
index 00000000000..641a437c7b7
--- /dev/null
+++ 
b/contributing-docs/quick-start-ide/contributors_quick_start_pycharm_intellij.rst
@@ -0,0 +1,330 @@
+ .. Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you under the Apache License, Version 2.0 (the
+    "License"); you may not use this file except in compliance
+    with the License.  You may obtain a copy of the License at
+
+ ..   http://www.apache.org/licenses/LICENSE-2.0
+
+ .. Unless required by applicable law or agreed to in writing,
+    software distributed under the License is distributed on an
+    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+    KIND, either express or implied.  See the License for the
+    specific language governing permissions and limitations
+    under the License.
+
+.. contents:: Table of Contents
+   :depth: 2
+   :local:
+
+Setup your project
+##################
+
+1. Open your IDE or source code editor and select the option to clone the 
repository
+
+   .. raw:: html
+
+      <div align="center" style="padding-bottom:10px">
+        <img src="images/pycharm_clone.png"
+             alt="Cloning github fork to Pycharm">
+      </div>
+
+2. Paste the repository link in the URL field and submit.
+
+   .. raw:: html
+
+      <div align="center" style="padding-bottom:10px">
+        <img src="images/pycharm_click_on_clone.png"
+             alt="Cloning github fork to Pycharm">
+      </div>
+
+3. Run the ``dev/ide_setup/setup_idea.py`` script to configure the project 
automatically.
+   The script runs ``uv sync`` to create the ``.venv`` virtualenv, detects the 
Python SDK, and
+   generates the ``.idea/airflow.iml``, ``.idea/modules.xml``, and 
``.idea/misc.xml`` files.
+
+   The script supports two modes — **single-module** and **multi-module** — and
+   **auto-detects** which one to use based on the installed IDE:
+
+   * If **IntelliJ IDEA** is detected → defaults to **multi-module**.
+   * If only **PyCharm** is detected (or no IDE is found) → defaults to 
**single-module**.
+
+   You can override auto-detection with ``--multi-module`` or 
``--single-module``.
+
+   **Single-module mode** — all source roots are registered under one IntelliJ 
module.
+   This works in both PyCharm and IntelliJ IDEA:
+
+    .. code-block:: bash
+
+      $ uv run dev/ide_setup/setup_idea.py --single-module
+
+   **Multi-module mode** — each distribution/package gets its own IntelliJ 
module with a
+   separate ``.iml`` file (e.g. ``airflow-core/airflow-core.iml``,
+   ``providers/amazon/providers-amazon.iml``).  This gives better per-module 
SDK control and
+   cleaner project structure in the IDE's Project view.
+
+    .. code-block:: bash
+
+      $ uv run dev/ide_setup/setup_idea.py --multi-module
+
+   .. note::
+
+      **Multi-module mode requires IntelliJ IDEA Ultimate** — it does **not** 
work in PyCharm
+      (Community or Professional).  PyCharm does not support multiple content 
roots pointing to
+      sub-directories of the project root that each carry their own ``.iml`` 
module file; it
+      silently ignores or mishandles sub-modules.  IntelliJ IDEA Ultimate with 
the Python plugin
+      handles this correctly because it has full support for the IntelliJ 
multi-module project
+      model.  If you use PyCharm, stick with single-module mode.
+
+   Then restart PyCharm/IntelliJ IDEA.
+
+   .. raw:: html
+
+      <div align="center" style="padding-bottom:10px">
+        <img src="images/pycharm-airflow.iml.png"
+             alt="airflow.iml">
+      </div>
+
+   .. raw:: html
+
+        <div align="center" style="padding-bottom:10px">
+          <img src="images/pycharm-modules.xml.png"
+              alt="modules.xml">
+        </div>
+
+   Script options
+   ==============
+
+   ``--python VERSION``
+     Choose the Python minor version for the virtualenv (e.g. ``3.12``).  The 
version is passed
+     to ``uv sync --python`` and must be compatible with the project's 
``requires-python``
+     constraint.  When omitted, ``uv`` picks the default version.
+
+      .. code-block:: bash
+
+        $ uv run dev/ide_setup/setup_idea.py --python 3.12
+
+   ``--multi-module`` / ``--single-module``
+     Control whether the project is configured as a single IntelliJ module 
(all source roots in
+     one ``.iml`` file) or as multiple modules (one ``.iml`` per 
distribution/package, e.g.
+     ``airflow-core/airflow-core.iml``, 
``providers/amazon/providers-amazon.iml``).
+
+     **By default the script auto-detects which IDE is installed** and picks 
the appropriate
+     mode: multi-module when IntelliJ IDEA is found, single-module when only 
PyCharm is found
+     (or when no IDE can be detected).  Use ``--multi-module`` or 
``--single-module`` to
+     override the auto-detected default.
+
+     In multi-module mode the script also creates a dedicated ``dev/breeze`` 
virtualenv
+     (via a second ``uv sync``) with its own Python SDK named *Python X.Y 
(breeze)*.
+     All other sub-modules inherit the project-level SDK.
+
+      .. code-block:: bash
+
+        # Force multi-module (requires IntelliJ IDEA)
+        $ uv run dev/ide_setup/setup_idea.py --multi-module
+
+        # Force single-module (works in both PyCharm and IntelliJ IDEA)
+        $ uv run dev/ide_setup/setup_idea.py --single-module
+
+   ``--confirm``
+     Skip the interactive confirmation prompt that asks whether 
PyCharm/IntelliJ IDEA has been
+     closed.  Useful for non-interactive or scripted runs where you have 
already ensured the IDE
+     is not running.
+
+      .. code-block:: bash
+
+        $ uv run dev/ide_setup/setup_idea.py --confirm
+
+   ``--no-kill``
+     Do not attempt to detect and kill running PyCharm/IntelliJ IDEA 
processes.  By default,
+     the script looks for running IDE processes, asks for confirmation, sends 
``SIGTERM``, and
+     falls back to ``SIGKILL`` if they don't exit within 5 seconds.  Use 
``--no-kill`` to
+     disable this behaviour and fall back to the manual confirmation prompt 
instead.
+
+      .. code-block:: bash
+
+        $ uv run dev/ide_setup/setup_idea.py --no-kill
+
+   ``--idea-path PATH``
+     Path to the JetBrains configuration directory to update instead of 
auto-detecting all
+     installed IDEs.  Can point to the base JetBrains directory
+     (e.g. ``~/Library/Application Support/JetBrains``) or a specific product 
directory
+     (e.g. ``.../JetBrains/IntelliJIdea2025.1``).  Useful when auto-detection 
does not find
+     your IDE or when you want to target a specific installation.
+
+      .. code-block:: bash
+
+        $ uv run dev/ide_setup/setup_idea.py --idea-path 
~/Library/Application\ Support/JetBrains/IntelliJIdea2025.1
+
+   ``--exclude MODULE_OR_GROUP``
+     Exclude modules from the generated project configuration.  Can be 
specified multiple times.
+     Useful when you only work on a subset of the codebase and want faster IDE 
indexing.
+
+     A value can be either a module path relative to the project root (e.g. 
``providers/amazon``,
+     ``dev/breeze``) or one of the recognised group names:
+
+     * ``providers`` — all provider modules under ``providers/``
+     * ``shared`` — all shared libraries under ``shared/``
+     * ``dev`` — the ``dev`` module
+     * ``tests`` — test-only modules (``docker-tests``, ``kubernetes-tests``, 
etc.)
+
+     Examples:
+
+      .. code-block:: bash
+
+        # Exclude all providers and shared libraries
+        $ uv run dev/ide_setup/setup_idea.py --exclude providers --exclude 
shared
+
+        # Exclude a single provider
+        $ uv run dev/ide_setup/setup_idea.py --exclude providers/amazon
+
+        # Multi-module with only core modules
+        $ uv run dev/ide_setup/setup_idea.py --multi-module --exclude 
providers --exclude shared
+
+   Options can be combined freely.  For instance, to create a multi-module 
project with
+   Python 3.12 excluding all providers:
+
+    .. code-block:: bash
+
+      $ uv run dev/ide_setup/setup_idea.py --multi-module --python 3.12 
--exclude providers
+
+   What the script generates
+   =========================
+
+   * ``.idea/airflow.iml`` — root module definition with source roots 
(single-module mode) or
+     exclude-only root module (multi-module mode).
+   * ``.idea/modules.xml`` — module registry listing all IntelliJ modules.
+   * ``.idea/misc.xml`` — project-level Python SDK reference (derived from 
``.venv``).
+   * ``.idea/.name`` — sets the PyCharm project name to ``airflow-<dirname>`` 
so the
+     auto-detected SDK name matches the configuration.
+   * ``<module>/<module>.iml`` — per-module files (multi-module mode only).
+
+   The script also registers the Python SDKs (root project and Breeze) in the 
global
+   JetBrains ``jdk.table.xml`` configuration using the ``uv (<name>)`` naming 
convention
+   that matches PyCharm's auto-detected uv interpreters.  This means the SDKs 
are
+   immediately available when you open the project — no manual interpreter 
setup needed.
+
+   The script also configures project-wide exclusion patterns (``__pycache__``,
+   ``node_modules``, ``*.egg-info``, cache directories, etc.) so that IntelliJ 
does not
+   index or search generated/build artifacts.
+
+4. Alternatively, you can configure your project manually. Configure the 
source root directories
+   for ``airflow-core``, ``task-sdk``, ``airflow-ctl`` and ``devel-common``. 
You also have to set
+   "source" and "tests" root directories for each provider you want to develop 
(!).
+
+   In Airflow 3.0 we split ``airflow-core``, ``task-sdk``, ``airflow-ctl``, 
``devel-common``,
+   and each provider to be separate distribution — each with separate 
``pyproject.toml`` file,
+   so you need to separately add ``src`` and ``tests`` directories for each 
provider you develop
+   to be respectively "source roots" and "test roots".
+
+   .. raw:: html
+
+      <div align="center" style="padding-bottom:10px">
+        <img src="images/pycharm_add_provider_sources_and_tests.png"
+             alt="Adding Source Root directories to Pycharm">
+      </div>
+
+   You also need to add ``task-sdk`` sources (and ``devel-common`` in similar 
way).
+
+   .. raw:: html
+
+      <div align="center" style="padding-bottom:10px">
+        <img src="images/pycharm_add_task_sdk_sources.png"
+             alt="Adding Source Root directories to Pycharm">
+      </div>
+
+5. If you configured the project manually (step 4), configure the Python 
interpreter to use
+   the virtualenv created by ``uv sync``:  go to ``File → Settings → Project → 
Python Interpreter``,
+   click the gear icon, choose *Add Interpreter → Existing*, and point to 
``.venv/bin/python``.
+   If you used the setup script (step 3), the SDK is already registered 
globally — just
+   restart the IDE and the interpreter will be available automatically.
+
+    .. raw:: html
+
+        <div align="center" style="padding-bottom:10px">
+          <img src="images/pycharm_add_interpreter.png"
+              alt="Configuring Python Interpreter">
+        </div>
+
+6. It is recommended to invalidate caches and restart PyCharm after setting up 
the project.
+
+   .. raw:: html
+
+      <div align="center" style="padding-bottom:10px">
+        <img src="images/pycharm_invalidate_caches.png"
+             alt="Invalidate caches and restart Pycharm">
+      </div>
+
+
+
+Setting up debugging
+####################
+
+It requires "airflow-env" virtual environment configured locally.
+
+1. Configuring Airflow database connection
+
+- Airflow is by default configured to use SQLite database. Configuration can 
be seen on local machine
+  ``~/airflow/airflow.cfg`` under ``sql_alchemy_conn``.
+
+- Installing required dependency for MySQL connection in ``airflow-env`` on 
local machine.
+
+  .. code-block:: bash
+
+    $ pyenv activate airflow-env
+    $ pip install PyMySQL
+
+- Now set ``sql_alchemy_conn = 
mysql+pymysql://root:@127.0.0.1:23306/airflow?charset=utf8mb4`` in file
+  ``~/airflow/airflow.cfg`` on local machine.
+
+2. Debugging an example Dag
+
+- Add Interpreter to PyCharm pointing interpreter path to 
``~/.pyenv/versions/airflow-env/bin/python``, which is virtual
+  environment ``airflow-env`` created with pyenv earlier. For adding an 
Interpreter go to ``File -> Setting -> Project:
+  airflow -> Python Interpreter``.
+
+  .. raw:: html
+
+    <div align="center" style="padding-bottom:10px">
+      <img src="images/pycharm_add_interpreter.png"
+           alt="Adding existing interpreter">
+    </div>
+
+- In PyCharm IDE open Airflow project, directory ``/files/dags`` of local 
machine is by default mounted to docker
+  machine when breeze Airflow is started. So any Dag file present in this 
directory will be picked automatically by
+  scheduler running in docker machine and same can be seen on 
``http://127.0.0.1:28080``.
+
+- Copy any example Dag present in the ``/airflow/example_dags`` directory to 
``/files/dags/``.
+
+- Add a ``__main__`` block at the end of your Dag file to make it runnable:
+
+  .. code-block:: python
+
+    if __name__ == "__main__":
+        dag.test()
+
+- Run the file.
+
+Creating a branch
+#################
+
+1. Click on the branch symbol in the status bar
+
+   .. raw:: html
+
+      <div align="center" style="padding-bottom:10px">
+        <img src="images/pycharm_creating_branch_1.png"
+             alt="Creating a new branch">
+      </div>
+
+2. Give a name to a branch and checkout
+
+   .. raw:: html
+
+      <div align="center" style="padding-bottom:10px">
+        <img src="images/pycharm_creating_branch_2.png"
+             alt="Giving a name to a branch">
+      </div>
+
+Follow the `Quick start 
<../03b_contributors_quick_start_seasoned_developers.rst>`_ for typical 
development tasks.
diff --git a/dev/ide_setup/setup_idea.py b/dev/ide_setup/setup_idea.py
index 597be56ea53..59682bef88f 100755
--- a/dev/ide_setup/setup_idea.py
+++ b/dev/ide_setup/setup_idea.py
@@ -19,35 +19,203 @@
 # requires-python = ">=3.10"
 # dependencies = [
 #   "rich>=13.6.0",
+#   "packaging>=24.0",
 # ]
 # ///
 from __future__ import annotations
 
+import argparse
+import os
+import platform
+import re
+import signal
+import subprocess
+import sys
+import time
+import uuid
+import xml.etree.ElementTree as ET
 from pathlib import Path
 
+from packaging.specifiers import SpecifierSet
+from packaging.version import Version
 from rich import print
 from rich.prompt import Confirm
 
-iml_xml_template = """<?xml version="1.0" encoding="UTF-8"?>
-<module type="PYTHON_MODULE" version="4">
-  <component name="NewModuleRootManager">
-     <content url="file://$MODULE_DIR$">
-        {SOURCE_ROOT_MODULE_PATH}
-        <excludeFolder url="file://$MODULE_DIR$/.build" />
-        <excludeFolder url="file://$MODULE_DIR$/.kube" />
-        <excludeFolder url="file://$MODULE_DIR$/.venv" />
-        <excludeFolder url="file://$MODULE_DIR$/dist" />
-        <excludeFolder url="file://$MODULE_DIR$/files" />
-        <excludeFolder url="file://$MODULE_DIR$/logs" />
-        <excludeFolder url="file://$MODULE_DIR$/out" />
-        <excludeFolder url="file://$MODULE_DIR$/tmp" />
-        <excludeFolder url="file://$MODULE_DIR$/airflow-core/dist" />
-        <excludeFolder url="file://$MODULE_DIR$/generated/" />
-        <excludeFolder url="file://$MODULE_DIR$/dev/breeze/.venv" />
-    </content>
-    <orderEntry type="jdk" jdkName="Python 3.10 (airflow)" jdkType="Python 
SDK" />
-    <orderEntry type="sourceFolder" forTests="false" />
-  </component>
+ROOT_AIRFLOW_FOLDER_PATH = Path(__file__).parents[2]
+IDEA_FOLDER_PATH = ROOT_AIRFLOW_FOLDER_PATH / ".idea"
+AIRFLOW_IML_FILE = IDEA_FOLDER_PATH / "airflow.iml"
+MODULES_XML_FILE = IDEA_FOLDER_PATH / "modules.xml"
+MISC_XML_FILE = IDEA_FOLDER_PATH / "misc.xml"
+IDEA_NAME_FILE = IDEA_FOLDER_PATH / ".name"
+BREEZE_PATH = ROOT_AIRFLOW_FOLDER_PATH / "dev" / "breeze"
+
+STATIC_MODULES: list[str] = [
+    "airflow-core",
+    "airflow-ctl",
+    "task-sdk",
+    "devel-common",
+    "dev",
+    "dev/breeze",
+    "docker-tests",
+    "kubernetes-tests",
+    "helm-tests",
+    "task-sdk-integration-tests",
+]
+
+# Well-known module groups for --exclude.
+MODULE_GROUPS: dict[str, str] = {
+    "providers": "providers/",
+    "shared": "shared/",
+    "dev": "dev",
+    "tests": "tests",
+}
+
+source_root_module_pattern: str = '<sourceFolder 
url="file://$MODULE_DIR$/{path}" isTestSource="{status}" />'
+
+# ---------------------------------------------------------------------------
+# Exclude configuration
+# ---------------------------------------------------------------------------
+
+# Directories excluded by pattern (matched recursively against directory names 
in all content roots).
+# Derived from .gitignore entries that can appear at any directory level.
+# NOTE: "dist" is intentionally NOT here — providers/fab and providers/edge3 
have legitimate
+# static asset dist/ directories whitelisted in .gitignore.
+EXCLUDE_PATTERNS: list[str] = [
+    # Python bytecode / packaging
+    "__pycache__",
+    "*.egg-info",
+    ".eggs",
+    "build",
+    "develop-eggs",
+    "eggs",
+    "sdist",
+    "wheels",
+    "downloads",
+    "pip-wheel-metadata",
+    # Test / coverage / lint caches
+    ".mypy_cache",
+    ".pytest_cache",
+    ".ruff_cache",
+    ".ruff-cache",
+    ".hypothesis",
+    ".cache",
+    ".tox",
+    "htmlcov",
+    # Node / frontend
+    "node_modules",
+    ".vite",
+    ".pnpm-store",
+    # Generated documentation
+    "_build",
+    "_doctree",
+    "_inventory_cache",
+    "_api",
+    # Virtualenvs (recursive — root .venv is also in ROOT_EXCLUDE_FOLDERS)
+    "venv",
+    # Infrastructure / IaC
+    ".terraform",
+    "target",
+    # IDE / editor directories
+    ".vscode",
+    ".cursor",
+    # Legacy / misc
+    ".scrapy",
+    ".ropeproject",
+    ".spyderproject",
+    ".webassets-cache",
+    ".ipynb_checkpoints",
+]
+
+# Directories excluded by explicit path (relative to $MODULE_DIR$, i.e. the 
project root).
+# Derived from root-anchored .gitignore entries (those starting with /).
+ROOT_EXCLUDE_FOLDERS: list[str] = [
+    ".build",
+    ".kube",
+    ".venv",
+    ".uv-cache",
+    "dist",
+    "files",
+    "logs",
+    "out",
+    "tmp",
+    "images",
+    "hive_scratch_dir",
+    "airflow-core/dist",
+    "airflow-core/src/airflow/ui/coverage",
+    "generated",
+    "docker-context-files",
+    "target-airflow",
+    "dev/breeze/.venv",
+    "dev/registry/output",
+    "dev/registry/logos",
+    "3rd-party-licenses",
+    "licenses",
+    "registry/src/_data/versions",
+]
+
+# ---------------------------------------------------------------------------
+# Python version helpers
+# ---------------------------------------------------------------------------
+
+# All minor versions we consider.  Keep the upper bound a step ahead of the
+# latest CPython release so newly released interpreters are recognised.
+_ALL_MINOR_VERSIONS = [f"3.{m}" for m in range(9, 16)]
+
+
+def _read_requires_python(pyproject_path: Path) -> str:
+    """Return the ``requires-python`` value from *pyproject_path*."""
+    text = pyproject_path.read_text()
+    match = re.search(r'requires-python\s*=\s*"([^"]+)"', text)
+    if not match:
+        print(f"[red]Error:[/] could not find requires-python in 
{pyproject_path}")
+        sys.exit(1)
+    return match.group(1)
+
+
+def get_supported_python_versions(pyproject_path: Path) -> list[str]:
+    """Return the list of supported ``X.Y`` Python versions according to 
*pyproject_path*."""
+    spec = SpecifierSet(_read_requires_python(pyproject_path))
+    return [v for v in _ALL_MINOR_VERSIONS if Version(f"{v}.0") in spec]
+
+
+# ---------------------------------------------------------------------------
+# XML helpers
+# ---------------------------------------------------------------------------
+
+
+def _build_exclude_patterns_xml(indent: str = "      ") -> str:
+    """Build XML lines for <excludePattern> entries."""
+    return "\n".join(f'{indent}<excludePattern pattern="{pattern}" />' for 
pattern in EXCLUDE_PATTERNS)
+
+
+def _build_exclude_folders_xml(
+    folders: list[str], indent: str = "      ", url_prefix: str = 
"file://$MODULE_DIR$"
+) -> str:
+    """Build XML lines for <excludeFolder> entries."""
+    return "\n".join(f'{indent}<excludeFolder url="{url_prefix}/{folder}" />' 
for folder in folders)
+
+
+def _build_content_xml(
+    source_lines: str,
+    *,
+    include_root_excludes: bool,
+    indent: str = "    ",
+    url: str = "file://$MODULE_DIR$",
+) -> str:
+    """Build a complete <content> element with sources, exclude folders, and 
exclude patterns."""
+    parts = [f'{indent}<content url="{url}">']
+    if source_lines:
+        parts.append(source_lines)
+    if include_root_excludes:
+        parts.append(_build_exclude_folders_xml(ROOT_EXCLUDE_FOLDERS, 
indent=f"{indent}  "))
+    parts.append(_build_exclude_patterns_xml(indent=f"{indent}  "))
+    parts.append(f"{indent}</content>")
+    return "\n".join(parts)
+
+
+# --- Templates ---
+
+_iml_common_components = """\
   <component name="PyDocumentationSettings">
     <option name="format" value="PLAIN" />
     <option name="myDocStringFormat" value="Plain" />
@@ -61,82 +229,937 @@ iml_xml_template = """<?xml version="1.0" 
encoding="UTF-8"?>
   </component>
   <component name="TestRunnerService">
     <option name="PROJECT_TEST_RUNNER" value="py.test" />
+  </component>"""
+
+single_module_modules_xml_template = """\
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/airflow.iml" 
filepath="$PROJECT_DIR$/.idea/airflow.iml" />
+    </modules>
   </component>
-</module>"""
+</project>"""
 
-module_xml_template = """<?xml version="1.0" encoding="UTF-8"?>
+multi_module_modules_xml_template = """\
+<?xml version="1.0" encoding="UTF-8"?>
 <project version="4">
   <component name="ProjectModuleManager">
     <modules>
       <module fileurl="file://$PROJECT_DIR$/.idea/airflow.iml" 
filepath="$PROJECT_DIR$/.idea/airflow.iml" />
+      {MODULE_ENTRIES}
     </modules>
   </component>
 </project>"""
 
-source_root_module_patter: str = '<sourceFolder 
url="file://$MODULE_DIR$/{path}" isTestSource="{status}" />'
+multi_module_entry_template = (
+    '<module fileurl="file://$PROJECT_DIR$/{iml_path}" 
filepath="$PROJECT_DIR$/{iml_path}" />'
+)
 
-source_root_modules: list[str] = [
-    "airflow-core",
-    "airflow-ctl",
-    "task-sdk",
-    "devel-common",
-    "dev",
-    "dev/breeze",
-    "docker-tests",
-    "kubernetes-tests",
-    "helm-tests",
-    "task-sdk-integration-tests",
-]
 
-all_module_paths: list[str] = []
+def _build_root_iml(sdk_name: str, source_lines: str = "") -> str:
+    """Build a complete root .iml file (with project-level excludes and common 
components)."""
+    content = _build_content_xml(source_lines, include_root_excludes=True, 
indent="    ")
+    return (
+        '<?xml version="1.0" encoding="UTF-8"?>\n'
+        '<module type="PYTHON_MODULE" version="4">\n'
+        '  <component name="NewModuleRootManager">\n'
+        f"{content}\n"
+        f'    <orderEntry type="jdk" jdkName="{sdk_name}" jdkType="Python SDK" 
/>\n'
+        '    <orderEntry type="sourceFolder" forTests="false" />\n'
+        "  </component>\n"
+        f"{_iml_common_components}\n"
+        "</module>"
+    )
 
-ROOT_AIRFLOW_FOLDER_PATH = Path(__file__).parents[2]
-IDEA_FOLDER_PATH = ROOT_AIRFLOW_FOLDER_PATH / ".idea"
-AIRFLOW_IML_FILE = IDEA_FOLDER_PATH / "airflow.iml"
-MODULES_XML_FILE = IDEA_FOLDER_PATH / "modules.xml"
 
+def _build_sub_module_iml(source_lines: str, *, sdk_name: str = "") -> str:
+    """Build a sub-module .iml file.
+
+    When *sdk_name* is provided the module gets its own explicit Python SDK;
+    otherwise it inherits the project SDK.
+    """
+    content = _build_content_xml(source_lines, include_root_excludes=False, 
indent="    ")
+    if sdk_name:
+        jdk_entry = f'    <orderEntry type="jdk" jdkName="{sdk_name}" 
jdkType="Python SDK" />'
+    else:
+        jdk_entry = '    <orderEntry type="inheritedJdk" />'
+    return (
+        '<?xml version="1.0" encoding="UTF-8"?>\n'
+        '<module type="PYTHON_MODULE" version="4">\n'
+        '  <component name="NewModuleRootManager">\n'
+        f"{content}\n"
+        f"{jdk_entry}\n"
+        '    <orderEntry type="sourceFolder" forTests="false" />\n'
+        "  </component>\n"
+        '  <component name="TestRunnerService">\n'
+        '    <option name="PROJECT_TEST_RUNNER" value="py.test" />\n'
+        "  </component>\n"
+        "</module>"
+    )
+
+
+misc_xml_template = """\
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_25" 
project-jdk-name="{SDK_NAME}" project-jdk-type="Python SDK" />
+</project>"""
+
+# ---------------------------------------------------------------------------
+# uv sync / SDK detection
+# ---------------------------------------------------------------------------
+
+
+def run_uv_sync(project_dir: Path, label: str, *, python_version: str = ""):
+    """Run ``uv sync`` in *project_dir* to create / update its .venv.
+
+    When *python_version* is given (e.g. ``"3.12"``), ``--python <version>``
+    is passed to ``uv sync`` so that the venv is created with that interpreter.
+    """
+    cmd: list[str] = ["uv", "sync"]
+    if python_version:
+        cmd += ["--python", python_version]
+    version_info = f" (python {python_version})" if python_version else ""
+    print(f"[cyan]Running uv sync in {label}{version_info} ...[/]")
+    env = {k: v for k, v in os.environ.items() if k != "VIRTUAL_ENV"}
+    result = subprocess.run(cmd, cwd=project_dir, env=env, check=False)
+    if result.returncode != 0:
+        print(f"[red]Error:[/] uv sync failed in {label}. Check the output 
above.")
+        sys.exit(1)
+    print(f"[green]uv sync completed in {label}.[/]\n")
+
+
+def get_sdk_name(venv_dir: Path, *, label: str = "") -> str:
+    """Return an IntelliJ SDK name for the venv in *venv_dir*.
+
+    Uses the ``uv (<label>)`` naming convention, matching PyCharm's
+    auto-detected uv interpreter names.  When *label* is not given the
+    directory name is used (e.g. ``uv (airflow-clone)``).
+    """
+    venv_python = venv_dir / ".venv" / "bin" / "python"
+    if not venv_python.exists():
+        print(f"[red]Error:[/] {venv_python} not found even after uv sync.")
+        sys.exit(1)
+    if not label:
+        label = venv_dir.name
+    return f"uv ({label})"
+
+
+# ---------------------------------------------------------------------------
+# Global JetBrains SDK registration
+# ---------------------------------------------------------------------------
+
+# Product directory prefixes recognised when scanning for JetBrains config 
dirs.
+_JETBRAINS_PRODUCT_PREFIXES = ("IntelliJIdea", "PyCharm", "IU", "PC")
+
+# Prefixes that identify IntelliJ IDEA (Ultimate / Community via Toolbox "IU" 
code).
+_INTELLIJ_PREFIXES = ("IntelliJIdea", "IU")
+# Prefixes that identify PyCharm (Professional / Community via Toolbox "PC" 
code).
+_PYCHARM_PREFIXES = ("PyCharm", "PC")
+
+
+def _detect_installed_ides(idea_path: Path | None = None) -> tuple[bool, bool]:
+    """Detect which JetBrains IDEs are installed.
+
+    Returns a ``(has_intellij, has_pycharm)`` tuple.  When *idea_path* is
+    given, only that directory is inspected.
+    """
+    if idea_path is not None:
+        # If pointing at a specific product dir, check its name.
+        name = idea_path.name
+        has_intellij = any(name.startswith(p) for p in _INTELLIJ_PREFIXES)
+        has_pycharm = any(name.startswith(p) for p in _PYCHARM_PREFIXES)
+        if has_intellij or has_pycharm:
+            return has_intellij, has_pycharm
+        # Treat as base directory — scan children.
+        if idea_path.exists():
+            for child in idea_path.iterdir():
+                if any(child.name.startswith(p) for p in _INTELLIJ_PREFIXES):
+                    has_intellij = True
+                if any(child.name.startswith(p) for p in _PYCHARM_PREFIXES):
+                    has_pycharm = True
+            return has_intellij, has_pycharm
+        return False, False
+
+    base = _find_jetbrains_config_base()
+    if base is None or not base.exists():
+        return False, False
+    has_intellij = False
+    has_pycharm = False
+    for child in base.iterdir():
+        if any(child.name.startswith(p) for p in _INTELLIJ_PREFIXES):
+            has_intellij = True
+        if any(child.name.startswith(p) for p in _PYCHARM_PREFIXES):
+            has_pycharm = True
+    return has_intellij, has_pycharm
+
+
+# Process names used by JetBrains IDEs (matched case-insensitively against 
running processes).
+_JETBRAINS_PROCESS_KEYWORDS = ("idea", "pycharm", "intellij")
+
+
+def _find_jetbrains_pids() -> list[tuple[int, str]]:
+    """Return a list of ``(pid, command)`` tuples for running JetBrains IDE 
processes.
+
+    Excludes the current process and its parent to avoid the script killing 
itself
+    (since ``setup_idea.py`` contains "idea" which matches the process keyword 
filter).
+    """
+    own_pids = {os.getpid(), os.getppid()}
+    system = platform.system()
+    if system == "Darwin":
+        # On macOS, look for .app bundles in the process list.
+        try:
+            result = subprocess.run(
+                ["ps", "-eo", "pid,comm"],
+                capture_output=True,
+                text=True,
+                check=True,
+            )
+        except (FileNotFoundError, subprocess.CalledProcessError):
+            return []
+        pids: list[tuple[int, str]] = []
+        for line in result.stdout.strip().splitlines()[1:]:
+            parts = line.strip().split(None, 1)
+            if len(parts) != 2:
+                continue
+            pid_str, comm = parts
+            comm_lower = comm.lower()
+            if any(kw in comm_lower for kw in _JETBRAINS_PROCESS_KEYWORDS):
+                try:
+                    pid = int(pid_str)
+                    if pid not in own_pids:
+                        pids.append((pid, comm))
+                except ValueError:
+                    pass
+        return pids
+    if system == "Linux":
+        try:
+            result = subprocess.run(
+                ["ps", "-eo", "pid,args"],
+                capture_output=True,
+                text=True,
+                check=True,
+            )
+        except (FileNotFoundError, subprocess.CalledProcessError):
+            return []
+        pids = []
+        for line in result.stdout.strip().splitlines()[1:]:
+            parts = line.strip().split(None, 1)
+            if len(parts) != 2:
+                continue
+            pid_str, args = parts
+            args_lower = args.lower()
+            if any(kw in args_lower for kw in _JETBRAINS_PROCESS_KEYWORDS):
+                try:
+                    pid = int(pid_str)
+                    if pid not in own_pids:
+                        pids.append((pid, args))
+                except ValueError:
+                    pass
+        return pids
+    return []
+
+
+def _kill_jetbrains_ides() -> bool:
+    """Attempt to gracefully terminate running JetBrains IDE processes.
+
+    Sends SIGTERM first and waits briefly, then SIGKILL if processes remain.
+    Returns *True* if processes were found (whether or not they were killed).
+    """
+    pids = _find_jetbrains_pids()
+    if not pids:
+        return False
+    print("[yellow]Detected running JetBrains IDE process(es):[/]")
+    for pid, comm in pids:
+        print(f"  PID {pid}: {comm}")
+    should_kill = Confirm.ask("\nKill these processes to proceed?")
+    if not should_kill:
+        return True
+    for pid, _comm in pids:
+        try:
+            os.kill(pid, signal.SIGTERM)
+        except OSError:
+            pass
+    # Wait up to 5 seconds for graceful shutdown.
+    for _ in range(10):
+        remaining = _find_jetbrains_pids()
+        if not remaining:
+            print("[green]All JetBrains IDE processes terminated.[/]\n")
+            return True
+        time.sleep(0.5)
+    # Force-kill any remaining processes.
+    remaining = _find_jetbrains_pids()
+    for pid, _comm in remaining:
+        try:
+            os.kill(pid, signal.SIGKILL)
+        except OSError:
+            pass
+    print("[green]JetBrains IDE processes force-killed.[/]\n")
+    return True
+
+
+def _find_jetbrains_config_base() -> Path | None:
+    """Return the base JetBrains configuration directory for the current 
platform."""
+    system = platform.system()
+    if system == "Darwin":
+        return Path.home() / "Library" / "Application Support" / "JetBrains"
+    if system == "Linux":
+        xdg = os.environ.get("XDG_CONFIG_HOME", "")
+        return Path(xdg) / "JetBrains" if xdg else Path.home() / ".config" / 
"JetBrains"
+    if system == "Windows":
+        appdata = os.environ.get("APPDATA", "")
+        return Path(appdata) / "JetBrains" if appdata else None
+    return None
+
+
+def _find_all_jdk_table_xmls(idea_path: Path | None = None) -> list[Path]:
+    """Find all ``jdk.table.xml`` files across installed JetBrains IDEs.
+
+    When *idea_path* is given, it is used as the JetBrains configuration base
+    directory (or a specific product directory) instead of auto-detecting.
+
+    Returns paths sorted descending so the most recent version comes first.
+    """
+    if idea_path is not None:
+        # Allow pointing directly at a product directory (e.g. 
IntelliJIdea2025.1)
+        # or the parent JetBrains directory.
+        direct = idea_path / "options" / "jdk.table.xml"
+        if direct.exists():
+            return [direct]
+        # Treat as base directory — scan for product subdirectories.
+        if idea_path.exists():
+            candidates: list[Path] = []
+            for prefix in _JETBRAINS_PRODUCT_PREFIXES:
+                candidates.extend(idea_path.glob(f"{prefix}*"))
+            result: list[Path] = []
+            for config_dir in sorted(candidates, reverse=True):
+                jdk_table = config_dir / "options" / "jdk.table.xml"
+                if jdk_table.exists():
+                    result.append(jdk_table)
+            return result
+        return []
+
+    base = _find_jetbrains_config_base()
+    if base is None or not base.exists():
+        return []
+    candidates = []
+    for prefix in _JETBRAINS_PRODUCT_PREFIXES:
+        candidates.extend(base.glob(f"{prefix}*"))
+    result = []
+    for config_dir in sorted(candidates, reverse=True):
+        jdk_table = config_dir / "options" / "jdk.table.xml"
+        if jdk_table.exists():
+            result.append(jdk_table)
+    return result
+
+
+def _home_var_path(path: str) -> str:
+    """Replace the user's home directory prefix with ``$USER_HOME$``."""
+    home = str(Path.home())
+    if path.startswith(home):
+        return "$USER_HOME$" + path[len(home) :]
+    return path
+
+
+def _get_venv_python_paths(venv_python: Path) -> tuple[str, str, str]:
+    """Return *(stdlib, lib_dynload, site_packages)* paths for 
*venv_python*."""
+    result = subprocess.run(
+        [
+            str(venv_python),
+            "-c",
+            "import sysconfig, site; "
+            "print(sysconfig.get_path('stdlib')); "
+            "print(sysconfig.get_path('platstdlib')); "
+            "print(site.getsitepackages()[0])",
+        ],
+        capture_output=True,
+        text=True,
+        check=True,
+    )
+    lines = result.stdout.strip().splitlines()
+    stdlib = lines[0]
+    lib_dynload = str(Path(lines[1]) / "lib-dynload")
+    site_packages = lines[2]
+    return stdlib, lib_dynload, site_packages
+
+
+def _build_sdk_jdk_element(
+    name: str,
+    version_string: str,
+    home_path: str,
+    stdlib_path: str,
+    lib_dynload_path: str,
+    site_packages_path: str,
+    working_dir: str,
+    sdk_uuid: str | None = None,
+) -> ET.Element:
+    """Build an ``<jdk>`` XML element suitable for insertion into 
``jdk.table.xml``."""
+    if sdk_uuid is None:
+        sdk_uuid = str(uuid.uuid4())
+
+    jdk = ET.Element("jdk", version="2")
+    ET.SubElement(jdk, "name", value=name)
+    ET.SubElement(jdk, "type", value="Python SDK")
+    ET.SubElement(jdk, "version", value=version_string)
+    ET.SubElement(jdk, "homePath", value=_home_var_path(home_path))
+
+    roots = ET.SubElement(jdk, "roots")
 
-def setup_idea():
-    # Providers discovery
+    # annotationsPath
+    ann = ET.SubElement(roots, "annotationsPath")
+    ET.SubElement(ann, "root", type="composite")
+
+    # classPath — stdlib, lib-dynload, site-packages
+    cp = ET.SubElement(roots, "classPath")
+    cp_composite = ET.SubElement(cp, "root", type="composite")
+    ET.SubElement(
+        cp_composite,
+        "root",
+        url=f"file://{_home_var_path(stdlib_path)}",
+        type="simple",
+    )
+    ET.SubElement(
+        cp_composite,
+        "root",
+        url=f"file://{_home_var_path(lib_dynload_path)}",
+        type="simple",
+    )
+    ET.SubElement(
+        cp_composite,
+        "root",
+        url=f"file://{_home_var_path(site_packages_path)}",
+        type="simple",
+    )
+
+    # javadocPath
+    jd = ET.SubElement(roots, "javadocPath")
+    ET.SubElement(jd, "root", type="composite")
+
+    # sourcePath
+    sp = ET.SubElement(roots, "sourcePath")
+    ET.SubElement(sp, "root", type="composite")
+
+    # additional — UV flavour metadata
+    additional = ET.SubElement(
+        jdk,
+        "additional",
+        SDK_UUID=sdk_uuid,
+        IS_UV="true",
+        UV_WORKING_DIR=_home_var_path(working_dir),
+    )
+    ET.SubElement(additional, "setting", name="FLAVOR_ID", value="UvSdkFlavor")
+    ET.SubElement(additional, "setting", name="FLAVOR_DATA", value="{}")
+
+    return jdk
+
+
+def _register_sdk_in_file(
+    jdk_table_path: Path,
+    name: str,
+    venv_python: Path,
+    working_dir: Path,
+) -> str:
+    """Register or reuse an SDK in a single ``jdk.table.xml`` file.
+
+    Returns a status string: ``"reused"``, ``"renamed"``, or ``"created"``.
+    """
+    home_path_var = _home_var_path(str(venv_python))
+
+    tree = ET.parse(jdk_table_path)
+    root = tree.getroot()
+    component = root.find(".//component[@name='ProjectJdkTable']")
+    if component is None:
+        component = ET.SubElement(root, "component", name="ProjectJdkTable")
+
+    # 1. Look for an existing SDK whose homePath matches the target venv 
python.
+    #    If found, reuse it (preserving IntelliJ's stubs/typeshed entries) and
+    #    rename it to the expected name if necessary.
+    for jdk_elem in component.findall("jdk"):
+        home_elem = jdk_elem.find("homePath")
+        if home_elem is not None and home_elem.get("value") == home_path_var:
+            name_elem = jdk_elem.find("name")
+            current_name = name_elem.get("value") if name_elem is not None 
else ""
+            if current_name == name:
+                return "reused"
+            # Rename the existing SDK to match the expected convention.
+            if name_elem is not None:
+                name_elem.set("value", name)
+            ET.indent(tree, space="  ")
+            tree.write(str(jdk_table_path), encoding="unicode", 
xml_declaration=False)
+            return "renamed"
+
+    # 2. No existing SDK matches — create a new bare-bones entry.
+    #    IntelliJ will populate stubs/typeshed paths on first load.
+    result = subprocess.run(
+        [str(venv_python), "--version"],
+        capture_output=True,
+        text=True,
+        check=True,
+    )
+    version_string = result.stdout.strip()
+    stdlib, lib_dynload, site_packages = _get_venv_python_paths(venv_python)
+
+    new_jdk = _build_sdk_jdk_element(
+        name=name,
+        version_string=version_string,
+        home_path=str(venv_python),
+        stdlib_path=stdlib,
+        lib_dynload_path=lib_dynload,
+        site_packages_path=site_packages,
+        working_dir=str(working_dir),
+    )
+    component.append(new_jdk)
+
+    ET.indent(tree, space="  ")
+    tree.write(str(jdk_table_path), encoding="unicode", xml_declaration=False)
+    return "created"
+
+
+def register_sdk(name: str, venv_dir: Path, working_dir: Path, *, idea_path: 
Path | None = None) -> bool:
+    """Register (or reuse) a Python SDK in all global ``jdk.table.xml`` files.
+
+    If an SDK already exists whose ``homePath`` matches the target venv, it is
+    reused as-is (preserving IntelliJ's classPath entries such as python_stubs
+    and typeshed) and renamed to *name* if necessary.  A new bare-bones entry
+    is only created when no matching SDK exists.
+
+    When *idea_path* is given, only ``jdk.table.xml`` files under that path
+    are considered instead of auto-detecting all installed JetBrains IDEs.
+
+    Returns *True* if at least one ``jdk.table.xml`` was processed, *False* if
+    none could be found (no JetBrains IDE installed).
+    """
+    jdk_table_paths = _find_all_jdk_table_xmls(idea_path=idea_path)
+    if not jdk_table_paths:
+        print(
+            f"[yellow]Warning:[/] Could not find jdk.table.xml — "
+            f"SDK [bold]{name}[/] not registered globally. "
+            f"You can register it manually in PyCharm settings."
+        )
+        return False
+
+    venv_python = venv_dir / ".venv" / "bin" / "python"
+
+    for jdk_table_path in jdk_table_paths:
+        status = _register_sdk_in_file(jdk_table_path, name, venv_python, 
working_dir)
+        ide_version = jdk_table_path.parent.parent.name
+        if status == "reused":
+            print(f"[green]SDK already registered:[/] [bold]{name}[/] in 
{ide_version}")
+        elif status == "renamed":
+            print(f"[green]Renamed existing SDK to:[/] [bold]{name}[/] in 
{ide_version}")
+        else:
+            print(f"[green]Created SDK:[/] [bold]{name}[/] in {ide_version}")
+    return True
+
+
+# ---------------------------------------------------------------------------
+# Module discovery
+# ---------------------------------------------------------------------------
+
+EXCLUDED_PREFIXES = ("out/", ".build/", "dist/", ".venv/", "generated/")
+
+
+def discover_modules(*, exclude_modules: set[str] | None = None) -> list[str]:
+    """Discover all modules (static + providers + shared) and return sorted 
list.
+
+    *exclude_modules* is an optional set of module paths (e.g. 
``{"dev/breeze"}``)
+    or prefixes (e.g. ``{"providers/"}``) to skip.  Prefix entries end with 
``/``
+    and match any module whose path starts with that prefix.
+    """
+    exclude_modules = exclude_modules or set()
+
+    def _is_excluded(module: str) -> bool:
+        if module in exclude_modules:
+            return True
+        return any(module.startswith(prefix) for prefix in exclude_modules if 
prefix.endswith("/"))
+
+    modules = list(STATIC_MODULES)
     for pyproject_toml_file in 
ROOT_AIRFLOW_FOLDER_PATH.rglob("providers/**/pyproject.toml"):
         relative_path = 
pyproject_toml_file.relative_to(ROOT_AIRFLOW_FOLDER_PATH).parent.as_posix()
-        source_root_modules.append(f"{relative_path}")
-    # Shared discovery
+        if any(relative_path.startswith(prefix) for prefix in 
EXCLUDED_PREFIXES):
+            continue
+        modules.append(relative_path)
     for pyproject_toml_file in 
ROOT_AIRFLOW_FOLDER_PATH.rglob("shared/*/pyproject.toml"):
         relative_path = 
pyproject_toml_file.relative_to(ROOT_AIRFLOW_FOLDER_PATH).parent.as_posix()
-        source_root_modules.append(f"{relative_path}")
+        if any(relative_path.startswith(prefix) for prefix in 
EXCLUDED_PREFIXES):
+            continue
+        modules.append(relative_path)
+    modules.sort()
+    if exclude_modules:
+        before = len(modules)
+        modules = [m for m in modules if not _is_excluded(m)]
+        skipped = before - len(modules)
+        if skipped:
+            print(f"[yellow]Excluded {skipped} module(s)[/]")
+    return modules
+
+
+def get_module_name(module_path: str) -> str:
+    """Convert a module path to an IntelliJ module name (e.g. providers/amazon 
-> providers-amazon)."""
+    return module_path.replace("/", "-")
+
+
+# ---------------------------------------------------------------------------
+# Cleanup of previously generated files
+# ---------------------------------------------------------------------------
+
+# Directories that are never scanned for leftover .iml files.
+_CLEANUP_SKIP_DIRS = {".idea", "node_modules", ".venv", ".git", ".build", 
"out", "dist"}
+
+
+def _find_previous_iml_files() -> list[Path]:
+    """Find ``.iml`` files created by a previous run of this script.
+
+    Scans the project tree for ``*.iml`` files, skipping directories that
+    are known to contain unrelated ``.iml`` files (e.g. ``node_modules``,
+    ``.idea``).
+    """
+    results: list[Path] = []
+    for iml_file in ROOT_AIRFLOW_FOLDER_PATH.rglob("*.iml"):
+        rel = iml_file.relative_to(ROOT_AIRFLOW_FOLDER_PATH)
+        if any(part in _CLEANUP_SKIP_DIRS for part in rel.parts):
+            continue
+        results.append(iml_file)
+    return sorted(results)
+
+
+def cleanup_previous_setup() -> None:
+    """Remove files created by a previous run of this script.
+
+    Deletes:
+    * Sub-module ``.iml`` files scattered across the project tree.
+    * The four ``.idea/`` files managed by the script: ``airflow.iml``,
+      ``modules.xml``, ``misc.xml``, and ``.name``.
 
-    source_root_modules.sort()
-    for module in source_root_modules:
-        print(f"[green]Adding[/] module: [blue]{module}[/]")
+    Prints the number of files found and deleted.
+    """
+    managed_idea_files = [AIRFLOW_IML_FILE, MODULES_XML_FILE, MISC_XML_FILE, 
IDEA_NAME_FILE]
+    previous_iml_files = _find_previous_iml_files()
+
+    files_to_delete: list[Path] = []
+    for f in managed_idea_files:
+        if f.exists():
+            files_to_delete.append(f)
+    files_to_delete.extend(previous_iml_files)
+
+    if not files_to_delete:
+        print("[green]No files from a previous setup found.[/]\n")
+        return
+
+    print(f"[yellow]Found {len(files_to_delete)} file(s) from a previous setup 
— deleting:[/]")
+    idea_count = sum(1 for f in files_to_delete if f.parent == 
IDEA_FOLDER_PATH)
+    iml_count = len(files_to_delete) - idea_count
+    if idea_count:
+        print(f"  [dim]·[/] {idea_count} managed file(s) in .idea/")
+    if iml_count:
+        print(f"  [dim]·[/] {iml_count} sub-module .iml file(s)")
+    for f in files_to_delete:
+        f.unlink()
+    print(f"[green]Deleted {len(files_to_delete)} file(s).[/]\n")
+
+
+# ---------------------------------------------------------------------------
+# Setup modes
+# ---------------------------------------------------------------------------
+
+
+def setup_idea_single_module(sdk_name: str, project_name: str, modules: 
list[str]):
+    """Set up a single IntelliJ module with all source roots (original 
behaviour)."""
+    all_module_paths: list[str] = []
+
+    for module in modules:
+        print(f"[green]Adding[/] source root: [blue]{module}[/]")
         if (ROOT_AIRFLOW_FOLDER_PATH / module / "src").exists():
-            
all_module_paths.append(source_root_module_patter.format(path=f"{module}/src", 
status="false"))
+            
all_module_paths.append(source_root_module_pattern.format(path=f"{module}/src", 
status="false"))
         if (ROOT_AIRFLOW_FOLDER_PATH / module / "tests").exists():
-            
all_module_paths.append(source_root_module_patter.format(path=f"{module}/tests",
 status="true"))
+            
all_module_paths.append(source_root_module_pattern.format(path=f"{module}/tests",
 status="true"))
         if module == "dev":
-            
all_module_paths.append(source_root_module_patter.format(path=f"{module}", 
status="false"))
-    source_root_module_path = "\n\t\t".join(all_module_paths)
+            
all_module_paths.append(source_root_module_pattern.format(path=module, 
status="false"))
 
-    base_source_root_xml = 
iml_xml_template.format(SOURCE_ROOT_MODULE_PATH=source_root_module_path)
+    source_lines = "\n".join(f"      {line}" for line in all_module_paths)
+    iml_content = _build_root_iml(sdk_name, source_lines=source_lines)
 
     IDEA_FOLDER_PATH.mkdir(exist_ok=True)
-    AIRFLOW_IML_FILE.write_text(base_source_root_xml)
-    MODULES_XML_FILE.write_text(module_xml_template)
+    AIRFLOW_IML_FILE.write_text(iml_content)
+    MODULES_XML_FILE.write_text(single_module_modules_xml_template)
+    MISC_XML_FILE.write_text(misc_xml_template.format(SDK_NAME=sdk_name))
+    IDEA_NAME_FILE.write_text(f"{project_name}\n")
 
+    print(f"\n[green]Updated:[/] {AIRFLOW_IML_FILE}")
+    print(f"[green]Updated:[/] {MODULES_XML_FILE}")
+    print(f"[green]Updated:[/] {MISC_XML_FILE}")
+    print(f"[green]Updated:[/] {IDEA_NAME_FILE}")
 
-if __name__ == "__main__":
-    print("\n[yellow]Warning!!![/] This script will update the 
PyCharm/IntelliJ IDEA configuration files:\n")
-    print(f"* {AIRFLOW_IML_FILE}")
-    print(f"* {MODULES_XML_FILE}\n")
-    should_continue = Confirm.ask("Overwrite the files?")
-    if should_continue:
-        print()
-        setup_idea()
-        print("\n[green]Success\n")
+
+def setup_idea_multi_module(sdk_name: str, project_name: str, breeze_sdk_name: 
str, modules: list[str]):
+    """Set up multiple IntelliJ modules -- one per distribution/package."""
+    module_entries: list[str] = []
+    created_iml_files: list[Path] = []
+
+    for module in modules:
+        module_name = get_module_name(module)
+        source_folders: list[str] = []
+
+        if (ROOT_AIRFLOW_FOLDER_PATH / module / "src").exists():
+            source_folders.append('<sourceFolder url="file://$MODULE_DIR$/src" 
isTestSource="false" />')
+        if (ROOT_AIRFLOW_FOLDER_PATH / module / "tests").exists():
+            source_folders.append('<sourceFolder 
url="file://$MODULE_DIR$/tests" isTestSource="true" />')
+        if module == "dev":
+            source_folders.append('<sourceFolder url="file://$MODULE_DIR$" 
isTestSource="false" />')
+
+        if not source_folders:
+            continue
+
+        print(f"[green]Adding[/] module: [blue]{module_name}[/]")
+
+        source_lines = "\n".join(f"      {line}" for line in source_folders)
+        module_sdk = breeze_sdk_name if module == "dev/breeze" else ""
+        iml_content = _build_sub_module_iml(source_lines, sdk_name=module_sdk)
+
+        iml_path = ROOT_AIRFLOW_FOLDER_PATH / module / f"{module_name}.iml"
+        iml_path.write_text(iml_content)
+        created_iml_files.append(iml_path)
+
+        relative_iml_path = f"{module}/{module_name}.iml"
+        
module_entries.append(multi_module_entry_template.format(iml_path=relative_iml_path))
+
+    # Root module with excludes only
+    root_iml_content = _build_root_iml(sdk_name)
+
+    IDEA_FOLDER_PATH.mkdir(exist_ok=True)
+    AIRFLOW_IML_FILE.write_text(root_iml_content)
+
+    modules_xml_content = multi_module_modules_xml_template.format(
+        MODULE_ENTRIES="\n      ".join(module_entries),
+    )
+    MODULES_XML_FILE.write_text(modules_xml_content)
+    MISC_XML_FILE.write_text(misc_xml_template.format(SDK_NAME=sdk_name))
+    IDEA_NAME_FILE.write_text(f"{project_name}\n")
+
+    print(f"\n[green]Updated:[/] {AIRFLOW_IML_FILE} (root module)")
+    print(f"[green]Updated:[/] {MODULES_XML_FILE}")
+    print(f"[green]Updated:[/] {MISC_XML_FILE}")
+    print(f"[green]Updated:[/] {IDEA_NAME_FILE}")
+    print(f"[green]Created:[/] {len(created_iml_files)} sub-module .iml files")
+
+
+# ---------------------------------------------------------------------------
+# CLI
+# ---------------------------------------------------------------------------
+
+
+def _build_parser() -> argparse.ArgumentParser:
+    parser = argparse.ArgumentParser(
+        description="Set up PyCharm/IntelliJ IDEA project configuration for 
Airflow."
+    )
+    module_group = parser.add_mutually_exclusive_group()
+    module_group.add_argument(
+        "--multi-module",
+        action="store_true",
+        default=None,
+        help="Create separate IntelliJ modules for each distribution/package "
+        "instead of a single module with multiple source roots. "
+        "This is the default when IntelliJ IDEA is detected (multi-module "
+        "is not supported by PyCharm).",
+    )
+    module_group.add_argument(
+        "--single-module",
+        action="store_true",
+        default=None,
+        help="Create a single IntelliJ module with all source roots. "
+        "This is the default when only PyCharm is detected or when no "
+        "IDE can be detected. Use this to override auto-detection.",
+    )
+    parser.add_argument(
+        "--confirm",
+        action="store_true",
+        help="Skip the confirmation prompt asking whether PyCharm/IntelliJ 
IDEA "
+        "has been closed. Useful for non-interactive or scripted runs.",
+    )
+    parser.add_argument(
+        "--no-kill",
+        action="store_true",
+        help="Do not attempt to detect and kill running PyCharm/IntelliJ IDEA "
+        "processes. By default the script looks for running IDE processes, "
+        "asks for confirmation, sends SIGTERM and falls back to SIGKILL if "
+        "they don't exit within 5 seconds.",
+    )
+    parser.add_argument(
+        "--idea-path",
+        metavar="PATH",
+        help="Path to the JetBrains configuration directory to update instead 
of "
+        "auto-detecting all installed IDEs. Can point to the base JetBrains "
+        "directory (e.g. '~/Library/Application Support/JetBrains') or a "
+        "specific product directory (e.g. 
'.../JetBrains/IntelliJIdea2025.1').",
+    )
+
+    # --- Python version ---
+    parser.add_argument(
+        "--python",
+        metavar="VERSION",
+        help="Python minor version to use for the venv, e.g. '3.12'. "
+        "Passed as --python to 'uv sync'. Must be compatible with the "
+        "project's requires-python. By default uv picks the version.",
+    )
+
+    # --- Module exclusion ---
+    parser.add_argument(
+        "--exclude",
+        action="append",
+        default=[],
+        metavar="MODULE_OR_GROUP",
+        help="Exclude a module or module group from the project.  Can be "
+        "specified multiple times.  A module is a path relative to the "
+        "project root (e.g. 'providers/amazon', 'dev/breeze').  "
+        f"Recognised groups: {', '.join(sorted(MODULE_GROUPS))} "
+        "(e.g. '--exclude providers' excludes all providers).",
+    )
+    return parser
+
+
+def _resolve_excludes(raw: list[str]) -> set[str]:
+    """Expand group names and return a set of module paths / prefixes to 
exclude."""
+    result: set[str] = set()
+    for item in raw:
+        if item in MODULE_GROUPS:
+            prefix = MODULE_GROUPS[item]
+            # Groups that map to a directory prefix get a trailing "/" so
+            # the discover_modules prefix matching works; exact module
+            # names (like "dev") are kept as-is.
+            result.add(prefix if prefix.endswith("/") else prefix)
+        else:
+            result.add(item)
+    return result
+
+
+def _validate_python_version(version: str) -> None:
+    """Validate that *version* is supported by the project's 
``requires-python``."""
+    supported = get_supported_python_versions(ROOT_AIRFLOW_FOLDER_PATH / 
"pyproject.toml")
+    if version not in supported:
         print(
-            f"Updated {AIRFLOW_IML_FILE} and {MODULES_XML_FILE} files. "
-            f"Now restart the PyCharm/IntelliJ IDEA\n"
+            f"[red]Error:[/] Python {version} is not compatible with the 
project's "
+            f"requires-python constraint.\n"
+            f"Supported versions: [bold]{', '.join(supported)}[/]"
         )
+        sys.exit(1)
+
+
+def main():
+    parser = _build_parser()
+    args = parser.parse_args()
+
+    # --- Validate --python early ---
+    python_version: str = args.python or ""
+    if python_version:
+        _validate_python_version(python_version)
+
+    # --- Resolve --idea-path ---
+    idea_path: Path | None = Path(args.idea_path).expanduser() if 
args.idea_path else None
+
+    # --- Resolve multi-module mode ---
+    if args.multi_module:
+        multi_module = True
+    elif args.single_module:
+        multi_module = False
     else:
+        # Auto-detect based on installed IDE(s).
+        has_intellij, has_pycharm = _detect_installed_ides(idea_path)
+        if has_intellij:
+            multi_module = True
+            print("[cyan]Detected IntelliJ IDEA installation — defaulting to 
multi-module mode.[/]")
+        elif has_pycharm:
+            multi_module = False
+            print(
+                "[cyan]Detected PyCharm installation — "
+                "defaulting to single-module mode "
+                "(multi-module is not supported by PyCharm).[/]"
+            )
+        else:
+            multi_module = False
+            print("[cyan]No JetBrains IDE detected — defaulting to 
single-module mode.[/]")
+        print("[dim]Use --multi-module or --single-module to override.[/]\n")
+
+    # --- Show available versions on request ---
+    supported = get_supported_python_versions(ROOT_AIRFLOW_FOLDER_PATH / 
"pyproject.toml")
+    print(f"[cyan]Supported Python versions:[/] {', '.join(supported)}")
+
+    # --- Kill or confirm IDE is closed ---
+    if not args.no_kill:
+        pids = _find_jetbrains_pids()
+        if pids:
+            _kill_jetbrains_ides()
+        else:
+            print("[green]No running IntelliJ IDEA / PyCharm processes 
detected — safe to proceed.[/]\n")
+    elif not args.confirm:
+        print(
+            "\n[yellow]Warning:[/] PyCharm/IntelliJ IDEA must be closed before 
running this script, "
+            "otherwise the IDE may overwrite the changes on exit.\n"
+        )
+        ide_closed = Confirm.ask("Have you closed PyCharm/IntelliJ IDEA?")
+        if not ide_closed:
+            print("[yellow]Please close PyCharm/IntelliJ IDEA and run this 
script again.[/]\n")
+            return
+
+    # --- uv sync ---
+    run_uv_sync(ROOT_AIRFLOW_FOLDER_PATH, "project root", 
python_version=python_version)
+    sdk_name = get_sdk_name(ROOT_AIRFLOW_FOLDER_PATH)
+    project_name = f"[airflow]:{ROOT_AIRFLOW_FOLDER_PATH.name}"
+    print(f"[cyan]Detected Python SDK:[/] [bold]{sdk_name}[/]")
+
+    run_uv_sync(BREEZE_PATH, "dev/breeze", python_version=python_version)
+    breeze_sdk_name = get_sdk_name(BREEZE_PATH, 
label=f"{ROOT_AIRFLOW_FOLDER_PATH.name}:breeze")
+    print(f"[cyan]Detected Breeze SDK:[/] [bold]{breeze_sdk_name}[/]")
+
+    # --- Module discovery ---
+    exclude_set = _resolve_excludes(args.exclude)
+    modules = discover_modules(exclude_modules=exclude_set)
+
+    print(f"[cyan]Mode:[/] [bold]{'multi-module' if multi_module else 
'single-module'}[/]")
+    print(f"[cyan]Modules:[/] [bold]{len(modules)}[/]\n")
+
+    files_to_update = [AIRFLOW_IML_FILE, MODULES_XML_FILE, MISC_XML_FILE, 
IDEA_NAME_FILE]
+    print("[yellow]Warning!!![/] This script will update the PyCharm/IntelliJ 
IDEA configuration files:\n")
+    for f in files_to_update:
+        print(f"* {f}")
+    if multi_module:
+        print("* <module>/<module>.iml for each discovered module\n")
+    else:
+        print()
+
+    previous_iml_files = _find_previous_iml_files()
+    managed_to_delete = [f for f in files_to_update if f.exists()]
+    if previous_iml_files or managed_to_delete:
+        total = len(previous_iml_files) + len(managed_to_delete)
+        print(
+            f"[yellow]Note:[/] {total} file(s) from a previous setup will also 
be [bold]deleted[/] "
+            "before writing the new configuration:"
+        )
+        if managed_to_delete:
+            print(f"  [dim]·[/] {len(managed_to_delete)} managed file(s) in 
.idea/")
+        if previous_iml_files:
+            print(f"  [dim]·[/] {len(previous_iml_files)} sub-module .iml 
file(s)")
+        print()
+
+    should_continue = Confirm.ask("Overwrite the files?")
+    if not should_continue:
         print("[yellow]Skipped\n")
-        print(f"Not updated {AIRFLOW_IML_FILE} and {MODULES_XML_FILE} files\n")
+        return
+
+    print()
+    cleanup_previous_setup()
+    if multi_module:
+        setup_idea_multi_module(sdk_name, project_name, breeze_sdk_name, 
modules)
+    else:
+        setup_idea_single_module(sdk_name, project_name, modules)
+
+    # --- Register SDKs in global JetBrains configuration ---
+    print()
+    register_sdk(sdk_name, ROOT_AIRFLOW_FOLDER_PATH, ROOT_AIRFLOW_FOLDER_PATH, 
idea_path=idea_path)
+    register_sdk(breeze_sdk_name, BREEZE_PATH, BREEZE_PATH, 
idea_path=idea_path)
+
+    print("\n[green]Success[/]\n")
+    print("[yellow]Important:[/] Restart PyCharm/IntelliJ IDEA to pick up the 
new configuration.\n")
+
+
+if __name__ == "__main__":
+    main()

Reply via email to