falyx 0.1.21__tar.gz → 0.1.23__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 (43) hide show
  1. {falyx-0.1.21 → falyx-0.1.23}/PKG-INFO +1 -1
  2. {falyx-0.1.21 → falyx-0.1.23}/falyx/__main__.py +3 -6
  3. {falyx-0.1.21 → falyx-0.1.23}/falyx/action_factory.py +1 -0
  4. {falyx-0.1.21 → falyx-0.1.23}/falyx/command.py +6 -0
  5. falyx-0.1.23/falyx/config.py +214 -0
  6. falyx-0.1.23/falyx/config_schema.py +76 -0
  7. {falyx-0.1.21 → falyx-0.1.23}/falyx/execution_registry.py +3 -2
  8. {falyx-0.1.21 → falyx-0.1.23}/falyx/falyx.py +74 -32
  9. falyx-0.1.23/falyx/init.py +136 -0
  10. {falyx-0.1.21 → falyx-0.1.23}/falyx/io_action.py +25 -11
  11. {falyx-0.1.21 → falyx-0.1.23}/falyx/parsers.py +13 -1
  12. {falyx-0.1.21 → falyx-0.1.23}/falyx/prompt_utils.py +1 -0
  13. {falyx-0.1.21 → falyx-0.1.23}/falyx/protocols.py +1 -0
  14. {falyx-0.1.21 → falyx-0.1.23}/falyx/select_file_action.py +39 -5
  15. {falyx-0.1.21 → falyx-0.1.23}/falyx/selection.py +72 -62
  16. {falyx-0.1.21 → falyx-0.1.23}/falyx/selection_action.py +6 -5
  17. {falyx-0.1.21 → falyx-0.1.23}/falyx/signal_action.py +1 -0
  18. falyx-0.1.23/falyx/version.py +1 -0
  19. {falyx-0.1.21 → falyx-0.1.23}/pyproject.toml +1 -1
  20. falyx-0.1.21/falyx/config.py +0 -119
  21. falyx-0.1.21/falyx/init.py +0 -76
  22. falyx-0.1.21/falyx/version.py +0 -1
  23. {falyx-0.1.21 → falyx-0.1.23}/LICENSE +0 -0
  24. {falyx-0.1.21 → falyx-0.1.23}/README.md +0 -0
  25. {falyx-0.1.21 → falyx-0.1.23}/falyx/.pytyped +0 -0
  26. {falyx-0.1.21 → falyx-0.1.23}/falyx/__init__.py +0 -0
  27. {falyx-0.1.21 → falyx-0.1.23}/falyx/action.py +0 -0
  28. {falyx-0.1.21 → falyx-0.1.23}/falyx/bottom_bar.py +0 -0
  29. {falyx-0.1.21 → falyx-0.1.23}/falyx/context.py +0 -0
  30. {falyx-0.1.21 → falyx-0.1.23}/falyx/debug.py +0 -0
  31. {falyx-0.1.21 → falyx-0.1.23}/falyx/exceptions.py +0 -0
  32. {falyx-0.1.21 → falyx-0.1.23}/falyx/hook_manager.py +0 -0
  33. {falyx-0.1.21 → falyx-0.1.23}/falyx/hooks.py +0 -0
  34. {falyx-0.1.21 → falyx-0.1.23}/falyx/http_action.py +0 -0
  35. {falyx-0.1.21 → falyx-0.1.23}/falyx/menu_action.py +0 -0
  36. {falyx-0.1.21 → falyx-0.1.23}/falyx/options_manager.py +0 -0
  37. {falyx-0.1.21 → falyx-0.1.23}/falyx/retry.py +0 -0
  38. {falyx-0.1.21 → falyx-0.1.23}/falyx/retry_utils.py +0 -0
  39. {falyx-0.1.21 → falyx-0.1.23}/falyx/signals.py +0 -0
  40. {falyx-0.1.21 → falyx-0.1.23}/falyx/tagged_table.py +0 -0
  41. {falyx-0.1.21 → falyx-0.1.23}/falyx/themes/colors.py +0 -0
  42. {falyx-0.1.21 → falyx-0.1.23}/falyx/utils.py +0 -0
  43. {falyx-0.1.21 → falyx-0.1.23}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.21
3
+ Version: 0.1.23
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -6,6 +6,7 @@ Licensed under the MIT License. See LICENSE file for details.
6
6
  """
7
7
 
8
8
  import asyncio
9
+ import os
9
10
  import sys
10
11
  from argparse import Namespace
11
12
  from pathlib import Path
@@ -22,6 +23,7 @@ def find_falyx_config() -> Path | None:
22
23
  Path.cwd() / "falyx.toml",
23
24
  Path.cwd() / ".falyx.yaml",
24
25
  Path.cwd() / ".falyx.toml",
26
+ Path(os.environ.get("FALYX_CONFIG", "falyx.yaml")),
25
27
  Path.home() / ".config" / "falyx" / "falyx.yaml",
26
28
  Path.home() / ".config" / "falyx" / "falyx.toml",
27
29
  Path.home() / ".falyx.yaml",
@@ -67,12 +69,7 @@ def run(args: Namespace) -> Any:
67
69
  print("No Falyx config file found. Exiting.")
68
70
  return None
69
71
 
70
- flx = Falyx(
71
- title="🛠️ Config-Driven CLI",
72
- cli_args=args,
73
- columns=4,
74
- )
75
- flx.add_commands(loader(bootstrap_path))
72
+ flx: Falyx = loader(bootstrap_path)
76
73
  return asyncio.run(flx.run())
77
74
 
78
75
 
@@ -1,3 +1,4 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
1
2
  from typing import Any
2
3
 
3
4
  from rich.tree import Tree
@@ -272,15 +272,21 @@ class Command(BaseModel):
272
272
  if hasattr(self.action, "preview") and callable(self.action.preview):
273
273
  tree = Tree(label)
274
274
  await self.action.preview(parent=tree)
275
+ if self.help_text:
276
+ tree.add(f"[dim]💡 {self.help_text}[/dim]")
275
277
  console.print(tree)
276
278
  elif callable(self.action) and not isinstance(self.action, BaseAction):
277
279
  console.print(f"{label}")
280
+ if self.help_text:
281
+ console.print(f"[dim]💡 {self.help_text}[/dim]")
278
282
  console.print(
279
283
  f"[{OneColors.LIGHT_RED_b}]→ Would call:[/] {self.action.__name__}"
280
284
  f"[dim](args={self.args}, kwargs={self.kwargs})[/dim]"
281
285
  )
282
286
  else:
283
287
  console.print(f"{label}")
288
+ if self.help_text:
289
+ console.print(f"[dim]💡 {self.help_text}[/dim]")
284
290
  console.print(
285
291
  f"[{OneColors.DARK_RED}]⚠️ Action is not callable or lacks a preview method.[/]"
286
292
  )
@@ -0,0 +1,214 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """config.py
3
+ Configuration loader for Falyx CLI commands."""
4
+ from __future__ import annotations
5
+
6
+ import importlib
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any, Callable
10
+
11
+ import toml
12
+ import yaml
13
+ from pydantic import BaseModel, Field, field_validator, model_validator
14
+ from rich.console import Console
15
+
16
+ from falyx.action import Action, BaseAction
17
+ from falyx.command import Command
18
+ from falyx.falyx import Falyx
19
+ from falyx.retry import RetryPolicy
20
+ from falyx.themes.colors import OneColors
21
+ from falyx.utils import logger
22
+
23
+ console = Console(color_system="auto")
24
+
25
+
26
+ def wrap_if_needed(obj: Any, name=None) -> BaseAction | Command:
27
+ if isinstance(obj, (BaseAction, Command)):
28
+ return obj
29
+ elif callable(obj):
30
+ return Action(name=name or getattr(obj, "__name__", "unnamed"), action=obj)
31
+ else:
32
+ raise TypeError(
33
+ f"Cannot wrap object of type '{type(obj).__name__}'. "
34
+ "Expected a function or BaseAction."
35
+ )
36
+
37
+
38
+ def import_action(dotted_path: str) -> Any:
39
+ """Dynamically imports a callable from a dotted path like 'my.module.func'."""
40
+ module_path, _, attr = dotted_path.rpartition(".")
41
+ if not module_path:
42
+ console.print(f"[{OneColors.DARK_RED}]❌ Invalid action path:[/] {dotted_path}")
43
+ sys.exit(1)
44
+ try:
45
+ module = importlib.import_module(module_path)
46
+ except ModuleNotFoundError as error:
47
+ logger.error("Failed to import module '%s': %s", module_path, error)
48
+ console.print(
49
+ f"[{OneColors.DARK_RED}]❌ Could not import '{dotted_path}': {error}[/]\n"
50
+ f"[{OneColors.COMMENT_GREY}]Ensure the module is installed and discoverable via PYTHONPATH."
51
+ )
52
+ sys.exit(1)
53
+ try:
54
+ action = getattr(module, attr)
55
+ except AttributeError as error:
56
+ logger.error(
57
+ "Module '%s' does not have attribute '%s': %s", module_path, attr, error
58
+ )
59
+ console.print(
60
+ f"[{OneColors.DARK_RED}]❌ Module '{module_path}' has no attribute '{attr}': {error}[/]"
61
+ )
62
+ sys.exit(1)
63
+ return action
64
+
65
+
66
+ class RawCommand(BaseModel):
67
+ key: str
68
+ description: str
69
+ action: str
70
+
71
+ args: tuple[Any, ...] = ()
72
+ kwargs: dict[str, Any] = {}
73
+ aliases: list[str] = []
74
+ tags: list[str] = []
75
+ style: str = "white"
76
+
77
+ confirm: bool = False
78
+ confirm_message: str = "Are you sure?"
79
+ preview_before_confirm: bool = True
80
+
81
+ spinner: bool = False
82
+ spinner_message: str = "Processing..."
83
+ spinner_type: str = "dots"
84
+ spinner_style: str = "cyan"
85
+ spinner_kwargs: dict[str, Any] = {}
86
+
87
+ before_hooks: list[Callable] = []
88
+ success_hooks: list[Callable] = []
89
+ error_hooks: list[Callable] = []
90
+ after_hooks: list[Callable] = []
91
+ teardown_hooks: list[Callable] = []
92
+
93
+ logging_hooks: bool = False
94
+ retry: bool = False
95
+ retry_all: bool = False
96
+ retry_policy: RetryPolicy = Field(default_factory=RetryPolicy)
97
+ requires_input: bool | None = None
98
+ hidden: bool = False
99
+ help_text: str = ""
100
+
101
+ @field_validator("retry_policy")
102
+ @classmethod
103
+ def validate_retry_policy(cls, value: dict | RetryPolicy) -> RetryPolicy:
104
+ if isinstance(value, RetryPolicy):
105
+ return value
106
+ if not isinstance(value, dict):
107
+ raise ValueError("retry_policy must be a dictionary.")
108
+ return RetryPolicy(**value)
109
+
110
+
111
+ def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]:
112
+ commands = []
113
+ for entry in raw_commands:
114
+ raw_command = RawCommand(**entry)
115
+ commands.append(
116
+ Command.model_validate(
117
+ {
118
+ **raw_command.model_dump(exclude={"action"}),
119
+ "action": wrap_if_needed(
120
+ import_action(raw_command.action), name=raw_command.description
121
+ ),
122
+ }
123
+ )
124
+ )
125
+ return commands
126
+
127
+
128
+ class FalyxConfig(BaseModel):
129
+ title: str = "Falyx CLI"
130
+ prompt: str | list[tuple[str, str]] | list[list[str]] = [
131
+ (OneColors.BLUE_b, "FALYX > ")
132
+ ]
133
+ columns: int = 4
134
+ welcome_message: str = ""
135
+ exit_message: str = ""
136
+ commands: list[Command] | list[dict] = []
137
+
138
+ @model_validator(mode="after")
139
+ def validate_prompt_format(self) -> FalyxConfig:
140
+ if isinstance(self.prompt, list):
141
+ for pair in self.prompt:
142
+ if not isinstance(pair, (list, tuple)) or len(pair) != 2:
143
+ raise ValueError(
144
+ "Prompt list must contain 2-element (style, text) pairs"
145
+ )
146
+ return self
147
+
148
+ def to_falyx(self) -> Falyx:
149
+ flx = Falyx(
150
+ title=self.title,
151
+ prompt=self.prompt,
152
+ columns=self.columns,
153
+ welcome_message=self.welcome_message,
154
+ exit_message=self.exit_message,
155
+ )
156
+ flx.add_commands(self.commands)
157
+ return flx
158
+
159
+
160
+ def loader(file_path: Path | str) -> Falyx:
161
+ """
162
+ Load command definitions from a YAML or TOML file.
163
+
164
+ Each command should be defined as a dictionary with at least:
165
+ - key: a unique single-character key
166
+ - description: short description
167
+ - action: dotted import path to the action function/class
168
+
169
+ Args:
170
+ file_path (str): Path to the config file (YAML or TOML).
171
+
172
+ Returns:
173
+ Falyx: An instance of the Falyx CLI with loaded commands.
174
+
175
+ Raises:
176
+ ValueError: If the file format is unsupported or file cannot be parsed.
177
+ """
178
+ if isinstance(file_path, (str, Path)):
179
+ path = Path(file_path)
180
+ else:
181
+ raise TypeError("file_path must be a string or Path object.")
182
+
183
+ if not path.is_file():
184
+ raise FileNotFoundError(f"No such config file: {file_path}")
185
+
186
+ suffix = path.suffix
187
+ with path.open("r", encoding="UTF-8") as config_file:
188
+ if suffix in (".yaml", ".yml"):
189
+ raw_config = yaml.safe_load(config_file)
190
+ elif suffix == ".toml":
191
+ raw_config = toml.load(config_file)
192
+ else:
193
+ raise ValueError(f"Unsupported config format: {suffix}")
194
+
195
+ if not isinstance(raw_config, dict):
196
+ raise ValueError(
197
+ "Configuration file must contain a dictionary with a list of commands.\n"
198
+ "Example:\n"
199
+ "title: 'My CLI'\n"
200
+ "commands:\n"
201
+ " - key: 'a'\n"
202
+ " description: 'Example command'\n"
203
+ " action: 'my_module.my_function'"
204
+ )
205
+
206
+ commands = convert_commands(raw_config["commands"])
207
+ return FalyxConfig(
208
+ title=raw_config.get("title", f"[{OneColors.BLUE_b}]Falyx CLI"),
209
+ prompt=raw_config.get("prompt", [(OneColors.BLUE_b, "FALYX > ")]),
210
+ columns=raw_config.get("columns", 4),
211
+ welcome_message=raw_config.get("welcome_message", ""),
212
+ exit_message=raw_config.get("exit_message", ""),
213
+ commands=commands,
214
+ ).to_falyx()
@@ -0,0 +1,76 @@
1
+ FALYX_CONFIG_SCHEMA = {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "title": "Falyx CLI Config",
4
+ "type": "object",
5
+ "properties": {
6
+ "title": {"type": "string", "description": "Title shown at top of menu"},
7
+ "prompt": {
8
+ "oneOf": [
9
+ {"type": "string"},
10
+ {
11
+ "type": "array",
12
+ "items": {
13
+ "type": "array",
14
+ "prefixItems": [
15
+ {
16
+ "type": "string",
17
+ "description": "Style string (e.g., 'bold #ff0000 italic')",
18
+ },
19
+ {"type": "string", "description": "Text content"},
20
+ ],
21
+ "minItems": 2,
22
+ "maxItems": 2,
23
+ },
24
+ },
25
+ ]
26
+ },
27
+ "columns": {
28
+ "type": "integer",
29
+ "minimum": 1,
30
+ "description": "Number of menu columns",
31
+ },
32
+ "welcome_message": {"type": "string"},
33
+ "exit_message": {"type": "string"},
34
+ "commands": {
35
+ "type": "array",
36
+ "items": {
37
+ "type": "object",
38
+ "required": ["key", "description", "action"],
39
+ "properties": {
40
+ "key": {"type": "string", "minLength": 1},
41
+ "description": {"type": "string"},
42
+ "action": {
43
+ "type": "string",
44
+ "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*(\\.[a-zA-Z_][a-zA-Z0-9_]*)+$",
45
+ "description": "Dotted import path (e.g., mymodule.task)",
46
+ },
47
+ "args": {"type": "array"},
48
+ "kwargs": {"type": "object"},
49
+ "aliases": {"type": "array", "items": {"type": "string"}},
50
+ "tags": {"type": "array", "items": {"type": "string"}},
51
+ "style": {"type": "string"},
52
+ "confirm": {"type": "boolean"},
53
+ "confirm_message": {"type": "string"},
54
+ "preview_before_confirm": {"type": "boolean"},
55
+ "spinner": {"type": "boolean"},
56
+ "spinner_message": {"type": "string"},
57
+ "spinner_type": {"type": "string"},
58
+ "spinner_style": {"type": "string"},
59
+ "logging_hooks": {"type": "boolean"},
60
+ "retry": {"type": "boolean"},
61
+ "retry_all": {"type": "boolean"},
62
+ "retry_policy": {
63
+ "type": "object",
64
+ "properties": {
65
+ "enabled": {"type": "boolean"},
66
+ "max_retries": {"type": "integer"},
67
+ "delay": {"type": "number"},
68
+ "backoff": {"type": "number"},
69
+ },
70
+ },
71
+ },
72
+ },
73
+ },
74
+ },
75
+ "required": ["commands"],
76
+ }
@@ -9,6 +9,7 @@ from rich.console import Console
9
9
  from rich.table import Table
10
10
 
11
11
  from falyx.context import ExecutionContext
12
+ from falyx.themes.colors import OneColors
12
13
  from falyx.utils import logger
13
14
 
14
15
 
@@ -66,10 +67,10 @@ class ExecutionRegistry:
66
67
  duration = f"{ctx.duration:.3f}s" if ctx.duration else "n/a"
67
68
 
68
69
  if ctx.exception:
69
- status = "[bold red]❌ Error"
70
+ status = f"[{OneColors.DARK_RED}]❌ Error"
70
71
  result = repr(ctx.exception)
71
72
  else:
72
- status = "[green]✅ Success"
73
+ status = f"[{OneColors.GREEN}]✅ Success"
73
74
  result = repr(ctx.result)
74
75
  if len(result) > 1000:
75
76
  result = f"{result[:1000]}..."
@@ -283,7 +283,7 @@ class Falyx:
283
283
  self.console.print(table, justify="center")
284
284
  if self.mode == FalyxMode.MENU:
285
285
  self.console.print(
286
- f"📦 Tip: Type '[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command before running it.\n",
286
+ f"📦 Tip: '[{OneColors.LIGHT_YELLOW}]?[KEY][/]' to preview a command before running it.\n",
287
287
  justify="center",
288
288
  )
289
289
 
@@ -291,7 +291,7 @@ class Falyx:
291
291
  """Returns the help command for the menu."""
292
292
  return Command(
293
293
  key="H",
294
- aliases=["HELP"],
294
+ aliases=["HELP", "?"],
295
295
  description="Help",
296
296
  action=self._show_help,
297
297
  style=OneColors.LIGHT_YELLOW,
@@ -343,7 +343,9 @@ class Falyx:
343
343
  error_message = " ".join(message_lines)
344
344
 
345
345
  def validator(text):
346
- _, choice = self.get_command(text, from_validate=True)
346
+ is_preview, choice = self.get_command(text, from_validate=True)
347
+ if is_preview and choice is None:
348
+ return True
347
349
  return True if choice else False
348
350
 
349
351
  return Validator.from_callable(
@@ -558,10 +560,24 @@ class Falyx:
558
560
  self.add_command(key, description, submenu.menu, style=style)
559
561
  submenu.update_exit_command(key="B", description="Back", aliases=["BACK"])
560
562
 
561
- def add_commands(self, commands: list[dict]) -> None:
562
- """Adds multiple commands to the menu."""
563
+ def add_commands(self, commands: list[Command] | list[dict]) -> None:
564
+ """Adds a list of Command instances or config dicts."""
563
565
  for command in commands:
564
- self.add_command(**command)
566
+ if isinstance(command, dict):
567
+ self.add_command(**command)
568
+ elif isinstance(command, Command):
569
+ self.add_command_from_command(command)
570
+ else:
571
+ raise FalyxError(
572
+ "Command must be a dictionary or an instance of Command."
573
+ )
574
+
575
+ def add_command_from_command(self, command: Command) -> None:
576
+ """Adds a command to the menu from an existing Command object."""
577
+ if not isinstance(command, Command):
578
+ raise FalyxError("command must be an instance of Command.")
579
+ self._validate_command_key(command.key)
580
+ self.commands[command.key] = command
565
581
 
566
582
  def add_command(
567
583
  self,
@@ -694,6 +710,16 @@ class Falyx:
694
710
  ) -> tuple[bool, Command | None]:
695
711
  """Returns the selected command based on user input. Supports keys, aliases, and abbreviations."""
696
712
  is_preview, choice = self.parse_preview_command(choice)
713
+ if is_preview and not choice and self.help_command:
714
+ is_preview = False
715
+ choice = "?"
716
+ elif is_preview and not choice:
717
+ if not from_validate:
718
+ self.console.print(
719
+ f"[{OneColors.DARK_RED}]❌ You must enter a command for preview mode.[/]"
720
+ )
721
+ return is_preview, None
722
+
697
723
  choice = choice.upper()
698
724
  name_map = self._name_map
699
725
 
@@ -788,12 +814,17 @@ class Falyx:
788
814
  async def run_key(self, command_key: str, return_context: bool = False) -> Any:
789
815
  """Run a command by key without displaying the menu (non-interactive mode)."""
790
816
  self.debug_hooks()
791
- _, selected_command = self.get_command(command_key)
817
+ is_preview, selected_command = self.get_command(command_key)
792
818
  self.last_run_command = selected_command
793
819
 
794
820
  if not selected_command:
795
821
  return None
796
822
 
823
+ if is_preview:
824
+ logger.info(f"Preview command '{selected_command.key}' selected.")
825
+ await selected_command.preview()
826
+ return None
827
+
797
828
  logger.info(
798
829
  "[run_key] 🚀 Executing: %s — %s",
799
830
  selected_command.key,
@@ -877,28 +908,29 @@ class Falyx:
877
908
  self.debug_hooks()
878
909
  if self.welcome_message:
879
910
  self.print_message(self.welcome_message)
880
- while True:
881
- if callable(self.render_menu):
882
- self.render_menu(self)
883
- else:
884
- self.console.print(self.table, justify="center")
885
- try:
886
- task = asyncio.create_task(self.process_command())
887
- should_continue = await task
888
- if not should_continue:
911
+ try:
912
+ while True:
913
+ if callable(self.render_menu):
914
+ self.render_menu(self)
915
+ else:
916
+ self.console.print(self.table, justify="center")
917
+ try:
918
+ task = asyncio.create_task(self.process_command())
919
+ should_continue = await task
920
+ if not should_continue:
921
+ break
922
+ except (EOFError, KeyboardInterrupt):
923
+ logger.info("EOF or KeyboardInterrupt. Exiting menu.")
889
924
  break
890
- except (EOFError, KeyboardInterrupt):
891
- logger.info("EOF or KeyboardInterrupt. Exiting menu.")
892
- break
893
- except QuitSignal:
894
- logger.info("QuitSignal received. Exiting menu.")
895
- break
896
- except BackSignal:
897
- logger.info("BackSignal received.")
898
- finally:
899
- logger.info(f"Exiting menu: {self.get_title()}")
900
- if self.exit_message:
901
- self.print_message(self.exit_message)
925
+ except QuitSignal:
926
+ logger.info("QuitSignal received. Exiting menu.")
927
+ break
928
+ except BackSignal:
929
+ logger.info("BackSignal received.")
930
+ finally:
931
+ logger.info(f"Exiting menu: {self.get_title()}")
932
+ if self.exit_message:
933
+ self.print_message(self.exit_message)
902
934
 
903
935
  async def run(self) -> None:
904
936
  """Run Falyx CLI with structured subcommands."""
@@ -943,11 +975,14 @@ class Falyx:
943
975
 
944
976
  if self.cli_args.command == "run":
945
977
  self.mode = FalyxMode.RUN
946
- _, command = self.get_command(self.cli_args.name)
978
+ is_preview, command = self.get_command(self.cli_args.name)
979
+ if is_preview:
980
+ if command is None:
981
+ sys.exit(1)
982
+ logger.info(f"Preview command '{command.key}' selected.")
983
+ await command.preview()
984
+ sys.exit(0)
947
985
  if not command:
948
- self.console.print(
949
- f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found.[/]"
950
- )
951
986
  sys.exit(1)
952
987
  self._set_retry_policy(command)
953
988
  try:
@@ -955,6 +990,9 @@ class Falyx:
955
990
  except FalyxError as error:
956
991
  self.console.print(f"[{OneColors.DARK_RED}]❌ Error: {error}[/]")
957
992
  sys.exit(1)
993
+
994
+ if self.cli_args.summary:
995
+ er.summary()
958
996
  sys.exit(0)
959
997
 
960
998
  if self.cli_args.command == "run-all":
@@ -976,6 +1014,10 @@ class Falyx:
976
1014
  for cmd in matching:
977
1015
  self._set_retry_policy(cmd)
978
1016
  await self.run_key(cmd.key)
1017
+
1018
+ if self.cli_args.summary:
1019
+ er.summary()
1020
+
979
1021
  sys.exit(0)
980
1022
 
981
1023
  await self.menu()