Repository: dbt-labs/dbt-core
Pull request: Flags work with Click
resolves #5529

### Description

Adds a new version of the flags module.

### Checklist

- [x] I have read [the contributing guide](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md) and understand what's expected of me
- [x] I have signed the [CLA](https://docs.getdbt.com/docs/contributor-license-agreements)
- [x] I have run this code in development and it appears to resolve the stated issue
- [x] This PR includes tests, or tests are not required/relevant for this PR
- [x] I have [opened an issue to add/update docs](https://github.com/dbt-labs/docs.getdbt.com/issues/new/choose), or docs changes are not required/relevant for this PR
- [x] I have run `changie new` to [create a changelog entry](https://github.com/dbt-labs/dbt-core/blob/main/CONTRIBUTING.md#Adding-CHANGELOG-Entry)

Problem statement:
[CT-905] Flags work with Click
User Story:
As a developer of `dbt-core` I want env vars to be handled in the same consistent manner as cli command parameters.

Acceptance Criteria:
* All Flags are implemented as click params / default values and as such as available within the context of a click command.
* Existing flag logic is removed

Hints from issue discussion:

Commit message: PR feedback

Patch to review:
diff --git a/.changes/unreleased/Features-20220908-123650.yaml b/.changes/unreleased/Features-20220908-123650.yaml
new file mode 100644
index 00000000000..a68f1d55b23
--- /dev/null
+++ b/.changes/unreleased/Features-20220908-123650.yaml
@@ -0,0 +1,7 @@
+kind: Features
+body: Flags work with new Click CLI
+time: 2022-09-08T12:36:50.386978-05:00
+custom:
+  Author: iknox-fa
+  Issue: "5529"
+  PR: "5790"
diff --git a/core/dbt/cli/README.md b/core/dbt/cli/README.md
new file mode 100644
index 00000000000..1333ed77b7e
--- /dev/null
+++ b/core/dbt/cli/README.md
@@ -0,0 +1 @@
+TODO
diff --git a/core/dbt/cli/__init__.py b/core/dbt/cli/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/core/dbt/cli/flags.py b/core/dbt/cli/flags.py
new file mode 100644
index 00000000000..1bda236a8b6
--- /dev/null
+++ b/core/dbt/cli/flags.py
@@ -0,0 +1,46 @@
+# TODO  Move this to /core/dbt/flags.py when we're ready to break things
+import os
+from dataclasses import dataclass
+from multiprocessing import get_context
+from pprint import pformat as pf
+
+from click import get_current_context
+
+if os.name != "nt":
+    # https://bugs.python.org/issue41567
+    import multiprocessing.popen_spawn_posix  # type: ignore  # noqa: F401
+
+
+@dataclass(frozen=True)
+class Flags:
+    def __init__(self, ctx=None) -> None:
+
+        if ctx is None:
+            ctx = get_current_context()
+
+        def assign_params(ctx):
+            """Recursively adds all click params to flag object"""
+            for param_name, param_value in ctx.params.items():
+                # N.B. You have to use the base MRO method (object.__setattr__) to set attributes
+                # when using frozen dataclasses.
+                # https://docs.python.org/3/library/dataclasses.html#frozen-instances
+                if hasattr(self, param_name):
+                    raise Exception(f"Duplicate flag names found in click command: {param_name}")
+                object.__setattr__(self, param_name.upper(), param_value)
+            if ctx.parent:
+                assign_params(ctx.parent)
+
+        assign_params(ctx)
+
+        # Hard coded flags
+        #  TODO: This is listed as for backwards compat... Still needed?
+        object.__setattr__(self, "STRICT_MODE", False)
+        object.__setattr__(self, "WHICH", ctx.info_name)
+        object.__setattr__(self, "MP_CONTEXT", get_context("spawn"))
+
+        # Support console DO NOT TRACK initiave
+        if os.getenv("DO_NOT_TRACK", "").lower() in (1, "t", "true", "y", "yes"):
+            object.__setattr__(self, "ANONYMOUS_USAGE_STATS", False)
+
+    def __str__(self) -> str:
+        return str(pf(self.__dict__))
diff --git a/core/dbt/cli/main.py b/core/dbt/cli/main.py
new file mode 100644
index 00000000000..4a402f97d64
--- /dev/null
+++ b/core/dbt/cli/main.py
@@ -0,0 +1,386 @@
+import inspect  # This is temporary for RAT-ing
+import sys
+from pprint import pformat as pf  # This is temporary for RAT-ing
+
+import click
+from dbt.cli import params as p
+from dbt.cli.flags import Flags
+
+
+# dbt
+@click.group(
+    context_settings={"help_option_names": ["-h", "--help"]},
+    invoke_without_command=True,
+    no_args_is_help=True,
+    epilog="Specify one of these sub-commands and you can find more help from there.",
+)
+@click.pass_context
+@p.anonymous_usage_stats
+@p.cache_selected_only
+@p.debug
+@p.enable_legacy_logger
+@p.event_buffer_size
+@p.fail_fast
+@p.log_cache_events
+@p.log_format
+@p.macro_debugging
+@p.partial_parse
+@p.print
+@p.printer_width
+@p.quiet
+@p.record_timing_info
+@p.static_parser
+@p.use_colors
+@p.use_experimental_parser
+@p.version
+@p.version_check
+@p.warn_error
+@p.write_json
+def cli(ctx, **kwargs):
+    """An ELT tool for managing your SQL transformations and data models.
+    For more documentation on these commands, visit: docs.getdbt.com
+    """
+    if kwargs.get("version", False):
+        click.echo(f"`version` called\n ctx.params: {pf(ctx.params)}")
+        sys.exit()
+    else:
+        del ctx.params["version"]
+
+
+# dbt build
+@cli.command("build")
+@click.pass_context
+@p.defer
+@p.exclude
+@p.fail_fast
+@p.full_refresh
+@p.indirect_selection
+@p.log_path
+@p.models
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.show
+@p.state
+@p.store_failures
+@p.target
+@p.target_path
+@p.threads
+@p.vars
+@p.version_check
+def build(ctx, **kwargs):
+    """Run all Seeds, Models, Snapshots, and tests in DAG order"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt clean
+@cli.command("clean")
+@click.pass_context
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.target
+@p.vars
+def clean(ctx, **kwargs):
+    """Delete all folders in the clean-targets list (usually the dbt_packages and target directories.)"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt docs
+@cli.group()
+@click.pass_context
+def docs(ctx, **kwargs):
+    """Generate or serve the documentation website for your project"""
+
+
+# dbt docs generate
+@docs.command("generate")
+@click.pass_context
+@p.compile_docs
+@p.defer
+@p.exclude
+@p.log_path
+@p.models
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.state
+@p.target
+@p.target_path
+@p.threads
+@p.vars
+@p.version_check
+def docs_generate(ctx, **kwargs):
+    """Generate the documentation website for your project"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt docs serve
+@docs.command("serve")
+@click.pass_context
+@p.browser
+@p.port
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.target
+@p.vars
+def docs_serve(ctx, **kwargs):
+    """Serve the documentation website for your project"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt compile
+@cli.command("compile")
+@click.pass_context
+@p.defer
+@p.exclude
+@p.full_refresh
+@p.log_path
+@p.models
+@p.parse_only
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.state
+@p.target
+@p.target_path
+@p.threads
+@p.vars
+@p.version_check
+def compile(ctx, **kwargs):
+    """Generates executable SQL from source, model, test, and analysis files. Compiled SQL files are written to the target/ directory."""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt debug
+@cli.command("debug")
+@click.pass_context
+@p.config_dir
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.target
+@p.vars
+@p.version_check
+def debug(ctx, **kwargs):
+    """Show some helpful information about dbt for debugging. Not to be confused with the --debug option which increases verbosity."""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt deps
+@cli.command("deps")
+@click.pass_context
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.target
+@p.vars
+def deps(ctx, **kwargs):
+    """Pull the most recent version of the dependencies listed in packages.yml"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt init
+@cli.command("init")
+@click.pass_context
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.skip_profile_setup
+@p.target
+@p.vars
+def init(ctx, **kwargs):
+    """Initialize a new DBT project."""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt list
+# TODO: Figure out aliasing for ls (or just c/p?)
+@cli.command("list")
+@click.pass_context
+@p.exclude
+@p.indirect_selection
+@p.models
+@p.output
+@p.output_keys
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.resource_type
+@p.selector
+@p.state
+@p.target
+@p.vars
+def list(ctx, **kwargs):
+    """List the resources in your project"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt parse
+@cli.command("parse")
+@click.pass_context
+@p.compile_parse
+@p.log_path
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.target
+@p.target_path
+@p.threads
+@p.vars
+@p.version_check
+@p.write_manifest
+def parse(ctx, **kwargs):
+    """Parses the project and provides information on performance"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt run
+@cli.command("run")
+@click.pass_context
+@p.defer
+@p.exclude
+@p.fail_fast
+@p.full_refresh
+@p.log_path
+@p.models
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.state
+@p.target
+@p.target_path
+@p.threads
+@p.vars
+@p.version_check
+def run(ctx, **kwargs):
+    """Compile SQL and execute against the current target database."""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt run operation
+@cli.command("run-operation")
+@click.pass_context
+@p.args
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.target
+@p.vars
+def run_operation(ctx, **kwargs):
+    """Run the named macro with any supplied arguments."""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt seed
+@cli.command("seed")
+@click.pass_context
+@p.exclude
+@p.full_refresh
+@p.log_path
+@p.models
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.show
+@p.state
+@p.target
+@p.target_path
+@p.threads
+@p.vars
+@p.version_check
+def seed(ctx, **kwargs):
+    """Load data from csv files into your data warehouse."""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt snapshot
+@cli.command("snapshot")
+@click.pass_context
+@p.defer
+@p.exclude
+@p.models
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.state
+@p.target
+@p.threads
+@p.vars
+def snapshot(ctx, **kwargs):
+    """Execute snapshots defined in your project"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt source
+@cli.group()
+@click.pass_context
+def source(ctx, **kwargs):
+    """Manage your project's sources"""
+
+
+# dbt source freshness
+@source.command("freshness")
+@click.pass_context
+@p.exclude
+@p.models
+@p.output_path  # TODO: Is this ok to re-use?  We have three different output params, how much can we consolidate?
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.state
+@p.target
+@p.threads
+@p.vars
+def freshness(ctx, **kwargs):
+    """Snapshots the current freshness of the project's sources"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt test
+@cli.command("test")
+@click.pass_context
+@p.defer
+@p.exclude
+@p.fail_fast
+@p.indirect_selection
+@p.log_path
+@p.models
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.state
+@p.store_failures
+@p.target
+@p.target_path
+@p.threads
+@p.vars
+@p.version_check
+def test(ctx, **kwargs):
+    """Runs tests on data in deployed models. Run this after `dbt run`"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
diff --git a/core/dbt/cli/option_types.py b/core/dbt/cli/option_types.py
new file mode 100644
index 00000000000..523df651775
--- /dev/null
+++ b/core/dbt/cli/option_types.py
@@ -0,0 +1,33 @@
+from click import ParamType
+import yaml
+
+
+class YAML(ParamType):
+    """The Click YAML type. Converts YAML strings into objects."""
+
+    name = "YAML"
+
+    def convert(self, value, param, ctx):
+        # assume non-string values are a problem
+        if not isinstance(value, str):
+            self.fail(f"Cannot load YAML from type {type(value)}", param, ctx)
+        try:
+            return yaml.load(value, Loader=yaml.Loader)
+        except yaml.parser.ParserError:
+            self.fail(f"String '{value}' is not valid YAML", param, ctx)
+
+
+class Truthy(ParamType):
+    """The Click Truthy type.  Converts strings into a "truthy" type"""
+
+    name = "TRUTHY"
+
+    def convert(self, value, param, ctx):
+        # assume non-string / non-None values are a problem
+        if not isinstance(value, (str, None)):
+            self.fail(f"Cannot load TRUTHY from type {type(value)}", param, ctx)
+
+        if value is None or value.lower() in ("0", "false", "f"):
+            return None
+        else:
+            return value
diff --git a/core/dbt/cli/params.py b/core/dbt/cli/params.py
new file mode 100644
index 00000000000..6b68f46e18d
--- /dev/null
+++ b/core/dbt/cli/params.py
@@ -0,0 +1,386 @@
+from pathlib import Path, PurePath
+
+import click
+from dbt.cli.option_types import YAML
+
+# TODO:  The name (reflected in flags) is a correction!
+# The original name was `SEND_ANONYMOUS_USAGE_STATS` and used an env var called "DBT_SEND_ANONYMOUS_USAGE_STATS"
+# Both of which break existing naming conventions (doesn't match param flag).
+# This will need to be fixed before use in the main codebase and communicated as a change to the community!
+anonymous_usage_stats = click.option(
+    "--anonymous-usage-stats/--no-anonymous-usage-stats",
+    envvar="DBT_ANONYMOUS_USAGE_STATS",
+    help="Send anonymous usage stats to dbt Labs.",
+    default=True,
+)
+
+args = click.option(
+    "--args",
+    envvar=None,
+    help="Supply arguments to the macro. This dictionary will be mapped to the keyword arguments defined in the selected macro. This argument should be a YAML string, eg. '{my_variable: my_value}'",
+    type=YAML(),
+)
+
+browser = click.option(
+    "--browser/--no-browser",
+    envvar=None,
+    help="Wether or not to open a local web browser after starting the server",
+    default=True,
+)
+
+cache_selected_only = click.option(
+    "--cache-selected-only/--no-cache-selected-only",
+    envvar="DBT_CACHE_SELECTED_ONLY",
+    help="Pre cache database objects relevant to selected resource only.",
+)
+
+compile_docs = click.option(
+    "--compile/--no-compile",
+    envvar=None,
+    help="Wether or not to run 'dbt compile' as part of docs generation",
+    default=True,
+)
+
+compile_parse = click.option(
+    "--compile/--no-compile",
+    envvar=None,
+    help="TODO: No help text currently available",
+    default=True,
+)
+
+config_dir = click.option(
+    "--config-dir",
+    envvar=None,
+    help="If specified, DBT will show path information for this project",
+    type=click.STRING,
+)
+
+debug = click.option(
+    "--debug/--no-debug",
+    "-d/ ",
+    envvar="DBT_DEBUG",
+    help="Display debug logging during dbt execution. Useful for debugging and making bug reports.",
+)
+
+# TODO:  The env var and name (reflected in flags) are corrections!
+# The original name was `DEFER_MODE` and used an env var called "DBT_DEFER_TO_STATE"
+# Both of which break existing naming conventions.
+# This will need to be fixed before use in the main codebase and communicated as a change to the community!
+defer = click.option(
+    "--defer/--no-defer",
+    envvar="DBT_DEFER",
+    help="If set, defer to the state variable for resolving unselected nodes.",
+)
+
+enable_legacy_logger = click.option(
+    "--enable-legacy-logger/--no-enable-legacy-logger",
+    envvar="DBT_ENABLE_LEGACY_LOGGER",
+    hidden=True,
+)
+
+event_buffer_size = click.option(
+    "--event-buffer-size",
+    envvar="DBT_EVENT_BUFFER_SIZE",
+    help="Sets the max number of events to buffer in EVENT_HISTORY.",
+    default=100000,
+    type=click.INT,
+)
+
+exclude = click.option("--exclude", envvar=None, help="Specify the nodes to exclude.")
+
+fail_fast = click.option(
+    "--fail-fast/--no-fail-fast",
+    "-x/ ",
+    envvar="DBT_FAIL_FAST",
+    help="Stop execution on first failure.",
+)
+
+full_refresh = click.option(
+    "--full-refresh",
+    envvar="DBT_FULL_REFRESH",
+    help="If specified, dbt will drop incremental models and fully-recalculate the incremental table from the model definition.",
+    is_flag=True,
+)
+
+indirect_selection = click.option(
+    "--indirect-selection",
+    envvar="DBT_INDIRECT_SELECTION",
+    help="Select all tests that are adjacent to selected resources, even if they those resources have been explicitly selected.",
+    type=click.Choice(["eager", "cautious"], case_sensitive=False),
+    default="eager",
+)
+
+#  TODO: Should this be exposed in the help text?
+log_cache_events = click.option(
+    "--log-cache-events/--no-log-cache-events",
+    envvar="DBT_LOG_CACHE_EVENTS",
+    hidden=True,
+)
+
+log_format = click.option(
+    "--log-format",
+    envvar="DBT_LOG_FORMAT",
+    help="Specify the log format, overriding the command's default.",
+    type=click.Choice(["text", "json", "default"], case_sensitive=False),
+    default="default",
+)
+
+log_path = click.option(
+    "--log-path",
+    envvar="DBT_LOG_PATH",
+    help="Configure the 'log-path'. Only applies this setting for the current run. Overrides the 'DBT_LOG_PATH' if it is set.",
+    type=click.Path(),
+)
+
+macro_debugging = click.option(
+    "--macro-debugging/--no-macro-debugging",
+    envvar="DBT_MACRO_DEBUGGING",
+    hidden=True,
+)
+
+models = click.option(
+    "-m",
+    "-s",
+    "models",
+    envvar=None,
+    help="Specify the nodes to include.",
+    multiple=True,
+)
+
+output = click.option(
+    "--output",
+    envvar=None,
+    help="TODO: No current help text",
+    type=click.Choice(["json", "name", "path", "selector"], case_sensitive=False),
+    default="name",
+)
+
+output_keys = click.option(
+    "--output-keys", envvar=None, help="TODO: No current help text", type=click.STRING
+)
+
+output_path = click.option(
+    "--output",
+    "-o",
+    envvar=None,
+    help="Specify the output path for the json report. By default, outputs to 'target/sources.json'",
+    type=click.Path(file_okay=True, dir_okay=False, writable=True),
+    default=PurePath.joinpath(Path.cwd(), "target/sources.json"),
+)
+
+parse_only = click.option(
+    "--parse-only",
+    envvar=None,
+    help="TODO:  No help text currently available",
+    is_flag=True,
+)
+
+partial_parse = click.option(
+    "--partial-parse/--no-partial-parse",
+    envvar="DBT_PARTIAL_PARSE",
+    help="Allow for partial parsing by looking for and writing to a pickle file in the target directory. This overrides the user configuration file.",
+    default=True,
+)
+
+port = click.option(
+    "--port",
+    envvar=None,
+    help="Specify the port number for the docs server",
+    default=8080,
+    type=click.INT,
+)
+
+# TODO:  The env var and name (reflected in flags) are corrections!
+# The original name was `NO_PRINT` and used the env var `DBT_NO_PRINT`.
+# Both of which break existing naming conventions.
+# This will need to be fixed before use in the main codebase and communicated as a change to the community!
+print = click.option(
+    "--print/--no-print",
+    envvar="DBT_PRINT",
+    help="Output all {{ print() }} macro calls.",
+    default=True,
+)
+
+printer_width = click.option(
+    "--printer-width",
+    envvar="DBT_PRINTER_WIDTH",
+    help="Sets the width of terminal output",
+    type=click.INT,
+    default=80,
+)
+
+profile = click.option(
+    "--profile",
+    envvar=None,
+    help="Which profile to load. Overrides setting in dbt_project.yml.",
+)
+
+profiles_dir = click.option(
+    "--profiles-dir",
+    envvar="DBT_PROFILES_DIR",
+    help=f"Which directory to look in for the profiles.yml file. Default = {PurePath.joinpath(Path.home(), '.dbt')}",
+    default=PurePath.joinpath(Path.home(), ".dbt"),
+    type=click.Path(
+        exists=True,
+    ),
+)
+
+project_dir = click.option(
+    "--project-dir",
+    envvar=None,
+    help="Which directory to look in for the dbt_project.yml file. Default is the current working directory and its parents.",
+    default=Path.cwd(),
+    type=click.Path(exists=True),
+)
+
+quiet = click.option(
+    "--quiet/--no-quiet",
+    envvar="DBT_QUIET",
+    help="Suppress all non-error logging to stdout. Does not affect {{ print() }} macro calls.",
+)
+
+record_timing_info = click.option(
+    "--record-timing-info",
+    "-r",
+    envvar=None,
+    help="When this option is passed, dbt will output low-level timing stats to the specified file. Example: `--record-timing-info output.profile`",
+    is_flag=True,
+)
+
+resource_type = click.option(
+    "--resource-type",
+    envvar=None,
+    help="TODO: No current help text",
+    type=click.Choice(
+        [
+            "metric",
+            "source",
+            "analysis",
+            "model",
+            "test",
+            "exposure",
+            "snapshot",
+            "seed",
+            "default",
+            "all",
+        ],
+        case_sensitive=False,
+    ),
+    default="default",
+)
+
+selector = click.option(
+    "--selector", envvar=None, help="The selector name to use, as defined in selectors.yml"
+)
+
+show = click.option(
+    "--show", envvar=None, help="Show a sample of the loaded data in the terminal", is_flag=True
+)
+
+skip_profile_setup = click.option(
+    "--skip-profile-setup", "-s", envvar=None, help="Skip interative profile setup.", is_flag=True
+)
+
+# TODO:  The env var and name (reflected in flags) are corrections!
+# The original name was `ARTIFACT_STATE_PATH` and used the env var `DBT_ARTIFACT_STATE_PATH`.
+# Both of which break existing naming conventions.
+# This will need to be fixed before use in the main codebase and communicated as a change to the community!
+state = click.option(
+    "--state",
+    envvar="DBT_STATE",
+    help="If set, use the given directory as the source for json files to compare with this project.",
+    type=click.Path(
+        dir_okay=True,
+        exists=True,
+        file_okay=False,
+        readable=True,
+        resolve_path=True,
+    ),
+)
+
+static_parser = click.option(
+    "--static-parser/--no-static-parser",
+    envvar="DBT_STATIC_PARSER",
+    help="Use the static parser.",
+    default=True,
+)
+
+store_failures = click.option(
+    "--store-failures",
+    envvar="DBT_STORE_FAILURES",
+    help="Store test results (failing rows) in the database",
+    is_flag=True,
+)
+
+target = click.option(
+    "--target", "-t", envvar=None, help="Which target to load for the given profile"
+)
+
+target_path = click.option(
+    "--target-path",
+    envvar="DBT_TARGET_PATH",
+    help="Configure the 'target-path'. Only applies this setting for the current run. Overrides the 'DBT_TARGET_PATH' if it is set.",
+    type=click.Path(),
+)
+
+threads = click.option(
+    "--threads",
+    envvar=None,
+    help="Specify number of threads to use while executing models. Overrides settings in profiles.yml.",
+    default=1,
+    type=click.INT,
+)
+
+use_colors = click.option(
+    "--use-colors/--no-use-colors",
+    envvar="DBT_USE_COLORS",
+    help="Output is colorized by default and may also be set in a profile or at the command line.",
+    default=True,
+)
+
+use_experimental_parser = click.option(
+    "--use-experimental-parser/--no-use-experimental-parser",
+    envvar="DBT_USE_EXPERIMENTAL_PARSER",
+    help="Enable experimental parsing features.",
+)
+
+vars = click.option(
+    "--vars",
+    envvar=None,
+    help="Supply variables to the project. This argument overrides variables defined in your dbt_project.yml file. This argument should be a YAML string, eg. '{my_variable: my_value}'",
+    type=YAML(),
+)
+
+version = click.option(
+    "--version",
+    envvar=None,
+    help="Show version information",
+    is_flag=True,
+)
+
+version_check = click.option(
+    "--version-check/--no-version-check",
+    envvar="DBT_VERSION_CHECK",
+    help="Ensure dbt's version matches the one specified in the dbt_project.yml file ('require-dbt-version')",
+    default=True,
+)
+
+warn_error = click.option(
+    "--warn-error/--no-warn-error",
+    envvar="DBT_WARN_ERROR",
+    help="If dbt would normally warn, instead raise an exception. Examples include --models that selects nothing, deprecations, configurations with no associated models, invalid test configurations, and missing sources/refs in tests.",
+)
+
+write_json = click.option(
+    "--write-json/--no-write-json",
+    envvar="DBT_WRITE_JSON",
+    help="Writing the manifest and run_results.json files to disk",
+    default=True,
+)
+
+write_manifest = click.option(
+    "--write-manifest/--no-write-manifest",
+    envvar=None,
+    help="TODO: No help text currently available",
+    default=True,
+)
diff --git a/core/setup.py b/core/setup.py
index f893b7d5e43..2c254dcf6b0 100644
--- a/core/setup.py
+++ b/core/setup.py
@@ -43,9 +43,7 @@
     include_package_data=True,
     test_suite="test",
     entry_points={
-        "console_scripts": [
-            "dbt = dbt.main:main",
-        ],
+        "console_scripts": ["dbt = dbt.main:main", "dbt-cli = dbt.cli.main:cli"],
     },
     install_requires=[
         "Jinja2==3.1.2",
diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py
new file mode 100644
index 00000000000..07dda952a9d
--- /dev/null
+++ b/tests/unit/test_cli.py
@@ -0,0 +1,50 @@
+import ast
+from inspect import getsource
+
+import click
+from dbt.cli import params
+from dbt.cli.main import cli
+
+
+class TestCLI:
+    def test_commands_have_docstrings(self):
+        def run_test(commands):
+            for command in commands.values():
+                if type(command) is click.Command:
+                    assert command.__doc__ is not None
+                if type(command) is click.Group:
+                    run_test(command.commands)
+
+        run_test(cli.commands)
+
+    # TODO:  This isn't the ideal way to test params as
+    # they will be tested as many times as they are used as decorators.
+    # This is inefficent (obvs)
+    def test_unhidden_params_have_help_texts(self):
+        def run_test(command):
+            for param in command.params:
+                if not param.hidden:
+                    assert param.help is not None
+            if type(command) is click.Group:
+                for command in command.commands.values():
+                    run_test(command)
+
+        run_test(cli)
+
+    def test_param_names_match_envvars(self):
+        def run_test(command):
+            for param in command.params:
+                if param.envvar is not None:
+                    assert "DBT_" + param.name.upper() == param.envvar
+            if type(command) is click.Group:
+                for command in command.commands.values():
+                    run_test(command)
+
+        run_test(cli)
+
+    def test_params_are_alpha_sorted(self):
+        root_node = ast.parse(getsource(params))
+        param_var_names = [
+            node.targets[0].id for node in ast.walk(root_node) if isinstance(node, ast.Assign)
+        ]
+        assert param_var_names == sorted(param_var_names)


Code review comments:
diff_hunk: @@ -0,0 +1,386 @@
+from pathlib import Path, PurePath
+
+import click
+from dbt.cli.option_types import YAML
+
+# TODO:  The name (reflected in flags) is a correction!
+# The original name was `SEND_ANONYMOUS_USAGE_STATS` and used an env var called "DBT_SEND_ANONYMOUS_USAGE_STATS"
+# Both of which break existing naming conventions (doesn't match param flag).
+# This will need to be fixed before use in the main codebase and communicated as a change to the community!
+anonymous_usage_stats = click.option(
+    "--anonymous-usage-stats/--no-anonymous-usage-stats",
+    envvar="DBT_ANONYMOUS_USAGE_STATS",
+    help="Send anonymous usage stats to dbt Labs.",
+    default=True,
+)
+
+args = click.option(
+    "--args",
+    envvar=None,
+    help="Supply arguments to the macro. This dictionary will be mapped to the keyword arguments defined in the selected macro. This argument should be a YAML string, eg. '{my_variable: my_value}'",
+    type=YAML(),
+)
+
+browser = click.option(
+    "--browser/--no-browser",
+    envvar=None,
+    help="Wether or not to open a local web browser after starting the server",
+    default=True,
+)
+
+cache_selected_only = click.option(
+    "--cache-selected-only/--no-cache-selected-only",
+    envvar="DBT_CACHE_SELECTED_ONLY",
+    help="Pre cache database objects relevant to selected resource only.",
+)
+
+compile_docs = click.option(
+    "--compile/--no-compile",
+    envvar=None,
+    help="Wether or not to run 'dbt compile' as part of docs generation",
+    default=True,
+)
+
+compile_parse = click.option(
+    "--compile/--no-compile",
+    envvar=None,
+    help="TODO: No help text currently available",
+    default=True,
+)
+
+config_dir = click.option(
+    "--config-dir",
+    envvar=None,
+    help="If specified, DBT will show path information for this project",
+    type=click.STRING,
+)
+
+debug = click.option(
+    "--debug/--no-debug",
+    "-d/ ",
+    envvar="DBT_DEBUG",
+    help="Display debug logging during dbt execution. Useful for debugging and making bug reports.",
+)
+
+# TODO:  The env var and name (reflected in flags) are corrections!
+# The original name was `DEFER_MODE` and used an env var called "DBT_DEFER_TO_STATE"
+# Both of which break existing naming conventions.
+# This will need to be fixed before use in the main codebase and communicated as a change to the community!
+defer = click.option(
+    "--defer/--no-defer",
+    envvar="DBT_DEFER",
+    help="If set, defer to the state variable for resolving unselected nodes.",
+)
+
+enable_legacy_logger = click.option(
+    "--enable-legacy-logger/--no-enable-legacy-logger",
+    envvar="DBT_ENABLE_LEGACY_LOGGER",
+    hidden=True,
+)
+
+event_buffer_size = click.option(
+    "--event-buffer-size",
+    envvar="DBT_EVENT_BUFFER_SIZE",
+    help="Sets the max number of events to buffer in EVENT_HISTORY.",
+    default=100000,
+    type=click.INT,
+)
+
+exclude = click.option("--exclude", envvar=None, help="Specify the nodes to exclude.")
+
+fail_fast = click.option(
+    "--fail-fast/--no-fail-fast",
+    "-x/ ",
+    envvar="DBT_FAIL_FAST",
+    help="Stop execution on first failure.",
+)
+
+full_refresh = click.option(
+    "--full-refresh",
+    envvar="DBT_FULL_REFRESH",
+    help="If specified, dbt will drop incremental models and fully-recalculate the incremental table from the model definition.",
+    is_flag=True,
+)
+
+indirect_selection = click.option(
+    "--indirect-selection",
+    envvar="DBT_INDIRECT_SELECTION",
+    help="Select all tests that are adjacent to selected resources, even if they those resources have been explicitly selected.",
+    type=click.Choice(["eager", "cautious"], case_sensitive=False),
+    default="eager",
+)
+
+#  TODO: Should this be exposed in the help text?
path: core/dbt/cli/params.py
text: @user1:
I would just add a help text for consistency

@author:
@user2 I believe you did the original work on this feature, got any thoughts on whether or not this needs to be shown in the dbt help text?

@user2:
@user3-fa I'm unclear on why it would _not_ be included?  

Its purpose is to turn on verbose cache logging (for every event fired by `dbt/adapters/cache.py` I believe) if and only if the flag is turned on.

@author:
Well, it's currently not included in the help text so `not included` = `parity`.  Looks like we want to add it so I'll go ahead and do so.

diff_hunk: @@ -0,0 +1,386 @@
+import inspect  # This is temporary for RAT-ing
+import sys
+from pprint import pformat as pf  # This is temporary for RAT-ing
+
+import click
+from dbt.cli import params as p
+from dbt.cli.flags import Flags
+
+
+# dbt
+@click.group(
+    context_settings={"help_option_names": ["-h", "--help"]},
+    invoke_without_command=True,
+    no_args_is_help=True,
+    epilog="Specify one of these sub-commands and you can find more help from there.",
+)
+@click.pass_context
+@p.anonymous_usage_stats
+@p.cache_selected_only
+@p.debug
+@p.enable_legacy_logger
+@p.event_buffer_size
+@p.fail_fast
+@p.log_cache_events
+@p.log_format
+@p.macro_debugging
+@p.partial_parse
+@p.print
+@p.printer_width
+@p.quiet
+@p.record_timing_info
+@p.static_parser
+@p.use_colors
+@p.use_experimental_parser
+@p.version
+@p.version_check
+@p.warn_error
+@p.write_json
+def cli(ctx, **kwargs):
+    """An ELT tool for managing your SQL transformations and data models.
+    For more documentation on these commands, visit: docs.getdbt.com
+    """
+    if kwargs.get("version", False):
+        click.echo(f"`version` called\n ctx.params: {pf(ctx.params)}")
+        sys.exit()
+    else:
+        del ctx.params["version"]
+
+
+# dbt build
+@cli.command("build")
+@click.pass_context
+@p.defer
+@p.exclude
+@p.fail_fast
+@p.full_refresh
+@p.indirect_selection
+@p.log_path
+@p.models
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.show
+@p.state
+@p.store_failures
+@p.target
+@p.target_path
+@p.threads
+@p.vars
+@p.version_check
+def build(ctx, **kwargs):
+    """Run all Seeds, Models, Snapshots, and tests in DAG order"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt clean
+@cli.command("clean")
+@click.pass_context
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.target
+@p.vars
+def clean(ctx, **kwargs):
+    """Delete all folders in the clean-targets list (usually the dbt_packages and target directories.)"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt docs
+@cli.group()
+@click.pass_context
+def docs(ctx, **kwargs):
+    """Generate or serve the documentation website for your project"""
+
+
+# dbt docs generate
+@docs.command("generate")
+@click.pass_context
+@p.compile_docs
+@p.defer
+@p.exclude
+@p.log_path
+@p.models
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.state
+@p.target
+@p.target_path
+@p.threads
+@p.vars
+@p.version_check
+def docs_generate(ctx, **kwargs):
+    """Generate the documentation website for your project"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt docs serve
+@docs.command("serve")
+@click.pass_context
+@p.browser
+@p.port
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.target
+@p.vars
+def docs_serve(ctx, **kwargs):
+    """Serve the documentation website for your project"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt compile
+@cli.command("compile")
+@click.pass_context
+@p.defer
+@p.exclude
+@p.full_refresh
+@p.log_path
+@p.models
+@p.parse_only
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.selector
+@p.state
+@p.target
+@p.target_path
+@p.threads
+@p.vars
+@p.version_check
+def compile(ctx, **kwargs):
+    """Generates executable SQL from source, model, test, and analysis files. Compiled SQL files are written to the target/ directory."""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt debug
+@cli.command("debug")
+@click.pass_context
+@p.config_dir
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.target
+@p.vars
+@p.version_check
+def debug(ctx, **kwargs):
+    """Show some helpful information about dbt for debugging. Not to be confused with the --debug option which increases verbosity."""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt deps
+@cli.command("deps")
+@click.pass_context
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.target
+@p.vars
+def deps(ctx, **kwargs):
+    """Pull the most recent version of the dependencies listed in packages.yml"""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt init
+@cli.command("init")
+@click.pass_context
+@p.profile
+@p.profiles_dir
+@p.project_dir
+@p.skip_profile_setup
+@p.target
+@p.vars
+def init(ctx, **kwargs):
+    """Initialize a new DBT project."""
+    flags = Flags()
+    click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}")
+
+
+# dbt list
+# TODO: Figure out aliasing for ls (or just c/p?)
path: core/dbt/cli/main.py
text: @user1:
https://click.palletsprojects.com/en/8.1.x/advanced/ I am sure you already found this but just throw it here to make it easier to resolve all TODOs

@author:
I found a better way around aliases (I think).  Check out the cli_runner function at the top.

diff_hunk: @@ -0,0 +1,46 @@
+# TODO  Move this to /core/dbt/flags.py when we're ready to break things
+import os
+from dataclasses import dataclass
+from multiprocessing import get_context
+from pprint import pformat as pf
+
+from click import get_current_context
+
+if os.name != "nt":
+    # https://bugs.python.org/issue41567
+    import multiprocessing.popen_spawn_posix  # type: ignore  # noqa: F401
+
+
+@dataclass(frozen=True)
+class Flags:
+    def __init__(self, ctx=None) -> None:
+
+        if ctx is None:
+            ctx = get_current_context()
+
+        def assign_params(ctx):
+            """Recursively adds all click params to flag object"""
+            for param_name, param_value in ctx.params.items():
+                # N.B. You have to use the base MRO method (object.__setattr__) to set attributes
+                # when using frozen dataclasses.
+                # https://docs.python.org/3/library/dataclasses.html#frozen-instances
+                if hasattr(self, param_name):
+                    raise Exception(f"Duplicate flag names found in click command: {param_name}")
+                object.__setattr__(self, param_name.upper(), param_value)
+            if ctx.parent:
+                assign_params(ctx.parent)
+
+        assign_params(ctx)
+
+        # Hard coded flags
+        #  TODO: This is listed as for backwards compat... Still needed?
path: core/dbt/cli/flags.py
text: @author:
@user1 Do you know if we still need to track `STRICT_MODE`?  It appears unused in the core codebase...

@user1:
It's been unused for some time, and we officially deprecated it in v1.0, so no need to include it
https://docs.getdbt.com/reference/global-configs#strict