falyx 0.1.27__tar.gz → 0.1.29__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 (53) hide show
  1. {falyx-0.1.27 → falyx-0.1.29}/PKG-INFO +1 -1
  2. falyx-0.1.29/falyx/action/.pytyped +0 -0
  3. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/io_action.py +4 -1
  4. {falyx-0.1.27 → falyx-0.1.29}/falyx/command.py +100 -2
  5. {falyx-0.1.27 → falyx-0.1.29}/falyx/exceptions.py +4 -0
  6. {falyx-0.1.27 → falyx-0.1.29}/falyx/falyx.py +123 -38
  7. falyx-0.1.29/falyx/parsers/.pytyped +0 -0
  8. falyx-0.1.29/falyx/parsers/__init__.py +21 -0
  9. falyx-0.1.29/falyx/parsers/argparse.py +756 -0
  10. {falyx-0.1.27/falyx → falyx-0.1.29/falyx/parsers}/parsers.py +7 -1
  11. falyx-0.1.29/falyx/parsers/signature.py +71 -0
  12. falyx-0.1.29/falyx/parsers/utils.py +33 -0
  13. {falyx-0.1.27 → falyx-0.1.29}/falyx/protocols.py +7 -1
  14. {falyx-0.1.27 → falyx-0.1.29}/falyx/signals.py +7 -0
  15. falyx-0.1.29/falyx/version.py +1 -0
  16. {falyx-0.1.27 → falyx-0.1.29}/pyproject.toml +1 -1
  17. falyx-0.1.27/falyx/config_schema.py +0 -76
  18. falyx-0.1.27/falyx/version.py +0 -1
  19. {falyx-0.1.27 → falyx-0.1.29}/LICENSE +0 -0
  20. {falyx-0.1.27 → falyx-0.1.29}/README.md +0 -0
  21. {falyx-0.1.27 → falyx-0.1.29}/falyx/.pytyped +0 -0
  22. {falyx-0.1.27 → falyx-0.1.29}/falyx/__init__.py +0 -0
  23. {falyx-0.1.27 → falyx-0.1.29}/falyx/__main__.py +0 -0
  24. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/__init__.py +0 -0
  25. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/action.py +0 -0
  26. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/action_factory.py +0 -0
  27. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/http_action.py +0 -0
  28. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/menu_action.py +0 -0
  29. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/select_file_action.py +0 -0
  30. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/selection_action.py +0 -0
  31. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/signal_action.py +0 -0
  32. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/types.py +0 -0
  33. {falyx-0.1.27 → falyx-0.1.29}/falyx/action/user_input_action.py +0 -0
  34. {falyx-0.1.27 → falyx-0.1.29}/falyx/bottom_bar.py +0 -0
  35. {falyx-0.1.27 → falyx-0.1.29}/falyx/config.py +0 -0
  36. {falyx-0.1.27 → falyx-0.1.29}/falyx/context.py +0 -0
  37. {falyx-0.1.27 → falyx-0.1.29}/falyx/debug.py +0 -0
  38. {falyx-0.1.27 → falyx-0.1.29}/falyx/execution_registry.py +0 -0
  39. {falyx-0.1.27 → falyx-0.1.29}/falyx/hook_manager.py +0 -0
  40. {falyx-0.1.27 → falyx-0.1.29}/falyx/hooks.py +0 -0
  41. {falyx-0.1.27 → falyx-0.1.29}/falyx/init.py +0 -0
  42. {falyx-0.1.27 → falyx-0.1.29}/falyx/logger.py +0 -0
  43. {falyx-0.1.27 → falyx-0.1.29}/falyx/menu.py +0 -0
  44. {falyx-0.1.27 → falyx-0.1.29}/falyx/options_manager.py +0 -0
  45. {falyx-0.1.27 → falyx-0.1.29}/falyx/prompt_utils.py +0 -0
  46. {falyx-0.1.27 → falyx-0.1.29}/falyx/retry.py +0 -0
  47. {falyx-0.1.27 → falyx-0.1.29}/falyx/retry_utils.py +0 -0
  48. {falyx-0.1.27 → falyx-0.1.29}/falyx/selection.py +0 -0
  49. {falyx-0.1.27 → falyx-0.1.29}/falyx/tagged_table.py +0 -0
  50. {falyx-0.1.27 → falyx-0.1.29}/falyx/themes/__init__.py +0 -0
  51. {falyx-0.1.27 → falyx-0.1.29}/falyx/themes/colors.py +0 -0
  52. {falyx-0.1.27 → falyx-0.1.29}/falyx/utils.py +0 -0
  53. {falyx-0.1.27 → falyx-0.1.29}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.27
3
+ Version: 0.1.29
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
File without changes
@@ -224,7 +224,10 @@ class ShellAction(BaseIOAction):
224
224
  # Replace placeholder in template, or use raw input as full command
225
225
  command = self.command_template.format(parsed_input)
226
226
  if self.safe_mode:
227
- args = shlex.split(command)
227
+ try:
228
+ args = shlex.split(command)
229
+ except ValueError as error:
230
+ raise FalyxError(f"Invalid command template: {error}")
228
231
  result = subprocess.run(args, capture_output=True, text=True, check=True)
229
232
  else:
230
233
  result = subprocess.run(
@@ -18,6 +18,7 @@ in building robust interactive menus.
18
18
  """
19
19
  from __future__ import annotations
20
20
 
21
+ import shlex
21
22
  from functools import cached_property
22
23
  from typing import Any, Callable
23
24
 
@@ -26,7 +27,13 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, field_validator
26
27
  from rich.console import Console
27
28
  from rich.tree import Tree
28
29
 
29
- from falyx.action.action import Action, ActionGroup, BaseAction, ChainedAction
30
+ from falyx.action.action import (
31
+ Action,
32
+ ActionGroup,
33
+ BaseAction,
34
+ ChainedAction,
35
+ ProcessAction,
36
+ )
30
37
  from falyx.action.io_action import BaseIOAction
31
38
  from falyx.context import ExecutionContext
32
39
  from falyx.debug import register_debug_hooks
@@ -34,7 +41,13 @@ from falyx.execution_registry import ExecutionRegistry as er
34
41
  from falyx.hook_manager import HookManager, HookType
35
42
  from falyx.logger import logger
36
43
  from falyx.options_manager import OptionsManager
44
+ from falyx.parsers import (
45
+ CommandArgumentParser,
46
+ infer_args_from_func,
47
+ same_argument_definitions,
48
+ )
37
49
  from falyx.prompt_utils import confirm_async, should_prompt_user
50
+ from falyx.protocols import ArgParserProtocol
38
51
  from falyx.retry import RetryPolicy
39
52
  from falyx.retry_utils import enable_retries_recursively
40
53
  from falyx.signals import CancelSignal
@@ -87,6 +100,11 @@ class Command(BaseModel):
87
100
  tags (list[str]): Organizational tags for the command.
88
101
  logging_hooks (bool): Whether to attach logging hooks automatically.
89
102
  requires_input (bool | None): Indicates if the action needs input.
103
+ options_manager (OptionsManager): Manages global command-line options.
104
+ arg_parser (CommandArgumentParser): Parses command arguments.
105
+ custom_parser (ArgParserProtocol | None): Custom argument parser.
106
+ custom_help (Callable[[], str | None] | None): Custom help message generator.
107
+ auto_args (bool): Automatically infer arguments from the action.
90
108
 
91
109
  Methods:
92
110
  __call__(): Executes the command, respecting hooks and retries.
@@ -98,12 +116,13 @@ class Command(BaseModel):
98
116
 
99
117
  key: str
100
118
  description: str
101
- action: BaseAction | Callable[[], Any]
119
+ action: BaseAction | Callable[[Any], Any]
102
120
  args: tuple = ()
103
121
  kwargs: dict[str, Any] = Field(default_factory=dict)
104
122
  hidden: bool = False
105
123
  aliases: list[str] = Field(default_factory=list)
106
124
  help_text: str = ""
125
+ help_epilogue: str = ""
107
126
  style: str = OneColors.WHITE
108
127
  confirm: bool = False
109
128
  confirm_message: str = "Are you sure?"
@@ -121,11 +140,46 @@ class Command(BaseModel):
121
140
  logging_hooks: bool = False
122
141
  requires_input: bool | None = None
123
142
  options_manager: OptionsManager = Field(default_factory=OptionsManager)
143
+ arg_parser: CommandArgumentParser = Field(default_factory=CommandArgumentParser)
144
+ arguments: list[dict[str, Any]] = Field(default_factory=list)
145
+ argument_config: Callable[[CommandArgumentParser], None] | None = None
146
+ custom_parser: ArgParserProtocol | None = None
147
+ custom_help: Callable[[], str | None] | None = None
148
+ auto_args: bool = False
149
+ arg_metadata: dict[str, str | dict[str, Any]] = Field(default_factory=dict)
124
150
 
125
151
  _context: ExecutionContext | None = PrivateAttr(default=None)
126
152
 
127
153
  model_config = ConfigDict(arbitrary_types_allowed=True)
128
154
 
155
+ def parse_args(
156
+ self, raw_args: list[str] | str, from_validate: bool = False
157
+ ) -> tuple[tuple, dict]:
158
+ if self.custom_parser:
159
+ if isinstance(raw_args, str):
160
+ try:
161
+ raw_args = shlex.split(raw_args)
162
+ except ValueError:
163
+ logger.warning(
164
+ "[Command:%s] Failed to split arguments: %s",
165
+ self.key,
166
+ raw_args,
167
+ )
168
+ return ((), {})
169
+ return self.custom_parser(raw_args)
170
+
171
+ if isinstance(raw_args, str):
172
+ try:
173
+ raw_args = shlex.split(raw_args)
174
+ except ValueError:
175
+ logger.warning(
176
+ "[Command:%s] Failed to split arguments: %s",
177
+ self.key,
178
+ raw_args,
179
+ )
180
+ return ((), {})
181
+ return self.arg_parser.parse_args_split(raw_args, from_validate=from_validate)
182
+
129
183
  @field_validator("action", mode="before")
130
184
  @classmethod
131
185
  def wrap_callable_as_async(cls, action: Any) -> Any:
@@ -135,6 +189,35 @@ class Command(BaseModel):
135
189
  return ensure_async(action)
136
190
  raise TypeError("Action must be a callable or an instance of BaseAction")
137
191
 
192
+ def get_argument_definitions(self) -> list[dict[str, Any]]:
193
+ if self.arguments:
194
+ return self.arguments
195
+ elif self.argument_config:
196
+ self.argument_config(self.arg_parser)
197
+ elif self.auto_args:
198
+ if isinstance(self.action, (Action, ProcessAction)):
199
+ return infer_args_from_func(self.action.action, self.arg_metadata)
200
+ elif isinstance(self.action, ChainedAction):
201
+ if self.action.actions:
202
+ action = self.action.actions[0]
203
+ if isinstance(action, Action):
204
+ return infer_args_from_func(action.action, self.arg_metadata)
205
+ elif callable(action):
206
+ return infer_args_from_func(action, self.arg_metadata)
207
+ elif isinstance(self.action, ActionGroup):
208
+ arg_defs = same_argument_definitions(
209
+ self.action.actions, self.arg_metadata
210
+ )
211
+ if arg_defs:
212
+ return arg_defs
213
+ logger.debug(
214
+ "[Command:%s] auto_args disabled: mismatched ActionGroup arguments",
215
+ self.key,
216
+ )
217
+ elif callable(self.action):
218
+ return infer_args_from_func(self.action, self.arg_metadata)
219
+ return []
220
+
138
221
  def model_post_init(self, _: Any) -> None:
139
222
  """Post-initialization to set up the action and hooks."""
140
223
  if self.retry and isinstance(self.action, Action):
@@ -164,6 +247,9 @@ class Command(BaseModel):
164
247
  elif self.requires_input is None:
165
248
  self.requires_input = False
166
249
 
250
+ for arg_def in self.get_argument_definitions():
251
+ self.arg_parser.add_argument(*arg_def.pop("flags"), **arg_def)
252
+
167
253
  @cached_property
168
254
  def detect_requires_input(self) -> bool:
169
255
  """Detect if the action requires input based on its type."""
@@ -269,6 +355,18 @@ class Command(BaseModel):
269
355
  if self._context:
270
356
  self._context.log_summary()
271
357
 
358
+ def show_help(self) -> bool:
359
+ """Display the help message for the command."""
360
+ if self.custom_help:
361
+ output = self.custom_help()
362
+ if output:
363
+ console.print(output)
364
+ return True
365
+ if isinstance(self.arg_parser, CommandArgumentParser):
366
+ self.arg_parser.render_help()
367
+ return True
368
+ return False
369
+
272
370
  async def preview(self) -> None:
273
371
  label = f"[{OneColors.GREEN_b}]Command:[/] '{self.key}' — {self.description}"
274
372
 
@@ -28,3 +28,7 @@ class CircuitBreakerOpen(FalyxError):
28
28
 
29
29
  class EmptyChainError(FalyxError):
30
30
  """Exception raised when the chain is empty."""
31
+
32
+
33
+ class CommandArgumentError(FalyxError):
34
+ """Exception raised when there is an error in the command argument parser."""
@@ -23,6 +23,7 @@ from __future__ import annotations
23
23
 
24
24
  import asyncio
25
25
  import logging
26
+ import shlex
26
27
  import sys
27
28
  from argparse import Namespace
28
29
  from difflib import get_close_matches
@@ -34,7 +35,8 @@ from prompt_toolkit import PromptSession
34
35
  from prompt_toolkit.completion import WordCompleter
35
36
  from prompt_toolkit.formatted_text import AnyFormattedText
36
37
  from prompt_toolkit.key_binding import KeyBindings
37
- from prompt_toolkit.validation import Validator
38
+ from prompt_toolkit.patch_stdout import patch_stdout
39
+ from prompt_toolkit.validation import ValidationError, Validator
38
40
  from rich import box
39
41
  from rich.console import Console
40
42
  from rich.markdown import Markdown
@@ -47,6 +49,7 @@ from falyx.context import ExecutionContext
47
49
  from falyx.debug import log_after, log_before, log_error, log_success
48
50
  from falyx.exceptions import (
49
51
  CommandAlreadyExistsError,
52
+ CommandArgumentError,
50
53
  FalyxError,
51
54
  InvalidActionError,
52
55
  NotAFalyxError,
@@ -55,21 +58,42 @@ from falyx.execution_registry import ExecutionRegistry as er
55
58
  from falyx.hook_manager import Hook, HookManager, HookType
56
59
  from falyx.logger import logger
57
60
  from falyx.options_manager import OptionsManager
58
- from falyx.parsers import get_arg_parsers
61
+ from falyx.parsers import CommandArgumentParser, get_arg_parsers
62
+ from falyx.protocols import ArgParserProtocol
59
63
  from falyx.retry import RetryPolicy
60
- from falyx.signals import BackSignal, CancelSignal, QuitSignal
64
+ from falyx.signals import BackSignal, CancelSignal, FlowSignal, HelpSignal, QuitSignal
61
65
  from falyx.themes import OneColors, get_nord_theme
62
66
  from falyx.utils import CaseInsensitiveDict, _noop, chunks, get_program_invocation
63
67
  from falyx.version import __version__
64
68
 
65
69
 
66
- class FalyxMode(str, Enum):
70
+ class FalyxMode(Enum):
67
71
  MENU = "menu"
68
72
  RUN = "run"
69
73
  PREVIEW = "preview"
70
74
  RUN_ALL = "run-all"
71
75
 
72
76
 
77
+ class CommandValidator(Validator):
78
+ """Validator to check if the input is a valid command or toggle key."""
79
+
80
+ def __init__(self, falyx: Falyx, error_message: str) -> None:
81
+ super().__init__()
82
+ self.falyx = falyx
83
+ self.error_message = error_message
84
+
85
+ def validate(self, document) -> None:
86
+ text = document.text
87
+ is_preview, choice, _, __ = self.falyx.get_command(text, from_validate=True)
88
+ if is_preview:
89
+ return None
90
+ if not choice:
91
+ raise ValidationError(
92
+ message=self.error_message,
93
+ cursor_position=document.get_end_of_document_position(),
94
+ )
95
+
96
+
73
97
  class Falyx:
74
98
  """
75
99
  Main menu controller for Falyx CLI applications.
@@ -325,7 +349,7 @@ class Falyx:
325
349
  keys.extend(cmd.aliases)
326
350
  return WordCompleter(keys, ignore_case=True)
327
351
 
328
- def _get_validator(self) -> Validator:
352
+ def _get_validator_error_message(self) -> str:
329
353
  """Validator to check if the input is a valid command or toggle key."""
330
354
  keys = {self.exit_command.key.upper()}
331
355
  keys.update({alias.upper() for alias in self.exit_command.aliases})
@@ -354,18 +378,7 @@ class Falyx:
354
378
  if toggle_keys:
355
379
  message_lines.append(f" Toggles: {toggles_str}")
356
380
  error_message = " ".join(message_lines)
357
-
358
- def validator(text):
359
- is_preview, choice = self.get_command(text, from_validate=True)
360
- if is_preview and choice is None:
361
- return True
362
- return bool(choice)
363
-
364
- return Validator.from_callable(
365
- validator,
366
- error_message=error_message,
367
- move_cursor_to_end=True,
368
- )
381
+ return error_message
369
382
 
370
383
  def _invalidate_prompt_session_cache(self):
371
384
  """Forces the prompt session to be recreated on the next access."""
@@ -428,9 +441,11 @@ class Falyx:
428
441
  multiline=False,
429
442
  completer=self._get_completer(),
430
443
  reserve_space_for_menu=1,
431
- validator=self._get_validator(),
444
+ validator=CommandValidator(self, self._get_validator_error_message()),
432
445
  bottom_toolbar=self._get_bottom_bar_render(),
433
446
  key_bindings=self.key_bindings,
447
+ validate_while_typing=False,
448
+ interrupt_exception=FlowSignal,
434
449
  )
435
450
  return self._prompt_session
436
451
 
@@ -511,7 +526,7 @@ class Falyx:
511
526
  key: str = "X",
512
527
  description: str = "Exit",
513
528
  aliases: list[str] | None = None,
514
- action: Callable[[], Any] | None = None,
529
+ action: Callable[[Any], Any] | None = None,
515
530
  style: str = OneColors.DARK_RED,
516
531
  confirm: bool = False,
517
532
  confirm_message: str = "Are you sure?",
@@ -565,13 +580,14 @@ class Falyx:
565
580
  self,
566
581
  key: str,
567
582
  description: str,
568
- action: BaseAction | Callable[[], Any],
583
+ action: BaseAction | Callable[[Any], Any],
569
584
  *,
570
585
  args: tuple = (),
571
586
  kwargs: dict[str, Any] | None = None,
572
587
  hidden: bool = False,
573
588
  aliases: list[str] | None = None,
574
589
  help_text: str = "",
590
+ help_epilogue: str = "",
575
591
  style: str = OneColors.WHITE,
576
592
  confirm: bool = False,
577
593
  confirm_message: str = "Are you sure?",
@@ -593,9 +609,33 @@ class Falyx:
593
609
  retry_all: bool = False,
594
610
  retry_policy: RetryPolicy | None = None,
595
611
  requires_input: bool | None = None,
612
+ arg_parser: CommandArgumentParser | None = None,
613
+ arguments: list[dict[str, Any]] | None = None,
614
+ argument_config: Callable[[CommandArgumentParser], None] | None = None,
615
+ custom_parser: ArgParserProtocol | None = None,
616
+ custom_help: Callable[[], str | None] | None = None,
617
+ auto_args: bool = False,
618
+ arg_metadata: dict[str, str | dict[str, Any]] | None = None,
596
619
  ) -> Command:
597
620
  """Adds an command to the menu, preventing duplicates."""
598
621
  self._validate_command_key(key)
622
+
623
+ if arg_parser:
624
+ if not isinstance(arg_parser, CommandArgumentParser):
625
+ raise NotAFalyxError(
626
+ "arg_parser must be an instance of CommandArgumentParser."
627
+ )
628
+ arg_parser = arg_parser
629
+ else:
630
+ arg_parser = CommandArgumentParser(
631
+ command_key=key,
632
+ command_description=description,
633
+ command_style=style,
634
+ help_text=help_text,
635
+ help_epilogue=help_epilogue,
636
+ aliases=aliases,
637
+ )
638
+
599
639
  command = Command(
600
640
  key=key,
601
641
  description=description,
@@ -605,6 +645,7 @@ class Falyx:
605
645
  hidden=hidden,
606
646
  aliases=aliases if aliases else [],
607
647
  help_text=help_text,
648
+ help_epilogue=help_epilogue,
608
649
  style=style,
609
650
  confirm=confirm,
610
651
  confirm_message=confirm_message,
@@ -621,6 +662,13 @@ class Falyx:
621
662
  retry_policy=retry_policy or RetryPolicy(),
622
663
  requires_input=requires_input,
623
664
  options_manager=self.options,
665
+ arg_parser=arg_parser,
666
+ arguments=arguments or [],
667
+ argument_config=argument_config,
668
+ custom_parser=custom_parser,
669
+ custom_help=custom_help,
670
+ auto_args=auto_args,
671
+ arg_metadata=arg_metadata or {},
624
672
  )
625
673
 
626
674
  if hooks:
@@ -694,32 +742,57 @@ class Falyx:
694
742
  return False, input_str.strip()
695
743
 
696
744
  def get_command(
697
- self, choice: str, from_validate=False
698
- ) -> tuple[bool, Command | None]:
745
+ self, raw_choices: str, from_validate=False
746
+ ) -> tuple[bool, Command | None, tuple, dict[str, Any]]:
699
747
  """
700
748
  Returns the selected command based on user input.
701
749
  Supports keys, aliases, and abbreviations.
702
750
  """
751
+ args = ()
752
+ kwargs: dict[str, Any] = {}
753
+ try:
754
+ choice, *input_args = shlex.split(raw_choices)
755
+ except ValueError:
756
+ return False, None, args, kwargs
703
757
  is_preview, choice = self.parse_preview_command(choice)
704
758
  if is_preview and not choice and self.help_command:
705
759
  is_preview = False
706
760
  choice = "?"
707
761
  elif is_preview and not choice:
762
+ # No help command enabled
708
763
  if not from_validate:
709
764
  self.console.print(
710
765
  f"[{OneColors.DARK_RED}]❌ You must enter a command for preview mode."
711
766
  )
712
- return is_preview, None
767
+ return is_preview, None, args, kwargs
713
768
 
714
769
  choice = choice.upper()
715
770
  name_map = self._name_map
716
-
717
771
  if choice in name_map:
718
- return is_preview, name_map[choice]
772
+ if not from_validate:
773
+ logger.info("Command '%s' selected.", choice)
774
+ if input_args and name_map[choice].arg_parser:
775
+ try:
776
+ args, kwargs = name_map[choice].parse_args(input_args, from_validate)
777
+ except CommandArgumentError as error:
778
+ if not from_validate:
779
+ if not name_map[choice].show_help():
780
+ self.console.print(
781
+ f"[{OneColors.DARK_RED}]❌ Invalid arguments for '{choice}': {error}"
782
+ )
783
+ else:
784
+ name_map[choice].show_help()
785
+ raise ValidationError(
786
+ message=str(error), cursor_position=len(raw_choices)
787
+ )
788
+ return is_preview, None, args, kwargs
789
+ except HelpSignal:
790
+ return True, None, args, kwargs
791
+ return is_preview, name_map[choice], args, kwargs
719
792
 
720
793
  prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)]
721
794
  if len(prefix_matches) == 1:
722
- return is_preview, prefix_matches[0]
795
+ return is_preview, prefix_matches[0], args, kwargs
723
796
 
724
797
  fuzzy_matches = get_close_matches(choice, list(name_map.keys()), n=3, cutoff=0.7)
725
798
  if fuzzy_matches:
@@ -736,7 +809,7 @@ class Falyx:
736
809
  self.console.print(
737
810
  f"[{OneColors.LIGHT_YELLOW}]⚠️ Unknown command '{choice}'[/]"
738
811
  )
739
- return is_preview, None
812
+ return is_preview, None, args, kwargs
740
813
 
741
814
  def _create_context(self, selected_command: Command) -> ExecutionContext:
742
815
  """Creates a context dictionary for the selected command."""
@@ -759,8 +832,9 @@ class Falyx:
759
832
 
760
833
  async def process_command(self) -> bool:
761
834
  """Processes the action of the selected command."""
762
- choice = await self.prompt_session.prompt_async()
763
- is_preview, selected_command = self.get_command(choice)
835
+ with patch_stdout(raw=True):
836
+ choice = await self.prompt_session.prompt_async()
837
+ is_preview, selected_command, args, kwargs = self.get_command(choice)
764
838
  if not selected_command:
765
839
  logger.info("Invalid command '%s'.", choice)
766
840
  return True
@@ -789,8 +863,7 @@ class Falyx:
789
863
  context.start_timer()
790
864
  try:
791
865
  await self.hooks.trigger(HookType.BEFORE, context)
792
-
793
- result = await selected_command()
866
+ result = await selected_command(*args, **kwargs)
794
867
  context.result = result
795
868
  await self.hooks.trigger(HookType.ON_SUCCESS, context)
796
869
  except Exception as error:
@@ -803,10 +876,18 @@ class Falyx:
803
876
  await self.hooks.trigger(HookType.ON_TEARDOWN, context)
804
877
  return True
805
878
 
806
- async def run_key(self, command_key: str, return_context: bool = False) -> Any:
879
+ async def run_key(
880
+ self,
881
+ command_key: str,
882
+ return_context: bool = False,
883
+ args: tuple = (),
884
+ kwargs: dict[str, Any] | None = None,
885
+ ) -> Any:
807
886
  """Run a command by key without displaying the menu (non-interactive mode)."""
808
887
  self.debug_hooks()
809
- is_preview, selected_command = self.get_command(command_key)
888
+ is_preview, selected_command, _, __ = self.get_command(command_key)
889
+ kwargs = kwargs or {}
890
+
810
891
  self.last_run_command = selected_command
811
892
 
812
893
  if not selected_command:
@@ -827,7 +908,7 @@ class Falyx:
827
908
  context.start_timer()
828
909
  try:
829
910
  await self.hooks.trigger(HookType.BEFORE, context)
830
- result = await selected_command()
911
+ result = await selected_command(*args, **kwargs)
831
912
  context.result = result
832
913
 
833
914
  await self.hooks.trigger(HookType.ON_SUCCESS, context)
@@ -951,12 +1032,12 @@ class Falyx:
951
1032
  sys.exit(0)
952
1033
 
953
1034
  if self.cli_args.command == "version" or self.cli_args.version:
954
- self.console.print(f"[{OneColors.GREEN_b}]Falyx CLI v{__version__}[/]")
1035
+ self.console.print(f"[{OneColors.BLUE_b}]Falyx CLI v{__version__}[/]")
955
1036
  sys.exit(0)
956
1037
 
957
1038
  if self.cli_args.command == "preview":
958
1039
  self.mode = FalyxMode.PREVIEW
959
- _, command = self.get_command(self.cli_args.name)
1040
+ _, command, args, kwargs = self.get_command(self.cli_args.name)
960
1041
  if not command:
961
1042
  self.console.print(
962
1043
  f"[{OneColors.DARK_RED}]❌ Command '{self.cli_args.name}' not found."
@@ -970,7 +1051,7 @@ class Falyx:
970
1051
 
971
1052
  if self.cli_args.command == "run":
972
1053
  self.mode = FalyxMode.RUN
973
- is_preview, command = self.get_command(self.cli_args.name)
1054
+ is_preview, command, _, __ = self.get_command(self.cli_args.name)
974
1055
  if is_preview:
975
1056
  if command is None:
976
1057
  sys.exit(1)
@@ -981,7 +1062,11 @@ class Falyx:
981
1062
  sys.exit(1)
982
1063
  self._set_retry_policy(command)
983
1064
  try:
984
- await self.run_key(self.cli_args.name)
1065
+ args, kwargs = command.parse_args(self.cli_args.command_args)
1066
+ except HelpSignal:
1067
+ sys.exit(0)
1068
+ try:
1069
+ await self.run_key(self.cli_args.name, args=args, kwargs=kwargs)
985
1070
  except FalyxError as error:
986
1071
  self.console.print(f"[{OneColors.DARK_RED}]❌ Error: {error}[/]")
987
1072
  sys.exit(1)
File without changes
@@ -0,0 +1,21 @@
1
+ """
2
+ Falyx CLI Framework
3
+
4
+ Copyright (c) 2025 rtj.dev LLC.
5
+ Licensed under the MIT License. See LICENSE file for details.
6
+ """
7
+
8
+ from .argparse import Argument, ArgumentAction, CommandArgumentParser
9
+ from .parsers import FalyxParsers, get_arg_parsers
10
+ from .signature import infer_args_from_func
11
+ from .utils import same_argument_definitions
12
+
13
+ __all__ = [
14
+ "Argument",
15
+ "ArgumentAction",
16
+ "CommandArgumentParser",
17
+ "get_arg_parsers",
18
+ "FalyxParsers",
19
+ "infer_args_from_func",
20
+ "same_argument_definitions",
21
+ ]