falyx 0.1.28__py3-none-any.whl → 0.1.30__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.
falyx/action/.pytyped ADDED
File without changes
falyx/action/action.py CHANGED
@@ -47,6 +47,7 @@ from falyx.execution_registry import ExecutionRegistry as er
47
47
  from falyx.hook_manager import Hook, HookManager, HookType
48
48
  from falyx.logger import logger
49
49
  from falyx.options_manager import OptionsManager
50
+ from falyx.parsers.utils import same_argument_definitions
50
51
  from falyx.retry import RetryHandler, RetryPolicy
51
52
  from falyx.themes import OneColors
52
53
  from falyx.utils import ensure_async
@@ -101,6 +102,14 @@ class BaseAction(ABC):
101
102
  async def preview(self, parent: Tree | None = None):
102
103
  raise NotImplementedError("preview must be implemented by subclasses")
103
104
 
105
+ @abstractmethod
106
+ def get_infer_target(self) -> Callable[..., Any] | None:
107
+ """
108
+ Returns the callable to be used for argument inference.
109
+ By default, it returns None.
110
+ """
111
+ raise NotImplementedError("get_infer_target must be implemented by subclasses")
112
+
104
113
  def set_options_manager(self, options_manager: OptionsManager) -> None:
105
114
  self.options_manager = options_manager
106
115
 
@@ -246,6 +255,13 @@ class Action(BaseAction):
246
255
  if policy.enabled:
247
256
  self.enable_retry()
248
257
 
258
+ def get_infer_target(self) -> Callable[..., Any]:
259
+ """
260
+ Returns the callable to be used for argument inference.
261
+ By default, it returns the action itself.
262
+ """
263
+ return self.action
264
+
249
265
  async def _run(self, *args, **kwargs) -> Any:
250
266
  combined_args = args + self.args
251
267
  combined_kwargs = self._maybe_inject_last_result({**self.kwargs, **kwargs})
@@ -477,6 +493,14 @@ class ChainedAction(BaseAction, ActionListMixin):
477
493
  if hasattr(action, "register_teardown") and callable(action.register_teardown):
478
494
  action.register_teardown(self.hooks)
479
495
 
496
+ def get_infer_target(self) -> Callable[..., Any] | None:
497
+ if self.actions:
498
+ return self.actions[0].get_infer_target()
499
+ return None
500
+
501
+ def _clear_args(self):
502
+ return (), {}
503
+
480
504
  async def _run(self, *args, **kwargs) -> list[Any]:
481
505
  if not self.actions:
482
506
  raise EmptyChainError(f"[{self.name}] No actions to execute.")
@@ -505,12 +529,8 @@ class ChainedAction(BaseAction, ActionListMixin):
505
529
  continue
506
530
  shared_context.current_index = index
507
531
  prepared = action.prepare(shared_context, self.options_manager)
508
- last_result = shared_context.last_result()
509
532
  try:
510
- if self.requires_io_injection() and last_result is not None:
511
- result = await prepared(**{prepared.inject_into: last_result})
512
- else:
513
- result = await prepared(*args, **updated_kwargs)
533
+ result = await prepared(*args, **updated_kwargs)
514
534
  except Exception as error:
515
535
  if index + 1 < len(self.actions) and isinstance(
516
536
  self.actions[index + 1], FallbackAction
@@ -529,6 +549,7 @@ class ChainedAction(BaseAction, ActionListMixin):
529
549
  fallback._skip_in_chain = True
530
550
  else:
531
551
  raise
552
+ args, updated_kwargs = self._clear_args()
532
553
  shared_context.add_result(result)
533
554
  context.extra["results"].append(result)
534
555
  context.extra["rollback_stack"].append(prepared)
@@ -669,6 +690,16 @@ class ActionGroup(BaseAction, ActionListMixin):
669
690
  if hasattr(action, "register_teardown") and callable(action.register_teardown):
670
691
  action.register_teardown(self.hooks)
671
692
 
693
+ def get_infer_target(self) -> Callable[..., Any] | None:
694
+ arg_defs = same_argument_definitions(self.actions)
695
+ if arg_defs:
696
+ return self.actions[0].get_infer_target()
697
+ logger.debug(
698
+ "[%s] auto_args disabled: mismatched ActionGroup arguments",
699
+ self.name,
700
+ )
701
+ return None
702
+
672
703
  async def _run(self, *args, **kwargs) -> list[tuple[str, Any]]:
673
704
  shared_context = SharedContext(name=self.name, action=self, is_parallel=True)
674
705
  if self.shared_context:
@@ -787,8 +818,11 @@ class ProcessAction(BaseAction):
787
818
  self.executor = executor or ProcessPoolExecutor()
788
819
  self.is_retryable = True
789
820
 
790
- async def _run(self, *args, **kwargs):
791
- if self.inject_last_result:
821
+ def get_infer_target(self) -> Callable[..., Any] | None:
822
+ return self.action
823
+
824
+ async def _run(self, *args, **kwargs) -> Any:
825
+ if self.inject_last_result and self.shared_context:
792
826
  last_result = self.shared_context.last_result()
793
827
  if not self._validate_pickleable(last_result):
794
828
  raise ValueError(
@@ -1,6 +1,6 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
2
  """action_factory.py"""
3
- from typing import Any
3
+ from typing import Any, Callable
4
4
 
5
5
  from rich.tree import Tree
6
6
 
@@ -55,6 +55,9 @@ class ActionFactoryAction(BaseAction):
55
55
  def factory(self, value: ActionFactoryProtocol):
56
56
  self._factory = ensure_async(value)
57
57
 
58
+ def get_infer_target(self) -> Callable[..., Any]:
59
+ return self.factory
60
+
58
61
  async def _run(self, *args, **kwargs) -> Any:
59
62
  updated_kwargs = self._maybe_inject_last_result(kwargs)
60
63
  context = ExecutionContext(
falyx/action/io_action.py CHANGED
@@ -19,7 +19,7 @@ import asyncio
19
19
  import shlex
20
20
  import subprocess
21
21
  import sys
22
- from typing import Any
22
+ from typing import Any, Callable
23
23
 
24
24
  from rich.tree import Tree
25
25
 
@@ -81,15 +81,15 @@ class BaseIOAction(BaseAction):
81
81
  def to_output(self, result: Any) -> str | bytes:
82
82
  raise NotImplementedError
83
83
 
84
- async def _resolve_input(self, kwargs: dict[str, Any]) -> str | bytes:
85
- last_result = kwargs.pop(self.inject_into, None)
86
-
84
+ async def _resolve_input(
85
+ self, args: tuple[Any], kwargs: dict[str, Any]
86
+ ) -> str | bytes:
87
87
  data = await self._read_stdin()
88
88
  if data:
89
89
  return self.from_input(data)
90
90
 
91
- if last_result is not None:
92
- return last_result
91
+ if len(args) == 1:
92
+ return self.from_input(args[0])
93
93
 
94
94
  if self.inject_last_result and self.shared_context:
95
95
  return self.shared_context.last_result()
@@ -99,6 +99,9 @@ class BaseIOAction(BaseAction):
99
99
  )
100
100
  raise FalyxError("No input provided and no last result to inject.")
101
101
 
102
+ def get_infer_target(self) -> Callable[..., Any] | None:
103
+ return None
104
+
102
105
  async def __call__(self, *args, **kwargs):
103
106
  context = ExecutionContext(
104
107
  name=self.name,
@@ -117,8 +120,8 @@ class BaseIOAction(BaseAction):
117
120
  pass
118
121
  result = getattr(self, "_last_result", None)
119
122
  else:
120
- parsed_input = await self._resolve_input(kwargs)
121
- result = await self._run(parsed_input, *args, **kwargs)
123
+ parsed_input = await self._resolve_input(args, kwargs)
124
+ result = await self._run(parsed_input)
122
125
  output = self.to_output(result)
123
126
  await self._write_stdout(output)
124
127
  context.result = result
@@ -220,11 +223,19 @@ class ShellAction(BaseIOAction):
220
223
  )
221
224
  return raw.strip() if isinstance(raw, str) else raw.decode("utf-8").strip()
222
225
 
226
+ def get_infer_target(self) -> Callable[..., Any] | None:
227
+ if sys.stdin.isatty():
228
+ return self._run
229
+ return None
230
+
223
231
  async def _run(self, parsed_input: str) -> str:
224
232
  # Replace placeholder in template, or use raw input as full command
225
233
  command = self.command_template.format(parsed_input)
226
234
  if self.safe_mode:
227
- args = shlex.split(command)
235
+ try:
236
+ args = shlex.split(command)
237
+ except ValueError as error:
238
+ raise FalyxError(f"Invalid command template: {error}")
228
239
  result = subprocess.run(args, capture_output=True, text=True, check=True)
229
240
  else:
230
241
  result = subprocess.run(
@@ -73,6 +73,9 @@ class MenuAction(BaseAction):
73
73
  table.add_row(*row)
74
74
  return table
75
75
 
76
+ def get_infer_target(self) -> None:
77
+ return None
78
+
76
79
  async def _run(self, *args, **kwargs) -> Any:
77
80
  kwargs = self._maybe_inject_last_result(kwargs)
78
81
  context = ExecutionContext(
@@ -121,6 +121,9 @@ class SelectFileAction(BaseAction):
121
121
  logger.warning("[ERROR] Failed to parse %s: %s", file.name, error)
122
122
  return options
123
123
 
124
+ def get_infer_target(self) -> None:
125
+ return None
126
+
124
127
  async def _run(self, *args, **kwargs) -> Any:
125
128
  context = ExecutionContext(name=self.name, args=args, kwargs=kwargs, action=self)
126
129
  context.start_timer()
@@ -85,6 +85,9 @@ class SelectionAction(BaseAction):
85
85
  f"got {type(value).__name__}"
86
86
  )
87
87
 
88
+ def get_infer_target(self) -> None:
89
+ return None
90
+
88
91
  async def _run(self, *args, **kwargs) -> Any:
89
92
  kwargs = self._maybe_inject_last_result(kwargs)
90
93
  context = ExecutionContext(
@@ -43,6 +43,9 @@ class UserInputAction(BaseAction):
43
43
  self.console = console or Console(color_system="auto")
44
44
  self.prompt_session = prompt_session or PromptSession()
45
45
 
46
+ def get_infer_target(self) -> None:
47
+ return None
48
+
46
49
  async def _run(self, *args, **kwargs) -> str:
47
50
  context = ExecutionContext(
48
51
  name=self.name,
falyx/command.py CHANGED
@@ -29,13 +29,14 @@ from rich.tree import Tree
29
29
 
30
30
  from falyx.action.action import Action, ActionGroup, BaseAction, ChainedAction
31
31
  from falyx.action.io_action import BaseIOAction
32
- from falyx.argparse import CommandArgumentParser
33
32
  from falyx.context import ExecutionContext
34
33
  from falyx.debug import register_debug_hooks
35
34
  from falyx.execution_registry import ExecutionRegistry as er
36
35
  from falyx.hook_manager import HookManager, HookType
37
36
  from falyx.logger import logger
38
37
  from falyx.options_manager import OptionsManager
38
+ from falyx.parsers.argparse import CommandArgumentParser
39
+ from falyx.parsers.signature import infer_args_from_func
39
40
  from falyx.prompt_utils import confirm_async, should_prompt_user
40
41
  from falyx.protocols import ArgParserProtocol
41
42
  from falyx.retry import RetryPolicy
@@ -90,6 +91,11 @@ class Command(BaseModel):
90
91
  tags (list[str]): Organizational tags for the command.
91
92
  logging_hooks (bool): Whether to attach logging hooks automatically.
92
93
  requires_input (bool | None): Indicates if the action needs input.
94
+ options_manager (OptionsManager): Manages global command-line options.
95
+ arg_parser (CommandArgumentParser): Parses command arguments.
96
+ custom_parser (ArgParserProtocol | None): Custom argument parser.
97
+ custom_help (Callable[[], str | None] | None): Custom help message generator.
98
+ auto_args (bool): Automatically infer arguments from the action.
93
99
 
94
100
  Methods:
95
101
  __call__(): Executes the command, respecting hooks and retries.
@@ -101,12 +107,13 @@ class Command(BaseModel):
101
107
 
102
108
  key: str
103
109
  description: str
104
- action: BaseAction | Callable[[], Any]
110
+ action: BaseAction | Callable[..., Any]
105
111
  args: tuple = ()
106
112
  kwargs: dict[str, Any] = Field(default_factory=dict)
107
113
  hidden: bool = False
108
114
  aliases: list[str] = Field(default_factory=list)
109
115
  help_text: str = ""
116
+ help_epilogue: str = ""
110
117
  style: str = OneColors.WHITE
111
118
  confirm: bool = False
112
119
  confirm_message: str = "Are you sure?"
@@ -125,22 +132,44 @@ class Command(BaseModel):
125
132
  requires_input: bool | None = None
126
133
  options_manager: OptionsManager = Field(default_factory=OptionsManager)
127
134
  arg_parser: CommandArgumentParser = Field(default_factory=CommandArgumentParser)
135
+ arguments: list[dict[str, Any]] = Field(default_factory=list)
136
+ argument_config: Callable[[CommandArgumentParser], None] | None = None
128
137
  custom_parser: ArgParserProtocol | None = None
129
138
  custom_help: Callable[[], str | None] | None = None
139
+ auto_args: bool = True
140
+ arg_metadata: dict[str, str | dict[str, Any]] = Field(default_factory=dict)
130
141
 
131
142
  _context: ExecutionContext | None = PrivateAttr(default=None)
132
143
 
133
144
  model_config = ConfigDict(arbitrary_types_allowed=True)
134
145
 
135
- def parse_args(self, raw_args: list[str] | str) -> tuple[tuple, dict]:
146
+ def parse_args(
147
+ self, raw_args: list[str] | str, from_validate: bool = False
148
+ ) -> tuple[tuple, dict]:
136
149
  if self.custom_parser:
137
150
  if isinstance(raw_args, str):
138
- raw_args = shlex.split(raw_args)
151
+ try:
152
+ raw_args = shlex.split(raw_args)
153
+ except ValueError:
154
+ logger.warning(
155
+ "[Command:%s] Failed to split arguments: %s",
156
+ self.key,
157
+ raw_args,
158
+ )
159
+ return ((), {})
139
160
  return self.custom_parser(raw_args)
140
161
 
141
162
  if isinstance(raw_args, str):
142
- raw_args = shlex.split(raw_args)
143
- return self.arg_parser.parse_args_split(raw_args)
163
+ try:
164
+ raw_args = shlex.split(raw_args)
165
+ except ValueError:
166
+ logger.warning(
167
+ "[Command:%s] Failed to split arguments: %s",
168
+ self.key,
169
+ raw_args,
170
+ )
171
+ return ((), {})
172
+ return self.arg_parser.parse_args_split(raw_args, from_validate=from_validate)
144
173
 
145
174
  @field_validator("action", mode="before")
146
175
  @classmethod
@@ -151,11 +180,22 @@ class Command(BaseModel):
151
180
  return ensure_async(action)
152
181
  raise TypeError("Action must be a callable or an instance of BaseAction")
153
182
 
183
+ def get_argument_definitions(self) -> list[dict[str, Any]]:
184
+ if self.arguments:
185
+ return self.arguments
186
+ elif self.argument_config:
187
+ self.argument_config(self.arg_parser)
188
+ elif self.auto_args:
189
+ if isinstance(self.action, BaseAction):
190
+ return infer_args_from_func(
191
+ self.action.get_infer_target(), self.arg_metadata
192
+ )
193
+ elif callable(self.action):
194
+ return infer_args_from_func(self.action, self.arg_metadata)
195
+ return []
196
+
154
197
  def model_post_init(self, _: Any) -> None:
155
198
  """Post-initialization to set up the action and hooks."""
156
- if isinstance(self.arg_parser, CommandArgumentParser):
157
- self.arg_parser.command_description = self.description
158
-
159
199
  if self.retry and isinstance(self.action, Action):
160
200
  self.action.enable_retry()
161
201
  elif self.retry_policy and isinstance(self.action, Action):
@@ -183,6 +223,9 @@ class Command(BaseModel):
183
223
  elif self.requires_input is None:
184
224
  self.requires_input = False
185
225
 
226
+ for arg_def in self.get_argument_definitions():
227
+ self.arg_parser.add_argument(*arg_def.pop("flags"), **arg_def)
228
+
186
229
  @cached_property
187
230
  def detect_requires_input(self) -> bool:
188
231
  """Detect if the action requires input based on its type."""
falyx/falyx.py CHANGED
@@ -58,11 +58,12 @@ from falyx.execution_registry import ExecutionRegistry as er
58
58
  from falyx.hook_manager import Hook, HookManager, HookType
59
59
  from falyx.logger import logger
60
60
  from falyx.options_manager import OptionsManager
61
- from falyx.parsers import get_arg_parsers
61
+ from falyx.parsers import CommandArgumentParser, get_arg_parsers
62
+ from falyx.protocols import ArgParserProtocol
62
63
  from falyx.retry import RetryPolicy
63
- from falyx.signals import BackSignal, CancelSignal, HelpSignal, QuitSignal
64
+ from falyx.signals import BackSignal, CancelSignal, FlowSignal, HelpSignal, QuitSignal
64
65
  from falyx.themes import OneColors, get_nord_theme
65
- from falyx.utils import CaseInsensitiveDict, _noop, chunks, get_program_invocation
66
+ from falyx.utils import CaseInsensitiveDict, _noop, chunks
66
67
  from falyx.version import __version__
67
68
 
68
69
 
@@ -157,8 +158,8 @@ class Falyx:
157
158
  force_confirm: bool = False,
158
159
  cli_args: Namespace | None = None,
159
160
  options: OptionsManager | None = None,
160
- render_menu: Callable[["Falyx"], None] | None = None,
161
- custom_table: Callable[["Falyx"], Table] | Table | None = None,
161
+ render_menu: Callable[[Falyx], None] | None = None,
162
+ custom_table: Callable[[Falyx], Table] | Table | None = None,
162
163
  ) -> None:
163
164
  """Initializes the Falyx object."""
164
165
  self.title: str | Markdown = title
@@ -182,8 +183,8 @@ class Falyx:
182
183
  self._never_prompt: bool = never_prompt
183
184
  self._force_confirm: bool = force_confirm
184
185
  self.cli_args: Namespace | None = cli_args
185
- self.render_menu: Callable[["Falyx"], None] | None = render_menu
186
- self.custom_table: Callable[["Falyx"], Table] | Table | None = custom_table
186
+ self.render_menu: Callable[[Falyx], None] | None = render_menu
187
+ self.custom_table: Callable[[Falyx], Table] | Table | None = custom_table
187
188
  self.validate_options(cli_args, options)
188
189
  self._prompt_session: PromptSession | None = None
189
190
  self.mode = FalyxMode.MENU
@@ -444,6 +445,7 @@ class Falyx:
444
445
  bottom_toolbar=self._get_bottom_bar_render(),
445
446
  key_bindings=self.key_bindings,
446
447
  validate_while_typing=False,
448
+ interrupt_exception=FlowSignal,
447
449
  )
448
450
  return self._prompt_session
449
451
 
@@ -524,7 +526,7 @@ class Falyx:
524
526
  key: str = "X",
525
527
  description: str = "Exit",
526
528
  aliases: list[str] | None = None,
527
- action: Callable[[], Any] | None = None,
529
+ action: Callable[..., Any] | None = None,
528
530
  style: str = OneColors.DARK_RED,
529
531
  confirm: bool = False,
530
532
  confirm_message: str = "Are you sure?",
@@ -578,13 +580,14 @@ class Falyx:
578
580
  self,
579
581
  key: str,
580
582
  description: str,
581
- action: BaseAction | Callable[[], Any],
583
+ action: BaseAction | Callable[..., Any],
582
584
  *,
583
585
  args: tuple = (),
584
586
  kwargs: dict[str, Any] | None = None,
585
587
  hidden: bool = False,
586
588
  aliases: list[str] | None = None,
587
589
  help_text: str = "",
590
+ help_epilogue: str = "",
588
591
  style: str = OneColors.WHITE,
589
592
  confirm: bool = False,
590
593
  confirm_message: str = "Are you sure?",
@@ -606,9 +609,33 @@ class Falyx:
606
609
  retry_all: bool = False,
607
610
  retry_policy: RetryPolicy | None = None,
608
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 = True,
618
+ arg_metadata: dict[str, str | dict[str, Any]] | None = None,
609
619
  ) -> Command:
610
620
  """Adds an command to the menu, preventing duplicates."""
611
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
+
612
639
  command = Command(
613
640
  key=key,
614
641
  description=description,
@@ -618,6 +645,7 @@ class Falyx:
618
645
  hidden=hidden,
619
646
  aliases=aliases if aliases else [],
620
647
  help_text=help_text,
648
+ help_epilogue=help_epilogue,
621
649
  style=style,
622
650
  confirm=confirm,
623
651
  confirm_message=confirm_message,
@@ -634,6 +662,13 @@ class Falyx:
634
662
  retry_policy=retry_policy or RetryPolicy(),
635
663
  requires_input=requires_input,
636
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 {},
637
672
  )
638
673
 
639
674
  if hooks:
@@ -715,7 +750,10 @@ class Falyx:
715
750
  """
716
751
  args = ()
717
752
  kwargs: dict[str, Any] = {}
718
- choice, *input_args = shlex.split(raw_choices)
753
+ try:
754
+ choice, *input_args = shlex.split(raw_choices)
755
+ except ValueError:
756
+ return False, None, args, kwargs
719
757
  is_preview, choice = self.parse_preview_command(choice)
720
758
  if is_preview and not choice and self.help_command:
721
759
  is_preview = False
@@ -735,7 +773,7 @@ class Falyx:
735
773
  logger.info("Command '%s' selected.", choice)
736
774
  if input_args and name_map[choice].arg_parser:
737
775
  try:
738
- args, kwargs = name_map[choice].parse_args(input_args)
776
+ args, kwargs = name_map[choice].parse_args(input_args, from_validate)
739
777
  except CommandArgumentError as error:
740
778
  if not from_validate:
741
779
  if not name_map[choice].show_help():
@@ -748,6 +786,8 @@ class Falyx:
748
786
  message=str(error), cursor_position=len(raw_choices)
749
787
  )
750
788
  return is_preview, None, args, kwargs
789
+ except HelpSignal:
790
+ return True, None, args, kwargs
751
791
  return is_preview, name_map[choice], args, kwargs
752
792
 
753
793
  prefix_matches = [cmd for key, cmd in name_map.items() if key.startswith(choice)]
@@ -804,15 +844,6 @@ class Falyx:
804
844
  await selected_command.preview()
805
845
  return True
806
846
 
807
- if selected_command.requires_input:
808
- program = get_program_invocation()
809
- self.console.print(
810
- f"[{OneColors.LIGHT_YELLOW}]⚠️ Command '{selected_command.key}' requires"
811
- f" input and must be run via [{OneColors.MAGENTA}]'{program} run"
812
- f"'[{OneColors.LIGHT_YELLOW}] with proper piping or arguments.[/]"
813
- )
814
- return True
815
-
816
847
  self.last_run_command = selected_command
817
848
 
818
849
  if selected_command == self.exit_command:
@@ -823,7 +854,6 @@ class Falyx:
823
854
  context.start_timer()
824
855
  try:
825
856
  await self.hooks.trigger(HookType.BEFORE, context)
826
- print(args, kwargs)
827
857
  result = await selected_command(*args, **kwargs)
828
858
  context.result = result
829
859
  await self.hooks.trigger(HookType.ON_SUCCESS, context)
@@ -964,8 +994,6 @@ class Falyx:
964
994
  logger.info("BackSignal received.")
965
995
  except CancelSignal:
966
996
  logger.info("CancelSignal received.")
967
- except HelpSignal:
968
- logger.info("HelpSignal received.")
969
997
  finally:
970
998
  logger.info("Exiting menu: %s", self.get_title())
971
999
  if self.exit_message:
@@ -995,7 +1023,7 @@ class Falyx:
995
1023
  sys.exit(0)
996
1024
 
997
1025
  if self.cli_args.command == "version" or self.cli_args.version:
998
- self.console.print(f"[{OneColors.GREEN_b}]Falyx CLI v{__version__}[/]")
1026
+ self.console.print(f"[{OneColors.BLUE_b}]Falyx CLI v{__version__}[/]")
999
1027
  sys.exit(0)
1000
1028
 
1001
1029
  if self.cli_args.command == "preview":
falyx/parsers/.pytyped ADDED
File without changes
@@ -0,0 +1,17 @@
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
+
11
+ __all__ = [
12
+ "Argument",
13
+ "ArgumentAction",
14
+ "CommandArgumentParser",
15
+ "get_arg_parsers",
16
+ "FalyxParsers",
17
+ ]
@@ -5,7 +5,8 @@ from enum import Enum
5
5
  from typing import Any, Iterable
6
6
 
7
7
  from rich.console import Console
8
- from rich.table import Table
8
+ from rich.markup import escape
9
+ from rich.text import Text
9
10
 
10
11
  from falyx.exceptions import CommandArgumentError
11
12
  from falyx.signals import HelpSignal
@@ -40,6 +41,70 @@ class Argument:
40
41
  nargs: int | str = 1 # int, '?', '*', '+'
41
42
  positional: bool = False # True if no leading - or -- in flags
42
43
 
44
+ def get_positional_text(self) -> str:
45
+ """Get the positional text for the argument."""
46
+ text = ""
47
+ if self.positional:
48
+ if self.choices:
49
+ text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
50
+ else:
51
+ text = self.dest
52
+ return text
53
+
54
+ def get_choice_text(self) -> str:
55
+ """Get the choice text for the argument."""
56
+ choice_text = ""
57
+ if self.choices:
58
+ choice_text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
59
+ elif (
60
+ self.action
61
+ in (
62
+ ArgumentAction.STORE,
63
+ ArgumentAction.APPEND,
64
+ ArgumentAction.EXTEND,
65
+ )
66
+ and not self.positional
67
+ ):
68
+ choice_text = self.dest.upper()
69
+ elif isinstance(self.nargs, str):
70
+ choice_text = self.dest
71
+
72
+ if self.nargs == "?":
73
+ choice_text = f"[{choice_text}]"
74
+ elif self.nargs == "*":
75
+ choice_text = f"[{choice_text} ...]"
76
+ elif self.nargs == "+":
77
+ choice_text = f"{choice_text} [{choice_text} ...]"
78
+ return choice_text
79
+
80
+ def __eq__(self, other: object) -> bool:
81
+ if not isinstance(other, Argument):
82
+ return False
83
+ return (
84
+ self.flags == other.flags
85
+ and self.dest == other.dest
86
+ and self.action == other.action
87
+ and self.type == other.type
88
+ and self.choices == other.choices
89
+ and self.required == other.required
90
+ and self.nargs == other.nargs
91
+ and self.positional == other.positional
92
+ )
93
+
94
+ def __hash__(self) -> int:
95
+ return hash(
96
+ (
97
+ tuple(self.flags),
98
+ self.dest,
99
+ self.action,
100
+ self.type,
101
+ tuple(self.choices or []),
102
+ self.required,
103
+ self.nargs,
104
+ self.positional,
105
+ )
106
+ )
107
+
43
108
 
44
109
  class CommandArgumentParser:
45
110
  """
@@ -61,10 +126,25 @@ class CommandArgumentParser:
61
126
  - Render Help using Rich library.
62
127
  """
63
128
 
64
- def __init__(self) -> None:
129
+ def __init__(
130
+ self,
131
+ command_key: str = "",
132
+ command_description: str = "",
133
+ command_style: str = "bold",
134
+ help_text: str = "",
135
+ help_epilogue: str = "",
136
+ aliases: list[str] | None = None,
137
+ ) -> None:
65
138
  """Initialize the CommandArgumentParser."""
66
- self.command_description: str = ""
139
+ self.command_key: str = command_key
140
+ self.command_description: str = command_description
141
+ self.command_style: str = command_style
142
+ self.help_text: str = help_text
143
+ self.help_epilogue: str = help_epilogue
144
+ self.aliases: list[str] = aliases or []
67
145
  self._arguments: list[Argument] = []
146
+ self._positional: list[Argument] = []
147
+ self._keyword: list[Argument] = []
68
148
  self._flag_map: dict[str, Argument] = {}
69
149
  self._dest_set: set[str] = set()
70
150
  self._add_help()
@@ -73,10 +153,10 @@ class CommandArgumentParser:
73
153
  def _add_help(self):
74
154
  """Add help argument to the parser."""
75
155
  self.add_argument(
76
- "--help",
77
156
  "-h",
157
+ "--help",
78
158
  action=ArgumentAction.HELP,
79
- help="Show this help message and exit.",
159
+ help="Show this help message.",
80
160
  dest="help",
81
161
  )
82
162
 
@@ -304,10 +384,31 @@ class CommandArgumentParser:
304
384
  )
305
385
  self._flag_map[flag] = argument
306
386
  self._arguments.append(argument)
387
+ if positional:
388
+ self._positional.append(argument)
389
+ else:
390
+ self._keyword.append(argument)
307
391
 
308
392
  def get_argument(self, dest: str) -> Argument | None:
309
393
  return next((a for a in self._arguments if a.dest == dest), None)
310
394
 
395
+ def to_definition_list(self) -> list[dict[str, Any]]:
396
+ defs = []
397
+ for arg in self._arguments:
398
+ defs.append(
399
+ {
400
+ "flags": arg.flags,
401
+ "dest": arg.dest,
402
+ "action": arg.action,
403
+ "type": arg.type,
404
+ "choices": arg.choices,
405
+ "required": arg.required,
406
+ "nargs": arg.nargs,
407
+ "positional": arg.positional,
408
+ }
409
+ )
410
+ return defs
411
+
311
412
  def _consume_nargs(
312
413
  self, args: list[str], start: int, spec: Argument
313
414
  ) -> tuple[list[str], int]:
@@ -405,7 +506,9 @@ class CommandArgumentParser:
405
506
 
406
507
  return i
407
508
 
408
- def parse_args(self, args: list[str] | None = None) -> dict[str, Any]:
509
+ def parse_args(
510
+ self, args: list[str] | None = None, from_validate: bool = False
511
+ ) -> dict[str, Any]:
409
512
  """Parse Falyx Command arguments."""
410
513
  if args is None:
411
514
  args = []
@@ -423,7 +526,8 @@ class CommandArgumentParser:
423
526
  action = spec.action
424
527
 
425
528
  if action == ArgumentAction.HELP:
426
- self.render_help()
529
+ if not from_validate:
530
+ self.render_help()
427
531
  raise HelpSignal()
428
532
  elif action == ArgumentAction.STORE_TRUE:
429
533
  result[spec.dest] = True
@@ -550,13 +654,15 @@ class CommandArgumentParser:
550
654
  result.pop("help", None)
551
655
  return result
552
656
 
553
- def parse_args_split(self, args: list[str]) -> tuple[tuple[Any, ...], dict[str, Any]]:
657
+ def parse_args_split(
658
+ self, args: list[str], from_validate: bool = False
659
+ ) -> tuple[tuple[Any, ...], dict[str, Any]]:
554
660
  """
555
661
  Returns:
556
662
  tuple[args, kwargs] - Positional arguments in defined order,
557
663
  followed by keyword argument mapping.
558
664
  """
559
- parsed = self.parse_args(args)
665
+ parsed = self.parse_args(args, from_validate)
560
666
  args_list = []
561
667
  kwargs_dict = {}
562
668
  for arg in self._arguments:
@@ -568,20 +674,74 @@ class CommandArgumentParser:
568
674
  kwargs_dict[arg.dest] = parsed[arg.dest]
569
675
  return tuple(args_list), kwargs_dict
570
676
 
571
- def render_help(self):
572
- table = Table(title=f"{self.command_description} Help")
573
- table.add_column("Flags")
574
- table.add_column("Help")
575
- for arg in self._arguments:
576
- if arg.dest == "help":
577
- continue
578
- flag_str = ", ".join(arg.flags) if not arg.positional else arg.dest
579
- table.add_row(flag_str, arg.help or "")
580
- table.add_section()
581
- arg = self.get_argument("help")
582
- flag_str = ", ".join(arg.flags) if not arg.positional else arg.dest
583
- table.add_row(flag_str, arg.help or "")
584
- self.console.print(table)
677
+ def render_help(self) -> None:
678
+ # Options
679
+ # Add all keyword arguments to the options list
680
+ options_list = []
681
+ for arg in self._keyword:
682
+ choice_text = arg.get_choice_text()
683
+ if choice_text:
684
+ options_list.extend([f"[{arg.flags[0]} {choice_text}]"])
685
+ else:
686
+ options_list.extend([f"[{arg.flags[0]}]"])
687
+
688
+ # Add positional arguments to the options list
689
+ for arg in self._positional:
690
+ choice_text = arg.get_choice_text()
691
+ if isinstance(arg.nargs, int):
692
+ choice_text = " ".join([choice_text] * arg.nargs)
693
+ options_list.append(escape(choice_text))
694
+
695
+ options_text = " ".join(options_list)
696
+ command_keys = " | ".join(
697
+ [f"[{self.command_style}]{self.command_key}[/{self.command_style}]"]
698
+ + [
699
+ f"[{self.command_style}]{alias}[/{self.command_style}]"
700
+ for alias in self.aliases
701
+ ]
702
+ )
703
+
704
+ usage = f"usage: {command_keys} {options_text}"
705
+ self.console.print(f"[bold]{usage}[/bold]\n")
706
+
707
+ # Description
708
+ if self.help_text:
709
+ self.console.print(self.help_text + "\n")
710
+
711
+ # Arguments
712
+ if self._arguments:
713
+ if self._positional:
714
+ self.console.print("[bold]positional:[/bold]")
715
+ for arg in self._positional:
716
+ flags = arg.get_positional_text()
717
+ arg_line = Text(f" {flags:<30} ")
718
+ help_text = arg.help or ""
719
+ arg_line.append(help_text)
720
+ self.console.print(arg_line)
721
+ self.console.print("[bold]options:[/bold]")
722
+ for arg in self._keyword:
723
+ flags = ", ".join(arg.flags)
724
+ flags_choice = f"{flags} {arg.get_choice_text()}"
725
+ arg_line = Text(f" {flags_choice:<30} ")
726
+ help_text = arg.help or ""
727
+ arg_line.append(help_text)
728
+ self.console.print(arg_line)
729
+
730
+ # Epilogue
731
+ if self.help_epilogue:
732
+ self.console.print("\n" + self.help_epilogue, style="dim")
733
+
734
+ def __eq__(self, other: object) -> bool:
735
+ if not isinstance(other, CommandArgumentParser):
736
+ return False
737
+
738
+ def sorted_args(parser):
739
+ return sorted(parser._arguments, key=lambda a: a.dest)
740
+
741
+ return sorted_args(self) == sorted_args(other)
742
+
743
+ def __hash__(self) -> int:
744
+ return hash(tuple(sorted(self._arguments, key=lambda a: a.dest)))
585
745
 
586
746
  def __str__(self) -> str:
587
747
  positional = sum(arg.positional for arg in self._arguments)
@@ -0,0 +1,74 @@
1
+ import inspect
2
+ from typing import Any, Callable
3
+
4
+ from falyx.logger import logger
5
+
6
+
7
+ def infer_args_from_func(
8
+ func: Callable[[Any], Any] | None,
9
+ arg_metadata: dict[str, str | dict[str, Any]] | None = None,
10
+ ) -> list[dict[str, Any]]:
11
+ """
12
+ Infer argument definitions from a callable's signature.
13
+ Returns a list of kwargs suitable for CommandArgumentParser.add_argument.
14
+ """
15
+ if not callable(func):
16
+ logger.debug("Provided argument is not callable: %s", func)
17
+ return []
18
+ arg_metadata = arg_metadata or {}
19
+ signature = inspect.signature(func)
20
+ arg_defs = []
21
+
22
+ for name, param in signature.parameters.items():
23
+ raw_metadata = arg_metadata.get(name, {})
24
+ metadata = (
25
+ {"help": raw_metadata} if isinstance(raw_metadata, str) else raw_metadata
26
+ )
27
+
28
+ if param.kind not in (
29
+ inspect.Parameter.POSITIONAL_ONLY,
30
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
31
+ inspect.Parameter.KEYWORD_ONLY,
32
+ ):
33
+ continue
34
+
35
+ arg_type = (
36
+ param.annotation if param.annotation is not inspect.Parameter.empty else str
37
+ )
38
+ default = param.default if param.default is not inspect.Parameter.empty else None
39
+ is_required = param.default is inspect.Parameter.empty
40
+ if is_required:
41
+ flags = [f"{name.replace('_', '-')}"]
42
+ else:
43
+ flags = [f"--{name.replace('_', '-')}"]
44
+ action = "store"
45
+ nargs: int | str = 1
46
+
47
+ if arg_type is bool:
48
+ if param.default is False:
49
+ action = "store_true"
50
+ else:
51
+ action = "store_false"
52
+
53
+ if arg_type is list:
54
+ action = "append"
55
+ if is_required:
56
+ nargs = "+"
57
+ else:
58
+ nargs = "*"
59
+
60
+ arg_defs.append(
61
+ {
62
+ "flags": flags,
63
+ "dest": name,
64
+ "type": arg_type,
65
+ "default": default,
66
+ "required": is_required,
67
+ "nargs": nargs,
68
+ "action": action,
69
+ "help": metadata.get("help", ""),
70
+ "choices": metadata.get("choices"),
71
+ }
72
+ )
73
+
74
+ return arg_defs
falyx/parsers/utils.py ADDED
@@ -0,0 +1,27 @@
1
+ from typing import Any
2
+
3
+ from falyx import logger
4
+ from falyx.parsers.signature import infer_args_from_func
5
+
6
+
7
+ def same_argument_definitions(
8
+ actions: list[Any],
9
+ arg_metadata: dict[str, str | dict[str, Any]] | None = None,
10
+ ) -> list[dict[str, Any]] | None:
11
+ from falyx.action.action import BaseAction
12
+
13
+ arg_sets = []
14
+ for action in actions:
15
+ if isinstance(action, BaseAction):
16
+ arg_defs = infer_args_from_func(action.get_infer_target(), arg_metadata)
17
+ elif callable(action):
18
+ arg_defs = infer_args_from_func(action, arg_metadata)
19
+ else:
20
+ logger.debug("Auto args unsupported for action: %s", action)
21
+ return None
22
+ arg_sets.append(arg_defs)
23
+
24
+ first = arg_sets[0]
25
+ if all(arg_set == first for arg_set in arg_sets[1:]):
26
+ return first
27
+ return None
falyx/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.1.28"
1
+ __version__ = "0.1.30"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.28
3
+ Version: 0.1.30
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -1,34 +1,38 @@
1
1
  falyx/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  falyx/__init__.py,sha256=L40665QyjAqHQxHdxxY2_yPeDa4p0LE7Nu_2dkm08Ls,650
3
3
  falyx/__main__.py,sha256=g_LwJieofK3DJzCYtpkAMEeOXhzSLQenb7pRVUqcf-Y,2152
4
+ falyx/action/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
5
  falyx/action/__init__.py,sha256=zpOK5g4DybydV8d3QI0Zq52aWaKFPYi-J6szAQTsQ2c,974
5
- falyx/action/action.py,sha256=CJB9eeeEqBGkZHjMpG24eXHRjouKSfESCI1zWzoE7JQ,32488
6
- falyx/action/action_factory.py,sha256=qNtEnsbKsNl-WijChbTQYfdI3k14fN-1bzDsGFx8yZI,4517
6
+ falyx/action/action.py,sha256=f1u7emC3Qa5JUaUsqOFTXMgc5JCv72-MZzckk1N_VO8,33537
7
+ falyx/action/action_factory.py,sha256=ex2Wxpe5mW23FnHuTChzbbw4hmXYuDHKJ330iLBQKIQ,4610
7
8
  falyx/action/http_action.py,sha256=aIieGHyZSkz1ZGay-fwgDYZ0QF17XypAWtKeVAYp5f4,5806
8
- falyx/action/io_action.py,sha256=zdDq07zSLlaShBQ3ztXTRC6aZL0JoERNZSmvHy1V22w,9718
9
- falyx/action/menu_action.py,sha256=cboCpXyl0fZUxpFsvEPu0dGhFfr_vdfllceQnICA0gU,5683
10
- falyx/action/select_file_action.py,sha256=hHLhmTSacWaUXhRTeIIiXt8gR7zbjkXJ2MAkKQYCpp4,7799
11
- falyx/action/selection_action.py,sha256=22rF7UqRrQAMjGIheDqAbUizVMBg9aCl9e4VOLLZZJo,8811
9
+ falyx/action/io_action.py,sha256=TAa3keVfhCCDgXsp0hWGPx2-B6TKxlvkUGaAD7K-25Q,10051
10
+ falyx/action/menu_action.py,sha256=S_Y_hmyALHrZkfkPXtBKY2qwczfNugAA4M0xOAgOTrM,5744
11
+ falyx/action/select_file_action.py,sha256=5kzN2Kx6wN86Sl5tSLdUs1bAlbSG9AGAi3x1p5HlP6A,7860
12
+ falyx/action/selection_action.py,sha256=aEoCdinMfiKL7tnVOPJ8b56oZ1X51mGfV9rkn_DJKtc,8872
12
13
  falyx/action/signal_action.py,sha256=5UMqvzy7fBnLANGwYUWoe1VRhrr7e-yOVeLdOnCBiJo,1350
13
14
  falyx/action/types.py,sha256=iVD-bHm1GRXOTIlHOeT_KcDBRZm4Hz5Xzl_BOalvEf4,961
14
- falyx/action/user_input_action.py,sha256=LSTzC_3TfsfXdz-qV3GlOIGpZWAOgO9J5DnNsHO7ee8,3398
15
- falyx/argparse.py,sha256=kI_tLYD6KOiWvXEHEvOi8bprx5sQm9KTrgens2_nWKU,24498
15
+ falyx/action/user_input_action.py,sha256=ne9ytu96QVnuM4jOjjAzB_dd6XS162961uNURtJIRXU,3459
16
16
  falyx/bottom_bar.py,sha256=iWxgOKWgn5YmREeZBuGA50FzqzEfz1-Vnqm0V_fhldc,7383
17
- falyx/command.py,sha256=Vr5YHehZB6GavBbHe9diNbdeXNWfnWTkuDZ-NfymcFo,13437
17
+ falyx/command.py,sha256=BT_zl_R0nYPYKjD_n-ww73zLBmKQB9MmDuWk8ndR4VU,15345
18
18
  falyx/config.py,sha256=8dkQfL-Ro-vWw1AcO2fD1PGZ92Cyfnwl885ZlpLkp4Y,9636
19
- falyx/config_schema.py,sha256=j5GQuHVlaU-VLxLF9t8idZRjqOP9MIKp1hyd9NhpAGU,3124
20
19
  falyx/context.py,sha256=FNF-IS7RMDxel2l3kskEqQImZ0mLO6zvGw_xC9cIzgI,10338
21
20
  falyx/debug.py,sha256=oWWTLOF8elrx_RGZ1G4pbzfFr46FjB0woFXpVU2wmjU,1567
22
21
  falyx/exceptions.py,sha256=kK9k1v7LVNjJSwYztRa9Krhr3ZOI-6Htq2ZjlYICPKg,922
23
22
  falyx/execution_registry.py,sha256=rctsz0mrIHPToLZqylblVjDdKWdq1x_JBc8GwMP5sJ8,4710
24
- falyx/falyx.py,sha256=3OrmQ0p7ZOpjLaH5mqTYVEZ96KKTygSLYlPqg3eH5eM,43061
23
+ falyx/falyx.py,sha256=7luGMkM692puSAQ0j0NE3Pox9QbM3Lc2IFvI0D-3UWc,44137
25
24
  falyx/hook_manager.py,sha256=GuGxVVz9FXrU39Tk220QcsLsMXeut7ZDkGW3hU9GcwQ,2952
26
25
  falyx/hooks.py,sha256=IV2nbj5FjY2m3_L7x4mYBnaRDG45E8tWQU90i4butlw,2940
27
26
  falyx/init.py,sha256=abcSlPmxVeByLIHdUkNjqtO_tEkO3ApC6f9WbxsSEWg,3393
28
27
  falyx/logger.py,sha256=1Mfb_vJFJ1tQwziuyU2p-cSMi2Js8N2byniFEnI6vOQ,132
29
28
  falyx/menu.py,sha256=faxGgocqQYY6HtzVbenHaFj8YqsmycBEyziC8Ahzqjo,2870
30
29
  falyx/options_manager.py,sha256=dFAnQw543tQ6Xupvh1PwBrhiSWlSACHw8K-sHP_lUh4,2842
31
- falyx/parsers.py,sha256=KsDFEmJLM86d2X4Kh4SHA9mBbUk351NjLhhFYzQkaPk,5762
30
+ falyx/parsers/.pytyped,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ falyx/parsers/__init__.py,sha256=l0QMf89uJHhTpOqQfiV3tx7aAHvELqDFWAyjCbwEgBQ,370
32
+ falyx/parsers/argparse.py,sha256=ElWg4nl1ygkDY9Q4lTfjj4KMXI0FKDLVdc08eiGw4AQ,29889
33
+ falyx/parsers/parsers.py,sha256=KsDFEmJLM86d2X4Kh4SHA9mBbUk351NjLhhFYzQkaPk,5762
34
+ falyx/parsers/signature.py,sha256=kniazHBDFIY-cb4JC-gxPL4fviAsoYf8wX0AmWKetGM,2252
35
+ falyx/parsers/utils.py,sha256=g9C_8eJm-2A52SfAL-qyow0VmJXwI-YeRve0phoFiFk,845
32
36
  falyx/prompt_utils.py,sha256=qgk0bXs7mwzflqzWyFhEOTpKQ_ZtMIqGhKeg-ocwNnE,1542
33
37
  falyx/protocols.py,sha256=mesdq5CjPF_5Kyu7Evwr6qMT71tUHlw0SjjtmnggTZw,495
34
38
  falyx/retry.py,sha256=UUzY6FlKobr84Afw7yJO9rj3AIQepDk2fcWs6_1gi6Q,3788
@@ -40,9 +44,9 @@ falyx/themes/__init__.py,sha256=1CZhEUCin9cUk8IGYBUFkVvdHRNNJBEFXccHwpUKZCA,284
40
44
  falyx/themes/colors.py,sha256=4aaeAHJetmeNInI0Zytg4E3YqKfPFelpf04vtjSvsS8,19776
41
45
  falyx/utils.py,sha256=u3puR4Bh-unNBw9a0V9sw7PDTIzRaNLolap0oz5bVIk,6718
42
46
  falyx/validators.py,sha256=t5iyzVpY8tdC4rfhr4isEfWpD5gNTzjeX_Hbi_Uq6sA,1328
43
- falyx/version.py,sha256=MWZDdAHrdUZS0c3VlLqX4O1eaxPodI7irMtEvknKQ94,23
44
- falyx-0.1.28.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
45
- falyx-0.1.28.dist-info/METADATA,sha256=n-VK44OFgjFxyRn1-LdysM1ciDF8RKxlKfAy_0Yz_Zc,5521
46
- falyx-0.1.28.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
47
- falyx-0.1.28.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
48
- falyx-0.1.28.dist-info/RECORD,,
47
+ falyx/version.py,sha256=2GUJJyX8g8EAXKUqyj7DGVzG-jNXOGaqVSWilvGYuX8,23
48
+ falyx-0.1.30.dist-info/LICENSE,sha256=B0yqgaHuSdhN7T3OBmgQSiDTy8HqT5Oe_dLypRe4Ra4,1073
49
+ falyx-0.1.30.dist-info/METADATA,sha256=U_0NoWJB6xbD0oxYzA8wwVEuIFJvgIB6D4UrgOyt9pc,5521
50
+ falyx-0.1.30.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
51
+ falyx-0.1.30.dist-info/entry_points.txt,sha256=j8owOSl2j1Ss8DtGMnKfgehKaolqnIPhVFHaUBLUnMs,45
52
+ falyx-0.1.30.dist-info/RECORD,,
falyx/config_schema.py DELETED
@@ -1,76 +0,0 @@
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
- }
File without changes
File without changes