bijux-cli 0.1.0__py3-none-any.whl

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 (108) hide show
  1. bijux_cli/__init__.py +39 -0
  2. bijux_cli/__main__.py +417 -0
  3. bijux_cli/__version__.py +42 -0
  4. bijux_cli/api.py +326 -0
  5. bijux_cli/cli.py +68 -0
  6. bijux_cli/commands/__init__.py +170 -0
  7. bijux_cli/commands/audit.py +293 -0
  8. bijux_cli/commands/config/__init__.py +85 -0
  9. bijux_cli/commands/config/clear.py +124 -0
  10. bijux_cli/commands/config/export.py +148 -0
  11. bijux_cli/commands/config/get.py +141 -0
  12. bijux_cli/commands/config/list_cmd.py +127 -0
  13. bijux_cli/commands/config/load.py +128 -0
  14. bijux_cli/commands/config/py.typed +0 -0
  15. bijux_cli/commands/config/reload.py +125 -0
  16. bijux_cli/commands/config/service.py +107 -0
  17. bijux_cli/commands/config/set.py +260 -0
  18. bijux_cli/commands/config/unset.py +140 -0
  19. bijux_cli/commands/dev/__init__.py +46 -0
  20. bijux_cli/commands/dev/di.py +285 -0
  21. bijux_cli/commands/dev/list_plugins.py +66 -0
  22. bijux_cli/commands/dev/py.typed +0 -0
  23. bijux_cli/commands/dev/service.py +120 -0
  24. bijux_cli/commands/docs.py +329 -0
  25. bijux_cli/commands/doctor.py +181 -0
  26. bijux_cli/commands/help.py +436 -0
  27. bijux_cli/commands/history/__init__.py +43 -0
  28. bijux_cli/commands/history/clear.py +160 -0
  29. bijux_cli/commands/history/py.typed +0 -0
  30. bijux_cli/commands/history/service.py +362 -0
  31. bijux_cli/commands/memory/__init__.py +59 -0
  32. bijux_cli/commands/memory/clear.py +114 -0
  33. bijux_cli/commands/memory/delete.py +144 -0
  34. bijux_cli/commands/memory/get.py +146 -0
  35. bijux_cli/commands/memory/list.py +115 -0
  36. bijux_cli/commands/memory/py.typed +0 -0
  37. bijux_cli/commands/memory/service.py +242 -0
  38. bijux_cli/commands/memory/set.py +136 -0
  39. bijux_cli/commands/memory/utils.py +54 -0
  40. bijux_cli/commands/plugins/__init__.py +59 -0
  41. bijux_cli/commands/plugins/check.py +240 -0
  42. bijux_cli/commands/plugins/info.py +143 -0
  43. bijux_cli/commands/plugins/install.py +327 -0
  44. bijux_cli/commands/plugins/list.py +73 -0
  45. bijux_cli/commands/plugins/py.typed +0 -0
  46. bijux_cli/commands/plugins/scaffold.py +287 -0
  47. bijux_cli/commands/plugins/uninstall.py +206 -0
  48. bijux_cli/commands/plugins/utils.py +158 -0
  49. bijux_cli/commands/py.typed +0 -0
  50. bijux_cli/commands/repl.py +709 -0
  51. bijux_cli/commands/sleep.py +169 -0
  52. bijux_cli/commands/status.py +289 -0
  53. bijux_cli/commands/utilities.py +637 -0
  54. bijux_cli/commands/version.py +152 -0
  55. bijux_cli/contracts/__init__.py +48 -0
  56. bijux_cli/contracts/audit.py +66 -0
  57. bijux_cli/contracts/config.py +127 -0
  58. bijux_cli/contracts/context.py +93 -0
  59. bijux_cli/contracts/docs.py +65 -0
  60. bijux_cli/contracts/doctor.py +32 -0
  61. bijux_cli/contracts/emitter.py +58 -0
  62. bijux_cli/contracts/history.py +101 -0
  63. bijux_cli/contracts/memory.py +76 -0
  64. bijux_cli/contracts/observability.py +94 -0
  65. bijux_cli/contracts/process.py +48 -0
  66. bijux_cli/contracts/py.typed +0 -0
  67. bijux_cli/contracts/registry.py +115 -0
  68. bijux_cli/contracts/retry.py +53 -0
  69. bijux_cli/contracts/serializer.py +100 -0
  70. bijux_cli/contracts/telemetry.py +46 -0
  71. bijux_cli/core/__init__.py +44 -0
  72. bijux_cli/core/constants.py +21 -0
  73. bijux_cli/core/context.py +218 -0
  74. bijux_cli/core/di.py +695 -0
  75. bijux_cli/core/engine.py +199 -0
  76. bijux_cli/core/enums.py +52 -0
  77. bijux_cli/core/exceptions.py +184 -0
  78. bijux_cli/core/paths.py +21 -0
  79. bijux_cli/core/py.typed +0 -0
  80. bijux_cli/httpapi.py +552 -0
  81. bijux_cli/infra/__init__.py +43 -0
  82. bijux_cli/infra/emitter.py +140 -0
  83. bijux_cli/infra/observability.py +148 -0
  84. bijux_cli/infra/process.py +177 -0
  85. bijux_cli/infra/py.typed +0 -0
  86. bijux_cli/infra/retry.py +300 -0
  87. bijux_cli/infra/serializer.py +573 -0
  88. bijux_cli/infra/telemetry.py +159 -0
  89. bijux_cli/py.typed +0 -0
  90. bijux_cli/services/__init__.py +233 -0
  91. bijux_cli/services/audit.py +268 -0
  92. bijux_cli/services/config.py +677 -0
  93. bijux_cli/services/docs.py +170 -0
  94. bijux_cli/services/doctor.py +34 -0
  95. bijux_cli/services/history.py +527 -0
  96. bijux_cli/services/memory.py +129 -0
  97. bijux_cli/services/plugins/__init__.py +346 -0
  98. bijux_cli/services/plugins/entrypoints.py +151 -0
  99. bijux_cli/services/plugins/groups.py +177 -0
  100. bijux_cli/services/plugins/hooks.py +113 -0
  101. bijux_cli/services/plugins/py.typed +0 -0
  102. bijux_cli/services/plugins/registry.py +284 -0
  103. bijux_cli/services/py.typed +0 -0
  104. bijux_cli/services/utils.py +68 -0
  105. bijux_cli-0.1.0.dist-info/METADATA +297 -0
  106. bijux_cli-0.1.0.dist-info/RECORD +108 -0
  107. bijux_cli-0.1.0.dist-info/WHEEL +4 -0
  108. bijux_cli-0.1.0.dist-info/entry_points.txt +2 -0
bijux_cli/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright © 2025 Bijan Mousavi
3
+
4
+ """The top-level package for the Bijux CLI application.
5
+
6
+ This module serves as the main public entry point for the `bijux-cli` package.
7
+ It exposes the key components required for both command-line execution and
8
+ programmatic integration.
9
+
10
+ The primary exports are:
11
+ * `entry_point`: The function used by `console_scripts` to start the CLI.
12
+ * `BijuxAPI`: A high-level, synchronous facade for using the CLI's
13
+ functionality within other Python applications.
14
+ * `version` and `api_version`: The application and plugin API versions.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from bijux_cli.__main__ import main
20
+ from bijux_cli.__version__ import api_version, version
21
+ from bijux_cli.api import BijuxAPI
22
+
23
+
24
+ def entry_point() -> int | None:
25
+ """The primary entry point for the `console_scripts` definition.
26
+
27
+ This function calls the main CLI orchestrator and catches `SystemExit`
28
+ exceptions to ensure a proper integer exit code is returned to the shell.
29
+
30
+ Returns:
31
+ int | None: The integer exit code of the CLI process.
32
+ """
33
+ try:
34
+ return main()
35
+ except SystemExit as exc:
36
+ return int(exc.code or 0)
37
+
38
+
39
+ __all__ = ["version", "api_version", "BijuxAPI", "entry_point"]
bijux_cli/__main__.py ADDED
@@ -0,0 +1,417 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright © 2025 Bijan Mousavi
3
+
4
+ """Provides the main entry point and lifecycle orchestration for the Bijux CLI.
5
+
6
+ This module is the primary entry point when the CLI is executed. It is
7
+ responsible for orchestrating the entire lifecycle of a command invocation,
8
+ from initial setup to final exit.
9
+
10
+ Key responsibilities include:
11
+ * **Environment Setup:** Configures structured logging (`structlog`) and
12
+ disables terminal colors for tests.
13
+ * **Argument Pre-processing:** Cleans and validates command-line arguments
14
+ before they are passed to the command parser.
15
+ * **Service Initialization:** Initializes the dependency injection container,
16
+ registers all default services, and starts the core `Engine`.
17
+ * **Application Assembly:** Builds the main `Typer` application, including
18
+ all commands and dynamic plugins.
19
+ * **Execution and Error Handling:** Invokes the Typer application, catches
20
+ all top-level exceptions (including `Typer` errors, custom `CommandError`
21
+ exceptions, and `KeyboardInterrupt`), and translates them into
22
+ structured error messages and standardized exit codes.
23
+ * **History Recording:** Persists the command to the history service after
24
+ execution.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import contextlib
30
+ from contextlib import suppress
31
+ import importlib.metadata as importlib_metadata
32
+ import io
33
+ import json
34
+ import logging
35
+ import os
36
+ import sys
37
+ import time
38
+ from typing import IO, Any, AnyStr
39
+
40
+ import click
41
+ from click.exceptions import NoSuchOption, UsageError
42
+ import structlog
43
+ import typer
44
+
45
+ from bijux_cli.cli import build_app
46
+ from bijux_cli.core.di import DIContainer
47
+ from bijux_cli.core.engine import Engine
48
+ from bijux_cli.core.enums import OutputFormat
49
+ from bijux_cli.core.exceptions import CommandError
50
+ from bijux_cli.services import register_default_services
51
+ from bijux_cli.services.history import History
52
+
53
+ _orig_stderr = sys.stderr
54
+ _orig_click_echo = click.echo
55
+ _orig_click_secho = click.secho
56
+
57
+
58
+ class _FilteredStderr(io.TextIOBase):
59
+ """A proxy for `sys.stderr` that filters a specific noisy plugin warning."""
60
+
61
+ def write(self, data: str) -> int:
62
+ """Writes data to stderr, suppressing a specific known warning.
63
+
64
+ Args:
65
+ data (str): The string to write.
66
+
67
+ Returns:
68
+ int: The number of characters written, or 0 if suppressed.
69
+ """
70
+ noise = "Plugin 'test-src' does not expose a Typer app via 'cli()' or 'app'"
71
+ if noise in data:
72
+ return 0
73
+
74
+ if _orig_stderr.closed:
75
+ return 0
76
+
77
+ return _orig_stderr.write(data)
78
+
79
+ def flush(self) -> None:
80
+ """Flushes the underlying stderr stream."""
81
+ if not _orig_stderr.closed:
82
+ _orig_stderr.flush()
83
+
84
+ def __getattr__(self, name: str) -> Any:
85
+ """Delegates attribute access to the original `sys.stderr`.
86
+
87
+ Args:
88
+ name (str): The name of the attribute to access.
89
+
90
+ Returns:
91
+ Any: The attribute from the original `sys.stderr`.
92
+ """
93
+ return getattr(_orig_stderr, name)
94
+
95
+
96
+ sys.stderr = _FilteredStderr()
97
+
98
+
99
+ def _filtered_echo(
100
+ message: Any = None,
101
+ file: IO[AnyStr] | None = None,
102
+ nl: bool = True,
103
+ err: bool = False,
104
+ color: bool | None = None,
105
+ **styles: Any,
106
+ ) -> None:
107
+ """A replacement for `click.echo` that filters a known plugin warning.
108
+
109
+ Args:
110
+ message (Any): The message to print.
111
+ file (IO[AnyStr] | None): The file to write to.
112
+ nl (bool): If True, appends a newline.
113
+ err (bool): If True, writes to stderr instead of stdout.
114
+ color (bool | None): If True, enables color output.
115
+ **styles (Any): Additional style arguments for colored output.
116
+
117
+ Returns:
118
+ None
119
+ """
120
+ text = "" if message is None else str(message)
121
+ if (
122
+ text.startswith("[WARN] Plugin 'test-src'")
123
+ and "does not expose a Typer app" in text
124
+ ):
125
+ return
126
+
127
+ if styles:
128
+ _orig_click_secho(message, file=file, nl=nl, err=err, color=color, **styles)
129
+ else:
130
+ _orig_click_echo(message, file=file, nl=nl, err=err, color=color)
131
+
132
+
133
+ click.echo = _filtered_echo
134
+ click.secho = _filtered_echo
135
+ typer.echo = _filtered_echo
136
+ typer.secho = _filtered_echo
137
+
138
+
139
+ def disable_cli_colors_for_test() -> None:
140
+ """Disables color output from various libraries for test environments.
141
+
142
+ This function checks for the `BIJUXCLI_TEST_MODE` environment variable and,
143
+ if set, attempts to disable color output to ensure clean, predictable
144
+ test results.
145
+ """
146
+ if os.environ.get("BIJUXCLI_TEST_MODE") != "1":
147
+ return
148
+ os.environ["NO_COLOR"] = "1"
149
+ try:
150
+ from rich.console import Console
151
+
152
+ Console().no_color = True
153
+ except ImportError:
154
+ pass
155
+ try:
156
+ import colorama # pyright: ignore[reportMissingModuleSource]
157
+
158
+ colorama.deinit()
159
+ except ImportError:
160
+ pass
161
+ try:
162
+ import prompt_toolkit
163
+
164
+ prompt_toolkit.shortcuts.set_title = (
165
+ lambda text: None # pyright: ignore[reportUnknownLambdaType]
166
+ )
167
+ except ImportError: # pragma: no cover
168
+ pass
169
+
170
+
171
+ def should_record_command_history(command_line: list[str]) -> bool:
172
+ """Determines whether the given command should be recorded in the history.
173
+
174
+ History recording is disabled under the following conditions:
175
+ * The `BIJUXCLI_DISABLE_HISTORY` environment variable is set to "1".
176
+ * The command line is empty.
177
+ * The command is "history" or "help".
178
+
179
+ Args:
180
+ command_line (list[str]): The list of command-line input tokens.
181
+
182
+ Returns:
183
+ bool: True if the command should be recorded, otherwise False.
184
+ """
185
+ if os.environ.get("BIJUXCLI_DISABLE_HISTORY") == "1":
186
+ return False
187
+ if not command_line:
188
+ return False
189
+ return command_line[0].lower() not in {"history", "help"}
190
+
191
+
192
+ def is_quiet_mode(args: list[str]) -> bool:
193
+ """Checks if the CLI was invoked with a quiet flag.
194
+
195
+ Args:
196
+ args (list[str]): The list of command-line arguments.
197
+
198
+ Returns:
199
+ bool: True if `--quiet` or `-q` is present, otherwise False.
200
+ """
201
+ return any(arg in ("--quiet", "-q") for arg in args)
202
+
203
+
204
+ def print_json_error(msg: str, code: int = 2, quiet: bool = False) -> None:
205
+ """Prints a structured JSON error message.
206
+
207
+ The message is printed to stdout for usage errors (code 2) and stderr for
208
+ all other errors, unless quiet mode is enabled.
209
+
210
+ Args:
211
+ msg (str): The error message.
212
+ code (int): The error code to include in the JSON payload.
213
+ quiet (bool): If True, suppresses all output.
214
+ """
215
+ if not quiet:
216
+ print(
217
+ json.dumps({"error": msg, "code": code}),
218
+ file=sys.stdout if code == 2 else sys.stderr,
219
+ )
220
+
221
+
222
+ def get_usage_for_args(args: list[str], app: typer.Typer) -> str:
223
+ """Gets the CLI help message for a given set of arguments.
224
+
225
+ This function simulates invoking the CLI with `--help` to capture the
226
+ contextual help message without exiting the process.
227
+
228
+ Args:
229
+ args (list[str]): The CLI arguments leading up to the help flag.
230
+ app (typer.Typer): The `Typer` application instance.
231
+
232
+ Returns:
233
+ str: The generated help/usage message.
234
+ """
235
+ from contextlib import redirect_stdout
236
+ import io
237
+
238
+ subcmds = []
239
+ for arg in args:
240
+ if arg in ("--help", "-h"):
241
+ break
242
+ subcmds.append(arg)
243
+
244
+ with io.StringIO() as buf, redirect_stdout(buf):
245
+ with suppress(SystemExit):
246
+ app(subcmds + ["--help"], standalone_mode=False)
247
+ return buf.getvalue()
248
+
249
+
250
+ def _strip_format_help(args: list[str]) -> list[str]:
251
+ """Removes an ambiguous `--format --help` combination from arguments.
252
+
253
+ This prevents a parsing error where `--help` could be interpreted as the
254
+ value for the `--format` option.
255
+
256
+ Args:
257
+ args (list[str]): The original list of command-line arguments.
258
+
259
+ Returns:
260
+ list[str]: A filtered list of arguments.
261
+ """
262
+ new_args = []
263
+ skip_next = False
264
+ for i, arg in enumerate(args):
265
+ if skip_next:
266
+ skip_next = False
267
+ continue
268
+ if (
269
+ arg in ("--format", "-f")
270
+ and i + 1 < len(args)
271
+ and args[i + 1] in ("--help", "-h")
272
+ ):
273
+ skip_next = True
274
+ continue
275
+ new_args.append(arg)
276
+ return new_args
277
+
278
+
279
+ def check_missing_format_argument(args: list[str]) -> str | None:
280
+ """Checks if a `--format` or `-f` flag is missing its required value.
281
+
282
+ Args:
283
+ args (list[str]): The list of command-line arguments.
284
+
285
+ Returns:
286
+ str | None: An error message if the value is missing, otherwise None.
287
+ """
288
+ for i, arg in enumerate(args):
289
+ if arg in ("--format", "-f"):
290
+ if i + 1 >= len(args):
291
+ return "Option '--format' requires an argument"
292
+ next_arg = args[i + 1]
293
+ if next_arg.startswith("-"):
294
+ return "Option '--format' requires an argument"
295
+ return None
296
+
297
+
298
+ def setup_structlog(debug: bool = False) -> None:
299
+ """Configures `structlog` for the application.
300
+
301
+ Args:
302
+ debug (bool): If True, configures human-readable console output at the
303
+ DEBUG level. If False, configures JSON output at the CRITICAL level.
304
+ """
305
+ level = logging.DEBUG if debug else logging.CRITICAL
306
+ logging.basicConfig(level=level, stream=sys.stderr, format="%(message)s")
307
+
308
+ structlog.configure(
309
+ processors=[
310
+ structlog.contextvars.merge_contextvars,
311
+ structlog.stdlib.add_log_level,
312
+ structlog.processors.TimeStamper(fmt="iso"),
313
+ structlog.processors.UnicodeDecoder(),
314
+ (
315
+ structlog.dev.ConsoleRenderer()
316
+ if debug
317
+ else structlog.processors.JSONRenderer()
318
+ ),
319
+ ],
320
+ logger_factory=structlog.stdlib.LoggerFactory(),
321
+ wrapper_class=structlog.stdlib.BoundLogger,
322
+ cache_logger_on_first_use=True,
323
+ )
324
+
325
+
326
+ def main() -> int:
327
+ """The main entry point for the Bijux CLI.
328
+
329
+ This function orchestrates the entire lifecycle of a CLI command, from
330
+ argument parsing and setup to execution and history recording.
331
+
332
+ Returns:
333
+ int: The final exit code of the command.
334
+ * `0`: Success.
335
+ * `1`: A generic command error occurred.
336
+ * `2`: A usage error or invalid option was provided.
337
+ * `130`: The process was interrupted by the user (Ctrl+C).
338
+ """
339
+ args = _strip_format_help(sys.argv[1:])
340
+
341
+ quiet = is_quiet_mode(args)
342
+ if quiet:
343
+ with contextlib.suppress(Exception):
344
+ sys.stderr = open(os.devnull, "w") # noqa: SIM115
345
+ debug = "--debug" in sys.argv or os.environ.get("BIJUXCLI_DEBUG") == "1"
346
+ setup_structlog(debug)
347
+ disable_cli_colors_for_test()
348
+
349
+ if any(a in ("--version", "-V") for a in args):
350
+ try:
351
+ ver = importlib_metadata.version("bijux-cli")
352
+ except importlib_metadata.PackageNotFoundError:
353
+ ver = "unknown"
354
+ print(json.dumps({"version": ver}))
355
+ return 0
356
+
357
+ container = DIContainer.current()
358
+ register_default_services(
359
+ container, debug=False, output_format=OutputFormat.JSON, quiet=False
360
+ )
361
+
362
+ Engine()
363
+ app = build_app()
364
+
365
+ if any(a in ("-h", "--help") for a in args):
366
+ print(get_usage_for_args(args, app))
367
+ return 0
368
+
369
+ missing_format_msg = check_missing_format_argument(args)
370
+ if missing_format_msg:
371
+ print_json_error(missing_format_msg, 2, quiet)
372
+ return 2
373
+
374
+ command_line = args
375
+ start = time.time()
376
+ exit_code = 0
377
+
378
+ try:
379
+ result = app(args=command_line, standalone_mode=False)
380
+ exit_code = int(result) if isinstance(result, int) else 0
381
+ except typer.Exit as exc:
382
+ exit_code = exc.exit_code
383
+ except NoSuchOption as exc:
384
+ print_json_error(f"No such option: {exc.option_name}", 2, quiet)
385
+ exit_code = 2
386
+ except UsageError as exc:
387
+ print_json_error(str(exc), 2, quiet)
388
+ exit_code = 2
389
+ except CommandError as exc:
390
+ print_json_error(str(exc), 1, quiet)
391
+ exit_code = 1
392
+ except KeyboardInterrupt:
393
+ print_json_error("Aborted by user", 130, quiet)
394
+ exit_code = 130
395
+ except Exception as exc:
396
+ print_json_error(f"Unexpected error: {exc}", 1, quiet)
397
+ exit_code = 1
398
+
399
+ if should_record_command_history(command_line):
400
+ try:
401
+ history_service = container.resolve(History)
402
+ history_service.add(
403
+ command=" ".join(command_line),
404
+ params=command_line[1:],
405
+ success=(exit_code == 0),
406
+ return_code=exit_code,
407
+ duration_ms=int((time.time() - start) * 1000),
408
+ )
409
+ except Exception as exc:
410
+ print(f"[error] Could not record command history: {exc}", file=sys.stderr)
411
+ exit_code = 1
412
+
413
+ return exit_code
414
+
415
+
416
+ if __name__ == "__main__":
417
+ sys.exit(main()) # pragma: no cover
@@ -0,0 +1,42 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright © 2025 Bijan Mousavi
3
+
4
+ """Provides version metadata for the Bijux CLI package.
5
+
6
+ This module dynamically retrieves version information from the installed
7
+ package's metadata and the project's pyproject.toml file. This ensures that
8
+ versions are managed from a single source of truth.
9
+
10
+ It exposes two primary versions:
11
+ * `version`: The main application version (`packaging.version.Version`).
12
+ * `api_version`: A separate version for the plugin API, used for
13
+ compatibility checks.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from importlib.metadata import PackageNotFoundError
19
+ from importlib.metadata import version as _get_version
20
+ from pathlib import Path
21
+ import tomllib
22
+
23
+ from packaging.version import Version
24
+
25
+ try:
26
+ __version__: str = _get_version("bijux-cli")
27
+ except PackageNotFoundError:
28
+ __version__ = "0.1.0"
29
+
30
+
31
+ try:
32
+ pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
33
+ with pyproject_path.open("rb") as f:
34
+ pyproject_data = tomllib.load(f)
35
+ __api_version__: str = pyproject_data["tool"]["bijux"]["api_version"]
36
+ except (FileNotFoundError, KeyError):
37
+ __api_version__ = "0.1.0"
38
+
39
+ version = Version(__version__)
40
+ api_version = Version(__api_version__)
41
+
42
+ __all__ = ["version", "api_version", "__version__", "__api_version__"]