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

ephraimanierobi pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit 9eb17092ef3d5bae9b60b8c648f8e355f7745097
Author: Bugra Ozturk <[email protected]>
AuthorDate: Sat Jan 10 14:54:10 2026 +0100

    Sync airflowctl to v3-1-test (#60350)
---
 airflow-ctl-tests/pyproject.toml                   |   6 +-
 .../tests/airflowctl_tests/conftest.py             | 111 ++++-------
 .../tests/airflowctl_tests/constants.py            |   7 +-
 .../airflowctl_tests/fixtures/pools_export.json    |  38 ++++
 .../fixtures/test_connections.json                 |  11 +
 .../airflowctl_tests/fixtures/test_pools.json      |   8 +
 .../airflowctl_tests/fixtures/test_variables.json  |   7 +
 .../fixtures/variables_export.json                 |   8 +
 .../airflowctl_tests/test_airflowctl_commands.py   | 222 ++++++++++++++++-----
 airflow-ctl/.pre-commit-config.yaml                |   2 +-
 airflow-ctl/NOTICE                                 |   2 +-
 airflow-ctl/RELEASE_NOTES.rst                      |   4 +-
 airflow-ctl/docs/conf.py                           |  12 +-
 airflow-ctl/docs/images/command_hashes.txt         |   4 +-
 airflow-ctl/docs/images/output_connections.svg     | 141 ++++++-------
 airflow-ctl/docs/images/output_variables.svg       | 114 +++++------
 .../docs/installation/installing-from-pypi.rst     |   5 +-
 .../docs/installation/installing-from-sources.rst  |  22 +-
 airflow-ctl/newsfragments/59850.significant.rst    |  30 +++
 airflow-ctl/pyproject.toml                         |  20 +-
 airflow-ctl/src/airflowctl/__init__.py             |   2 +-
 airflow-ctl/src/airflowctl/ctl/cli_config.py       |  30 +--
 .../airflowctl/ctl/commands/variable_command.py    |  22 --
 airflow-ctl/tests/airflow_ctl/api/test_client.py   |   2 +-
 .../tests/airflow_ctl/api/test_operations.py       |   8 +-
 .../airflow_ctl/ctl/commands/test_dag_command.py   |   8 +-
 .../ctl/commands/test_variable_command.py          |  20 --
 .../ci/prek/check_airflowctl_command_coverage.py   | 150 ++++++++++++++
 .../in_container/run_capture_airflowctl_help.py    |   6 +-
 29 files changed, 640 insertions(+), 382 deletions(-)

diff --git a/airflow-ctl-tests/pyproject.toml b/airflow-ctl-tests/pyproject.toml
index 24c7f561ed1..2cb3a8109b2 100644
--- a/airflow-ctl-tests/pyproject.toml
+++ b/airflow-ctl-tests/pyproject.toml
@@ -26,7 +26,7 @@ description = "Airflow CTL tests for Apache Airflow"
 classifiers = [
     "Private :: Do Not Upload",
 ]
-requires-python = ">=3.10,!=3.13"
+requires-python = ">=3.10,!=3.14"
 authors = [
     { name = "Apache Software Foundation", email = "[email protected]" },
 ]
@@ -69,3 +69,7 @@ exclude = ["*"]
 
 [tool.hatch.build.targets.wheel]
 bypass-selection = true
+
+[tool.ruff.lint.per-file-ignores]
+
+"!test/*" = ["S101"]
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py 
b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
index 13dacf5d8c5..a34b4804d65 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/conftest.py
@@ -20,7 +20,6 @@ import os
 import subprocess
 import sys
 
-import pytest
 from python_on_whales import DockerClient, docker
 
 from airflowctl_tests import console
@@ -30,14 +29,18 @@ from airflowctl_tests.constants import (
     DOCKER_IMAGE,
 )
 
-docker_client = None
+
+class _CtlTestState:
+    docker_client: DockerClient | None = None
 
 
 # Pytest hook to run at the start of the session
 def pytest_sessionstart(session):
     """Install airflowctl at the very start of the pytest session."""
-    airflow_ctl_version = os.environ.get("AIRFLOW_CTL_VERSION", "1.0.0")
-    console.print(f"[yellow]Installing 
apache-airflow-ctl=={airflow_ctl_version} via pytest_sessionstart...")
+    airflow_ctl_version = os.environ.get("AIRFLOW_CTL_VERSION", "0.1.0")
+    console.print(
+        f"[yellow]Installing apache-airflow-ctl=={airflow_ctl_version} via 
pytest_sessionstart..."
+    )
 
     airflow_ctl_path = AIRFLOW_ROOT_PATH / "airflow-ctl"
     console.print(f"[blue]Installing from: {airflow_ctl_path}")
@@ -50,12 +53,16 @@ def pytest_sessionstart(session):
         cmd = ["uv", "pip", "install", str(airflow_ctl_path)]
         console.print(f"[cyan]Running command: {' '.join(cmd)}")
         subprocess.check_call(cmd)
-        console.print("[green]airflowctl installed successfully to UV 
environment via pytest_sessionstart!")
+        console.print(
+            "[green]airflowctl installed successfully to UV environment via 
pytest_sessionstart!"
+        )
     except (subprocess.CalledProcessError, FileNotFoundError) as e:
         console.print(f"[yellow]UV installation failed: {e}")
         raise
 
-    console.print("[yellow]Verifying airflowctl installation via 
pytest_sessionstart...")
+    console.print(
+        "[yellow]Verifying airflowctl installation via pytest_sessionstart..."
+    )
     try:
         result = subprocess.run(
             [
@@ -69,7 +76,9 @@ def pytest_sessionstart(session):
         )
         console.print(f"[green]{result.stdout.strip()}")
     except subprocess.CalledProcessError as e:
-        console.print("[red]❌ airflowctl import verification failed via 
pytest_sessionstart:")
+        console.print(
+            "[red]❌ airflowctl import verification failed via 
pytest_sessionstart:"
+        )
         console.print(f"[red]Return code: {e.returncode}")
         console.print(f"[red]Stdout: {e.stdout}")
         console.print(f"[red]Stderr: {e.stderr}")
@@ -115,7 +124,9 @@ def debug_environment():
 
     console.print(f"[blue]Python executable exists: 
{Path(sys.executable).exists()}")
     if Path(sys.executable).is_symlink():
-        console.print(f"[blue]Python executable is symlink to: 
{Path(sys.executable).readlink()}")
+        console.print(
+            f"[blue]Python executable is symlink to: 
{Path(sys.executable).readlink()}"
+        )
 
     try:
         uv_python = subprocess.check_output(["uv", "python", "find"], 
text=True).strip()
@@ -124,7 +135,9 @@ def debug_environment():
 
         console.print(f"[cyan]UV Python exists: {Path(uv_python).exists()}")
         if Path(uv_python).is_symlink():
-            console.print(f"[cyan]UV Python is symlink to: 
{Path(uv_python).readlink()}")
+            console.print(
+                f"[cyan]UV Python is symlink to: {Path(uv_python).readlink()}"
+            )
     except Exception as e:
         console.print(f"[red]UV Python error: {e}")
 
@@ -143,8 +156,6 @@ def docker_compose_up(tmp_path_factory):
     """Fixture to spin up Docker Compose environment for the test session."""
     from shutil import copyfile
 
-    global docker_client
-
     tmp_dir = tmp_path_factory.mktemp("airflow-ctl-test")
     console.print(f"[yellow]Tests are run in {tmp_dir}")
 
@@ -167,14 +178,20 @@ def docker_compose_up(tmp_path_factory):
     os.environ["ENV_FILE_PATH"] = str(tmp_dir / ".env")
 
     # Initialize Docker client
-    docker_client = DockerClient(compose_files=[str(tmp_docker_compose_file)])
+    _CtlTestState.docker_client = DockerClient(
+        compose_files=[str(tmp_docker_compose_file)]
+    )
 
     try:
         console.print(f"[blue]Spinning up airflow environment using 
{DOCKER_IMAGE}")
-        docker_client.compose.up(detach=True, wait=True)
+        _CtlTestState.docker_client.compose.up(detach=True, wait=True)
         console.print("[green]Docker compose started for airflowctl test\n")
     except Exception:
-        print_diagnostics(docker_client.compose, 
docker_client.compose.version(), docker.version())
+        print_diagnostics(
+            _CtlTestState.docker_client.compose,
+            _CtlTestState.docker_client.compose.version(),
+            docker.version(),
+        )
         debug_environment()
         docker_compose_down()
         raise
@@ -182,71 +199,13 @@ def docker_compose_up(tmp_path_factory):
 
 def docker_compose_down():
     """Tear down Docker Compose environment."""
-    if docker_client:
-        docker_client.compose.down(remove_orphans=True, volumes=True, 
quiet=True)
+    if _CtlTestState.docker_client:
+        _CtlTestState.docker_client.compose.down(
+            remove_orphans=True, volumes=True, quiet=True
+        )
 
 
 def pytest_sessionfinish(session, exitstatus):
     """Tear down test environment at the end of the pytest session."""
     if not os.environ.get("SKIP_DOCKER_COMPOSE_DELETION"):
         docker_compose_down()
-
-
-# Fixtures for tests
[email protected]
-def login_command():
-    # Passing password via command line is insecure but acceptable for testing 
purposes
-    # Please do not do this in production, it enables possibility of exposing 
your credentials
-    return "auth login --username airflow --password airflow"
-
-
[email protected]
-def login_output():
-    return "Login successful! Welcome to airflowctl!"
-
-
[email protected]
-def date_param():
-    import random
-    from datetime import datetime, timedelta
-
-    from dateutil.relativedelta import relativedelta
-
-    # original datetime string
-    dt_str = "2025-10-25T00:02:00+00:00"
-
-    # parse to datetime object
-    dt = datetime.fromisoformat(dt_str)
-
-    # boundaries
-    start = dt - relativedelta(months=1)
-    end = dt + relativedelta(months=1)
-
-    # pick random time between start and end
-    delta = end - start
-    random_seconds = random.randint(0, int(delta.total_seconds()))
-    random_dt = start + timedelta(seconds=random_seconds)
-    return random_dt.isoformat()
-
-
[email protected]
-def test_commands(login_command, date_param):
-    # Define test commands to run with actual running API server
-    return [
-        login_command,
-        "backfill list",
-        "config get --section core --option executor",
-        "connections create --connection-id=test_con --conn-type=mysql 
--password=TEST_PASS -o json",
-        "connections list",
-        "connections list -o yaml",
-        "connections list -o tabledags list",
-        f"dagrun trigger --dag-id=example_bash_operator 
--logical-date={date_param} --run-after={date_param}",
-        "dagrun list --dag-id example_bash_operator --state success --limit=1",
-        "jobs list",
-        "pools create --name=test_pool --slots=5",
-        "pools list",
-        "providers list",
-        "variables create --key=test_key --value=test_value",
-        "variables list",
-        "version --remote",
-    ]
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/constants.py 
b/airflow-ctl-tests/tests/airflowctl_tests/constants.py
index 549a9ad8c74..3f40c99d84b 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/constants.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/constants.py
@@ -28,5 +28,10 @@ DOCKER_IMAGE = os.environ.get("DOCKER_IMAGE") or 
DEFAULT_DOCKER_IMAGE
 DOCKER_COMPOSE_HOST_PORT = os.environ.get("HOST_PORT", "localhost:8080")
 
 DOCKER_COMPOSE_FILE_PATH = (
-    AIRFLOW_ROOT_PATH / "airflow-core" / "docs" / "howto" / "docker-compose" / 
"docker-compose.yaml"
+    AIRFLOW_ROOT_PATH
+    / "airflow-core"
+    / "docs"
+    / "howto"
+    / "docker-compose"
+    / "docker-compose.yaml"
 )
diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/fixtures/pools_export.json 
b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/pools_export.json
new file mode 100644
index 00000000000..4752ad7801f
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/pools_export.json
@@ -0,0 +1,38 @@
+[
+    {
+        "deferred_slots": 0,
+        "description": "Default pool",
+        "include_deferred": false,
+        "name": "default_pool",
+        "occupied_slots": 0,
+        "open_slots": 128,
+        "queued_slots": 0,
+        "running_slots": 0,
+        "scheduled_slots": 0,
+        "slots": 128
+    },
+    {
+        "deferred_slots": 0,
+        "description": null,
+        "include_deferred": false,
+        "name": "test_pool",
+        "occupied_slots": 0,
+        "open_slots": 10,
+        "queued_slots": 0,
+        "running_slots": 0,
+        "scheduled_slots": 0,
+        "slots": 10
+    },
+    {
+        "deferred_slots": 0,
+        "description": "Test pool for import",
+        "include_deferred": false,
+        "name": "test_import_pool",
+        "occupied_slots": 0,
+        "open_slots": 10,
+        "queued_slots": 0,
+        "running_slots": 0,
+        "scheduled_slots": 0,
+        "slots": 10
+    }
+]
diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_connections.json 
b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_connections.json
new file mode 100644
index 00000000000..0141f399ec2
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_connections.json
@@ -0,0 +1,11 @@
+{
+  "test_import_conn": {
+    "conn_type": "postgres",
+    "host": "localhost",
+    "login": "testuser",
+    "password": "testpass",
+    "port": 5432,
+    "extra": "{}",
+    "description": "Test connection for import"
+  }
+}
diff --git a/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_pools.json 
b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_pools.json
new file mode 100644
index 00000000000..691e1d62d56
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_pools.json
@@ -0,0 +1,8 @@
+[
+  {
+    "name": "test_import_pool",
+    "slots": 10,
+    "description": "Test pool for import",
+    "include_deferred": false
+  }
+]
diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_variables.json 
b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_variables.json
new file mode 100644
index 00000000000..05de7d2fc8b
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/test_variables.json
@@ -0,0 +1,7 @@
+{
+  "test_import_var": "import_value",
+  "test_import_var_with_desc": {
+    "value": "import_value_with_description",
+    "description": "Test variable with description"
+  }
+}
diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/fixtures/variables_export.json 
b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/variables_export.json
new file mode 100644
index 00000000000..f8aed8ce379
--- /dev/null
+++ b/airflow-ctl-tests/tests/airflowctl_tests/fixtures/variables_export.json
@@ -0,0 +1,8 @@
+{
+    "test_import_var": "import_value",
+    "test_import_var_with_desc": {
+        "description": "Test variable with description",
+        "value": "import_value_with_description"
+    },
+    "test_key": "updated_value"
+}
diff --git 
a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py 
b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
index d17e646cb06..9ba407b1aa8 100644
--- a/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
+++ b/airflow-ctl-tests/tests/airflowctl_tests/test_airflowctl_commands.py
@@ -19,59 +19,181 @@ from __future__ import annotations
 import os
 from subprocess import PIPE, STDOUT, Popen
 
+import pytest
+
 from airflowctl_tests import console
 
 
-def test_airflowctl_commands(login_command, login_output, test_commands):
+def date_param():
+    import random
+    from datetime import datetime, timedelta
+
+    from dateutil.relativedelta import relativedelta
+
+    # original datetime string
+    dt_str = "2025-10-25T00:02:00+00:00"
+
+    # parse to datetime object
+    dt = datetime.fromisoformat(dt_str)
+
+    # boundaries
+    start = dt - relativedelta(months=1)
+    end = dt + relativedelta(months=1)
+
+    # pick random time between start and end
+    delta = end - start
+    random_seconds = random.randint(0, int(delta.total_seconds()))
+    random_dt = start + timedelta(seconds=random_seconds)
+    return random_dt.isoformat()
+
+
+LOGIN_COMMAND = "auth login --username airflow --password airflow"
+LOGIN_OUTPUT = "Login successful! Welcome to airflowctl!"
+ONE_DATE_PARAM = date_param()
+TEST_COMMANDS = [
+    # Passing password via command line is insecure but acceptable for testing 
purposes
+    # Please do not do this in production, it enables possibility of exposing 
your credentials
+    LOGIN_COMMAND,
+    # Assets commands
+    "assets list",
+    "assets get --asset-id=1",
+    "assets create-event --asset-id=1",
+    # Backfill commands
+    "backfill list",
+    # Config commands
+    "config get --section core --option executor",
+    "config list",
+    "config lint",
+    # Connections commands
+    "connections create --connection-id=test_con --conn-type=mysql 
--password=TEST_PASS -o json",
+    "connections list",
+    "connections list -o yaml",
+    "connections list -o table",
+    "connections get --conn-id=test_con",
+    "connections get --conn-id=test_con -o json",
+    "connections update --connection-id=test_con --conn-type=postgres",
+    "connections import tests/airflowctl_tests/fixtures/test_connections.json",
+    "connections delete --conn-id=test_con",
+    "connections delete --conn-id=test_import_conn",
+    # DAGs commands
+    "dags list",
+    "dags get --dag-id=example_bash_operator",
+    "dags get-details --dag-id=example_bash_operator",
+    "dags get-stats --dag-ids=example_bash_operator",
+    "dags get-version --dag-id=example_bash_operator --version-number=1",
+    "dags list-import-errors",
+    "dags list-version --dag-id=example_bash_operator",
+    "dags list-warning",
+    # Order of trigger and pause/unpause is important for test stability 
because state checked
+    f"dags trigger --dag-id=example_bash_operator 
--logical-date={ONE_DATE_PARAM} --run-after={ONE_DATE_PARAM}",
+    "dags pause example_bash_operator",
+    "dags unpause example_bash_operator",
+    # DAG Run commands
+    f'dagrun get --dag-id=example_bash_operator 
--dag-run-id="manual__{ONE_DATE_PARAM}"',
+    "dags update --dag-id=example_bash_operator --no-is-paused",
+    # DAG Run commands
+    "dagrun list --dag-id example_bash_operator --state success --limit=1",
+    # Jobs commands
+    "jobs list",
+    # Pools commands
+    "pools create --name=test_pool --slots=5",
+    "pools list",
+    "pools get --pool-name=test_pool",
+    "pools get --pool-name=test_pool -o yaml",
+    "pools update --pool=test_pool --slots=10",
+    "pools import tests/airflowctl_tests/fixtures/test_pools.json",
+    "pools delete --pool=test_pool",
+    "pools delete --pool=test_import_pool",
+    # Providers commands
+    "providers list",
+    # Variables commands
+    "variables create --key=test_key --value=test_value",
+    "variables list",
+    "variables get --variable-key=test_key",
+    "variables get --variable-key=test_key -o table",
+    "variables update --key=test_key --value=updated_value",
+    "variables import tests/airflowctl_tests/fixtures/test_variables.json",
+    "variables delete --variable-key=test_key",
+    "variables delete --variable-key=test_import_var",
+    "variables delete --variable-key=test_import_var_with_desc",
+    # Version command
+    "version --remote",
+]
+
+
[email protected](reruns=3, reruns_delay=1)
[email protected](
+    "command",
+    TEST_COMMANDS,
+    ids=[" ".join(command.split(" ", 2)[:2]) for command in TEST_COMMANDS],
+)
+def test_airflowctl_commands(command: str):
     """Test airflowctl commands using docker-compose environment."""
     host_envs = os.environ.copy()
     host_envs["AIRFLOW_CLI_DEBUG_MODE"] = "true"
-    # Testing commands of airflowctl
-    for command in test_commands:
-        command_from_config = f"airflowctl {command}"
-        # We need to run auth login first for all commands except login itself
-        if command != login_command:
-            run_command = f"airflowctl {login_command} && 
{command_from_config}"
-        else:
-            run_command = command_from_config
-        console.print(f"[yellow]Running command: {command}")
-
-        # Give some time for the command to execute and output to be ready
-        proc = Popen(run_command.encode(), stdout=PIPE, stderr=STDOUT, 
stdin=PIPE, shell=True, env=host_envs)
-        stdout_result, stderr_result = proc.communicate(timeout=60)
-
-        # CLI command gave errors
-        if stderr_result:
-            console.print(
-                f"[red]Errors while executing command 
'{command_from_config}':\n{stderr_result.decode()}"
-            )
-
-        # Decode the output
-        stdout_result = stdout_result.decode()
-        # We need to trim auth login output if the command is not login itself 
and clean backspaces
-        if command != login_command:
-            if login_output not in stdout_result:
-                console.print(
-                    f"[red]❌ Login output not found before command output for 
'{command_from_config}'"
-                )
-                console.print(f"[red]Full output:\n{stdout_result}\n")
-                raise AssertionError("Login output not found before command 
output")
-            stdout_result = stdout_result.split(f"{login_output}\n")[1].strip()
-        else:
-            stdout_result = stdout_result.strip()
-
-        # This is a common error message that is thrown by client when 
something is wrong
-        # Please ensure it is aligning with 
airflowctl.api.client.get_json_error
-        airflowctl_client_server_response_error = "Server error"
-        airflowctl_command_error = "command error: argument GROUP_OR_COMMAND: 
invalid choice"
-        if (
-            airflowctl_client_server_response_error in stdout_result
-            or airflowctl_command_error in stdout_result
-        ):
-            console.print(f"[red]❌ Output contained unexpected text for 
command '{command_from_config}'")
-            console.print(f"[red]Did not expect to 
find:\n{airflowctl_client_server_response_error}\n")
-            console.print(f"[red]But got:\n{stdout_result}\n")
-            raise AssertionError(f"Output contained unexpected 
text\nOutput:\n{stdout_result}")
-        console.print(f"[green]✅ Output did not contain unexpected text for 
command '{command_from_config}'")
-        console.print(f"[cyan]Result:\n{stdout_result}\n")
-        proc.kill()
+
+    command_from_config = f"airflowctl {command}"
+    # We need to run auth login first for all commands except login itself
+    if command != LOGIN_COMMAND:
+        run_command = f"airflowctl {LOGIN_COMMAND} && {command_from_config}"
+    else:
+        run_command = command_from_config
+    console.print(f"[yellow]Running command: {command}")
+
+    # Give some time for the command to execute and output to be ready
+    proc = Popen(
+        run_command.encode(),
+        stdout=PIPE,
+        stderr=STDOUT,
+        stdin=PIPE,
+        shell=True,
+        env=host_envs,
+    )
+    stdout_bytes, stderr_result = proc.communicate(timeout=60)
+
+    # CLI command gave errors
+    assert not stderr_result, (
+        f"Errors while executing command 
'{command_from_config}':\n{stderr_result.decode()}"
+    )
+
+    # Decode the output
+    stdout_result = stdout_bytes.decode()
+    # We need to trim auth login output if the command is not login itself and 
clean backspaces
+    if command != LOGIN_COMMAND:
+        assert LOGIN_OUTPUT in stdout_result, (
+            f"❌ Login output not found before command output for 
'{command_from_config}'",
+            f"\nFull output:\n{stdout_result}",
+        )
+        stdout_result = stdout_result.split(f"{LOGIN_OUTPUT}\n")[1].strip()
+    else:
+        stdout_result = stdout_result.strip()
+
+    # Check for non-zero exit code
+    assert proc.returncode == 0, (
+        f"❌ Command '{command_from_config}' exited with code 
{proc.returncode}",
+        f"\nOutput:\n{stdout_result}",
+    )
+
+    # Error patterns to detect failures that might otherwise slip through
+    # Please ensure it is aligning with airflowctl.api.client.get_json_error
+    error_patterns = [
+        "Server error",
+        "command error",
+        "unrecognized arguments",
+        "invalid choice",
+        "Traceback (most recent call last):",
+    ]
+    matched_error = next(
+        (error for error in error_patterns if error in stdout_result), None
+    )
+    assert not matched_error, (
+        f"❌ Output contained unexpected text for command 
'{command_from_config}'",
+        f"\nMatched error pattern: {matched_error}",
+        f"\nOutput:\n{stdout_result}",
+    )
+
+    console.print(
+        f"[green]✅ Output did not contain unexpected text for command 
'{command_from_config}'"
+    )
+    console.print(f"[cyan]Result:\n{stdout_result}\n")
+    proc.kill()
diff --git a/airflow-ctl/.pre-commit-config.yaml 
b/airflow-ctl/.pre-commit-config.yaml
index 8926b9e7336..00eada7f645 100644
--- a/airflow-ctl/.pre-commit-config.yaml
+++ b/airflow-ctl/.pre-commit-config.yaml
@@ -16,7 +16,7 @@
 # under the License.
 ---
 default_stages: [pre-commit, pre-push]
-minimum_prek_version: '0.2.0'
+minimum_prek_version: '0.0.28'
 default_language_version:
   python: python3
   node: 22.19.0
diff --git a/airflow-ctl/NOTICE b/airflow-ctl/NOTICE
index e02aab0589f..a51bd9390d0 100644
--- a/airflow-ctl/NOTICE
+++ b/airflow-ctl/NOTICE
@@ -1,5 +1,5 @@
 Apache Airflow
-Copyright 2016-2025 The Apache Software Foundation
+Copyright 2016-2026 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/airflow-ctl/RELEASE_NOTES.rst b/airflow-ctl/RELEASE_NOTES.rst
index 70d19556a9f..58f1ecbeacb 100644
--- a/airflow-ctl/RELEASE_NOTES.rst
+++ b/airflow-ctl/RELEASE_NOTES.rst
@@ -15,10 +15,10 @@
     specific language governing permissions and limitations
     under the License.
 
-airflowctl 0.1.0 (2025-11-05)
+airflowctl 0.1.0 (2025-11-03)
 -----------------------------
 
-Release of airflowctl, a command-line tool. There are lots of great features 
to use from start.
+Beta Release of airflowctl, a command-line tool. There are lots of great 
features to use from start.
 Please check the documentation for quick start and usage instructions.
 
 Please visit quick start guide: :doc:`/start`
diff --git a/airflow-ctl/docs/conf.py b/airflow-ctl/docs/conf.py
index 5a19b2adea3..08f867ee43f 100644
--- a/airflow-ctl/docs/conf.py
+++ b/airflow-ctl/docs/conf.py
@@ -207,18 +207,12 @@ jinja_contexts = {
     "config_ctx": {"configs": configs, "deprecated_options": 
deprecated_options},
     "quick_start_ctx": {"doc_root_url": 
f"https://airflow.apache.org/docs/apache-airflow/{PACKAGE_VERSION}/"},
     "official_download_page": {
-        "base_url": 
f"https://downloads.apache.org/airflow/airflow-ctl/{PACKAGE_VERSION}";,
-        "closer_lua_url": 
f"https://www.apache.org/dyn/closer.lua/airflow/airflow-ctl/{PACKAGE_VERSION}";,
-        "airflowctl_version": PACKAGE_VERSION,
+        "base_url": f"https://downloads.apache.org/airflow/{PACKAGE_VERSION}";,
+        "closer_lua_url": 
f"https://www.apache.org/dyn/closer.lua/airflow/{PACKAGE_VERSION}";,
+        "airflow_version": PACKAGE_VERSION,
     },
 }
 
-# Use for generate rst_epilog and other post-generation substitutions
-global_substitutions = {
-    "version": PACKAGE_VERSION,
-    "experimental": "This is an :ref:`experimental feature <experimental>`.",
-}
-
 # -- Options for sphinx.ext.autodoc 
--------------------------------------------
 # See: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html
 
diff --git a/airflow-ctl/docs/images/command_hashes.txt 
b/airflow-ctl/docs/images/command_hashes.txt
index 6792a0234ef..c0e3b5995fa 100644
--- a/airflow-ctl/docs/images/command_hashes.txt
+++ b/airflow-ctl/docs/images/command_hashes.txt
@@ -3,12 +3,12 @@ assets:b3ae2b933e54528bf486ff28e887804d
 auth:f396d4bce90215599dde6ad0a8f30f29
 backfill:bbce9859a2d1ce054ad22db92dea8c05
 config:cb175bedf29e8a2c2c6a2ebd13d770a7
-connections:a16225e1c7d28488d0da612752669b4b
+connections:e34b6b93f64714986139958c1f370428
 dags:287a128a71c97d2b537e09a5c7c73c09
 dagrun:f47ed2a89ed0f8c71f79dba53a3a3882
 jobs:7f8680afff230eb9940bc7fca727bd52
 pools:03fc7d948cbecf16ff8d640eb8f0ce43
 providers:1c0afb2dff31d93ab2934b032a2250ab
-variables:0b04188937b3c364204ef4cc9a541c62
+variables:0354f8f4b0dde1c3771ed1568692c6ae
 version:d4a7a6229b3a204f114283b62eac789b
 auth login:5277c653ff6dce51f37472dc0bda9775
diff --git a/airflow-ctl/docs/images/output_connections.svg 
b/airflow-ctl/docs/images/output_connections.svg
index 8283756115f..7dc48cce16a 100644
--- a/airflow-ctl/docs/images/output_connections.svg
+++ b/airflow-ctl/docs/images/output_connections.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 811 562.4" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 933 489.2" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -19,123 +19,110 @@
         font-weight: 700;
     }
 
-    .terminal-3087056112-matrix {
+    .terminal-1162206820-matrix {
         font-family: Fira Code, monospace;
         font-size: 20px;
         line-height: 24.4px;
         font-variant-east-asian: full-width;
     }
 
-    .terminal-3087056112-title {
+    .terminal-1162206820-title {
         font-size: 18px;
         font-weight: bold;
         font-family: arial;
     }
 
-    .terminal-3087056112-r1 { fill: #ff8700 }
-.terminal-3087056112-r2 { fill: #c5c8c6 }
-.terminal-3087056112-r3 { fill: #808080 }
-.terminal-3087056112-r4 { fill: #68a0b3 }
-.terminal-3087056112-r5 { fill: #c5c8c6;font-weight: bold }
+    .terminal-1162206820-r1 { fill: #ff8700 }
+.terminal-1162206820-r2 { fill: #c5c8c6 }
+.terminal-1162206820-r3 { fill: #808080 }
+.terminal-1162206820-r4 { fill: #68a0b3 }
     </style>
 
     <defs>
-    <clipPath id="terminal-3087056112-clip-terminal">
-      <rect x="0" y="0" width="792.0" height="511.4" />
+    <clipPath id="terminal-1162206820-clip-terminal">
+      <rect x="0" y="0" width="914.0" height="438.2" />
     </clipPath>
-    <clipPath id="terminal-3087056112-line-0">
-    <rect x="0" y="1.5" width="793" height="24.65"/>
+    <clipPath id="terminal-1162206820-line-0">
+    <rect x="0" y="1.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-1">
-    <rect x="0" y="25.9" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-1">
+    <rect x="0" y="25.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-2">
-    <rect x="0" y="50.3" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-2">
+    <rect x="0" y="50.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-3">
-    <rect x="0" y="74.7" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-3">
+    <rect x="0" y="74.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-4">
-    <rect x="0" y="99.1" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-4">
+    <rect x="0" y="99.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-5">
-    <rect x="0" y="123.5" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-5">
+    <rect x="0" y="123.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-6">
-    <rect x="0" y="147.9" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-6">
+    <rect x="0" y="147.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-7">
-    <rect x="0" y="172.3" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-7">
+    <rect x="0" y="172.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-8">
-    <rect x="0" y="196.7" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-8">
+    <rect x="0" y="196.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-9">
-    <rect x="0" y="221.1" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-9">
+    <rect x="0" y="221.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-10">
-    <rect x="0" y="245.5" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-10">
+    <rect x="0" y="245.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-11">
-    <rect x="0" y="269.9" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-11">
+    <rect x="0" y="269.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-12">
-    <rect x="0" y="294.3" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-12">
+    <rect x="0" y="294.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-13">
-    <rect x="0" y="318.7" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-13">
+    <rect x="0" y="318.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-14">
-    <rect x="0" y="343.1" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-14">
+    <rect x="0" y="343.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-15">
-    <rect x="0" y="367.5" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-15">
+    <rect x="0" y="367.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-3087056112-line-16">
-    <rect x="0" y="391.9" width="793" height="24.65"/>
-            </clipPath>
-<clipPath id="terminal-3087056112-line-17">
-    <rect x="0" y="416.3" width="793" height="24.65"/>
-            </clipPath>
-<clipPath id="terminal-3087056112-line-18">
-    <rect x="0" y="440.7" width="793" height="24.65"/>
-            </clipPath>
-<clipPath id="terminal-3087056112-line-19">
-    <rect x="0" y="465.1" width="793" height="24.65"/>
+<clipPath id="terminal-1162206820-line-16">
+    <rect x="0" y="391.9" width="915" height="24.65"/>
             </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="809" height="560.4" rx="8"/>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="931" height="487.2" rx="8"/>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
             <circle cx="44" cy="0" r="7" fill="#28c840"/>
             </g>
         
-    <g transform="translate(9, 41)" 
clip-path="url(#terminal-3087056112-clip-terminal)">
+    <g transform="translate(9, 41)" 
clip-path="url(#terminal-1162206820-clip-terminal)">
     
-    <g class="terminal-3087056112-matrix">
-    <text class="terminal-3087056112-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-3087056112-line-0)">Usage:</text><text 
class="terminal-3087056112-r3" x="85.4" y="20" textLength="268.4" 
clip-path="url(#terminal-3087056112-line-0)">airflowctl&#160;connections</text><text
 class="terminal-3087056112-r2" x="353.8" y="20" textLength="24.4" 
clip-path="url(#terminal-3087056112-line-0)">&#160;[</text><text 
class="terminal-3087056112-r4" x="378.2" y="20" textLength="24.4" clip-pat [...]
-</text><text class="terminal-3087056112-r2" x="793" y="44.4" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-1)">
-</text><text class="terminal-3087056112-r2" x="0" y="68.8" textLength="366" 
clip-path="url(#terminal-3087056112-line-2)">Perform&#160;Connections&#160;operations</text><text
 class="terminal-3087056112-r2" x="793" y="68.8" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-2)">
-</text><text class="terminal-3087056112-r2" x="793" y="93.2" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-3)">
-</text><text class="terminal-3087056112-r1" x="0" y="117.6" textLength="256.2" 
clip-path="url(#terminal-3087056112-line-4)">Positional&#160;Arguments:</text><text
 class="terminal-3087056112-r2" x="793" y="117.6" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-4)">
-</text><text class="terminal-3087056112-r4" x="24.4" y="142" textLength="85.4" 
clip-path="url(#terminal-3087056112-line-5)">COMMAND</text><text 
class="terminal-3087056112-r2" x="793" y="142" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-5)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="166.4" 
textLength="73.2" 
clip-path="url(#terminal-3087056112-line-6)">create</text><text 
class="terminal-3087056112-r2" x="231.8" y="166.4" textLength="292.8" 
clip-path="url(#terminal-3087056112-line-6)">Perform&#160;create&#160;operation</text><text
 class="terminal-3087056112-r2" x="793" y="166.4" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-6)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="190.8" 
textLength="183" 
clip-path="url(#terminal-3087056112-line-7)">create-defaults</text><text 
class="terminal-3087056112-r2" x="793" y="190.8" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-7)">
-</text><text class="terminal-3087056112-r2" x="231.8" y="215.2" 
textLength="402.6" 
clip-path="url(#terminal-3087056112-line-8)">Perform&#160;create_defaults&#160;operation</text><text
 class="terminal-3087056112-r2" x="793" y="215.2" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-8)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="239.6" 
textLength="73.2" 
clip-path="url(#terminal-3087056112-line-9)">delete</text><text 
class="terminal-3087056112-r2" x="231.8" y="239.6" textLength="292.8" 
clip-path="url(#terminal-3087056112-line-9)">Perform&#160;delete&#160;operation</text><text
 class="terminal-3087056112-r2" x="793" y="239.6" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-9)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="264" textLength="36.6" 
clip-path="url(#terminal-3087056112-line-10)">get</text><text 
class="terminal-3087056112-r2" x="231.8" y="264" textLength="256.2" 
clip-path="url(#terminal-3087056112-line-10)">Perform&#160;get&#160;operation</text><text
 class="terminal-3087056112-r2" x="793" y="264" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-10)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="288.4" 
textLength="73.2" 
clip-path="url(#terminal-3087056112-line-11)">import</text><text 
class="terminal-3087056112-r2" x="231.8" y="288.4" textLength="549" 
clip-path="url(#terminal-3087056112-line-11)">Import&#160;connections&#160;from&#160;a&#160;file.&#160;This&#160;feature&#160;</text><text
 class="terminal-3087056112-r2" x="793" y="288.4" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-11)">
-</text><text class="terminal-3087056112-r2" x="0" y="312.8" textLength="390.4" 
clip-path="url(#terminal-3087056112-line-12)">is&#160;compatible&#160;with&#160;airflow&#160;CLI&#160;`</text><text
 class="terminal-3087056112-r5" x="390.4" y="312.8" textLength="329.4" 
clip-path="url(#terminal-3087056112-line-12)">airflow&#160;connections&#160;export&#160;</text><text
 class="terminal-3087056112-r2" x="793" y="312.8" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-12)">
-</text><text class="terminal-3087056112-r5" x="0" y="337.2" textLength="73.2" 
clip-path="url(#terminal-3087056112-line-13)">a.json</text><text 
class="terminal-3087056112-r2" x="73.2" y="337.2" textLength="329.4" 
clip-path="url(#terminal-3087056112-line-13)">`&#160;command.&#160;Export&#160;it&#160;from&#160;`</text><text
 class="terminal-3087056112-r5" x="402.6" y="337.2" textLength="134.2" 
clip-path="url(#terminal-3087056112-line-13)">airflow&#160;CLI</text><text 
class="terminal-30870561 [...]
-</text><text class="terminal-3087056112-r2" x="0" y="361.6" textLength="317.2" 
clip-path="url(#terminal-3087056112-line-14)">securely&#160;via&#160;this&#160;command.</text><text
 class="terminal-3087056112-r2" x="793" y="361.6" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-14)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="386" textLength="48.8" 
clip-path="url(#terminal-3087056112-line-15)">list</text><text 
class="terminal-3087056112-r2" x="231.8" y="386" textLength="268.4" 
clip-path="url(#terminal-3087056112-line-15)">Perform&#160;list&#160;operation</text><text
 class="terminal-3087056112-r2" x="793" y="386" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-15)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="410.4" 
textLength="48.8" 
clip-path="url(#terminal-3087056112-line-16)">test</text><text 
class="terminal-3087056112-r2" x="231.8" y="410.4" textLength="268.4" 
clip-path="url(#terminal-3087056112-line-16)">Perform&#160;test&#160;operation</text><text
 class="terminal-3087056112-r2" x="793" y="410.4" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-16)">
-</text><text class="terminal-3087056112-r4" x="48.8" y="434.8" 
textLength="73.2" 
clip-path="url(#terminal-3087056112-line-17)">update</text><text 
class="terminal-3087056112-r2" x="231.8" y="434.8" textLength="292.8" 
clip-path="url(#terminal-3087056112-line-17)">Perform&#160;update&#160;operation</text><text
 class="terminal-3087056112-r2" x="793" y="434.8" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-17)">
-</text><text class="terminal-3087056112-r2" x="793" y="459.2" 
textLength="12.2" clip-path="url(#terminal-3087056112-line-18)">
-</text><text class="terminal-3087056112-r1" x="0" y="483.6" textLength="97.6" 
clip-path="url(#terminal-3087056112-line-19)">Options:</text><text 
class="terminal-3087056112-r2" x="793" y="483.6" textLength="12.2" 
clip-path="url(#terminal-3087056112-line-19)">
-</text><text class="terminal-3087056112-r4" x="24.4" y="508" textLength="24.4" 
clip-path="url(#terminal-3087056112-line-20)">-h</text><text 
class="terminal-3087056112-r2" x="48.8" y="508" textLength="24.4" 
clip-path="url(#terminal-3087056112-line-20)">,&#160;</text><text 
class="terminal-3087056112-r4" x="73.2" y="508" textLength="73.2" 
clip-path="url(#terminal-3087056112-line-20)">--help</text><text 
class="terminal-3087056112-r2" x="231.8" y="508" textLength="378.2" 
clip-path="url(#termi [...]
+    <g class="terminal-1162206820-matrix">
+    <text class="terminal-1162206820-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-1162206820-line-0)">Usage:</text><text 
class="terminal-1162206820-r3" x="85.4" y="20" textLength="268.4" 
clip-path="url(#terminal-1162206820-line-0)">airflowctl&#160;connections</text><text
 class="terminal-1162206820-r2" x="353.8" y="20" textLength="24.4" 
clip-path="url(#terminal-1162206820-line-0)">&#160;[</text><text 
class="terminal-1162206820-r4" x="378.2" y="20" textLength="24.4" clip-pat [...]
+</text><text class="terminal-1162206820-r2" x="915" y="44.4" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-1)">
+</text><text class="terminal-1162206820-r2" x="0" y="68.8" textLength="366" 
clip-path="url(#terminal-1162206820-line-2)">Perform&#160;Connections&#160;operations</text><text
 class="terminal-1162206820-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-2)">
+</text><text class="terminal-1162206820-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-3)">
+</text><text class="terminal-1162206820-r1" x="0" y="117.6" textLength="256.2" 
clip-path="url(#terminal-1162206820-line-4)">Positional&#160;Arguments:</text><text
 class="terminal-1162206820-r2" x="915" y="117.6" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-4)">
+</text><text class="terminal-1162206820-r4" x="24.4" y="142" textLength="85.4" 
clip-path="url(#terminal-1162206820-line-5)">COMMAND</text><text 
class="terminal-1162206820-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-5)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="166.4" 
textLength="73.2" 
clip-path="url(#terminal-1162206820-line-6)">create</text><text 
class="terminal-1162206820-r2" x="231.8" y="166.4" textLength="292.8" 
clip-path="url(#terminal-1162206820-line-6)">Perform&#160;create&#160;operation</text><text
 class="terminal-1162206820-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-6)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="190.8" 
textLength="183" 
clip-path="url(#terminal-1162206820-line-7)">create-defaults</text><text 
class="terminal-1162206820-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-7)">
+</text><text class="terminal-1162206820-r2" x="231.8" y="215.2" 
textLength="402.6" 
clip-path="url(#terminal-1162206820-line-8)">Perform&#160;create_defaults&#160;operation</text><text
 class="terminal-1162206820-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-8)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="239.6" 
textLength="73.2" 
clip-path="url(#terminal-1162206820-line-9)">delete</text><text 
class="terminal-1162206820-r2" x="231.8" y="239.6" textLength="292.8" 
clip-path="url(#terminal-1162206820-line-9)">Perform&#160;delete&#160;operation</text><text
 class="terminal-1162206820-r2" x="915" y="239.6" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-9)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="264" textLength="36.6" 
clip-path="url(#terminal-1162206820-line-10)">get</text><text 
class="terminal-1162206820-r2" x="231.8" y="264" textLength="256.2" 
clip-path="url(#terminal-1162206820-line-10)">Perform&#160;get&#160;operation</text><text
 class="terminal-1162206820-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-10)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="288.4" 
textLength="73.2" 
clip-path="url(#terminal-1162206820-line-11)">import</text><text 
class="terminal-1162206820-r2" x="231.8" y="288.4" textLength="671" 
clip-path="url(#terminal-1162206820-line-11)">Import&#160;connections&#160;from&#160;a&#160;file&#160;exported&#160;with&#160;local&#160;CLI.</text><text
 class="terminal-1162206820-r2" x="915" y="288.4" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-11)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="312.8" 
textLength="48.8" 
clip-path="url(#terminal-1162206820-line-12)">list</text><text 
class="terminal-1162206820-r2" x="231.8" y="312.8" textLength="268.4" 
clip-path="url(#terminal-1162206820-line-12)">Perform&#160;list&#160;operation</text><text
 class="terminal-1162206820-r2" x="915" y="312.8" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-12)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="337.2" 
textLength="48.8" 
clip-path="url(#terminal-1162206820-line-13)">test</text><text 
class="terminal-1162206820-r2" x="231.8" y="337.2" textLength="268.4" 
clip-path="url(#terminal-1162206820-line-13)">Perform&#160;test&#160;operation</text><text
 class="terminal-1162206820-r2" x="915" y="337.2" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-13)">
+</text><text class="terminal-1162206820-r4" x="48.8" y="361.6" 
textLength="73.2" 
clip-path="url(#terminal-1162206820-line-14)">update</text><text 
class="terminal-1162206820-r2" x="231.8" y="361.6" textLength="292.8" 
clip-path="url(#terminal-1162206820-line-14)">Perform&#160;update&#160;operation</text><text
 class="terminal-1162206820-r2" x="915" y="361.6" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-14)">
+</text><text class="terminal-1162206820-r2" x="915" y="386" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-15)">
+</text><text class="terminal-1162206820-r1" x="0" y="410.4" textLength="97.6" 
clip-path="url(#terminal-1162206820-line-16)">Options:</text><text 
class="terminal-1162206820-r2" x="915" y="410.4" textLength="12.2" 
clip-path="url(#terminal-1162206820-line-16)">
+</text><text class="terminal-1162206820-r4" x="24.4" y="434.8" 
textLength="24.4" clip-path="url(#terminal-1162206820-line-17)">-h</text><text 
class="terminal-1162206820-r2" x="48.8" y="434.8" textLength="24.4" 
clip-path="url(#terminal-1162206820-line-17)">,&#160;</text><text 
class="terminal-1162206820-r4" x="73.2" y="434.8" textLength="73.2" 
clip-path="url(#terminal-1162206820-line-17)">--help</text><text 
class="terminal-1162206820-r2" x="231.8" y="434.8" textLength="378.2" 
clip-path="ur [...]
 </text>
     </g>
     </g>
diff --git a/airflow-ctl/docs/images/output_variables.svg 
b/airflow-ctl/docs/images/output_variables.svg
index 31599fa47c9..78c24d31074 100644
--- a/airflow-ctl/docs/images/output_variables.svg
+++ b/airflow-ctl/docs/images/output_variables.svg
@@ -1,4 +1,4 @@
-<svg class="rich-terminal" viewBox="0 0 811 440.4" 
xmlns="http://www.w3.org/2000/svg";>
+<svg class="rich-terminal" viewBox="0 0 933 416.0" 
xmlns="http://www.w3.org/2000/svg";>
     <!-- Generated with Rich https://www.textualize.io -->
     <style>
 
@@ -19,102 +19,98 @@
         font-weight: 700;
     }
 
-    .terminal-4094392876-matrix {
+    .terminal-938427142-matrix {
         font-family: Fira Code, monospace;
         font-size: 20px;
         line-height: 24.4px;
         font-variant-east-asian: full-width;
     }
 
-    .terminal-4094392876-title {
+    .terminal-938427142-title {
         font-size: 18px;
         font-weight: bold;
         font-family: arial;
     }
 
-    .terminal-4094392876-r1 { fill: #ff8700 }
-.terminal-4094392876-r2 { fill: #c5c8c6 }
-.terminal-4094392876-r3 { fill: #808080 }
-.terminal-4094392876-r4 { fill: #68a0b3 }
+    .terminal-938427142-r1 { fill: #ff8700 }
+.terminal-938427142-r2 { fill: #c5c8c6 }
+.terminal-938427142-r3 { fill: #808080 }
+.terminal-938427142-r4 { fill: #68a0b3 }
     </style>
 
     <defs>
-    <clipPath id="terminal-4094392876-clip-terminal">
-      <rect x="0" y="0" width="792.0" height="389.4" />
+    <clipPath id="terminal-938427142-clip-terminal">
+      <rect x="0" y="0" width="914.0" height="365.0" />
     </clipPath>
-    <clipPath id="terminal-4094392876-line-0">
-    <rect x="0" y="1.5" width="793" height="24.65"/>
+    <clipPath id="terminal-938427142-line-0">
+    <rect x="0" y="1.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-1">
-    <rect x="0" y="25.9" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-1">
+    <rect x="0" y="25.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-2">
-    <rect x="0" y="50.3" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-2">
+    <rect x="0" y="50.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-3">
-    <rect x="0" y="74.7" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-3">
+    <rect x="0" y="74.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-4">
-    <rect x="0" y="99.1" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-4">
+    <rect x="0" y="99.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-5">
-    <rect x="0" y="123.5" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-5">
+    <rect x="0" y="123.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-6">
-    <rect x="0" y="147.9" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-6">
+    <rect x="0" y="147.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-7">
-    <rect x="0" y="172.3" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-7">
+    <rect x="0" y="172.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-8">
-    <rect x="0" y="196.7" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-8">
+    <rect x="0" y="196.7" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-9">
-    <rect x="0" y="221.1" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-9">
+    <rect x="0" y="221.1" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-10">
-    <rect x="0" y="245.5" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-10">
+    <rect x="0" y="245.5" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-11">
-    <rect x="0" y="269.9" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-11">
+    <rect x="0" y="269.9" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-12">
-    <rect x="0" y="294.3" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-12">
+    <rect x="0" y="294.3" width="915" height="24.65"/>
             </clipPath>
-<clipPath id="terminal-4094392876-line-13">
-    <rect x="0" y="318.7" width="793" height="24.65"/>
-            </clipPath>
-<clipPath id="terminal-4094392876-line-14">
-    <rect x="0" y="343.1" width="793" height="24.65"/>
+<clipPath id="terminal-938427142-line-13">
+    <rect x="0" y="318.7" width="915" height="24.65"/>
             </clipPath>
     </defs>
 
-    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="809" height="438.4" rx="8"/>
+    <rect fill="#292929" stroke="rgba(255,255,255,0.35)" stroke-width="1" 
x="1" y="1" width="931" height="414" rx="8"/>
             <g transform="translate(26,22)">
             <circle cx="0" cy="0" r="7" fill="#ff5f57"/>
             <circle cx="22" cy="0" r="7" fill="#febc2e"/>
             <circle cx="44" cy="0" r="7" fill="#28c840"/>
             </g>
         
-    <g transform="translate(9, 41)" 
clip-path="url(#terminal-4094392876-clip-terminal)">
+    <g transform="translate(9, 41)" 
clip-path="url(#terminal-938427142-clip-terminal)">
     
-    <g class="terminal-4094392876-matrix">
-    <text class="terminal-4094392876-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-4094392876-line-0)">Usage:</text><text 
class="terminal-4094392876-r3" x="85.4" y="20" textLength="244" 
clip-path="url(#terminal-4094392876-line-0)">airflowctl&#160;variables</text><text
 class="terminal-4094392876-r2" x="329.4" y="20" textLength="24.4" 
clip-path="url(#terminal-4094392876-line-0)">&#160;[</text><text 
class="terminal-4094392876-r4" x="353.8" y="20" textLength="24.4" clip-path="u 
[...]
-</text><text class="terminal-4094392876-r2" x="793" y="44.4" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-1)">
-</text><text class="terminal-4094392876-r2" x="0" y="68.8" textLength="341.6" 
clip-path="url(#terminal-4094392876-line-2)">Perform&#160;Variables&#160;operations</text><text
 class="terminal-4094392876-r2" x="793" y="68.8" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-2)">
-</text><text class="terminal-4094392876-r2" x="793" y="93.2" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-3)">
-</text><text class="terminal-4094392876-r1" x="0" y="117.6" textLength="256.2" 
clip-path="url(#terminal-4094392876-line-4)">Positional&#160;Arguments:</text><text
 class="terminal-4094392876-r2" x="793" y="117.6" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-4)">
-</text><text class="terminal-4094392876-r4" x="24.4" y="142" textLength="85.4" 
clip-path="url(#terminal-4094392876-line-5)">COMMAND</text><text 
class="terminal-4094392876-r2" x="793" y="142" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-5)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="166.4" 
textLength="73.2" 
clip-path="url(#terminal-4094392876-line-6)">create</text><text 
class="terminal-4094392876-r2" x="170.8" y="166.4" textLength="292.8" 
clip-path="url(#terminal-4094392876-line-6)">Perform&#160;create&#160;operation</text><text
 class="terminal-4094392876-r2" x="793" y="166.4" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-6)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="190.8" 
textLength="73.2" 
clip-path="url(#terminal-4094392876-line-7)">delete</text><text 
class="terminal-4094392876-r2" x="170.8" y="190.8" textLength="292.8" 
clip-path="url(#terminal-4094392876-line-7)">Perform&#160;delete&#160;operation</text><text
 class="terminal-4094392876-r2" x="793" y="190.8" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-7)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="215.2" 
textLength="73.2" 
clip-path="url(#terminal-4094392876-line-8)">export</text><text 
class="terminal-4094392876-r2" x="170.8" y="215.2" textLength="244" 
clip-path="url(#terminal-4094392876-line-8)">Export&#160;all&#160;variables</text><text
 class="terminal-4094392876-r2" x="793" y="215.2" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-8)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="239.6" 
textLength="36.6" clip-path="url(#terminal-4094392876-line-9)">get</text><text 
class="terminal-4094392876-r2" x="170.8" y="239.6" textLength="256.2" 
clip-path="url(#terminal-4094392876-line-9)">Perform&#160;get&#160;operation</text><text
 class="terminal-4094392876-r2" x="793" y="239.6" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-9)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="264" textLength="73.2" 
clip-path="url(#terminal-4094392876-line-10)">import</text><text 
class="terminal-4094392876-r2" x="170.8" y="264" textLength="195.2" 
clip-path="url(#terminal-4094392876-line-10)">Import&#160;variables</text><text 
class="terminal-4094392876-r2" x="793" y="264" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-10)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="288.4" 
textLength="48.8" 
clip-path="url(#terminal-4094392876-line-11)">list</text><text 
class="terminal-4094392876-r2" x="170.8" y="288.4" textLength="268.4" 
clip-path="url(#terminal-4094392876-line-11)">Perform&#160;list&#160;operation</text><text
 class="terminal-4094392876-r2" x="793" y="288.4" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-11)">
-</text><text class="terminal-4094392876-r4" x="48.8" y="312.8" 
textLength="73.2" 
clip-path="url(#terminal-4094392876-line-12)">update</text><text 
class="terminal-4094392876-r2" x="170.8" y="312.8" textLength="292.8" 
clip-path="url(#terminal-4094392876-line-12)">Perform&#160;update&#160;operation</text><text
 class="terminal-4094392876-r2" x="793" y="312.8" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-12)">
-</text><text class="terminal-4094392876-r2" x="793" y="337.2" 
textLength="12.2" clip-path="url(#terminal-4094392876-line-13)">
-</text><text class="terminal-4094392876-r1" x="0" y="361.6" textLength="97.6" 
clip-path="url(#terminal-4094392876-line-14)">Options:</text><text 
class="terminal-4094392876-r2" x="793" y="361.6" textLength="12.2" 
clip-path="url(#terminal-4094392876-line-14)">
-</text><text class="terminal-4094392876-r4" x="24.4" y="386" textLength="24.4" 
clip-path="url(#terminal-4094392876-line-15)">-h</text><text 
class="terminal-4094392876-r2" x="48.8" y="386" textLength="24.4" 
clip-path="url(#terminal-4094392876-line-15)">,&#160;</text><text 
class="terminal-4094392876-r4" x="73.2" y="386" textLength="73.2" 
clip-path="url(#terminal-4094392876-line-15)">--help</text><text 
class="terminal-4094392876-r2" x="170.8" y="386" textLength="378.2" 
clip-path="url(#termi [...]
+    <g class="terminal-938427142-matrix">
+    <text class="terminal-938427142-r1" x="0" y="20" textLength="73.2" 
clip-path="url(#terminal-938427142-line-0)">Usage:</text><text 
class="terminal-938427142-r3" x="85.4" y="20" textLength="244" 
clip-path="url(#terminal-938427142-line-0)">airflowctl&#160;variables</text><text
 class="terminal-938427142-r2" x="329.4" y="20" textLength="24.4" 
clip-path="url(#terminal-938427142-line-0)">&#160;[</text><text 
class="terminal-938427142-r4" x="353.8" y="20" textLength="24.4" 
clip-path="url(#ter [...]
+</text><text class="terminal-938427142-r2" x="915" y="44.4" textLength="12.2" 
clip-path="url(#terminal-938427142-line-1)">
+</text><text class="terminal-938427142-r2" x="0" y="68.8" textLength="341.6" 
clip-path="url(#terminal-938427142-line-2)">Perform&#160;Variables&#160;operations</text><text
 class="terminal-938427142-r2" x="915" y="68.8" textLength="12.2" 
clip-path="url(#terminal-938427142-line-2)">
+</text><text class="terminal-938427142-r2" x="915" y="93.2" textLength="12.2" 
clip-path="url(#terminal-938427142-line-3)">
+</text><text class="terminal-938427142-r1" x="0" y="117.6" textLength="256.2" 
clip-path="url(#terminal-938427142-line-4)">Positional&#160;Arguments:</text><text
 class="terminal-938427142-r2" x="915" y="117.6" textLength="12.2" 
clip-path="url(#terminal-938427142-line-4)">
+</text><text class="terminal-938427142-r4" x="24.4" y="142" textLength="85.4" 
clip-path="url(#terminal-938427142-line-5)">COMMAND</text><text 
class="terminal-938427142-r2" x="915" y="142" textLength="12.2" 
clip-path="url(#terminal-938427142-line-5)">
+</text><text class="terminal-938427142-r4" x="48.8" y="166.4" 
textLength="73.2" 
clip-path="url(#terminal-938427142-line-6)">create</text><text 
class="terminal-938427142-r2" x="170.8" y="166.4" textLength="292.8" 
clip-path="url(#terminal-938427142-line-6)">Perform&#160;create&#160;operation</text><text
 class="terminal-938427142-r2" x="915" y="166.4" textLength="12.2" 
clip-path="url(#terminal-938427142-line-6)">
+</text><text class="terminal-938427142-r4" x="48.8" y="190.8" 
textLength="73.2" 
clip-path="url(#terminal-938427142-line-7)">delete</text><text 
class="terminal-938427142-r2" x="170.8" y="190.8" textLength="292.8" 
clip-path="url(#terminal-938427142-line-7)">Perform&#160;delete&#160;operation</text><text
 class="terminal-938427142-r2" x="915" y="190.8" textLength="12.2" 
clip-path="url(#terminal-938427142-line-7)">
+</text><text class="terminal-938427142-r4" x="48.8" y="215.2" 
textLength="36.6" clip-path="url(#terminal-938427142-line-8)">get</text><text 
class="terminal-938427142-r2" x="170.8" y="215.2" textLength="256.2" 
clip-path="url(#terminal-938427142-line-8)">Perform&#160;get&#160;operation</text><text
 class="terminal-938427142-r2" x="915" y="215.2" textLength="12.2" 
clip-path="url(#terminal-938427142-line-8)">
+</text><text class="terminal-938427142-r4" x="48.8" y="239.6" 
textLength="73.2" 
clip-path="url(#terminal-938427142-line-9)">import</text><text 
class="terminal-938427142-r2" x="170.8" y="239.6" textLength="646.6" 
clip-path="url(#terminal-938427142-line-9)">Import&#160;variables&#160;from&#160;a&#160;file&#160;exported&#160;with&#160;local&#160;CLI.</text><text
 class="terminal-938427142-r2" x="915" y="239.6" textLength="12.2" 
clip-path="url(#terminal-938427142-line-9)">
+</text><text class="terminal-938427142-r4" x="48.8" y="264" textLength="48.8" 
clip-path="url(#terminal-938427142-line-10)">list</text><text 
class="terminal-938427142-r2" x="170.8" y="264" textLength="268.4" 
clip-path="url(#terminal-938427142-line-10)">Perform&#160;list&#160;operation</text><text
 class="terminal-938427142-r2" x="915" y="264" textLength="12.2" 
clip-path="url(#terminal-938427142-line-10)">
+</text><text class="terminal-938427142-r4" x="48.8" y="288.4" 
textLength="73.2" 
clip-path="url(#terminal-938427142-line-11)">update</text><text 
class="terminal-938427142-r2" x="170.8" y="288.4" textLength="292.8" 
clip-path="url(#terminal-938427142-line-11)">Perform&#160;update&#160;operation</text><text
 class="terminal-938427142-r2" x="915" y="288.4" textLength="12.2" 
clip-path="url(#terminal-938427142-line-11)">
+</text><text class="terminal-938427142-r2" x="915" y="312.8" textLength="12.2" 
clip-path="url(#terminal-938427142-line-12)">
+</text><text class="terminal-938427142-r1" x="0" y="337.2" textLength="97.6" 
clip-path="url(#terminal-938427142-line-13)">Options:</text><text 
class="terminal-938427142-r2" x="915" y="337.2" textLength="12.2" 
clip-path="url(#terminal-938427142-line-13)">
+</text><text class="terminal-938427142-r4" x="24.4" y="361.6" 
textLength="24.4" clip-path="url(#terminal-938427142-line-14)">-h</text><text 
class="terminal-938427142-r2" x="48.8" y="361.6" textLength="24.4" 
clip-path="url(#terminal-938427142-line-14)">,&#160;</text><text 
class="terminal-938427142-r4" x="73.2" y="361.6" textLength="73.2" 
clip-path="url(#terminal-938427142-line-14)">--help</text><text 
class="terminal-938427142-r2" x="170.8" y="361.6" textLength="378.2" 
clip-path="url(#term [...]
 </text>
     </g>
     </g>
diff --git a/airflow-ctl/docs/installation/installing-from-pypi.rst 
b/airflow-ctl/docs/installation/installing-from-pypi.rst
index 9ec3a5e73ab..cecd66c354e 100644
--- a/airflow-ctl/docs/installation/installing-from-pypi.rst
+++ b/airflow-ctl/docs/installation/installing-from-pypi.rst
@@ -24,7 +24,7 @@ PyPI <https://pypi.org/project/apache-airflow-ctl/>`__.
 Installation tools
 ''''''''''''''''''
 
-Only ``pip`` and ``uv`` installation is currently officially supported.
+Only ``pip`` installation is currently officially supported.
 
 .. note::
 
@@ -33,8 +33,7 @@ Only ``pip`` and ``uv`` installation is currently officially 
supported.
   ``pip`` - especially when it comes to constraint vs. requirements management.
   Installing via ``Poetry`` or ``pip-tools`` is not currently supported. If 
you wish to install airflow
   using those tools you should use the constraints and convert them to 
appropriate
-  format and workflow that your tool requires. Uv follows ``pip`` approach
-  with ``uv pip`` so it should work similarly.
+  format and workflow that your tool requires.
 
   There are known issues with ``bazel`` that might lead to circular 
dependencies when using it to install
   Airflow. Please switch to ``pip`` if you encounter such problems. ``Bazel`` 
community works on fixing
diff --git a/airflow-ctl/docs/installation/installing-from-sources.rst 
b/airflow-ctl/docs/installation/installing-from-sources.rst
index 2d2f9a421f5..ad1e616873d 100644
--- a/airflow-ctl/docs/installation/installing-from-sources.rst
+++ b/airflow-ctl/docs/installation/installing-from-sources.rst
@@ -23,8 +23,9 @@ Released packages
 
 .. jinja:: official_download_page
 
-    This page describes downloading and verifying Airflow Ctl version 
``|version|`` using officially released packages.
-    You can also install ``airflowctl`` - as most Python packages - via 
:doc:`PyPI <installing-from-pypi>`.
+    This page describes downloading and verifying Airflow® version
+    ``{{ airflow_version }}`` using officially released packages.
+    You can also install ``Apache airflowctl`` - as most Python packages - via 
:doc:`PyPI <installing-from-pypi>`.
     You can choose different version of Airflow by selecting a different 
version from the drop-down at
     the top-left of the page.
 
@@ -33,13 +34,22 @@ can use if you want to verify the origin of the packages 
and want to verify chec
 the packages. The packages are available via the
 `Official Apache Software Foundations Downloads <https://dlcdn.apache.org/>`_
 
-The {{ airflowctl_version }} downloads of Airflow Ctl are available at:
+As of version 2.8 Airflow follows PEP 517/518 and uses ``pyproject.toml`` file 
to define build dependencies
+and build process and it requires relatively modern versions of packaging 
tools to get airflow built from
+local sources or ``sdist`` packages, as PEP 517 compliant build hooks are used 
to determine dynamic build
+dependencies. In case of ``pip`` it means that at least version 22.1.0 is 
needed (released at the beginning of
+2022) to build or install Airflow from sources. This does not affect the 
ability of installing Airflow from
+released wheel packages.
+
+The |version| downloads of airflowctl are available at:
 
 .. jinja:: official_download_page
 
-    * `Sources package for airflow-ctl: <{{ closer_lua_url 
}}/apache_airflow_ctl-{{ airflowctl_version }}-source.tar.gz>`__ (`asc <{{ 
base_url }}/apache_airflow_ctl-{{ airflowctl_version }}-source.tar.gz.asc>`__, 
`sha512 <{{ base_url }}/apache_airflow_ctl-{{ airflowctl_version 
}}-source.tar.gz.sha512>`__)
-    * `Sdist package for airflow-ctl distributions <{{ closer_lua_url 
}}/apache_airflow_ctl-{{ airflowctl_version }}.tar.gz>`__ (`asc <{{ base_url 
}}/apache_airflow_ctl-{{ airflowctl_version }}.tar.gz.asc>`__, `sha512 <{{ 
base_url }}/apache_airflow_ctl-{{ airflowctl_version }}.tar.gz.sha512>`__)
-    * `Whl package for airflow-ctl distribution <{{ closer_lua_url 
}}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl>`__ (`asc <{{ 
base_url }}/apache_airflow_ctl-{{ airflowctl_version 
}}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url }}/apache_airflow_ctl-{{ 
airflowctl_version }}-py3-none-any.whl.sha512>`__)
+    * `Sources package for airflow <{{ closer_lua_url }}/apache-airflow-ctl-{{ 
airflowctl_version }}-source.tar.gz>`__ (`asc <{{ base_url 
}}/apache-airflow-ctl-{{ airflowctl_version }}-source.tar.gz.asc>`__, `sha512 
<{{ base_url }}/apache-airflow-ctl-{{ airflowctl_version 
}}-source.tar.gz.sha512>`__)
+    * `Sdist package for airflow meta package <{{ closer_lua_url 
}}/apache-airflow-ctl-{{ airflowctl_version }}.tar.gz>`__ (`asc <{{ base_url 
}}/apache-airflow-ctl-{{ airflowctl_version }}.tar.gz.asc>`__, `sha512 <{{ 
base_url }}/apache-airflow-ctl-{{ airflowctl_version }}.tar.gz.sha512>`__)
+    * `Whl package for airflow meta package <{{ closer_lua_url 
}}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl>`__ (`asc <{{ 
base_url }}/apache_airflow_ctl-{{ airflowctl_version 
}}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url }}/apache_airflow_ctl-{{ 
airflowctl_version }}-py3-none-any.whl.sha512>`__)
+    * `Sdist package for airflow core package <{{ closer_lua_url 
}}/apache-airflow_ctl-{{ airflowctl_version }}.tar.gz>`__ (`asc <{{ base_url 
}}/apache-airflow_ctl-{{ airflowctl_version }}.tar.gz.asc>`__, `sha512 <{{ 
base_url }}/apache-airflow_ctl-{{ airflowctl_version }}.tar.gz.sha512>`__)
+    * `Whl package for airflow core package <{{ closer_lua_url 
}}/apache_airflow_ctl-{{ airflowctl_version }}-py3-none-any.whl>`__ (`asc <{{ 
base_url }}/apache_airflow_ctl-{{ airflowctl_version 
}}-py3-none-any.whl.asc>`__, `sha512 <{{ base_url }}/apache_airflow_ctl-{{ 
airflowctl_version }}-py3-none-any.whl.sha512>`__)
 
 If you want to install from the source code, you can download from the sources 
link above, it will contain
 a ``INSTALL`` file containing details on how you can build and install 
airflowctl.
diff --git a/airflow-ctl/newsfragments/59850.significant.rst 
b/airflow-ctl/newsfragments/59850.significant.rst
new file mode 100644
index 00000000000..dfb10ae852c
--- /dev/null
+++ b/airflow-ctl/newsfragments/59850.significant.rst
@@ -0,0 +1,30 @@
+Remove ``airflowctl variables export`` command
+
+The ``airflowctl variables export`` command has been removed from the remote 
CLI. Variable export functionality is only available through the local Airflow 
CLI.
+
+**What changed:**
+
+- Removed ``airflowctl variables export`` command
+
+**Migration:**
+
+Use the local Airflow CLI command for exporting variables:
+
+.. code-block:: bash
+
+   # Old (no longer available)
+   airflowctl variables export variables.json
+
+   # New (use local CLI)
+   airflow variables export variables.json
+
+* Types of change
+
+  * [ ] Dag changes
+  * [ ] Config changes
+  * [ ] API changes
+  * [x] CLI changes
+  * [ ] Behaviour changes
+  * [ ] Plugin changes
+  * [ ] Dependency changes
+  * [ ] Code interface changes
diff --git a/airflow-ctl/pyproject.toml b/airflow-ctl/pyproject.toml
index 8c2fe12a7c5..bbfeffcd72d 100644
--- a/airflow-ctl/pyproject.toml
+++ b/airflow-ctl/pyproject.toml
@@ -20,8 +20,6 @@ name = "apache-airflow-ctl"
 dynamic = ["version"]
 description = "Apache Airflow command line tool for communicating with an 
Apache Airflow, using the API."
 readme = { file = "README.md", content-type = "text/markdown" }
-license = "Apache-2.0"
-license-files = ["LICENSE", "NOTICE"]
 # We do not want to upper-bind Python version, as we do not know if we will 
support Python 3.14+
 # out-of-the box. Airflow-ctl is a small tool that does not have many 
dependencies and does not use
 # sophisticated features of Python, so it should work with Python 3.14+ once 
all it's dependencies are
@@ -46,20 +44,6 @@ classifiers = [
   "Framework :: Apache Airflow",
 ]
 
-[project.urls]
-"Bug Tracker" = "https://github.com/apache/airflow/issues";
-Documentation = 
"https://airflow.apache.org/docs/apache-airflow-ctl/stable/index.html";
-Downloads = "https://archive.apache.org/dist/airflow/airflow-ctl/";
-Homepage = "https://airflow.apache.org/";
-"Release Notes" = 
"https://airflow.apache.org/docs/apache-airflow-ctl/stable/changelog.html";
-"Slack Chat" = "https://s.apache.org/airflow-slack";
-"Source Code" = "https://github.com/apache/airflow";
-LinkedIn = "https://www.linkedin.com/company/apache-airflow/";
-Mastodon = "https://fosstodon.org/@airflow";
-Bluesky = "https://bsky.app/profile/apache-airflow.bsky.social";
-YouTube = "https://www.youtube.com/channel/UCSXwxpWZQ7XZ1WL3wqevChA/";
-
-
 [project.optional-dependencies]
 "dev" = [
     "keyrings.alt>=5.0.2",
@@ -163,7 +147,7 @@ input = 
"../airflow-core/src/airflow/api_fastapi/core_api/openapi/v2-rest-api-ge
 output = "src/airflowctl/api/datamodels/generated.py"
 
 ## pytest settings ##
-[tool.pytest]
+[tool.pytest.ini_options]
 addopts = [
     "--tb=short",
     "-rasl",
@@ -196,7 +180,7 @@ testpaths = [
 ]
 asyncio_default_fixture_loop_scope = "function"
 
-pythonpath = ["tests"]
+pythonpath = "tests"
 
 # Keep temporary directories (created by `tmp_path`) for 2 recent runs only 
failed tests.
 tmp_path_retention_count = "2"
diff --git a/airflow-ctl/src/airflowctl/__init__.py 
b/airflow-ctl/src/airflowctl/__init__.py
index 32a64efabc5..f3fd163907d 100644
--- a/airflow-ctl/src/airflowctl/__init__.py
+++ b/airflow-ctl/src/airflowctl/__init__.py
@@ -19,4 +19,4 @@ from __future__ import annotations
 
 __path__ = __import__("pkgutil").extend_path(__path__, __name__)
 
-__version__ = "0.1.0"
+__version__ = "0.1.1"
diff --git a/airflow-ctl/src/airflowctl/ctl/cli_config.py 
b/airflow-ctl/src/airflowctl/ctl/cli_config.py
index da548530eb0..a578396acd4 100644
--- a/airflow-ctl/src/airflowctl/ctl/cli_config.py
+++ b/airflow-ctl/src/airflowctl/ctl/cli_config.py
@@ -74,13 +74,11 @@ def safe_call_command(function: Callable, args: 
Iterable[Arg]) -> None:
 
     try:
         function(args)
-    except AirflowCtlCredentialNotFoundException as e:
-        rich.print(f"command failed due to {e}")
-        sys.exit(1)
-    except AirflowCtlConnectionException as e:
-        rich.print(f"command failed due to {e}")
-        sys.exit(1)
-    except AirflowCtlNotFoundException as e:
+    except (
+        AirflowCtlCredentialNotFoundException,
+        AirflowCtlConnectionException,
+        AirflowCtlNotFoundException,
+    ) as e:
         rich.print(f"command failed due to {e}")
         sys.exit(1)
     except (httpx.RemoteProtocolError, httpx.ReadError) as e:
@@ -202,8 +200,7 @@ class Password(argparse.Action):
 ARG_FILE = Arg(
     flags=("file",),
     metavar="FILEPATH",
-    help="File path to read from or write to. "
-    "For import commands, it is a file to read from. For export commands, it 
is a file to write to.",
+    help="File path to read from for import commands.",
 )
 ARG_OUTPUT = Arg(
     (
@@ -254,9 +251,8 @@ ARG_AUTH_PASSWORD = Arg(
 
 # Dag Commands Args
 ARG_DAG_ID = Arg(
-    flags=("--dag-id",),
+    flags=("dag_id",),
     type=str,
-    dest="dag_id",
     help="The DAG ID of the DAG to pause or unpause",
 )
 
@@ -804,9 +800,7 @@ CONFIG_COMMANDS = (
 CONNECTION_COMMANDS = (
     ActionCommand(
         name="import",
-        help="Import connections from a file. "
-        "This feature is compatible with airflow CLI `airflow connections 
export a.json` command. "
-        "Export it from `airflow CLI` and import it securely via this 
command.",
+        help="Import connections from a file exported with local CLI.",
         
func=lazy_load_command("airflowctl.ctl.commands.connection_command.import_"),
         args=(Arg(flags=("file",), metavar="FILEPATH", help="Connections JSON 
file"),),
     ),
@@ -854,16 +848,10 @@ POOL_COMMANDS = (
 VARIABLE_COMMANDS = (
     ActionCommand(
         name="import",
-        help="Import variables",
+        help="Import variables from a file exported with local CLI.",
         
func=lazy_load_command("airflowctl.ctl.commands.variable_command.import_"),
         args=(ARG_FILE, ARG_VARIABLE_ACTION_ON_EXISTING_KEY),
     ),
-    ActionCommand(
-        name="export",
-        help="Export all variables",
-        
func=lazy_load_command("airflowctl.ctl.commands.variable_command.export"),
-        args=(ARG_FILE,),
-    ),
 )
 
 core_commands: list[CLICommand] = [
diff --git a/airflow-ctl/src/airflowctl/ctl/commands/variable_command.py 
b/airflow-ctl/src/airflowctl/ctl/commands/variable_command.py
index d5ffed31d8c..466be2ccac7 100644
--- a/airflow-ctl/src/airflowctl/ctl/commands/variable_command.py
+++ b/airflow-ctl/src/airflowctl/ctl/commands/variable_command.py
@@ -19,7 +19,6 @@ from __future__ import annotations
 import json
 import os
 import sys
-from pathlib import Path
 
 import rich
 
@@ -79,24 +78,3 @@ def import_(args, api_client=NEW_API_CLIENT) -> list[str]:
 
     rich.print(success_message.format(success=result.create.success))
     return result.create.success
-
-
-@provide_api_client(kind=ClientKind.CLI)
-def export(args, api_client=NEW_API_CLIENT) -> None:
-    """Export all the variables to the file."""
-    success_message = "[green]Export successful! {total_entries} variable(s) 
to {file}[/green]"
-    var_dict = {}
-    variables = api_client.variables.list()
-
-    for variable in variables.variables:
-        if variable.description:
-            var_dict[variable.key] = {
-                "value": variable.value,
-                "description": variable.description,
-            }
-        else:
-            var_dict[variable.key] = variable.value
-
-    with open(Path(args.file), "w") as var_file:
-        json.dump(var_dict, var_file, sort_keys=True, indent=4)
-    rich.print(success_message.format(total_entries=variables.total_entries, 
file=args.file))
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_client.py 
b/airflow-ctl/tests/airflow_ctl/api/test_client.py
index e2adcadff5e..39ffc436706 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_client.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_client.py
@@ -87,7 +87,7 @@ class TestClient:
         assert err.value.args == ("Client error message: {'detail': 'Not 
found'}",)
 
     @pytest.mark.parametrize(
-        "base_url, client_kind, expected_base_url",
+        ("base_url", "client_kind", "expected_base_url"),
         [
             ("http://localhost:8080";, ClientKind.CLI, 
"http://localhost:8080/api/v2/";),
             ("http://localhost:8080";, ClientKind.AUTH, 
"http://localhost:8080/auth/";),
diff --git a/airflow-ctl/tests/airflow_ctl/api/test_operations.py 
b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
index 1e7391b99d2..0f92aedb3a3 100644
--- a/airflow-ctl/tests/airflow_ctl/api/test_operations.py
+++ b/airflow-ctl/tests/airflow_ctl/api/test_operations.py
@@ -128,7 +128,7 @@ class TestBaseOperations:
             client.connections.get("1")
 
     @pytest.mark.parametrize(
-        "total_entries, limit, expected_response",
+        ("total_entries", "limit", "expected_response"),
         [
             (1, 50, 
(HelloCollectionResponse(hellos=[HelloResponse(name="hello")], 
total_entries=1))),
             (
@@ -1066,7 +1066,7 @@ class TestPoolsOperations:
         pools=[pool_response],
         total_entries=1,
     )
-    pool_bulk_aresponse = BulkResponse(
+    pool_bulk_response = BulkResponse(
         create=BulkActionResponse(success=[pool_name], errors=[]),
         update=None,
         delete=None,
@@ -1102,11 +1102,11 @@ class TestPoolsOperations:
     def test_bulk(self):
         def handle_request(request: httpx.Request) -> httpx.Response:
             assert request.url.path == "/api/v2/pools"
-            return httpx.Response(200, 
json=json.loads(self.pool_bulk_aresponse.model_dump_json()))
+            return httpx.Response(200, 
json=json.loads(self.pool_bulk_response.model_dump_json()))
 
         client = make_api_client(transport=httpx.MockTransport(handle_request))
         response = client.pools.bulk(pools=self.pools_bulk_body)
-        assert response == self.pool_bulk_aresponse
+        assert response == self.pool_bulk_response
 
     def test_delete(self):
         def handle_request(request: httpx.Request) -> httpx.Response:
diff --git a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py 
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py
index 98fb318024b..fbc6ee1240b 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_dag_command.py
@@ -93,7 +93,7 @@ class TestDagCommands:
         )
         assert self.dag_response_paused.is_paused is False
         dag_response_dict = dag_command.pause(
-            self.parser.parse_args(["dags", "pause", "--dag-id", self.dag_id]),
+            self.parser.parse_args(["dags", "pause", self.dag_id]),
             api_client=api_client,
         )
         assert dag_response_dict["is_paused"] is False
@@ -107,7 +107,7 @@ class TestDagCommands:
         )
         with pytest.raises(SystemExit):
             dag_command.pause(
-                self.parser.parse_args(["dags", "pause", "--dag-id", 
self.dag_id]),
+                self.parser.parse_args(["dags", "pause", self.dag_id]),
                 api_client=api_client,
             )
 
@@ -120,7 +120,7 @@ class TestDagCommands:
         )
         assert self.dag_response_unpaused.is_paused is True
         dag_response_dict = dag_command.unpause(
-            self.parser.parse_args(["dags", "unpause", "--dag-id", 
self.dag_id]),
+            self.parser.parse_args(["dags", "unpause", self.dag_id]),
             api_client=api_client,
         )
         assert dag_response_dict["is_paused"] is True
@@ -134,6 +134,6 @@ class TestDagCommands:
         )
         with pytest.raises(SystemExit):
             dag_command.unpause(
-                self.parser.parse_args(["dags", "unpause", "--dag-id", 
self.dag_id]),
+                self.parser.parse_args(["dags", "unpause", self.dag_id]),
                 api_client=api_client,
             )
diff --git 
a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_variable_command.py 
b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_variable_command.py
index e0c9ce16d8d..9703c8f866b 100644
--- a/airflow-ctl/tests/airflow_ctl/ctl/commands/test_variable_command.py
+++ b/airflow-ctl/tests/airflow_ctl/ctl/commands/test_variable_command.py
@@ -17,7 +17,6 @@
 from __future__ import annotations
 
 import json
-import os
 
 import pytest
 
@@ -104,22 +103,3 @@ class TestCliVariableCommands:
                 self.parser.parse_args(["variables", "import", 
expected_json_path.as_posix()]),
                 api_client=api_client,
             )
-
-    def test_export(self, api_client_maker, tmp_path, monkeypatch):
-        api_client = api_client_maker(
-            path="/api/v2/variables",
-            response_json=self.variable_collection_response.model_dump(),
-            expected_http_status_code=200,
-            kind=ClientKind.CLI,
-        )
-
-        monkeypatch.chdir(tmp_path)
-        expected_json_path = (tmp_path / self.export_file_name).as_posix()
-        variable_command.export(
-            self.parser.parse_args(["variables", "export", 
expected_json_path]),
-            api_client=api_client,
-        )
-        assert os.path.exists(tmp_path / self.export_file_name)
-
-        with open(expected_json_path) as f:
-            assert json.load(f) == {self.key: {"description": 
self.description, "value": self.value}}
diff --git a/scripts/ci/prek/check_airflowctl_command_coverage.py 
b/scripts/ci/prek/check_airflowctl_command_coverage.py
new file mode 100755
index 00000000000..8a81dd098c6
--- /dev/null
+++ b/scripts/ci/prek/check_airflowctl_command_coverage.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python
+#
+# 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.
+# /// script
+# requires-python = ">=3.10,<3.11"
+# dependencies = [
+#   "rich>=13.6.0",
+# ]
+# ///
+"""
+Check that all airflowctl CLI commands have integration test coverage by 
comparing  commands from operations.py against test_commands in conftest.py.
+"""
+
+from __future__ import annotations
+
+import ast
+import re
+import sys
+from pathlib import Path
+
+sys.path.insert(0, str(Path(__file__).parent.resolve()))
+from common_prek_utils import AIRFLOW_ROOT_PATH, console
+
+OPERATIONS_FILE = AIRFLOW_ROOT_PATH / "airflow-ctl" / "src" / "airflowctl" / 
"api" / "operations.py"
+CTL_TESTS_FILE = (
+    AIRFLOW_ROOT_PATH / "airflow-ctl-tests" / "tests" / "airflowctl_tests" / 
"test_airflowctl_commands.py"
+)
+
+# Operations excluded from CLI (see cli_config.py)
+EXCLUDED_OPERATION_CLASSES = {"BaseOperations", "LoginOperations", 
"VersionOperations"}
+EXCLUDED_METHODS = {
+    "__init__",
+    "__init_subclass__",
+    "error",
+    "_check_flag_and_exit_if_server_response_error",
+    "bulk",
+    "export",
+}
+
+EXCLUDED_COMMANDS = {
+    "assets delete-dag-queued-events",
+    "assets delete-queued-event",
+    "assets delete-queued-events",
+    "assets get-by-alias",
+    "assets get-dag-queued-event",
+    "assets get-dag-queued-events",
+    "assets get-queued-events",
+    "assets list-by-alias",
+    "assets materialize",
+    "backfill cancel",
+    "backfill create",
+    "backfill create-dry-run",
+    "backfill get",
+    "backfill pause",
+    "backfill unpause",
+    "connections create-defaults",
+    "connections test",
+    "dags delete",
+    "dags get-import-error",
+    "dags get-tags",
+}
+
+
+def parse_operations() -> dict[str, list[str]]:
+    commands: dict[str, list[str]] = {}
+
+    with open(OPERATIONS_FILE) as f:
+        tree = ast.parse(f.read(), filename=str(OPERATIONS_FILE))
+
+    for node in ast.walk(tree):
+        if isinstance(node, ast.ClassDef) and node.name.endswith("Operations"):
+            if node.name in EXCLUDED_OPERATION_CLASSES:
+                continue
+
+            group_name = node.name.replace("Operations", "").lower()
+            commands[group_name] = []
+
+            for child in node.body:
+                if isinstance(child, ast.FunctionDef):
+                    method_name = child.name
+                    if method_name in EXCLUDED_METHODS or 
method_name.startswith("_"):
+                        continue
+                    subcommand = method_name.replace("_", "-")
+                    commands[group_name].append(subcommand)
+
+    return commands
+
+
+def parse_tested_commands() -> set[str]:
+    tested: set[str] = set()
+
+    with open(CTL_TESTS_FILE) as f:
+        content = f.read()
+
+    # Match command patterns like "assets list", "dags list-import-errors", 
etc.
+    # Also handles f-strings like f"dagrun get..." or f'dagrun get...'
+    pattern = r'f?["\']([a-z]+(?:-[a-z]+)*\s+[a-z]+(?:-[a-z]+)*)'
+    for match in re.findall(pattern, content):
+        parts = match.split()
+        if len(parts) >= 2:
+            tested.add(f"{parts[0]} {parts[1]}")
+
+    return tested
+
+
+def main():
+    available = parse_operations()
+    tested = parse_tested_commands()
+
+    missing = []
+    for group, subcommands in sorted(available.items()):
+        for subcommand in sorted(subcommands):
+            cmd = f"{group} {subcommand}"
+            if cmd not in tested and cmd not in EXCLUDED_COMMANDS:
+                missing.append(cmd)
+
+    if missing:
+        console.print("[red]ERROR: Commands not covered by integration 
tests:[/]")
+        for cmd in missing:
+            console.print(f"  [red]- {cmd}[/]")
+        console.print()
+        console.print("[yellow]Fix by either:[/]")
+        console.print(f"1. Add test to {CTL_TESTS_FILE}")
+        console.print(f"2. Add to EXCLUDED_COMMANDS in {__file__}")
+        sys.exit(1)
+
+    total = sum(len(cmds) for cmds in available.values())
+    console.print(
+        f"[green]All {total} CLI commands covered ({len(tested)} tested, 
{len(EXCLUDED_COMMANDS)} excluded)[/]"
+    )
+    sys.exit(0)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/scripts/in_container/run_capture_airflowctl_help.py 
b/scripts/in_container/run_capture_airflowctl_help.py
index 733cc3145b2..b35472bffca 100644
--- a/scripts/in_container/run_capture_airflowctl_help.py
+++ b/scripts/in_container/run_capture_airflowctl_help.py
@@ -84,7 +84,7 @@ def 
regenerate_help_images_for_all_airflowctl_commands(commands: list[str], skip
     os.makedirs(AIRFLOWCTL_IMAGES_PATH, exist_ok=True)
     env = os.environ.copy()
     env["TERM"] = "xterm-256color"
-    env["COLUMNS"] = "65"
+    env["COLUMNS"] = "75"
     old_hash_dict = {}
     new_hash_dict = {}
 
@@ -100,8 +100,8 @@ def 
regenerate_help_images_for_all_airflowctl_commands(commands: list[str], skip
 
     # Check for changes
     changed_commands = []
-    for command in commands:
-        command = command or "main"
+    for command_raw in commands:
+        command = command_raw or "main"
         console.print(f"[bright_blue]Checking command: {command}[/]", end="")
 
         if skip_hash_check:


Reply via email to