falyx 0.1.51__tar.gz → 0.1.53__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 (66) hide show
  1. {falyx-0.1.51 → falyx-0.1.53}/PKG-INFO +1 -1
  2. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/__init__.py +3 -2
  3. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/action.py +1 -1
  4. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/action_factory.py +1 -1
  5. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/action_group.py +13 -5
  6. falyx-0.1.51/falyx/action/types.py → falyx-0.1.53/falyx/action/action_types.py +5 -3
  7. falyx-0.1.51/falyx/action/base.py → falyx-0.1.53/falyx/action/base_action.py +1 -1
  8. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/chained_action.py +13 -5
  9. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/fallback_action.py +2 -0
  10. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/io_action.py +1 -94
  11. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/literal_input_action.py +2 -0
  12. falyx-0.1.53/falyx/action/load_file_action.py +28 -0
  13. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/menu_action.py +1 -1
  14. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/mixins.py +6 -2
  15. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/process_action.py +3 -1
  16. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/process_pool_action.py +3 -1
  17. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/prompt_menu_action.py +1 -1
  18. falyx-0.1.53/falyx/action/save_file_action.py +28 -0
  19. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/select_file_action.py +15 -15
  20. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/selection_action.py +2 -2
  21. falyx-0.1.53/falyx/action/shell_action.py +105 -0
  22. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/user_input_action.py +3 -1
  23. {falyx-0.1.51 → falyx-0.1.53}/falyx/command.py +2 -2
  24. {falyx-0.1.51 → falyx-0.1.53}/falyx/config.py +1 -1
  25. {falyx-0.1.51 → falyx-0.1.53}/falyx/falyx.py +2 -3
  26. {falyx-0.1.51 → falyx-0.1.53}/falyx/menu.py +1 -1
  27. {falyx-0.1.51 → falyx-0.1.53}/falyx/parser/__init__.py +3 -1
  28. falyx-0.1.53/falyx/parser/argument.py +98 -0
  29. falyx-0.1.53/falyx/parser/argument_action.py +27 -0
  30. falyx-0.1.51/falyx/parser/argparse.py → falyx-0.1.53/falyx/parser/command_argument_parser.py +4 -116
  31. {falyx-0.1.51 → falyx-0.1.53}/falyx/parser/parsers.py +15 -7
  32. {falyx-0.1.51 → falyx-0.1.53}/falyx/parser/signature.py +1 -0
  33. {falyx-0.1.51 → falyx-0.1.53}/falyx/parser/utils.py +2 -1
  34. {falyx-0.1.51 → falyx-0.1.53}/falyx/protocols.py +1 -1
  35. {falyx-0.1.51 → falyx-0.1.53}/falyx/retry_utils.py +1 -1
  36. falyx-0.1.53/falyx/version.py +1 -0
  37. {falyx-0.1.51 → falyx-0.1.53}/pyproject.toml +1 -1
  38. falyx-0.1.51/falyx/version.py +0 -1
  39. {falyx-0.1.51 → falyx-0.1.53}/LICENSE +0 -0
  40. {falyx-0.1.51 → falyx-0.1.53}/README.md +0 -0
  41. {falyx-0.1.51 → falyx-0.1.53}/falyx/.pytyped +0 -0
  42. {falyx-0.1.51 → falyx-0.1.53}/falyx/__init__.py +0 -0
  43. {falyx-0.1.51 → falyx-0.1.53}/falyx/__main__.py +0 -0
  44. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/.pytyped +0 -0
  45. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/http_action.py +0 -0
  46. {falyx-0.1.51 → falyx-0.1.53}/falyx/action/signal_action.py +0 -0
  47. {falyx-0.1.51 → falyx-0.1.53}/falyx/bottom_bar.py +0 -0
  48. {falyx-0.1.51 → falyx-0.1.53}/falyx/context.py +0 -0
  49. {falyx-0.1.51 → falyx-0.1.53}/falyx/debug.py +0 -0
  50. {falyx-0.1.51 → falyx-0.1.53}/falyx/exceptions.py +0 -0
  51. {falyx-0.1.51 → falyx-0.1.53}/falyx/execution_registry.py +0 -0
  52. {falyx-0.1.51 → falyx-0.1.53}/falyx/hook_manager.py +0 -0
  53. {falyx-0.1.51 → falyx-0.1.53}/falyx/hooks.py +0 -0
  54. {falyx-0.1.51 → falyx-0.1.53}/falyx/init.py +0 -0
  55. {falyx-0.1.51 → falyx-0.1.53}/falyx/logger.py +0 -0
  56. {falyx-0.1.51 → falyx-0.1.53}/falyx/options_manager.py +0 -0
  57. {falyx-0.1.51 → falyx-0.1.53}/falyx/parser/.pytyped +0 -0
  58. {falyx-0.1.51 → falyx-0.1.53}/falyx/prompt_utils.py +0 -0
  59. {falyx-0.1.51 → falyx-0.1.53}/falyx/retry.py +0 -0
  60. {falyx-0.1.51 → falyx-0.1.53}/falyx/selection.py +0 -0
  61. {falyx-0.1.51 → falyx-0.1.53}/falyx/signals.py +0 -0
  62. {falyx-0.1.51 → falyx-0.1.53}/falyx/tagged_table.py +0 -0
  63. {falyx-0.1.51 → falyx-0.1.53}/falyx/themes/__init__.py +0 -0
  64. {falyx-0.1.51 → falyx-0.1.53}/falyx/themes/colors.py +0 -0
  65. {falyx-0.1.51 → falyx-0.1.53}/falyx/utils.py +0 -0
  66. {falyx-0.1.51 → falyx-0.1.53}/falyx/validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: falyx
3
- Version: 0.1.51
3
+ Version: 0.1.53
4
4
  Summary: Reliable and introspectable async CLI action framework.
5
5
  License: MIT
6
6
  Author: Roland Thomas Jr
@@ -8,11 +8,11 @@ Licensed under the MIT License. See LICENSE file for details.
8
8
  from .action import Action
9
9
  from .action_factory import ActionFactoryAction
10
10
  from .action_group import ActionGroup
11
- from .base import BaseAction
11
+ from .base_action import BaseAction
12
12
  from .chained_action import ChainedAction
13
13
  from .fallback_action import FallbackAction
14
14
  from .http_action import HTTPAction
15
- from .io_action import BaseIOAction, ShellAction
15
+ from .io_action import BaseIOAction
16
16
  from .literal_input_action import LiteralInputAction
17
17
  from .menu_action import MenuAction
18
18
  from .process_action import ProcessAction
@@ -20,6 +20,7 @@ from .process_pool_action import ProcessPoolAction
20
20
  from .prompt_menu_action import PromptMenuAction
21
21
  from .select_file_action import SelectFileAction
22
22
  from .selection_action import SelectionAction
23
+ from .shell_action import ShellAction
23
24
  from .signal_action import SignalAction
24
25
  from .user_input_action import UserInputAction
25
26
 
@@ -6,7 +6,7 @@ from typing import Any, Callable
6
6
 
7
7
  from rich.tree import Tree
8
8
 
9
- from falyx.action.base import BaseAction
9
+ from falyx.action.base_action import BaseAction
10
10
  from falyx.context import ExecutionContext
11
11
  from falyx.execution_registry import ExecutionRegistry as er
12
12
  from falyx.hook_manager import HookManager, HookType
@@ -4,7 +4,7 @@ from typing import Any, Callable
4
4
 
5
5
  from rich.tree import Tree
6
6
 
7
- from falyx.action.base import BaseAction
7
+ from falyx.action.base_action import BaseAction
8
8
  from falyx.context import ExecutionContext
9
9
  from falyx.execution_registry import ExecutionRegistry as er
10
10
  from falyx.hook_manager import HookType
@@ -1,11 +1,13 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """action_group.py"""
1
3
  import asyncio
2
4
  import random
3
- from typing import Any, Callable
5
+ from typing import Any, Callable, Sequence
4
6
 
5
7
  from rich.tree import Tree
6
8
 
7
9
  from falyx.action.action import Action
8
- from falyx.action.base import BaseAction
10
+ from falyx.action.base_action import BaseAction
9
11
  from falyx.action.mixins import ActionListMixin
10
12
  from falyx.context import ExecutionContext, SharedContext
11
13
  from falyx.execution_registry import ExecutionRegistry as er
@@ -52,7 +54,7 @@ class ActionGroup(BaseAction, ActionListMixin):
52
54
  def __init__(
53
55
  self,
54
56
  name: str,
55
- actions: list[BaseAction] | None = None,
57
+ actions: Sequence[BaseAction | Callable[..., Any]] | None = None,
56
58
  *,
57
59
  hooks: HookManager | None = None,
58
60
  inject_last_result: bool = False,
@@ -68,7 +70,7 @@ class ActionGroup(BaseAction, ActionListMixin):
68
70
  if actions:
69
71
  self.set_actions(actions)
70
72
 
71
- def _wrap_if_needed(self, action: BaseAction | Any) -> BaseAction:
73
+ def _wrap_if_needed(self, action: BaseAction | Callable[..., Any]) -> BaseAction:
72
74
  if isinstance(action, BaseAction):
73
75
  return action
74
76
  elif callable(action):
@@ -79,12 +81,18 @@ class ActionGroup(BaseAction, ActionListMixin):
79
81
  f"{type(action).__name__}"
80
82
  )
81
83
 
82
- def add_action(self, action: BaseAction | Any) -> None:
84
+ def add_action(self, action: BaseAction | Callable[..., Any]) -> None:
83
85
  action = self._wrap_if_needed(action)
84
86
  super().add_action(action)
85
87
  if hasattr(action, "register_teardown") and callable(action.register_teardown):
86
88
  action.register_teardown(self.hooks)
87
89
 
90
+ def set_actions(self, actions: Sequence[BaseAction | Callable[..., Any]]) -> None:
91
+ """Replaces the current action list with a new one."""
92
+ self.actions.clear()
93
+ for action in actions:
94
+ self.add_action(action)
95
+
88
96
  def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
89
97
  arg_defs = same_argument_definitions(self.actions)
90
98
  if arg_defs:
@@ -1,9 +1,11 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """types.py"""
1
3
  from __future__ import annotations
2
4
 
3
5
  from enum import Enum
4
6
 
5
7
 
6
- class FileReturnType(Enum):
8
+ class FileType(Enum):
7
9
  """Enum for file return types."""
8
10
 
9
11
  TEXT = "text"
@@ -26,7 +28,7 @@ class FileReturnType(Enum):
26
28
  return aliases.get(value, value)
27
29
 
28
30
  @classmethod
29
- def _missing_(cls, value: object) -> FileReturnType:
31
+ def _missing_(cls, value: object) -> FileType:
30
32
  if isinstance(value, str):
31
33
  normalized = value.lower()
32
34
  alias = cls._get_alias(normalized)
@@ -34,7 +36,7 @@ class FileReturnType(Enum):
34
36
  if member.value == alias:
35
37
  return member
36
38
  valid = ", ".join(member.value for member in cls)
37
- raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}")
39
+ raise ValueError(f"Invalid FileType: '{value}'. Must be one of: {valid}")
38
40
 
39
41
 
40
42
  class SelectionReturnType(Enum):
@@ -1,5 +1,5 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
- """base.py
2
+ """base_action.py
3
3
 
4
4
  Core action system for Falyx.
5
5
 
@@ -1,11 +1,13 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """chained_action.py"""
1
3
  from __future__ import annotations
2
4
 
3
- from typing import Any, Callable
5
+ from typing import Any, Callable, Sequence
4
6
 
5
7
  from rich.tree import Tree
6
8
 
7
9
  from falyx.action.action import Action
8
- from falyx.action.base import BaseAction
10
+ from falyx.action.base_action import BaseAction
9
11
  from falyx.action.fallback_action import FallbackAction
10
12
  from falyx.action.literal_input_action import LiteralInputAction
11
13
  from falyx.action.mixins import ActionListMixin
@@ -45,7 +47,7 @@ class ChainedAction(BaseAction, ActionListMixin):
45
47
  def __init__(
46
48
  self,
47
49
  name: str,
48
- actions: list[BaseAction | Any] | None = None,
50
+ actions: Sequence[BaseAction | Callable[..., Any]] | None = None,
49
51
  *,
50
52
  hooks: HookManager | None = None,
51
53
  inject_last_result: bool = False,
@@ -65,7 +67,7 @@ class ChainedAction(BaseAction, ActionListMixin):
65
67
  if actions:
66
68
  self.set_actions(actions)
67
69
 
68
- def _wrap_if_needed(self, action: BaseAction | Any) -> BaseAction:
70
+ def _wrap_if_needed(self, action: BaseAction | Callable[..., Any]) -> BaseAction:
69
71
  if isinstance(action, BaseAction):
70
72
  return action
71
73
  elif callable(action):
@@ -73,7 +75,7 @@ class ChainedAction(BaseAction, ActionListMixin):
73
75
  else:
74
76
  return LiteralInputAction(action)
75
77
 
76
- def add_action(self, action: BaseAction | Any) -> None:
78
+ def add_action(self, action: BaseAction | Callable[..., Any]) -> None:
77
79
  action = self._wrap_if_needed(action)
78
80
  if self.actions and self.auto_inject and not action.inject_last_result:
79
81
  action.inject_last_result = True
@@ -81,6 +83,12 @@ class ChainedAction(BaseAction, ActionListMixin):
81
83
  if hasattr(action, "register_teardown") and callable(action.register_teardown):
82
84
  action.register_teardown(self.hooks)
83
85
 
86
+ def set_actions(self, actions: Sequence[BaseAction | Callable[..., Any]]) -> None:
87
+ """Replaces the current action list with a new one."""
88
+ self.actions.clear()
89
+ for action in actions:
90
+ self.add_action(action)
91
+
84
92
  def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
85
93
  if self.actions:
86
94
  return self.actions[0].get_infer_target()
@@ -1,3 +1,5 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """fallback_action.py"""
1
3
  from functools import cached_property
2
4
  from typing import Any
3
5
 
@@ -16,19 +16,15 @@ Common usage includes shell-like filters, input transformers, or any tool that
16
16
  needs to consume input from another process or pipeline.
17
17
  """
18
18
  import asyncio
19
- import shlex
20
- import subprocess
21
19
  import sys
22
20
  from typing import Any, Callable
23
21
 
24
22
  from rich.tree import Tree
25
23
 
26
- from falyx.action.base import BaseAction
24
+ from falyx.action.base_action import BaseAction
27
25
  from falyx.context import ExecutionContext
28
- from falyx.exceptions import FalyxError
29
26
  from falyx.execution_registry import ExecutionRegistry as er
30
27
  from falyx.hook_manager import HookManager, HookType
31
- from falyx.logger import logger
32
28
  from falyx.themes import OneColors
33
29
 
34
30
 
@@ -171,92 +167,3 @@ class BaseIOAction(BaseAction):
171
167
  parent.add("".join(label))
172
168
  else:
173
169
  self.console.print(Tree("".join(label)))
174
-
175
-
176
- class ShellAction(BaseIOAction):
177
- """
178
- ShellAction wraps a shell command template for CLI pipelines.
179
-
180
- This Action takes parsed input (from stdin, literal, or last_result),
181
- substitutes it into the provided shell command template, and executes
182
- the command asynchronously using subprocess.
183
-
184
- Designed for quick integration with shell tools like `grep`, `ping`, `jq`, etc.
185
-
186
- ⚠️ Security Warning:
187
- By default, ShellAction uses `shell=True`, which can be dangerous with
188
- unsanitized input. To mitigate this, set `safe_mode=True` to use `shell=False`
189
- with `shlex.split()`.
190
-
191
- Features:
192
- - Automatically handles input parsing (str/bytes)
193
- - `safe_mode=True` disables shell interpretation and runs with `shell=False`
194
- - Captures stdout and stderr from shell execution
195
- - Raises on non-zero exit codes with stderr as the error
196
- - Result is returned as trimmed stdout string
197
-
198
- Args:
199
- name (str): Name of the action.
200
- command_template (str): Shell command to execute. Must include `{}` to include
201
- input. If no placeholder is present, the input is not
202
- included.
203
- safe_mode (bool): If True, runs with `shell=False` using shlex parsing
204
- (default: False).
205
- """
206
-
207
- def __init__(
208
- self, name: str, command_template: str, safe_mode: bool = False, **kwargs
209
- ):
210
- super().__init__(name=name, **kwargs)
211
- self.command_template = command_template
212
- self.safe_mode = safe_mode
213
-
214
- def from_input(self, raw: str | bytes) -> str:
215
- if not isinstance(raw, (str, bytes)):
216
- raise TypeError(
217
- f"{self.name} expected str or bytes input, got {type(raw).__name__}"
218
- )
219
- return raw.strip() if isinstance(raw, str) else raw.decode("utf-8").strip()
220
-
221
- def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
222
- if sys.stdin.isatty():
223
- return self._run, {"parsed_input": {"help": self.command_template}}
224
- return None, None
225
-
226
- async def _run(self, parsed_input: str) -> str:
227
- # Replace placeholder in template, or use raw input as full command
228
- command = self.command_template.format(parsed_input)
229
- if self.safe_mode:
230
- try:
231
- args = shlex.split(command)
232
- except ValueError as error:
233
- raise FalyxError(f"Invalid command template: {error}")
234
- result = subprocess.run(args, capture_output=True, text=True, check=True)
235
- else:
236
- result = subprocess.run(
237
- command, shell=True, text=True, capture_output=True, check=True
238
- )
239
- if result.returncode != 0:
240
- raise RuntimeError(result.stderr.strip())
241
- return result.stdout.strip()
242
-
243
- def to_output(self, result: str) -> str:
244
- return result
245
-
246
- async def preview(self, parent: Tree | None = None):
247
- label = [f"[{OneColors.GREEN_b}]⚙ ShellAction[/] '{self.name}'"]
248
- label.append(f"\n[dim]Template:[/] {self.command_template}")
249
- label.append(
250
- f"\n[dim]Safe mode:[/] {'Enabled' if self.safe_mode else 'Disabled'}"
251
- )
252
- if self.inject_last_result:
253
- label.append(f" [dim](injects '{self.inject_into}')[/dim]")
254
- tree = parent.add("".join(label)) if parent else Tree("".join(label))
255
- if not parent:
256
- self.console.print(tree)
257
-
258
- def __str__(self):
259
- return (
260
- f"ShellAction(name={self.name!r}, command_template={self.command_template!r},"
261
- f" safe_mode={self.safe_mode})"
262
- )
@@ -1,3 +1,5 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """literal_input_action.py"""
1
3
  from __future__ import annotations
2
4
 
3
5
  from functools import cached_property
@@ -0,0 +1,28 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """load_file_action.py"""
3
+ from pathlib import Path
4
+
5
+ from rich.tree import Tree
6
+
7
+ from falyx.action.base_action import BaseAction
8
+
9
+
10
+ class LoadFileAction(BaseAction):
11
+ """ """
12
+
13
+ def __init__(self, name: str, file_path: str):
14
+ super().__init__(name=name)
15
+ self.file_path = file_path
16
+
17
+ def get_infer_target(self) -> tuple[None, None]:
18
+ return None, None
19
+
20
+ async def _run(self, *args, **kwargs):
21
+ raise NotImplementedError(
22
+ "LoadFileAction is not finished yet... Use primatives instead..."
23
+ )
24
+
25
+ async def preview(self, parent: Tree | None = None): ...
26
+
27
+ def __str__(self) -> str:
28
+ return f"LoadFileAction(file_path={self.file_path})"
@@ -7,7 +7,7 @@ from rich.console import Console
7
7
  from rich.table import Table
8
8
  from rich.tree import Tree
9
9
 
10
- from falyx.action.base import BaseAction
10
+ from falyx.action.base_action import BaseAction
11
11
  from falyx.context import ExecutionContext
12
12
  from falyx.execution_registry import ExecutionRegistry as er
13
13
  from falyx.hook_manager import HookType
@@ -1,4 +1,8 @@
1
- from falyx.action.base import BaseAction
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """mixins.py"""
3
+ from typing import Sequence
4
+
5
+ from falyx.action.base_action import BaseAction
2
6
 
3
7
 
4
8
  class ActionListMixin:
@@ -7,7 +11,7 @@ class ActionListMixin:
7
11
  def __init__(self) -> None:
8
12
  self.actions: list[BaseAction] = []
9
13
 
10
- def set_actions(self, actions: list[BaseAction]) -> None:
14
+ def set_actions(self, actions: Sequence[BaseAction]) -> None:
11
15
  """Replaces the current action list with a new one."""
12
16
  self.actions.clear()
13
17
  for action in actions:
@@ -1,3 +1,5 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """process_action.py"""
1
3
  from __future__ import annotations
2
4
 
3
5
  import asyncio
@@ -7,7 +9,7 @@ from typing import Any, Callable
7
9
 
8
10
  from rich.tree import Tree
9
11
 
10
- from falyx.action.base import BaseAction
12
+ from falyx.action.base_action import BaseAction
11
13
  from falyx.context import ExecutionContext
12
14
  from falyx.execution_registry import ExecutionRegistry as er
13
15
  from falyx.hook_manager import HookManager, HookType
@@ -1,3 +1,5 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """process_pool_action.py"""
1
3
  from __future__ import annotations
2
4
 
3
5
  import asyncio
@@ -9,7 +11,7 @@ from typing import Any, Callable
9
11
 
10
12
  from rich.tree import Tree
11
13
 
12
- from falyx.action.base import BaseAction
14
+ from falyx.action.base_action import BaseAction
13
15
  from falyx.context import ExecutionContext, SharedContext
14
16
  from falyx.execution_registry import ExecutionRegistry as er
15
17
  from falyx.hook_manager import HookManager, HookType
@@ -7,7 +7,7 @@ from prompt_toolkit.formatted_text import FormattedText, merge_formatted_text
7
7
  from rich.console import Console
8
8
  from rich.tree import Tree
9
9
 
10
- from falyx.action.base import BaseAction
10
+ from falyx.action.base_action import BaseAction
11
11
  from falyx.context import ExecutionContext
12
12
  from falyx.execution_registry import ExecutionRegistry as er
13
13
  from falyx.hook_manager import HookType
@@ -0,0 +1,28 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """save_file_action.py"""
3
+ from pathlib import Path
4
+
5
+ from rich.tree import Tree
6
+
7
+ from falyx.action.base_action import BaseAction
8
+
9
+
10
+ class SaveFileAction(BaseAction):
11
+ """ """
12
+
13
+ def __init__(self, name: str, file_path: str):
14
+ super().__init__(name=name)
15
+ self.file_path = file_path
16
+
17
+ def get_infer_target(self) -> tuple[None, None]:
18
+ return None, None
19
+
20
+ async def _run(self, *args, **kwargs):
21
+ raise NotImplementedError(
22
+ "SaveFileAction is not finished yet... Use primitives instead..."
23
+ )
24
+
25
+ async def preview(self, parent: Tree | None = None): ...
26
+
27
+ def __str__(self) -> str:
28
+ return f"SaveFileAction(file_path={self.file_path})"
@@ -14,8 +14,8 @@ from prompt_toolkit import PromptSession
14
14
  from rich.console import Console
15
15
  from rich.tree import Tree
16
16
 
17
- from falyx.action.base import BaseAction
18
- from falyx.action.types import FileReturnType
17
+ from falyx.action.action_types import FileType
18
+ from falyx.action.base_action import BaseAction
19
19
  from falyx.context import ExecutionContext
20
20
  from falyx.execution_registry import ExecutionRegistry as er
21
21
  from falyx.hook_manager import HookType
@@ -50,7 +50,7 @@ class SelectFileAction(BaseAction):
50
50
  prompt_message (str): Message to display when prompting for selection.
51
51
  style (str): Style for the selection options.
52
52
  suffix_filter (str | None): Restrict to certain file types.
53
- return_type (FileReturnType): What to return (path, content, parsed).
53
+ return_type (FileType): What to return (path, content, parsed).
54
54
  console (Console | None): Console instance for output.
55
55
  prompt_session (PromptSession | None): Prompt session for user input.
56
56
  """
@@ -65,7 +65,7 @@ class SelectFileAction(BaseAction):
65
65
  prompt_message: str = "Choose > ",
66
66
  style: str = OneColors.WHITE,
67
67
  suffix_filter: str | None = None,
68
- return_type: FileReturnType | str = FileReturnType.PATH,
68
+ return_type: FileType | str = FileType.PATH,
69
69
  number_selections: int | str = 1,
70
70
  separator: str = ",",
71
71
  allow_duplicates: bool = False,
@@ -104,35 +104,35 @@ class SelectFileAction(BaseAction):
104
104
  else:
105
105
  raise ValueError("number_selections must be a positive integer or one of '*'")
106
106
 
107
- def _coerce_return_type(self, return_type: FileReturnType | str) -> FileReturnType:
108
- if isinstance(return_type, FileReturnType):
107
+ def _coerce_return_type(self, return_type: FileType | str) -> FileType:
108
+ if isinstance(return_type, FileType):
109
109
  return return_type
110
- return FileReturnType(return_type)
110
+ return FileType(return_type)
111
111
 
112
112
  def get_options(self, files: list[Path]) -> dict[str, SelectionOption]:
113
113
  value: Any
114
114
  options = {}
115
115
  for index, file in enumerate(files):
116
116
  try:
117
- if self.return_type == FileReturnType.TEXT:
117
+ if self.return_type == FileType.TEXT:
118
118
  value = file.read_text(encoding="UTF-8")
119
- elif self.return_type == FileReturnType.PATH:
119
+ elif self.return_type == FileType.PATH:
120
120
  value = file
121
- elif self.return_type == FileReturnType.JSON:
121
+ elif self.return_type == FileType.JSON:
122
122
  value = json.loads(file.read_text(encoding="UTF-8"))
123
- elif self.return_type == FileReturnType.TOML:
123
+ elif self.return_type == FileType.TOML:
124
124
  value = toml.loads(file.read_text(encoding="UTF-8"))
125
- elif self.return_type == FileReturnType.YAML:
125
+ elif self.return_type == FileType.YAML:
126
126
  value = yaml.safe_load(file.read_text(encoding="UTF-8"))
127
- elif self.return_type == FileReturnType.CSV:
127
+ elif self.return_type == FileType.CSV:
128
128
  with open(file, newline="", encoding="UTF-8") as csvfile:
129
129
  reader = csv.reader(csvfile)
130
130
  value = list(reader)
131
- elif self.return_type == FileReturnType.TSV:
131
+ elif self.return_type == FileType.TSV:
132
132
  with open(file, newline="", encoding="UTF-8") as tsvfile:
133
133
  reader = csv.reader(tsvfile, delimiter="\t")
134
134
  value = list(reader)
135
- elif self.return_type == FileReturnType.XML:
135
+ elif self.return_type == FileType.XML:
136
136
  tree = ET.parse(file, parser=ET.XMLParser(encoding="UTF-8"))
137
137
  root = tree.getroot()
138
138
  value = ET.tostring(root, encoding="unicode")
@@ -6,8 +6,8 @@ from prompt_toolkit import PromptSession
6
6
  from rich.console import Console
7
7
  from rich.tree import Tree
8
8
 
9
- from falyx.action.base import BaseAction
10
- from falyx.action.types import SelectionReturnType
9
+ from falyx.action.action_types import SelectionReturnType
10
+ from falyx.action.base_action import BaseAction
11
11
  from falyx.context import ExecutionContext
12
12
  from falyx.execution_registry import ExecutionRegistry as er
13
13
  from falyx.hook_manager import HookType
@@ -0,0 +1,105 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """shell_action.py
3
+ Execute shell commands with input substitution."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import shlex
8
+ import subprocess
9
+ import sys
10
+ from typing import Any, Callable
11
+
12
+ from rich.tree import Tree
13
+
14
+ from falyx.action.io_action import BaseIOAction
15
+ from falyx.exceptions import FalyxError
16
+ from falyx.themes import OneColors
17
+
18
+
19
+ class ShellAction(BaseIOAction):
20
+ """
21
+ ShellAction wraps a shell command template for CLI pipelines.
22
+
23
+ This Action takes parsed input (from stdin, literal, or last_result),
24
+ substitutes it into the provided shell command template, and executes
25
+ the command asynchronously using subprocess.
26
+
27
+ Designed for quick integration with shell tools like `grep`, `ping`, `jq`, etc.
28
+
29
+ ⚠️ Security Warning:
30
+ By default, ShellAction uses `shell=True`, which can be dangerous with
31
+ unsanitized input. To mitigate this, set `safe_mode=True` to use `shell=False`
32
+ with `shlex.split()`.
33
+
34
+ Features:
35
+ - Automatically handles input parsing (str/bytes)
36
+ - `safe_mode=True` disables shell interpretation and runs with `shell=False`
37
+ - Captures stdout and stderr from shell execution
38
+ - Raises on non-zero exit codes with stderr as the error
39
+ - Result is returned as trimmed stdout string
40
+
41
+ Args:
42
+ name (str): Name of the action.
43
+ command_template (str): Shell command to execute. Must include `{}` to include
44
+ input. If no placeholder is present, the input is not
45
+ included.
46
+ safe_mode (bool): If True, runs with `shell=False` using shlex parsing
47
+ (default: False).
48
+ """
49
+
50
+ def __init__(
51
+ self, name: str, command_template: str, safe_mode: bool = False, **kwargs
52
+ ):
53
+ super().__init__(name=name, **kwargs)
54
+ self.command_template = command_template
55
+ self.safe_mode = safe_mode
56
+
57
+ def from_input(self, raw: str | bytes) -> str:
58
+ if not isinstance(raw, (str, bytes)):
59
+ raise TypeError(
60
+ f"{self.name} expected str or bytes input, got {type(raw).__name__}"
61
+ )
62
+ return raw.strip() if isinstance(raw, str) else raw.decode("utf-8").strip()
63
+
64
+ def get_infer_target(self) -> tuple[Callable[..., Any] | None, dict[str, Any] | None]:
65
+ if sys.stdin.isatty():
66
+ return self._run, {"parsed_input": {"help": self.command_template}}
67
+ return None, None
68
+
69
+ async def _run(self, parsed_input: str) -> str:
70
+ # Replace placeholder in template, or use raw input as full command
71
+ command = self.command_template.format(parsed_input)
72
+ if self.safe_mode:
73
+ try:
74
+ args = shlex.split(command)
75
+ except ValueError as error:
76
+ raise FalyxError(f"Invalid command template: {error}")
77
+ result = subprocess.run(args, capture_output=True, text=True, check=True)
78
+ else:
79
+ result = subprocess.run(
80
+ command, shell=True, text=True, capture_output=True, check=True
81
+ )
82
+ if result.returncode != 0:
83
+ raise RuntimeError(result.stderr.strip())
84
+ return result.stdout.strip()
85
+
86
+ def to_output(self, result: str) -> str:
87
+ return result
88
+
89
+ async def preview(self, parent: Tree | None = None):
90
+ label = [f"[{OneColors.GREEN_b}]⚙ ShellAction[/] '{self.name}'"]
91
+ label.append(f"\n[dim]Template:[/] {self.command_template}")
92
+ label.append(
93
+ f"\n[dim]Safe mode:[/] {'Enabled' if self.safe_mode else 'Disabled'}"
94
+ )
95
+ if self.inject_last_result:
96
+ label.append(f" [dim](injects '{self.inject_into}')[/dim]")
97
+ tree = parent.add("".join(label)) if parent else Tree("".join(label))
98
+ if not parent:
99
+ self.console.print(tree)
100
+
101
+ def __str__(self):
102
+ return (
103
+ f"ShellAction(name={self.name!r}, command_template={self.command_template!r},"
104
+ f" safe_mode={self.safe_mode})"
105
+ )
@@ -1,9 +1,11 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """user_input_action.py"""
1
3
  from prompt_toolkit import PromptSession
2
4
  from prompt_toolkit.validation import Validator
3
5
  from rich.console import Console
4
6
  from rich.tree import Tree
5
7
 
6
- from falyx.action.base import BaseAction
8
+ from falyx.action.base_action import BaseAction
7
9
  from falyx.context import ExecutionContext
8
10
  from falyx.execution_registry import ExecutionRegistry as er
9
11
  from falyx.hook_manager import HookType
@@ -27,14 +27,14 @@ from rich.console import Console
27
27
  from rich.tree import Tree
28
28
 
29
29
  from falyx.action.action import Action
30
- from falyx.action.base import BaseAction
30
+ from falyx.action.base_action import BaseAction
31
31
  from falyx.context import ExecutionContext
32
32
  from falyx.debug import register_debug_hooks
33
33
  from falyx.execution_registry import ExecutionRegistry as er
34
34
  from falyx.hook_manager import HookManager, HookType
35
35
  from falyx.logger import logger
36
36
  from falyx.options_manager import OptionsManager
37
- from falyx.parser.argparse import CommandArgumentParser
37
+ from falyx.parser.command_argument_parser import CommandArgumentParser
38
38
  from falyx.parser.signature import infer_args_from_func
39
39
  from falyx.prompt_utils import confirm_async, should_prompt_user
40
40
  from falyx.protocols import ArgParserProtocol
@@ -14,7 +14,7 @@ from pydantic import BaseModel, Field, field_validator, model_validator
14
14
  from rich.console import Console
15
15
 
16
16
  from falyx.action.action import Action
17
- from falyx.action.base import BaseAction
17
+ from falyx.action.base_action import BaseAction
18
18
  from falyx.command import Command
19
19
  from falyx.falyx import Falyx
20
20
  from falyx.logger import logger
@@ -43,7 +43,7 @@ from rich.markdown import Markdown
43
43
  from rich.table import Table
44
44
 
45
45
  from falyx.action.action import Action
46
- from falyx.action.base import BaseAction
46
+ from falyx.action.base_action import BaseAction
47
47
  from falyx.bottom_bar import BottomBar
48
48
  from falyx.command import Command
49
49
  from falyx.context import ExecutionContext
@@ -346,7 +346,6 @@ class Falyx:
346
346
  aliases=["HISTORY"],
347
347
  action=Action(name="View Execution History", action=er.summary),
348
348
  style=OneColors.DARK_YELLOW,
349
- simple_help_signature=True,
350
349
  arg_parser=parser,
351
350
  help_text="View the execution history of commands.",
352
351
  )
@@ -1152,7 +1151,7 @@ class Falyx:
1152
1151
  sys.exit(0)
1153
1152
 
1154
1153
  if self.cli_args.command == "version" or self.cli_args.version:
1155
- self.console.print(f"[{self.version_style}]{self.program} v{__version__}[/]")
1154
+ self.console.print(f"[{self.version_style}]{self.program} v{self.version}[/]")
1156
1155
  sys.exit(0)
1157
1156
 
1158
1157
  if self.cli_args.command == "preview":
@@ -4,7 +4,7 @@ from dataclasses import dataclass
4
4
 
5
5
  from prompt_toolkit.formatted_text import FormattedText
6
6
 
7
- from falyx.action.base import BaseAction
7
+ from falyx.action.base_action import BaseAction
8
8
  from falyx.signals import BackSignal, QuitSignal
9
9
  from falyx.themes import OneColors
10
10
  from falyx.utils import CaseInsensitiveDict
@@ -5,7 +5,9 @@ Copyright (c) 2025 rtj.dev LLC.
5
5
  Licensed under the MIT License. See LICENSE file for details.
6
6
  """
7
7
 
8
- from .argparse import Argument, ArgumentAction, CommandArgumentParser
8
+ from .argument import Argument
9
+ from .argument_action import ArgumentAction
10
+ from .command_argument_parser import CommandArgumentParser
9
11
  from .parsers import FalyxParsers, get_arg_parsers, get_root_parser, get_subparsers
10
12
 
11
13
  __all__ = [
@@ -0,0 +1,98 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """argument.py"""
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from falyx.action.base_action import BaseAction
7
+ from falyx.parser.argument_action import ArgumentAction
8
+
9
+
10
+ @dataclass
11
+ class Argument:
12
+ """Represents a command-line argument."""
13
+
14
+ flags: tuple[str, ...]
15
+ dest: str # Destination name for the argument
16
+ action: ArgumentAction = (
17
+ ArgumentAction.STORE
18
+ ) # Action to be taken when the argument is encountered
19
+ type: Any = str # Type of the argument (e.g., str, int, float) or callable
20
+ default: Any = None # Default value if the argument is not provided
21
+ choices: list[str] | None = None # List of valid choices for the argument
22
+ required: bool = False # True if the argument is required
23
+ help: str = "" # Help text for the argument
24
+ nargs: int | str | None = None # int, '?', '*', '+', None
25
+ positional: bool = False # True if no leading - or -- in flags
26
+ resolver: BaseAction | None = None # Action object for the argument
27
+
28
+ def get_positional_text(self) -> str:
29
+ """Get the positional text for the argument."""
30
+ text = ""
31
+ if self.positional:
32
+ if self.choices:
33
+ text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
34
+ else:
35
+ text = self.dest
36
+ return text
37
+
38
+ def get_choice_text(self) -> str:
39
+ """Get the choice text for the argument."""
40
+ choice_text = ""
41
+ if self.choices:
42
+ choice_text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
43
+ elif (
44
+ self.action
45
+ in (
46
+ ArgumentAction.STORE,
47
+ ArgumentAction.APPEND,
48
+ ArgumentAction.EXTEND,
49
+ )
50
+ and not self.positional
51
+ ):
52
+ choice_text = self.dest.upper()
53
+ elif self.action in (
54
+ ArgumentAction.STORE,
55
+ ArgumentAction.APPEND,
56
+ ArgumentAction.EXTEND,
57
+ ) or isinstance(self.nargs, str):
58
+ choice_text = self.dest
59
+
60
+ if self.nargs == "?":
61
+ choice_text = f"[{choice_text}]"
62
+ elif self.nargs == "*":
63
+ choice_text = f"[{choice_text} ...]"
64
+ elif self.nargs == "+":
65
+ choice_text = f"{choice_text} [{choice_text} ...]"
66
+ return choice_text
67
+
68
+ def __eq__(self, other: object) -> bool:
69
+ if not isinstance(other, Argument):
70
+ return False
71
+ return (
72
+ self.flags == other.flags
73
+ and self.dest == other.dest
74
+ and self.action == other.action
75
+ and self.type == other.type
76
+ and self.choices == other.choices
77
+ and self.required == other.required
78
+ and self.nargs == other.nargs
79
+ and self.positional == other.positional
80
+ and self.default == other.default
81
+ and self.help == other.help
82
+ )
83
+
84
+ def __hash__(self) -> int:
85
+ return hash(
86
+ (
87
+ tuple(self.flags),
88
+ self.dest,
89
+ self.action,
90
+ self.type,
91
+ tuple(self.choices or []),
92
+ self.required,
93
+ self.nargs,
94
+ self.positional,
95
+ self.default,
96
+ self.help,
97
+ )
98
+ )
@@ -0,0 +1,27 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """argument_action.py"""
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+
7
+
8
+ class ArgumentAction(Enum):
9
+ """Defines the action to be taken when the argument is encountered."""
10
+
11
+ ACTION = "action"
12
+ STORE = "store"
13
+ STORE_TRUE = "store_true"
14
+ STORE_FALSE = "store_false"
15
+ APPEND = "append"
16
+ EXTEND = "extend"
17
+ COUNT = "count"
18
+ HELP = "help"
19
+
20
+ @classmethod
21
+ def choices(cls) -> list[ArgumentAction]:
22
+ """Return a list of all argument actions."""
23
+ return list(cls)
24
+
25
+ def __str__(self) -> str:
26
+ """Return the string representation of the argument action."""
27
+ return self.value
@@ -1,134 +1,22 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
+ """command_argument_parser.py"""
2
3
  from __future__ import annotations
3
4
 
4
5
  from copy import deepcopy
5
- from dataclasses import dataclass
6
- from enum import Enum
7
6
  from typing import Any, Iterable
8
7
 
9
8
  from rich.console import Console
10
9
  from rich.markup import escape
11
10
  from rich.text import Text
12
11
 
13
- from falyx.action.base import BaseAction
12
+ from falyx.action.base_action import BaseAction
14
13
  from falyx.exceptions import CommandArgumentError
14
+ from falyx.parser.argument import Argument
15
+ from falyx.parser.argument_action import ArgumentAction
15
16
  from falyx.parser.utils import coerce_value
16
17
  from falyx.signals import HelpSignal
17
18
 
18
19
 
19
- class ArgumentAction(Enum):
20
- """Defines the action to be taken when the argument is encountered."""
21
-
22
- ACTION = "action"
23
- STORE = "store"
24
- STORE_TRUE = "store_true"
25
- STORE_FALSE = "store_false"
26
- APPEND = "append"
27
- EXTEND = "extend"
28
- COUNT = "count"
29
- HELP = "help"
30
-
31
- @classmethod
32
- def choices(cls) -> list[ArgumentAction]:
33
- """Return a list of all argument actions."""
34
- return list(cls)
35
-
36
- def __str__(self) -> str:
37
- """Return the string representation of the argument action."""
38
- return self.value
39
-
40
-
41
- @dataclass
42
- class Argument:
43
- """Represents a command-line argument."""
44
-
45
- flags: tuple[str, ...]
46
- dest: str # Destination name for the argument
47
- action: ArgumentAction = (
48
- ArgumentAction.STORE
49
- ) # Action to be taken when the argument is encountered
50
- type: Any = str # Type of the argument (e.g., str, int, float) or callable
51
- default: Any = None # Default value if the argument is not provided
52
- choices: list[str] | None = None # List of valid choices for the argument
53
- required: bool = False # True if the argument is required
54
- help: str = "" # Help text for the argument
55
- nargs: int | str | None = None # int, '?', '*', '+', None
56
- positional: bool = False # True if no leading - or -- in flags
57
- resolver: BaseAction | None = None # Action object for the argument
58
-
59
- def get_positional_text(self) -> str:
60
- """Get the positional text for the argument."""
61
- text = ""
62
- if self.positional:
63
- if self.choices:
64
- text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
65
- else:
66
- text = self.dest
67
- return text
68
-
69
- def get_choice_text(self) -> str:
70
- """Get the choice text for the argument."""
71
- choice_text = ""
72
- if self.choices:
73
- choice_text = f"{{{','.join([str(choice) for choice in self.choices])}}}"
74
- elif (
75
- self.action
76
- in (
77
- ArgumentAction.STORE,
78
- ArgumentAction.APPEND,
79
- ArgumentAction.EXTEND,
80
- )
81
- and not self.positional
82
- ):
83
- choice_text = self.dest.upper()
84
- elif self.action in (
85
- ArgumentAction.STORE,
86
- ArgumentAction.APPEND,
87
- ArgumentAction.EXTEND,
88
- ) or isinstance(self.nargs, str):
89
- choice_text = self.dest
90
-
91
- if self.nargs == "?":
92
- choice_text = f"[{choice_text}]"
93
- elif self.nargs == "*":
94
- choice_text = f"[{choice_text} ...]"
95
- elif self.nargs == "+":
96
- choice_text = f"{choice_text} [{choice_text} ...]"
97
- return choice_text
98
-
99
- def __eq__(self, other: object) -> bool:
100
- if not isinstance(other, Argument):
101
- return False
102
- return (
103
- self.flags == other.flags
104
- and self.dest == other.dest
105
- and self.action == other.action
106
- and self.type == other.type
107
- and self.choices == other.choices
108
- and self.required == other.required
109
- and self.nargs == other.nargs
110
- and self.positional == other.positional
111
- and self.default == other.default
112
- and self.help == other.help
113
- )
114
-
115
- def __hash__(self) -> int:
116
- return hash(
117
- (
118
- tuple(self.flags),
119
- self.dest,
120
- self.action,
121
- self.type,
122
- tuple(self.choices or []),
123
- self.required,
124
- self.nargs,
125
- self.positional,
126
- self.default,
127
- self.help,
128
- )
129
- )
130
-
131
-
132
20
  class CommandArgumentParser:
133
21
  """
134
22
  Custom argument parser for Falyx Commands.
@@ -76,14 +76,14 @@ def get_root_parser(
76
76
  help="Run in non-interactive mode with all prompts bypassed.",
77
77
  )
78
78
  parser.add_argument(
79
- "-v", "--verbose", action="store_true", help="Enable debug logging for Falyx."
79
+ "-v", "--verbose", action="store_true", help=f"Enable debug logging for {prog}."
80
80
  )
81
81
  parser.add_argument(
82
82
  "--debug-hooks",
83
83
  action="store_true",
84
84
  help="Enable default lifecycle debug logging",
85
85
  )
86
- parser.add_argument("--version", action="store_true", help="Show Falyx version")
86
+ parser.add_argument("--version", action="store_true", help=f"Show {prog} version")
87
87
  return parser
88
88
 
89
89
 
@@ -98,7 +98,6 @@ def get_subparsers(
98
98
  subparsers = parser.add_subparsers(
99
99
  title=title,
100
100
  description=description,
101
- metavar="COMMAND",
102
101
  dest="command",
103
102
  )
104
103
  return subparsers
@@ -124,6 +123,8 @@ def get_arg_parsers(
124
123
  subparsers: _SubParsersAction | None = None,
125
124
  ) -> FalyxParsers:
126
125
  """Returns the argument parser for the CLI."""
126
+ if epilog is None:
127
+ epilog = f"Tip: Use '{prog} run ?[COMMAND]' to preview any command from the CLI."
127
128
  if root_parser is None:
128
129
  parser = get_root_parser(
129
130
  prog=prog,
@@ -145,7 +146,14 @@ def get_arg_parsers(
145
146
  parser = root_parser
146
147
 
147
148
  if subparsers is None:
148
- subparsers = get_subparsers(parser)
149
+ if prog == "falyx":
150
+ subparsers = get_subparsers(
151
+ parser,
152
+ title="Falyx Commands",
153
+ description="Available commands for the Falyx CLI.",
154
+ )
155
+ else:
156
+ subparsers = get_subparsers(parser, title="subcommands", description=None)
149
157
  if not isinstance(subparsers, _SubParsersAction):
150
158
  raise TypeError("subparsers must be an instance of _SubParsersAction")
151
159
 
@@ -154,10 +162,10 @@ def get_arg_parsers(
154
162
  if isinstance(commands, dict):
155
163
  for command in commands.values():
156
164
  run_description.append(command.usage)
157
- command_description = command.description or command.help_text
165
+ command_description = command.help_text or command.description
158
166
  run_description.append(f"{' '*24}{command_description}")
159
167
  run_epilog = (
160
- "Tip: Use 'falyx run ?[COMMAND]' to preview commands by their key or alias."
168
+ f"Tip: Use '{prog} run ?[COMMAND]' to preview commands by their key or alias."
161
169
  )
162
170
  run_parser = subparsers.add_parser(
163
171
  "run",
@@ -259,7 +267,7 @@ def get_arg_parsers(
259
267
  "-t", "--tag", help="Filter commands by tag (case-insensitive)", default=None
260
268
  )
261
269
 
262
- version_parser = subparsers.add_parser("version", help="Show the Falyx version")
270
+ version_parser = subparsers.add_parser("version", help=f"Show {prog} version")
263
271
 
264
272
  return FalyxParsers(
265
273
  root=parser,
@@ -1,3 +1,4 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
1
2
  import inspect
2
3
  from typing import Any, Callable
3
4
 
@@ -1,3 +1,4 @@
1
+ # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
1
2
  import types
2
3
  from datetime import datetime
3
4
  from enum import EnumMeta
@@ -5,7 +6,7 @@ from typing import Any, Literal, Union, get_args, get_origin
5
6
 
6
7
  from dateutil import parser as date_parser
7
8
 
8
- from falyx.action.base import BaseAction
9
+ from falyx.action.base_action import BaseAction
9
10
  from falyx.logger import logger
10
11
  from falyx.parser.signature import infer_args_from_func
11
12
 
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from typing import Any, Awaitable, Protocol, runtime_checkable
6
6
 
7
- from falyx.action.base import BaseAction
7
+ from falyx.action.base_action import BaseAction
8
8
 
9
9
 
10
10
  @runtime_checkable
@@ -1,7 +1,7 @@
1
1
  # Falyx CLI Framework — (c) 2025 rtj.dev LLC — MIT Licensed
2
2
  """retry_utils.py"""
3
3
  from falyx.action.action import Action
4
- from falyx.action.base import BaseAction
4
+ from falyx.action.base_action import BaseAction
5
5
  from falyx.hook_manager import HookType
6
6
  from falyx.retry import RetryHandler, RetryPolicy
7
7
 
@@ -0,0 +1 @@
1
+ __version__ = "0.1.53"
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "falyx"
3
- version = "0.1.51"
3
+ version = "0.1.53"
4
4
  description = "Reliable and introspectable async CLI action framework."
5
5
  authors = ["Roland Thomas Jr <roland@rtj.dev>"]
6
6
  license = "MIT"
@@ -1 +0,0 @@
1
- __version__ = "0.1.51"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes