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.
- bijux_cli/__init__.py +39 -0
- bijux_cli/__main__.py +417 -0
- bijux_cli/__version__.py +42 -0
- bijux_cli/api.py +326 -0
- bijux_cli/cli.py +68 -0
- bijux_cli/commands/__init__.py +170 -0
- bijux_cli/commands/audit.py +293 -0
- bijux_cli/commands/config/__init__.py +85 -0
- bijux_cli/commands/config/clear.py +124 -0
- bijux_cli/commands/config/export.py +148 -0
- bijux_cli/commands/config/get.py +141 -0
- bijux_cli/commands/config/list_cmd.py +127 -0
- bijux_cli/commands/config/load.py +128 -0
- bijux_cli/commands/config/py.typed +0 -0
- bijux_cli/commands/config/reload.py +125 -0
- bijux_cli/commands/config/service.py +107 -0
- bijux_cli/commands/config/set.py +260 -0
- bijux_cli/commands/config/unset.py +140 -0
- bijux_cli/commands/dev/__init__.py +46 -0
- bijux_cli/commands/dev/di.py +285 -0
- bijux_cli/commands/dev/list_plugins.py +66 -0
- bijux_cli/commands/dev/py.typed +0 -0
- bijux_cli/commands/dev/service.py +120 -0
- bijux_cli/commands/docs.py +329 -0
- bijux_cli/commands/doctor.py +181 -0
- bijux_cli/commands/help.py +436 -0
- bijux_cli/commands/history/__init__.py +43 -0
- bijux_cli/commands/history/clear.py +160 -0
- bijux_cli/commands/history/py.typed +0 -0
- bijux_cli/commands/history/service.py +362 -0
- bijux_cli/commands/memory/__init__.py +59 -0
- bijux_cli/commands/memory/clear.py +114 -0
- bijux_cli/commands/memory/delete.py +144 -0
- bijux_cli/commands/memory/get.py +146 -0
- bijux_cli/commands/memory/list.py +115 -0
- bijux_cli/commands/memory/py.typed +0 -0
- bijux_cli/commands/memory/service.py +242 -0
- bijux_cli/commands/memory/set.py +136 -0
- bijux_cli/commands/memory/utils.py +54 -0
- bijux_cli/commands/plugins/__init__.py +59 -0
- bijux_cli/commands/plugins/check.py +240 -0
- bijux_cli/commands/plugins/info.py +143 -0
- bijux_cli/commands/plugins/install.py +327 -0
- bijux_cli/commands/plugins/list.py +73 -0
- bijux_cli/commands/plugins/py.typed +0 -0
- bijux_cli/commands/plugins/scaffold.py +287 -0
- bijux_cli/commands/plugins/uninstall.py +206 -0
- bijux_cli/commands/plugins/utils.py +158 -0
- bijux_cli/commands/py.typed +0 -0
- bijux_cli/commands/repl.py +709 -0
- bijux_cli/commands/sleep.py +169 -0
- bijux_cli/commands/status.py +289 -0
- bijux_cli/commands/utilities.py +637 -0
- bijux_cli/commands/version.py +152 -0
- bijux_cli/contracts/__init__.py +48 -0
- bijux_cli/contracts/audit.py +66 -0
- bijux_cli/contracts/config.py +127 -0
- bijux_cli/contracts/context.py +93 -0
- bijux_cli/contracts/docs.py +65 -0
- bijux_cli/contracts/doctor.py +32 -0
- bijux_cli/contracts/emitter.py +58 -0
- bijux_cli/contracts/history.py +101 -0
- bijux_cli/contracts/memory.py +76 -0
- bijux_cli/contracts/observability.py +94 -0
- bijux_cli/contracts/process.py +48 -0
- bijux_cli/contracts/py.typed +0 -0
- bijux_cli/contracts/registry.py +115 -0
- bijux_cli/contracts/retry.py +53 -0
- bijux_cli/contracts/serializer.py +100 -0
- bijux_cli/contracts/telemetry.py +46 -0
- bijux_cli/core/__init__.py +44 -0
- bijux_cli/core/constants.py +21 -0
- bijux_cli/core/context.py +218 -0
- bijux_cli/core/di.py +695 -0
- bijux_cli/core/engine.py +199 -0
- bijux_cli/core/enums.py +52 -0
- bijux_cli/core/exceptions.py +184 -0
- bijux_cli/core/paths.py +21 -0
- bijux_cli/core/py.typed +0 -0
- bijux_cli/httpapi.py +552 -0
- bijux_cli/infra/__init__.py +43 -0
- bijux_cli/infra/emitter.py +140 -0
- bijux_cli/infra/observability.py +148 -0
- bijux_cli/infra/process.py +177 -0
- bijux_cli/infra/py.typed +0 -0
- bijux_cli/infra/retry.py +300 -0
- bijux_cli/infra/serializer.py +573 -0
- bijux_cli/infra/telemetry.py +159 -0
- bijux_cli/py.typed +0 -0
- bijux_cli/services/__init__.py +233 -0
- bijux_cli/services/audit.py +268 -0
- bijux_cli/services/config.py +677 -0
- bijux_cli/services/docs.py +170 -0
- bijux_cli/services/doctor.py +34 -0
- bijux_cli/services/history.py +527 -0
- bijux_cli/services/memory.py +129 -0
- bijux_cli/services/plugins/__init__.py +346 -0
- bijux_cli/services/plugins/entrypoints.py +151 -0
- bijux_cli/services/plugins/groups.py +177 -0
- bijux_cli/services/plugins/hooks.py +113 -0
- bijux_cli/services/plugins/py.typed +0 -0
- bijux_cli/services/plugins/registry.py +284 -0
- bijux_cli/services/py.typed +0 -0
- bijux_cli/services/utils.py +68 -0
- bijux_cli-0.1.0.dist-info/METADATA +297 -0
- bijux_cli-0.1.0.dist-info/RECORD +108 -0
- bijux_cli-0.1.0.dist-info/WHEEL +4 -0
- bijux_cli-0.1.0.dist-info/entry_points.txt +2 -0
bijux_cli/api.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright © 2025 Bijan Mousavi
|
|
3
|
+
|
|
4
|
+
"""Provides a high-level, synchronous facade for the Bijux CLI's core engine.
|
|
5
|
+
|
|
6
|
+
This module defines the `BijuxAPI` class, which serves as the primary public
|
|
7
|
+
interface for programmatic interaction with the CLI. It wraps the asynchronous
|
|
8
|
+
core `Engine` and other services to present a stable, thread-safe, and
|
|
9
|
+
synchronous API.
|
|
10
|
+
|
|
11
|
+
This facade is intended for use in integrations, testing, or any scenario
|
|
12
|
+
where the CLI's command and plugin management logic needs to be embedded
|
|
13
|
+
within another Python application.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
from collections.abc import Callable, Coroutine
|
|
20
|
+
import importlib
|
|
21
|
+
import os
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
import sys
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from bijux_cli.commands.utilities import validate_common_flags
|
|
27
|
+
from bijux_cli.contracts import (
|
|
28
|
+
ObservabilityProtocol,
|
|
29
|
+
RegistryProtocol,
|
|
30
|
+
TelemetryProtocol,
|
|
31
|
+
)
|
|
32
|
+
from bijux_cli.core.di import DIContainer
|
|
33
|
+
from bijux_cli.core.engine import Engine
|
|
34
|
+
from bijux_cli.core.enums import OutputFormat
|
|
35
|
+
from bijux_cli.core.exceptions import BijuxError, CommandError, ServiceError
|
|
36
|
+
|
|
37
|
+
IGNORE = {"PS1", "LS_COLORS", "PROMPT_COMMAND", "GIT_PS1_FORMAT"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class BijuxAPI:
|
|
41
|
+
"""A thread-safe, synchronous access layer for the Bijux CLI engine.
|
|
42
|
+
|
|
43
|
+
This class provides a stable public API for registering commands, executing
|
|
44
|
+
them, and managing plugins. It wraps the internal asynchronous `Engine` to
|
|
45
|
+
allow for simpler, synchronous integration into other applications.
|
|
46
|
+
|
|
47
|
+
Attributes:
|
|
48
|
+
_di (DIContainer): The dependency injection container.
|
|
49
|
+
_engine (Engine): The core asynchronous runtime engine.
|
|
50
|
+
_registry (RegistryProtocol): The plugin registry service.
|
|
51
|
+
_obs (ObservabilityProtocol): The logging service.
|
|
52
|
+
_tel (TelemetryProtocol): The telemetry service.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, *, debug: bool = False) -> None:
|
|
56
|
+
"""Initializes the `BijuxAPI` and the underlying CLI engine.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
debug (bool): If True, enables debug mode for all underlying
|
|
60
|
+
services. Defaults to False.
|
|
61
|
+
"""
|
|
62
|
+
DIContainer.reset()
|
|
63
|
+
self._di = DIContainer.current()
|
|
64
|
+
self._engine = Engine(
|
|
65
|
+
self._di,
|
|
66
|
+
debug=debug,
|
|
67
|
+
fmt=OutputFormat.JSON,
|
|
68
|
+
)
|
|
69
|
+
self._registry: RegistryProtocol = self._di.resolve(RegistryProtocol)
|
|
70
|
+
self._obs: ObservabilityProtocol = self._di.resolve(ObservabilityProtocol)
|
|
71
|
+
self._tel: TelemetryProtocol = self._di.resolve(TelemetryProtocol)
|
|
72
|
+
|
|
73
|
+
def _schedule_event(self, name: str, payload: dict[str, Any]) -> None:
|
|
74
|
+
"""Schedules a "fire-and-forget" asynchronous telemetry event.
|
|
75
|
+
|
|
76
|
+
This helper handles the execution of async telemetry calls from a
|
|
77
|
+
synchronous context.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
name (str): The name of the telemetry event.
|
|
81
|
+
payload (dict[str, Any]): The data associated with the event.
|
|
82
|
+
"""
|
|
83
|
+
maybe_coro: Coroutine[Any, Any, None] | None = self._tel.event(name, payload)
|
|
84
|
+
if asyncio.iscoroutine(maybe_coro):
|
|
85
|
+
try:
|
|
86
|
+
loop = asyncio.get_running_loop()
|
|
87
|
+
loop.create_task(maybe_coro)
|
|
88
|
+
except RuntimeError:
|
|
89
|
+
asyncio.run(maybe_coro)
|
|
90
|
+
|
|
91
|
+
def register(self, name: str, callback: Callable[..., Any]) -> None:
|
|
92
|
+
"""Registers or replaces a Python callable as a CLI command.
|
|
93
|
+
|
|
94
|
+
The provided callable is wrapped to handle both synchronous and
|
|
95
|
+
asynchronous functions automatically.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
name (str): The command name to register.
|
|
99
|
+
callback (Callable[..., Any]): The Python function to be executed
|
|
100
|
+
when the command is run.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
BijuxError: If the command name is already in use or another
|
|
104
|
+
registration error occurs.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
class _Wrapper:
|
|
108
|
+
"""Wraps a user-provided callable to be executed asynchronously."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, cb: Callable[..., Any]) -> None:
|
|
111
|
+
"""Initializes the wrapper.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
cb (Callable[..., Any]): The callable to wrap.
|
|
115
|
+
"""
|
|
116
|
+
self._cb = cb
|
|
117
|
+
|
|
118
|
+
async def execute(self, *args: Any, **kwargs: Any) -> Any:
|
|
119
|
+
"""Execute the wrapped callable, awaiting if it's a coroutine.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
*args (Any): Positional arguments to pass to the callable.
|
|
123
|
+
**kwargs (Any): Keyword arguments to pass to the callable.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Any: The result of the callable execution.
|
|
127
|
+
"""
|
|
128
|
+
if asyncio.iscoroutinefunction(self._cb):
|
|
129
|
+
return await self._cb(*args, **kwargs)
|
|
130
|
+
return self._cb(*args, **kwargs)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
if self._registry.has(name):
|
|
134
|
+
self._registry.deregister(name)
|
|
135
|
+
self._registry.register(name, _Wrapper(callback))
|
|
136
|
+
self._obs.log("info", "Registered command", extra={"name": name})
|
|
137
|
+
self._schedule_event("api.register", {"name": name})
|
|
138
|
+
except ServiceError as exc:
|
|
139
|
+
self._schedule_event(
|
|
140
|
+
"api.register.error", {"name": name, "error": str(exc)}
|
|
141
|
+
)
|
|
142
|
+
raise BijuxError(
|
|
143
|
+
f"Could not register command {name}: {exc}", http_status=500
|
|
144
|
+
) from exc
|
|
145
|
+
|
|
146
|
+
def run_sync(
|
|
147
|
+
self,
|
|
148
|
+
name: str,
|
|
149
|
+
*args: Any,
|
|
150
|
+
quiet: bool = False,
|
|
151
|
+
verbose: bool = False,
|
|
152
|
+
fmt: str = "json",
|
|
153
|
+
pretty: bool = True,
|
|
154
|
+
debug: bool = False,
|
|
155
|
+
**kwargs: Any,
|
|
156
|
+
) -> Any:
|
|
157
|
+
"""Runs a command synchronously.
|
|
158
|
+
|
|
159
|
+
This method is a blocking wrapper around the asynchronous `run_async`
|
|
160
|
+
method. It manages the asyncio event loop to provide a simple,
|
|
161
|
+
synchronous interface.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
name (str): The name of the command to run.
|
|
165
|
+
*args (Any): Positional arguments for the command.
|
|
166
|
+
quiet (bool): If True, suppresses output.
|
|
167
|
+
verbose (bool): If True, enables verbose logging.
|
|
168
|
+
fmt (str): The output format ("json" or "yaml").
|
|
169
|
+
pretty (bool): If True, formats the output for readability.
|
|
170
|
+
debug (bool): If True, enables debug mode.
|
|
171
|
+
**kwargs (Any): Additional keyword arguments to pass to the command.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
Any: The result of the command's execution.
|
|
175
|
+
"""
|
|
176
|
+
try:
|
|
177
|
+
loop = asyncio.get_running_loop()
|
|
178
|
+
except RuntimeError:
|
|
179
|
+
return asyncio.run(
|
|
180
|
+
self.run_async(
|
|
181
|
+
name,
|
|
182
|
+
*args,
|
|
183
|
+
quiet=quiet,
|
|
184
|
+
verbose=verbose,
|
|
185
|
+
fmt=fmt,
|
|
186
|
+
pretty=pretty,
|
|
187
|
+
debug=debug,
|
|
188
|
+
**kwargs,
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
else:
|
|
192
|
+
return loop.run_until_complete(
|
|
193
|
+
self.run_async(
|
|
194
|
+
name,
|
|
195
|
+
*args,
|
|
196
|
+
quiet=quiet,
|
|
197
|
+
verbose=verbose,
|
|
198
|
+
fmt=fmt,
|
|
199
|
+
pretty=pretty,
|
|
200
|
+
debug=debug,
|
|
201
|
+
**kwargs,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
async def run_async(
|
|
206
|
+
self,
|
|
207
|
+
name: str,
|
|
208
|
+
*args: Any,
|
|
209
|
+
quiet: bool = False,
|
|
210
|
+
verbose: bool = False,
|
|
211
|
+
fmt: str = "json",
|
|
212
|
+
pretty: bool = True,
|
|
213
|
+
debug: bool = False,
|
|
214
|
+
**kwargs: Any,
|
|
215
|
+
) -> Any:
|
|
216
|
+
"""Runs a command asynchronously with validation.
|
|
217
|
+
|
|
218
|
+
This method performs validation of flags and environment variables
|
|
219
|
+
before dispatching the command to the internal engine for execution.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
name (str): The name of the command to execute.
|
|
223
|
+
*args (Any): Positional arguments for the command.
|
|
224
|
+
quiet (bool): If True, suppresses output.
|
|
225
|
+
verbose (bool): If True, enables verbose output.
|
|
226
|
+
fmt (str): The output format ("json" or "yaml").
|
|
227
|
+
pretty (bool): If True, formats the output for readability.
|
|
228
|
+
debug (bool): If True, enables debug mode.
|
|
229
|
+
**kwargs (Any): Additional keyword arguments to pass to the command.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Any: The result of the command's execution.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
BijuxError: For invalid flags, unsupported formats, or internal
|
|
236
|
+
execution errors.
|
|
237
|
+
"""
|
|
238
|
+
try:
|
|
239
|
+
fmt = fmt.lower()
|
|
240
|
+
if fmt not in ("json", "yaml"):
|
|
241
|
+
raise BijuxError("Unsupported format", http_status=400)
|
|
242
|
+
|
|
243
|
+
if quiet and (verbose or debug):
|
|
244
|
+
raise BijuxError(
|
|
245
|
+
"--quiet cannot be combined with --verbose/--debug", http_status=400
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
validate_common_flags(fmt, name, quiet, verbose or debug)
|
|
249
|
+
|
|
250
|
+
for k, v in os.environ.items():
|
|
251
|
+
if k in IGNORE:
|
|
252
|
+
continue
|
|
253
|
+
if not v.isascii():
|
|
254
|
+
raise BijuxError(
|
|
255
|
+
"Non-ASCII characters in environment", http_status=400
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
result = await self._engine.run_command(name, *args, **kwargs)
|
|
259
|
+
self._schedule_event("api.run", {"name": name})
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
except CommandError as exc:
|
|
263
|
+
self._schedule_event("api.run.error", {"name": name, "error": str(exc)})
|
|
264
|
+
raise BijuxError(
|
|
265
|
+
f"Failed to run command {name}: {exc}", http_status=500
|
|
266
|
+
) from exc
|
|
267
|
+
|
|
268
|
+
except ServiceError as exc:
|
|
269
|
+
self._schedule_event("api.run.error", {"name": name, "error": str(exc)})
|
|
270
|
+
raise BijuxError(
|
|
271
|
+
f"Failed to run command {name}: {exc}", http_status=500
|
|
272
|
+
) from exc
|
|
273
|
+
|
|
274
|
+
except BijuxError:
|
|
275
|
+
raise
|
|
276
|
+
|
|
277
|
+
except Exception as exc:
|
|
278
|
+
self._schedule_event("api.run.error", {"name": name, "error": str(exc)})
|
|
279
|
+
raise BijuxError(
|
|
280
|
+
f"Failed to run command {name}: {exc}", http_status=500
|
|
281
|
+
) from exc
|
|
282
|
+
|
|
283
|
+
def load_plugin(self, path: str | Path) -> None:
|
|
284
|
+
"""Loads or reloads a plugin module from a file path.
|
|
285
|
+
|
|
286
|
+
This method dynamically loads the specified plugin file, initializes it,
|
|
287
|
+
and registers it with the CLI system. If the plugin is already loaded,
|
|
288
|
+
it is reloaded.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
path (str | Path): The filesystem path to the plugin's Python file.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
BijuxError: If plugin loading, initialization, or registration fails.
|
|
295
|
+
"""
|
|
296
|
+
from bijux_cli.__version__ import __version__
|
|
297
|
+
from bijux_cli.services.plugins import load_plugin as _load_plugin
|
|
298
|
+
|
|
299
|
+
p = Path(path).expanduser().resolve()
|
|
300
|
+
module_name = f"bijux_plugin_{p.stem}"
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
if module_name in sys.modules:
|
|
304
|
+
importlib.reload(sys.modules[module_name])
|
|
305
|
+
|
|
306
|
+
plugin = _load_plugin(p, module_name)
|
|
307
|
+
plugin.startup(self._engine.di)
|
|
308
|
+
|
|
309
|
+
if self._registry.has(p.stem):
|
|
310
|
+
self._registry.deregister(p.stem)
|
|
311
|
+
|
|
312
|
+
self._registry.register(
|
|
313
|
+
p.stem,
|
|
314
|
+
plugin,
|
|
315
|
+
alias=str(__version__),
|
|
316
|
+
)
|
|
317
|
+
self._obs.log("info", "Loaded plugin", extra={"path": str(p)})
|
|
318
|
+
self._schedule_event("api.plugin_loaded", {"path": str(p)})
|
|
319
|
+
|
|
320
|
+
except Exception as exc:
|
|
321
|
+
self._schedule_event(
|
|
322
|
+
"api.plugin_load.error", {"path": str(p), "error": str(exc)}
|
|
323
|
+
)
|
|
324
|
+
raise BijuxError(
|
|
325
|
+
f"Failed to load plugin {p}: {exc}", http_status=500
|
|
326
|
+
) from exc
|
bijux_cli/cli.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright © 2025 Bijan Mousavi
|
|
3
|
+
|
|
4
|
+
"""Constructs the main `Typer` application for the Bijux CLI.
|
|
5
|
+
|
|
6
|
+
This module serves as the primary builder for the entire CLI. It defines the
|
|
7
|
+
root `Typer` app, orchestrates the registration of all core commands and
|
|
8
|
+
the discovery of dynamic plugins, and sets the default behavior for when the
|
|
9
|
+
CLI is invoked without any command.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import subprocess # nosec B404
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from typer import Context
|
|
19
|
+
|
|
20
|
+
from bijux_cli.commands import register_commands, register_dynamic_plugins
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def maybe_default_to_repl(ctx: Context) -> None:
|
|
24
|
+
"""Invokes the `repl` command if no other subcommand is specified.
|
|
25
|
+
|
|
26
|
+
This function is used as the root callback for the main `Typer` application.
|
|
27
|
+
It checks if a subcommand was invoked and, if not, re-executes the CLI
|
|
28
|
+
with the `repl` command.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
ctx (Context): The Typer context, used to check for an invoked subcommand.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
None:
|
|
35
|
+
"""
|
|
36
|
+
if ctx.invoked_subcommand is None:
|
|
37
|
+
subprocess.call([sys.argv[0], "repl"]) # noqa: S603 # nosec B603
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_app() -> typer.Typer:
|
|
41
|
+
"""Builds and configures the root `Typer` application.
|
|
42
|
+
|
|
43
|
+
This factory function performs the main steps of assembling the CLI:
|
|
44
|
+
1. Creates the root `Typer` app instance.
|
|
45
|
+
2. Registers all core, built-in commands.
|
|
46
|
+
3. Discovers and registers all dynamic plugins.
|
|
47
|
+
4. Sets the default callback to launch the REPL.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
typer.Typer: The fully constructed `Typer` application.
|
|
51
|
+
"""
|
|
52
|
+
app = typer.Typer(
|
|
53
|
+
help="Bijux CLI – Lean, plug-in‑driven command‑line interface.",
|
|
54
|
+
invoke_without_command=True,
|
|
55
|
+
context_settings={
|
|
56
|
+
"ignore_unknown_options": True,
|
|
57
|
+
"allow_extra_args": True,
|
|
58
|
+
},
|
|
59
|
+
)
|
|
60
|
+
register_commands(app)
|
|
61
|
+
register_dynamic_plugins(app)
|
|
62
|
+
app.callback(invoke_without_command=True)(maybe_default_to_repl)
|
|
63
|
+
return app
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
app = build_app()
|
|
67
|
+
|
|
68
|
+
__all__ = ["build_app", "app"]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright © 2025 Bijan Mousavi
|
|
3
|
+
|
|
4
|
+
"""Constructs the command structure for the Bijux CLI application.
|
|
5
|
+
|
|
6
|
+
This module is responsible for assembling the entire CLI by registering all
|
|
7
|
+
core command groups and dynamically discovering and loading external plugins.
|
|
8
|
+
It provides the central mechanism that makes the CLI's command system modular
|
|
9
|
+
and extensible.
|
|
10
|
+
|
|
11
|
+
The primary functions are:
|
|
12
|
+
* `register_commands`: Attaches all built-in command `Typer` applications
|
|
13
|
+
(e.g., `config_app`, `plugins_app`) to the main root application.
|
|
14
|
+
* `register_dynamic_plugins`: Scans for plugins from package entry points
|
|
15
|
+
and the local plugins directory, loading and attaching them to the root
|
|
16
|
+
application. Errors during this process are logged as warnings.
|
|
17
|
+
* `list_registered_command_names`: Provides a way to retrieve the names of all
|
|
18
|
+
successfully registered commands, including dynamic plugins.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
|
|
25
|
+
from typer import Typer
|
|
26
|
+
|
|
27
|
+
from bijux_cli.commands.audit import audit_app
|
|
28
|
+
from bijux_cli.commands.config import config_app
|
|
29
|
+
from bijux_cli.commands.dev import dev_app
|
|
30
|
+
from bijux_cli.commands.docs import docs_app
|
|
31
|
+
from bijux_cli.commands.doctor import doctor_app
|
|
32
|
+
from bijux_cli.commands.help import help_app
|
|
33
|
+
from bijux_cli.commands.history import history_app
|
|
34
|
+
from bijux_cli.commands.memory import memory_app
|
|
35
|
+
from bijux_cli.commands.plugins import plugins_app
|
|
36
|
+
from bijux_cli.commands.repl import repl_app
|
|
37
|
+
from bijux_cli.commands.sleep import sleep_app
|
|
38
|
+
from bijux_cli.commands.status import status_app
|
|
39
|
+
from bijux_cli.commands.version import version_app
|
|
40
|
+
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
|
|
43
|
+
_CORE_COMMANDS = {
|
|
44
|
+
"audit": audit_app,
|
|
45
|
+
"config": config_app,
|
|
46
|
+
"dev": dev_app,
|
|
47
|
+
"docs": docs_app,
|
|
48
|
+
"doctor": doctor_app,
|
|
49
|
+
"help": help_app,
|
|
50
|
+
"history": history_app,
|
|
51
|
+
"memory": memory_app,
|
|
52
|
+
"plugins": plugins_app,
|
|
53
|
+
"repl": repl_app,
|
|
54
|
+
"status": status_app,
|
|
55
|
+
"version": version_app,
|
|
56
|
+
"sleep": sleep_app,
|
|
57
|
+
}
|
|
58
|
+
_REGISTERED_COMMANDS: set[str] = set(_CORE_COMMANDS.keys())
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def register_commands(app: Typer) -> list[str]:
|
|
62
|
+
"""Registers all core, built-in commands with the main Typer application.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
app (Typer): The main Typer application to which commands will be added.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
list[str]: An alphabetically sorted list of the names of the registered
|
|
69
|
+
core commands.
|
|
70
|
+
"""
|
|
71
|
+
for name, cmd in sorted(_CORE_COMMANDS.items()):
|
|
72
|
+
app.add_typer(cmd, name=name, invoke_without_command=True)
|
|
73
|
+
_REGISTERED_COMMANDS.add(name)
|
|
74
|
+
return sorted(_CORE_COMMANDS.keys())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def register_dynamic_plugins(app: Typer) -> None:
|
|
78
|
+
"""Discovers and registers all third-party plugins.
|
|
79
|
+
|
|
80
|
+
This function scans for plugins from two sources:
|
|
81
|
+
1. Python package entry points registered under the `bijux_cli.plugins` group.
|
|
82
|
+
2. Subdirectories within the local plugins folder that contain a `plugin.py`.
|
|
83
|
+
|
|
84
|
+
For each discovered plugin, this function expects the loaded module to expose
|
|
85
|
+
either a callable `cli()` that returns a `Typer` app or an `app` attribute
|
|
86
|
+
that is a `Typer` instance. All discovery and loading errors are logged
|
|
87
|
+
and suppressed to prevent a single faulty plugin from crashing the CLI.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
app (Typer): The root Typer application to which discovered plugin
|
|
91
|
+
apps will be attached.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
None:
|
|
95
|
+
"""
|
|
96
|
+
import importlib.util
|
|
97
|
+
import sys
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
import importlib.metadata
|
|
101
|
+
|
|
102
|
+
eps = importlib.metadata.entry_points()
|
|
103
|
+
for ep in eps.select(group="bijux_cli.plugins"):
|
|
104
|
+
try:
|
|
105
|
+
plugin_app = ep.load()
|
|
106
|
+
app.add_typer(plugin_app, name=ep.name)
|
|
107
|
+
_REGISTERED_COMMANDS.add(ep.name)
|
|
108
|
+
except Exception as exc:
|
|
109
|
+
logger.debug("Failed to load entry-point plugin %r: %s", ep.name, exc)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
logger.debug("Entry points loading failed: %s", e)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
from bijux_cli.services.plugins import get_plugins_dir
|
|
115
|
+
|
|
116
|
+
plugins_dir = get_plugins_dir()
|
|
117
|
+
for pdir in plugins_dir.iterdir():
|
|
118
|
+
plug_py = pdir / "plugin.py"
|
|
119
|
+
if not plug_py.is_file():
|
|
120
|
+
continue
|
|
121
|
+
mod_name = f"_bijux_cli_plugin_{pdir.name}"
|
|
122
|
+
spec = importlib.util.spec_from_file_location(mod_name, plug_py)
|
|
123
|
+
if not spec or not spec.loader:
|
|
124
|
+
continue
|
|
125
|
+
module = importlib.util.module_from_spec(spec)
|
|
126
|
+
sys.modules[mod_name] = module
|
|
127
|
+
try:
|
|
128
|
+
spec.loader.exec_module(module)
|
|
129
|
+
plugin_app = None
|
|
130
|
+
if hasattr(module, "cli") and callable(module.cli):
|
|
131
|
+
plugin_app = module.cli()
|
|
132
|
+
elif hasattr(module, "app"):
|
|
133
|
+
plugin_app = module.app
|
|
134
|
+
else:
|
|
135
|
+
logger.debug(
|
|
136
|
+
"Plugin %r has no CLI entrypoint (no cli()/app).", pdir.name
|
|
137
|
+
)
|
|
138
|
+
continue
|
|
139
|
+
if isinstance(plugin_app, Typer):
|
|
140
|
+
app.add_typer(plugin_app, name=pdir.name)
|
|
141
|
+
_REGISTERED_COMMANDS.add(pdir.name)
|
|
142
|
+
else:
|
|
143
|
+
logger.debug(
|
|
144
|
+
"Plugin %r loaded but did not return a Typer app instance.",
|
|
145
|
+
pdir.name,
|
|
146
|
+
)
|
|
147
|
+
except Exception as exc:
|
|
148
|
+
logger.debug("Failed to load local plugin %r: %s", pdir.name, exc)
|
|
149
|
+
finally:
|
|
150
|
+
sys.modules.pop(mod_name, None)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
logger.debug("Dynamic plugin discovery failed: %s", e)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def list_registered_command_names() -> list[str]:
|
|
156
|
+
"""Returns a list of all registered command names.
|
|
157
|
+
|
|
158
|
+
This includes both core commands and any dynamically loaded plugins.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
list[str]: An alphabetically sorted list of all command names.
|
|
162
|
+
"""
|
|
163
|
+
return sorted(_REGISTERED_COMMANDS)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
__all__ = [
|
|
167
|
+
"register_commands",
|
|
168
|
+
"register_dynamic_plugins",
|
|
169
|
+
"list_registered_command_names",
|
|
170
|
+
]
|