reflex-hosting-cli 0.1.71a2__tar.gz → 0.1.72__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/CHANGELOG.md +12 -3
  2. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/PKG-INFO +1 -1
  3. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/utils/console.py +20 -6
  4. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/utils/hosting.py +71 -15
  5. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/utils/log.py +40 -1
  6. reflex_hosting_cli-0.1.72/src/reflex_cli/utils/output.py +202 -0
  7. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/apps.py +185 -121
  8. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/auth.py +3 -9
  9. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/cli.py +6 -5
  10. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/deploy.py +2 -6
  11. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/deployments.py +6 -0
  12. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/gcp.py +25 -7
  13. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/project.py +46 -92
  14. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/providers.py +7 -31
  15. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/scan.py +4 -16
  16. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/secrets.py +26 -29
  17. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/vmtypes_regions.py +23 -33
  18. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/.gitignore +0 -0
  19. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/README.md +0 -0
  20. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/news/.gitkeep +0 -0
  21. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/pyproject.toml +0 -0
  22. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/__init__.py +0 -0
  23. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/cli.py +0 -0
  24. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/constants/__init__.py +0 -0
  25. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/constants/base.py +0 -0
  26. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/constants/compiler.py +0 -0
  27. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/constants/hosting.py +0 -0
  28. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/constants/log_level.py +0 -0
  29. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/core/__init__.py +0 -0
  30. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/core/config.py +0 -0
  31. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/deployments.py +0 -0
  32. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/utils/__init__.py +0 -0
  33. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/utils/cli_options.py +0 -0
  34. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/utils/dependency.py +0 -0
  35. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/utils/exceptions.py +0 -0
  36. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/__init__.py +0 -0
  37. {reflex_hosting_cli-0.1.71a2 → reflex_hosting_cli-0.1.72}/src/reflex_cli/v2/utils.py +0 -0
@@ -1,11 +1,19 @@
1
- ## v0.1.71a2 (2026-08-28)
1
+ ## v0.1.72 (2026-09-11)
2
+
3
+ ### Breaking Changes
4
+
5
+ - Two `reflex cloud` defaults changed. `--interactive` now defaults to whether stdout is a terminal rather than to on, so a pipe, a CI job or an agent is refused with an error instead of waiting at a prompt that nobody answers -- `reflex cloud apps list` in CI with no token now exits 1 with "Token is required for non-interactive mode." Pass `--interactive` to restore the old behavior. And `reflex cloud apps logs --follow` now defaults to off, since following prompts between pages and so never returns on its own; pass `--follow true` for the old behavior. `reflex deploy` takes the same terminal-derived `--interactive`; it keeps `--json` for log records only, since its progress is a stream rather than a result. ([#6917](https://github.com/reflex-dev/reflex/issues/6917))
6
+
7
+ ### Features
8
+
9
+ - Every `reflex cloud` command now takes `--json`, writing one JSON document to stdout while human-readable messages move to stderr, so the output is parseable without reading a Rich table. Note that a message only lands on stderr if the installed `reflex-base` carries the reservation: against an older one, `--loglevel debug` still writes its records to stdout and they precede the document. ([#6917](https://github.com/reflex-dev/reflex/issues/6917))
2
10
 
3
11
  ### Bug Fixes
4
12
 
5
- - Fix for older reflex versions
13
+ - Fix version comparison bug that broke compatibility with reflex-0.7.6.post1 ([#6963](https://github.com/reflex-dev/reflex/issues/6963))
6
14
 
7
15
 
8
- ## v0.1.71a1 (2026-08-27)
16
+ ## v0.1.71 (2026-08-28)
9
17
 
10
18
  ### Breaking Changes
11
19
 
@@ -21,6 +29,7 @@
21
29
  ### Bug Fixes
22
30
 
23
31
  - The hosting config file (`hosting_v1.json`) is now written atomically. `save_token_to_config` and `delete_token_from_config` opened it with mode `"w"`, truncating it before writing, so a failed write — a full disk, an I/O error, an interrupted process — left an empty file and destroyed the stored access token and selected project. Neither helper reports write failures to the caller (`save_token_to_config` logs a warning, `delete_token_from_config` only a debug message), so this was easy to miss. Both now serialize to a temporary file alongside the target and move it into place, leaving the existing credentials untouched when a write fails. A config that exists but cannot be read is no longer treated as empty either, so `delete_token_from_config` leaves a malformed file alone instead of replacing it; `save_token_to_config` still starts fresh from one, so a corrupt config cannot block re-authenticating. This also covers `reflex login` and `reflex logout`, which share these helpers. ([#6918](https://github.com/reflex-dev/reflex/issues/6918))
32
+ - Fix for older reflex versions
24
33
 
25
34
  ### Miscellaneous
26
35
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: reflex-hosting-cli
3
- Version: 0.1.71a2
3
+ Version: 0.1.72
4
4
  Summary: Reflex Hosting CLI
5
5
  Author-email: Nikhil Rao <nikhil@reflex.dev>, Alek Petuskey <alek@reflex.dev>
6
6
  Maintainer-email: Simon Young <simon@reflex.dev>, Khaleel Al-Adhami <khaleel@reflex.dev>
@@ -11,7 +11,7 @@ from collections.abc import Sequence
11
11
  from typing import overload
12
12
 
13
13
  from reflex_cli.constants.base import LogLevel
14
- from reflex_cli.utils.log import HAS_REFLEX_BASE, is_json_mode
14
+ from reflex_cli.utils.log import HAS_REFLEX_BASE, is_json_mode, is_stdout_reserved
15
15
  from reflex_cli.utils.log import set_log_level as _set_log_level
16
16
 
17
17
  if HAS_REFLEX_BASE:
@@ -28,6 +28,16 @@ else:
28
28
  from rich.table import Table
29
29
 
30
30
  _console = Console(highlight=False)
31
+ _console_stderr = Console(stderr=True, highlight=False)
32
+
33
+ def _human_console() -> Console:
34
+ """Resolve the console human-readable output belongs on.
35
+
36
+ Returns:
37
+ The stderr console while stdout is carrying a machine-readable
38
+ document, and the stdout console otherwise.
39
+ """
40
+ return _console_stderr if is_stdout_reserved() else _console
31
41
 
32
42
  def print(msg: str, **kwargs):
33
43
  """Print a message.
@@ -36,7 +46,7 @@ else:
36
46
  msg: The message to print.
37
47
  kwargs: Keyword arguments to pass to the print function.
38
48
  """
39
- _console.print(msg, **kwargs)
49
+ _human_console().print(msg, **kwargs)
40
50
 
41
51
  def print_table(
42
52
  tabular_data: list[list[str]],
@@ -60,7 +70,7 @@ else:
60
70
  for row in tabular_data:
61
71
  table.add_row(*row)
62
72
 
63
- _console.print(table)
73
+ _human_console().print(table)
64
74
 
65
75
  def rule(title: str, **kwargs):
66
76
  """Print a horizontal rule with a title.
@@ -69,7 +79,7 @@ else:
69
79
  title: The title of the rule.
70
80
  kwargs: Keyword arguments to pass to the print function.
71
81
  """
72
- _console.rule(title, **kwargs)
82
+ _human_console().rule(title, **kwargs)
73
83
 
74
84
  @overload
75
85
  def ask(
@@ -105,7 +115,11 @@ else:
105
115
  A string with the user input.
106
116
  """
107
117
  return Prompt.ask(
108
- question, choices=choices, default=default, show_choices=show_choices
118
+ question,
119
+ choices=choices,
120
+ default=default,
121
+ show_choices=show_choices,
122
+ console=_human_console(),
109
123
  )
110
124
 
111
125
  def progress():
@@ -130,7 +144,7 @@ else:
130
144
  Returns:
131
145
  A new status.
132
146
  """
133
- return _console.status(*args, **kwargs)
147
+ return _human_console().status(*args, **kwargs)
134
148
 
135
149
 
136
150
  def set_log_level(log_level: LogLevel | str | None):
@@ -2640,20 +2640,24 @@ def get_app_logs(
2640
2640
  timeout=constants.Hosting.TIMEOUT,
2641
2641
  )
2642
2642
  response.raise_for_status()
2643
+ # None rather than an empty list on every path that failed to read: a
2644
+ # caller cannot tell "there are no logs" from "we could not fetch them" out
2645
+ # of `[]`, and the two call for opposite next steps -- one is an answer,
2646
+ # the other is worth retrying.
2643
2647
  except httpx.RequestError:
2644
- return []
2648
+ return None
2645
2649
  except httpx.HTTPStatusError as ex:
2646
2650
  try:
2647
2651
  ex_details = ex.response.json().get("detail")
2648
2652
  except json.JSONDecodeError:
2649
- return []
2653
+ return None
2650
2654
  else:
2651
2655
  return f"get app logs failed: {ex_details}"
2652
2656
  else:
2653
2657
  try:
2654
2658
  return response.json()
2655
2659
  except json.JSONDecodeError:
2656
- return []
2660
+ return None
2657
2661
 
2658
2662
 
2659
2663
  def list_apps(client: AuthenticatedClient, project: str | None = None) -> list[dict]:
@@ -3012,6 +3016,54 @@ def _report_deployment_failure(
3012
3016
  )
3013
3017
 
3014
3018
 
3019
+ # The phrases a deployment status is read for. Named here because two callers
3020
+ # classify the same string -- the watch loop below, which decides how to report
3021
+ # a failure, and `reflex cloud apps status`, which states one as a boolean an
3022
+ # agent branches on. A marker spelled twice is two answers waiting to disagree.
3023
+ _STATUS_BUILD_ERROR = "build error"
3024
+ _STATUS_UNKNOWN_ID = "unable to find status for given id"
3025
+ _STATUS_ERROR = "error"
3026
+ _STATUS_BAD_RESPONSE = "bad response"
3027
+ _STATUS_SUCCEEDED = "completed successfully"
3028
+ _STATUS_AWAITING_APPROVAL = "AwaitingApproval"
3029
+ # "failed" is not a phrase the watch loop tests for -- "error" already covers
3030
+ # the statuses it sees -- and is kept because it is what this predicate
3031
+ # replaced. Dropping it could only ever make the answer less strict.
3032
+ _STATUS_FAILED = "failed"
3033
+
3034
+
3035
+ def deployment_status_failed(status: str) -> bool:
3036
+ """Check whether a deployment status reports a failure.
3037
+
3038
+ Success is asked first, the order `watch_deployment_status` settles it in:
3039
+ a status that has completed is a completed one whatever else it mentions.
3040
+
3041
+ Args:
3042
+ status: The status string the hosting service returned.
3043
+
3044
+ Returns:
3045
+ True if the status says the deployment did not make it.
3046
+ """
3047
+ if any(
3048
+ marker in status
3049
+ for marker in (
3050
+ _STATUS_SUCCEEDED,
3051
+ _STATUS_AWAITING_APPROVAL,
3052
+ _STATUS_BAD_RESPONSE,
3053
+ )
3054
+ ):
3055
+ return False
3056
+ return any(
3057
+ marker in status
3058
+ for marker in (
3059
+ _STATUS_BUILD_ERROR,
3060
+ _STATUS_UNKNOWN_ID,
3061
+ _STATUS_ERROR,
3062
+ _STATUS_FAILED,
3063
+ )
3064
+ )
3065
+
3066
+
3015
3067
  def watch_deployment_status(deployment_id: str, client: AuthenticatedClient) -> bool:
3016
3068
  """Continuously watch the status of a specific deployment.
3017
3069
 
@@ -3035,31 +3087,31 @@ def watch_deployment_status(deployment_id: str, client: AuthenticatedClient) ->
3035
3087
  status = _get_deployment_status(
3036
3088
  deployment_id=deployment_id, token=client.token
3037
3089
  )
3038
- if "completed successfully" in status:
3090
+ if _STATUS_SUCCEEDED in status:
3039
3091
  logger.log(log.SUCCESS, status)
3040
3092
  break
3041
- if "AwaitingApproval" in status:
3093
+ if _STATUS_AWAITING_APPROVAL in status:
3042
3094
  logger.log(
3043
3095
  log.SUCCESS,
3044
3096
  "build submitted for approval; it will deploy automatically once an approver approves it.",
3045
3097
  )
3046
3098
  break
3047
- if "build error" in status:
3099
+ if _STATUS_BUILD_ERROR in status:
3048
3100
  _report_deployment_failure(
3049
3101
  deployment_id, client.token, status, offer_build_logs=True
3050
3102
  )
3051
3103
  return False
3052
- if "unable to find status for given id" in status:
3104
+ if _STATUS_UNKNOWN_ID in status:
3053
3105
  # Not a failed deployment but an id that resolves to nothing,
3054
3106
  # so there is no row to report on and nothing to ask for.
3055
3107
  logger.error(status)
3056
3108
  return False
3057
- if "error" in status:
3109
+ if _STATUS_ERROR in status:
3058
3110
  _report_deployment_failure(
3059
3111
  deployment_id, client.token, status, offer_build_logs=False
3060
3112
  )
3061
3113
  return False
3062
- if "bad response" in status:
3114
+ if _STATUS_BAD_RESPONSE in status:
3063
3115
  logger.warning(status)
3064
3116
  return True
3065
3117
  if status != current_status:
@@ -3285,13 +3337,16 @@ def read_config(
3285
3337
  return Config.from_yaml_or_toml_or_none()
3286
3338
 
3287
3339
 
3288
- def generate_config(interactive: bool = True, token: str | None = None):
3340
+ def generate_config(interactive: bool = True, token: str | None = None) -> Path | None:
3289
3341
  """Generate the config file with app-based prefilling.
3290
3342
 
3291
3343
  Args:
3292
3344
  interactive: Whether to use interactive mode for authentication and app selection.
3293
3345
  token: An existing authentication token to use instead of interactive auth.
3294
3346
 
3347
+ Returns:
3348
+ The path of the config file written, or None if none was.
3349
+
3295
3350
  Raises:
3296
3351
  click.exceptions.Exit: If authentication fails or user cancels operation.
3297
3352
  """
@@ -3299,11 +3354,12 @@ def generate_config(interactive: bool = True, token: str | None = None):
3299
3354
  import yaml
3300
3355
  except ImportError:
3301
3356
  logger.error("Please install PyYAML to use this command: pip install pyyaml")
3302
- return
3357
+ return None
3303
3358
 
3304
- if Path("cloud.yml").exists():
3359
+ config_path = Path("cloud.yml")
3360
+ if config_path.exists():
3305
3361
  logger.error("cloud.yml already exists.")
3306
- return
3362
+ return None
3307
3363
 
3308
3364
  try:
3309
3365
  authenticated_client = get_authenticated_client(
@@ -3344,13 +3400,13 @@ def generate_config(interactive: bool = True, token: str | None = None):
3344
3400
  )
3345
3401
  default = {"name": current_dir_name}
3346
3402
 
3347
- with Path("cloud.yml").open("w") as config_file:
3403
+ with config_path.open("w") as config_file:
3348
3404
  yaml.dump(default, config_file, default_flow_style=False, sort_keys=False)
3349
3405
  logger.log(log.SUCCESS, "cloud.yml created successfully.")
3350
3406
  logger.info(
3351
3407
  "For more configuration options, see: https://reflex.dev/docs/hosting/config-file/"
3352
3408
  )
3353
- return
3409
+ return config_path
3354
3410
 
3355
3411
 
3356
3412
  def log_out_on_browser():
@@ -82,8 +82,12 @@ except ImportError:
82
82
  """
83
83
  try:
84
84
  style, prefix = _style_for_level(record.levelno)
85
+ # Errors always go to stderr; everything else joins them there
86
+ # while stdout is carrying a document.
85
87
  console = (
86
- _console_stderr if record.levelno >= logging.ERROR else _console
88
+ _console_stderr
89
+ if record.levelno >= logging.ERROR or is_stdout_reserved()
90
+ else _console
87
91
  )
88
92
  # Markup is opt-in per record (``extra={"rich": True}``); plain
89
93
  # messages keep their literal brackets.
@@ -129,3 +133,38 @@ except ImportError:
129
133
  # no-op when the handler is already attached, so this stays idempotent.
130
134
  _CLI_LOGGER.propagate = False
131
135
  _CLI_LOGGER.addHandler(_handler)
136
+
137
+
138
+ # Asked separately from the names above, and that separation is the whole point.
139
+ # The reservation is younger than the rest of this shim: every published
140
+ # reflex-base exports SUCCESS, is_json_mode and set_log_level, and none of them
141
+ # exports these two. Importing all five together sent an installation with a
142
+ # perfectly good reflex-base down the fallback path entirely -- losing its
143
+ # console, its log parenting and is_json_mode to acquire a feature it was only
144
+ # ever meant to go without.
145
+ try:
146
+ from reflex_base.utils.log import is_stdout_reserved as is_stdout_reserved
147
+ from reflex_base.utils.log import reserve_stdout as reserve_stdout
148
+
149
+ except ImportError:
150
+ # Tracked here rather than delegated, so `--json` keeps its stdout even
151
+ # against a reflex-base that has never heard of the reservation. Only this
152
+ # shim's own sinks consult it; a reflex-base console cannot be told.
153
+ _stdout_reserved = False
154
+
155
+ def reserve_stdout(reserved: bool = True) -> None:
156
+ """Reserve stdout for a machine-readable document.
157
+
158
+ Args:
159
+ reserved: Whether stdout carries data rather than human output.
160
+ """
161
+ global _stdout_reserved
162
+ _stdout_reserved = reserved
163
+
164
+ def is_stdout_reserved() -> bool:
165
+ """Check whether stdout is reserved for machine-readable output.
166
+
167
+ Returns:
168
+ True while a document owns stdout.
169
+ """
170
+ return _stdout_reserved
@@ -0,0 +1,202 @@
1
+ """Machine-readable output, and the shared options that turn it on.
2
+
3
+ An agent driving the cloud CLI needs two things a person at a terminal does
4
+ not: output it can parse without regexing a Rich table, and the certainty that
5
+ nothing will stop and wait for a keystroke. ``--json`` answers the first and
6
+ reserves stdout for the document while it does; ``--interactive`` answers the
7
+ second by defaulting to whether stdout is a terminal.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import sys
14
+ from collections.abc import Sequence
15
+ from typing import Any
16
+
17
+ import click
18
+
19
+ # Through the CLI's own shim, not reflex_base directly: the hosting CLI has to
20
+ # import against a reflex-base that predates these functions, which is what
21
+ # tests/units/reflex_cli/utils/test_log.py pins.
22
+ from reflex_cli.utils import log
23
+
24
+ # The spellings that ask for JSON on the command line, and the one that
25
+ # refuses it. Read straight off argv so the group callback can reserve stdout
26
+ # before click has parsed the subcommand's options -- anything it says would
27
+ # otherwise land on stdout ahead of the document.
28
+ _JSON_FLAGS = frozenset({"--json", "-j"})
29
+ _JSON_SHORT = "j"
30
+ _NO_JSON_FLAG = "--no-json"
31
+
32
+
33
+ def _json_flag_state(arg: str) -> bool | None:
34
+ """Read what one command-line argument says about JSON output.
35
+
36
+ Args:
37
+ arg: A single command-line argument.
38
+
39
+ Returns:
40
+ True if it asks for JSON, False if it refuses it, None if it says
41
+ nothing either way.
42
+ """
43
+ if arg == _NO_JSON_FLAG:
44
+ return False
45
+ if arg in _JSON_FLAGS:
46
+ return True
47
+ # Short flags combine, so `-ij` is `-i -j`. Reading them means this scan
48
+ # can also fire on a `-j` that click would take as some other option's
49
+ # value, which is the direction to be wrong in: a message on stderr costs
50
+ # a little context, one inside the document costs the whole parse.
51
+ return (
52
+ True
53
+ if len(arg) > 1
54
+ and arg.startswith("-")
55
+ and not arg.startswith("--")
56
+ and _JSON_SHORT in arg[1:]
57
+ else None
58
+ )
59
+
60
+
61
+ def stdout_is_tty() -> bool:
62
+ """Check whether stdout is attached to a terminal.
63
+
64
+ Returns:
65
+ True if somebody is plausibly watching, False under a pipe, a CI job
66
+ or an agent.
67
+ """
68
+ isatty = getattr(sys.stdout, "isatty", None)
69
+ if isatty is None:
70
+ return False
71
+ try:
72
+ return bool(isatty())
73
+ except ValueError:
74
+ # A closed stream. Nobody is answering a prompt on it either way.
75
+ return False
76
+
77
+
78
+ def _resolve_interactive(
79
+ ctx: click.Context, param: click.Parameter, value: bool | None
80
+ ) -> bool:
81
+ """Resolve an unset ``--interactive`` against the terminal.
82
+
83
+ Args:
84
+ ctx: The click context.
85
+ param: The click parameter.
86
+ value: The flag's value, or None when neither spelling was passed.
87
+
88
+ Returns:
89
+ Whether the command may prompt.
90
+ """
91
+ return stdout_is_tty() if value is None else value
92
+
93
+
94
+ interactive_option = click.option(
95
+ "--interactive/--no-interactive",
96
+ "-i/",
97
+ "interactive",
98
+ default=None,
99
+ callback=_resolve_interactive,
100
+ help="Whether to prompt for confirmations and choices. Defaults to on when "
101
+ "stdout is a terminal and off otherwise, so a pipe, a CI job or an agent is "
102
+ "never left waiting at a prompt.",
103
+ )
104
+
105
+
106
+ def json_requested(argv: Sequence[str] | None = None) -> bool:
107
+ """Check whether a command line asks for JSON output.
108
+
109
+ Scanned back to front, so the last flag decides -- the same answer click
110
+ reaches for a boolean flag pair, which a set membership test cannot give:
111
+ ``--no-json --json`` enables JSON and ``--json --no-json`` does not.
112
+
113
+ Args:
114
+ argv: The arguments to inspect; defaults to this process's own.
115
+
116
+ Returns:
117
+ True if the command line asks for JSON output.
118
+ """
119
+ args = sys.argv[1:] if argv is None else argv
120
+ for arg in reversed(list(args)):
121
+ if (state := _json_flag_state(arg)) is not None:
122
+ return state
123
+ return False
124
+
125
+
126
+ def _hold_reservation(ctx: click.Context, reserved: bool) -> None:
127
+ """Reserve stdout for this context, releasing it again when it closes.
128
+
129
+ The reservation is process-global, so without an explicit release a
130
+ ``--json`` command leaves every later log line in the process writing to
131
+ stderr -- which a CLI process never notices, and an embedding one or a
132
+ second run in the same interpreter does.
133
+
134
+ Args:
135
+ ctx: The click context whose lifetime the reservation follows.
136
+ reserved: Whether stdout carries data rather than human output.
137
+ """
138
+ previous = log.is_stdout_reserved()
139
+ log.reserve_stdout(reserved)
140
+ ctx.call_on_close(lambda: log.reserve_stdout(previous))
141
+
142
+
143
+ def reserve_stdout_for_argv(
144
+ argv: Sequence[str] | None = None, *, ctx: click.Context | None = None
145
+ ) -> None:
146
+ """Reserve stdout up front when the command line asks for JSON.
147
+
148
+ Always writes the reservation rather than only setting it, so a long-lived
149
+ process (tests, an embedded runner) cannot inherit the previous
150
+ invocation's answer.
151
+
152
+ Args:
153
+ argv: The arguments to inspect; defaults to this process's own.
154
+ ctx: The click context to release the reservation with, if there is one.
155
+ """
156
+ reserved = json_requested(argv)
157
+ if ctx is None:
158
+ log.reserve_stdout(reserved)
159
+ return
160
+ _hold_reservation(ctx, reserved)
161
+
162
+
163
+ def _reserve_stdout(ctx: click.Context, param: click.Parameter, value: bool) -> bool:
164
+ """Reserve stdout for the document once ``--json`` is parsed.
165
+
166
+ Args:
167
+ ctx: The click context.
168
+ param: The click parameter.
169
+ value: Whether JSON output was asked for.
170
+
171
+ Returns:
172
+ The flag's value, unchanged.
173
+ """
174
+ if value:
175
+ _hold_reservation(ctx, True)
176
+ return value
177
+
178
+
179
+ json_option = click.option(
180
+ "--json/--no-json",
181
+ "-j",
182
+ "as_json",
183
+ is_flag=True,
184
+ is_eager=True,
185
+ callback=_reserve_stdout,
186
+ help="Output the result as a single JSON document on stdout. Human-readable "
187
+ "messages go to stderr instead, so stdout stays parseable.",
188
+ )
189
+
190
+
191
+ def print_json(payload: Any) -> None:
192
+ """Write one JSON document to stdout.
193
+
194
+ Deliberately not routed through :mod:`reflex_cli.utils.console`: this is the
195
+ output the command was asked for, not a message about it, so it goes to
196
+ stdout even while the console renders to stderr, and is never wrapped in a
197
+ log record.
198
+
199
+ Args:
200
+ payload: The value to serialize.
201
+ """
202
+ click.echo(json.dumps(payload, default=str))