diff --git a/falyx/__main__.py b/falyx/__main__.py index 2f3e2e6..a2ae816 100644 --- a/falyx/__main__.py +++ b/falyx/__main__.py @@ -6,6 +6,7 @@ Licensed under the MIT License. See LICENSE file for details. """ import asyncio +import os import sys from argparse import Namespace from pathlib import Path @@ -14,7 +15,6 @@ from typing import Any from falyx.config import loader from falyx.falyx import Falyx from falyx.parsers import FalyxParsers, get_arg_parsers -from falyx.themes.colors import OneColors def find_falyx_config() -> Path | None: @@ -23,6 +23,7 @@ def find_falyx_config() -> Path | None: Path.cwd() / "falyx.toml", Path.cwd() / ".falyx.yaml", Path.cwd() / ".falyx.toml", + Path(os.environ.get("FALYX_CONFIG", "falyx.yaml")), Path.home() / ".config" / "falyx" / "falyx.yaml", Path.home() / ".config" / "falyx" / "falyx.toml", Path.home() / ".falyx.yaml", @@ -68,13 +69,7 @@ def run(args: Namespace) -> Any: print("No Falyx config file found. Exiting.") return None - flx = Falyx( - title="๐Ÿ› ๏ธ Config-Driven CLI", - cli_args=args, - columns=4, - prompt=[(OneColors.BLUE_b, "FALYX > ")], - ) - flx.add_commands(loader(bootstrap_path)) + flx: Falyx = loader(bootstrap_path) return asyncio.run(flx.run()) diff --git a/falyx/action_factory.py b/falyx/action_factory.py index 444dfad..cb8425d 100644 --- a/falyx/action_factory.py +++ b/falyx/action_factory.py @@ -1,3 +1,4 @@ +# Falyx CLI Framework โ€” (c) 2025 rtj.dev LLC โ€” MIT Licensed from typing import Any from rich.tree import Tree diff --git a/falyx/config.py b/falyx/config.py index f0b1a21..a00f8c5 100644 --- a/falyx/config.py +++ b/falyx/config.py @@ -1,18 +1,21 @@ # Falyx CLI Framework โ€” (c) 2025 rtj.dev LLC โ€” MIT Licensed """config.py Configuration loader for Falyx CLI commands.""" +from __future__ import annotations import importlib import sys from pathlib import Path -from typing import Any +from typing import Any, Callable import toml import yaml +from pydantic import BaseModel, Field, field_validator, model_validator from rich.console import Console from falyx.action import Action, BaseAction from falyx.command import Command +from falyx.falyx import Falyx from falyx.retry import RetryPolicy from falyx.themes.colors import OneColors from falyx.utils import logger @@ -27,8 +30,8 @@ def wrap_if_needed(obj: Any, name=None) -> BaseAction | Command: return Action(name=name or getattr(obj, "__name__", "unnamed"), action=obj) else: raise TypeError( - f"Cannot wrap object of type '{type(obj).__name__}' as a BaseAction or Command. " - "It must be a callable or an instance of BaseAction." + f"Cannot wrap object of type '{type(obj).__name__}'. " + "Expected a function or BaseAction." ) @@ -60,7 +63,101 @@ def import_action(dotted_path: str) -> Any: return action -def loader(file_path: Path | str) -> list[dict[str, Any]]: +class RawCommand(BaseModel): + key: str + description: str + action: str + + args: tuple[Any, ...] = () + kwargs: dict[str, Any] = {} + aliases: list[str] = [] + tags: list[str] = [] + style: str = "white" + + confirm: bool = False + confirm_message: str = "Are you sure?" + preview_before_confirm: bool = True + + spinner: bool = False + spinner_message: str = "Processing..." + spinner_type: str = "dots" + spinner_style: str = "cyan" + spinner_kwargs: dict[str, Any] = {} + + before_hooks: list[Callable] = [] + success_hooks: list[Callable] = [] + error_hooks: list[Callable] = [] + after_hooks: list[Callable] = [] + teardown_hooks: list[Callable] = [] + + logging_hooks: bool = False + retry: bool = False + retry_all: bool = False + retry_policy: RetryPolicy = Field(default_factory=RetryPolicy) + requires_input: bool | None = None + hidden: bool = False + help_text: str = "" + + @field_validator("retry_policy") + @classmethod + def validate_retry_policy(cls, value: dict | RetryPolicy) -> RetryPolicy: + if isinstance(value, RetryPolicy): + return value + if not isinstance(value, dict): + raise ValueError("retry_policy must be a dictionary.") + return RetryPolicy(**value) + + +def convert_commands(raw_commands: list[dict[str, Any]]) -> list[Command]: + commands = [] + for entry in raw_commands: + raw_command = RawCommand(**entry) + commands.append( + Command.model_validate( + { + **raw_command.model_dump(exclude={"action"}), + "action": wrap_if_needed( + import_action(raw_command.action), name=raw_command.description + ), + } + ) + ) + return commands + + +class FalyxConfig(BaseModel): + title: str = "Falyx CLI" + prompt: str | list[tuple[str, str]] | list[list[str]] = [ + (OneColors.BLUE_b, "FALYX > ") + ] + columns: int = 4 + welcome_message: str = "" + exit_message: str = "" + commands: list[Command] | list[dict] = [] + + @model_validator(mode="after") + def validate_prompt_format(self) -> FalyxConfig: + if isinstance(self.prompt, list): + for pair in self.prompt: + if not isinstance(pair, (list, tuple)) or len(pair) != 2: + raise ValueError( + "Prompt list must contain 2-element (style, text) pairs" + ) + return self + + def to_falyx(self) -> Falyx: + flx = Falyx( + title=self.title, + prompt=self.prompt, + columns=self.columns, + welcome_message=self.welcome_message, + exit_message=self.exit_message, + ) + flx.add_commands(self.commands) + return flx + + +def loader(file_path: Path | str) -> Falyx: """ Load command definitions from a YAML or TOML file. @@ -73,15 +170,13 @@ def loader(file_path: Path | str) -> list[dict[str, Any]]: file_path (str): Path to the config file (YAML or TOML). Returns: - list[dict[str, Any]]: A list of command configuration dictionaries. + Falyx: An instance of the Falyx CLI with loaded commands. Raises: ValueError: If the file format is unsupported or file cannot be parsed. """ - if isinstance(file_path, str): + if isinstance(file_path, (str, Path)): path = Path(file_path) - elif isinstance(file_path, Path): - path = file_path else: raise TypeError("file_path must be a string or Path object.") @@ -97,48 +192,23 @@ def loader(file_path: Path | str) -> list[dict[str, Any]]: else: raise ValueError(f"Unsupported config format: {suffix}") - if not isinstance(raw_config, list): - raise ValueError("Configuration file must contain a list of command definitions.") + if not isinstance(raw_config, dict): + raise ValueError( + "Configuration file must contain a dictionary with a list of commands.\n" + "Example:\n" + "title: 'My CLI'\n" + "commands:\n" + " - key: 'a'\n" + " description: 'Example command'\n" + " action: 'my_module.my_function'" + ) - required = ["key", "description", "action"] - commands = [] - for entry in raw_config: - for field in required: - if field not in entry: - raise ValueError(f"Missing '{field}' in command entry: {entry}") - - command_dict = { - "key": entry["key"], - "description": entry["description"], - "action": wrap_if_needed( - import_action(entry["action"]), name=entry["description"] - ), - "args": tuple(entry.get("args", ())), - "kwargs": entry.get("kwargs", {}), - "hidden": entry.get("hidden", False), - "aliases": entry.get("aliases", []), - "help_text": entry.get("help_text", ""), - "style": entry.get("style", "white"), - "confirm": entry.get("confirm", False), - "confirm_message": entry.get("confirm_message", "Are you sure?"), - "preview_before_confirm": entry.get("preview_before_confirm", True), - "spinner": entry.get("spinner", False), - "spinner_message": entry.get("spinner_message", "Processing..."), - "spinner_type": entry.get("spinner_type", "dots"), - "spinner_style": entry.get("spinner_style", "cyan"), - "spinner_kwargs": entry.get("spinner_kwargs", {}), - "before_hooks": entry.get("before_hooks", []), - "success_hooks": entry.get("success_hooks", []), - "error_hooks": entry.get("error_hooks", []), - "after_hooks": entry.get("after_hooks", []), - "teardown_hooks": entry.get("teardown_hooks", []), - "retry": entry.get("retry", False), - "retry_all": entry.get("retry_all", False), - "retry_policy": RetryPolicy(**entry.get("retry_policy", {})), - "tags": entry.get("tags", []), - "logging_hooks": entry.get("logging_hooks", False), - "requires_input": entry.get("requires_input", None), - } - commands.append(command_dict) - - return commands + commands = convert_commands(raw_config["commands"]) + return FalyxConfig( + title=raw_config.get("title", f"[{OneColors.BLUE_b}]Falyx CLI"), + prompt=raw_config.get("prompt", [(OneColors.BLUE_b, "FALYX > ")]), + columns=raw_config.get("columns", 4), + welcome_message=raw_config.get("welcome_message", ""), + exit_message=raw_config.get("exit_message", ""), + commands=commands, + ).to_falyx() diff --git a/falyx/execution_registry.py b/falyx/execution_registry.py index 3534062..a664d0b 100644 --- a/falyx/execution_registry.py +++ b/falyx/execution_registry.py @@ -9,6 +9,7 @@ from rich.console import Console from rich.table import Table from falyx.context import ExecutionContext +from falyx.themes.colors import OneColors from falyx.utils import logger @@ -66,10 +67,10 @@ class ExecutionRegistry: duration = f"{ctx.duration:.3f}s" if ctx.duration else "n/a" if ctx.exception: - status = "[bold red]โŒ Error" + status = f"[{OneColors.DARK_RED}]โŒ Error" result = repr(ctx.exception) else: - status = "[green]โœ… Success" + status = f"[{OneColors.GREEN}]โœ… Success" result = repr(ctx.result) if len(result) > 1000: result = f"{result[:1000]}..." diff --git a/falyx/falyx.py b/falyx/falyx.py index 9a1489e..07a72c7 100644 --- a/falyx/falyx.py +++ b/falyx/falyx.py @@ -291,7 +291,7 @@ class Falyx: """Returns the help command for the menu.""" return Command( key="H", - aliases=["HELP"], + aliases=["HELP", "?"], description="Help", action=self._show_help, style=OneColors.LIGHT_YELLOW, @@ -560,10 +560,24 @@ class Falyx: self.add_command(key, description, submenu.menu, style=style) submenu.update_exit_command(key="B", description="Back", aliases=["BACK"]) - def add_commands(self, commands: list[dict]) -> None: - """Adds multiple commands to the menu.""" + def add_commands(self, commands: list[Command] | list[dict]) -> None: + """Adds a list of Command instances or config dicts.""" for command in commands: - self.add_command(**command) + if isinstance(command, dict): + self.add_command(**command) + elif isinstance(command, Command): + self.add_command_from_command(command) + else: + raise FalyxError( + "Command must be a dictionary or an instance of Command." + ) + + def add_command_from_command(self, command: Command) -> None: + """Adds a command to the menu from an existing Command object.""" + if not isinstance(command, Command): + raise FalyxError("command must be an instance of Command.") + self._validate_command_key(command.key) + self.commands[command.key] = command def add_command( self, @@ -696,7 +710,10 @@ class Falyx: ) -> tuple[bool, Command | None]: """Returns the selected command based on user input. Supports keys, aliases, and abbreviations.""" is_preview, choice = self.parse_preview_command(choice) - if is_preview and not choice: + if is_preview and not choice and self.help_command: + is_preview = False + choice = "?" + elif is_preview and not choice: if not from_validate: self.console.print( f"[{OneColors.DARK_RED}]โŒ You must enter a command for preview mode.[/]" @@ -891,28 +908,29 @@ class Falyx: self.debug_hooks() if self.welcome_message: self.print_message(self.welcome_message) - while True: - if callable(self.render_menu): - self.render_menu(self) - else: - self.console.print(self.table, justify="center") - try: - task = asyncio.create_task(self.process_command()) - should_continue = await task - if not should_continue: + try: + while True: + if callable(self.render_menu): + self.render_menu(self) + else: + self.console.print(self.table, justify="center") + try: + task = asyncio.create_task(self.process_command()) + should_continue = await task + if not should_continue: + break + except (EOFError, KeyboardInterrupt): + logger.info("EOF or KeyboardInterrupt. Exiting menu.") break - except (EOFError, KeyboardInterrupt): - logger.info("EOF or KeyboardInterrupt. Exiting menu.") - break - except QuitSignal: - logger.info("QuitSignal received. Exiting menu.") - break - except BackSignal: - logger.info("BackSignal received.") - finally: - logger.info(f"Exiting menu: {self.get_title()}") - if self.exit_message: - self.print_message(self.exit_message) + except QuitSignal: + logger.info("QuitSignal received. Exiting menu.") + break + except BackSignal: + logger.info("BackSignal received.") + finally: + logger.info(f"Exiting menu: {self.get_title()}") + if self.exit_message: + self.print_message(self.exit_message) async def run(self) -> None: """Run Falyx CLI with structured subcommands.""" diff --git a/falyx/init.py b/falyx/init.py index 173f9df..f9ebd13 100644 --- a/falyx/init.py +++ b/falyx/init.py @@ -4,27 +4,85 @@ from pathlib import Path from rich.console import Console TEMPLATE_TASKS = """\ -async def build(): - print("๐Ÿ”จ Building project...") - return "Build complete!" +# This file is used by falyx.yaml to define CLI actions. +# You can run: falyx run [key] or falyx list to see available commands. -async def test(): - print("๐Ÿงช Running tests...") - return "Tests complete!" +import asyncio +import json + +from falyx.action import Action, ChainedAction +from falyx.io_action import ShellAction +from falyx.selection_action import SelectionAction + + +post_ids = ["1", "2", "3", "4", "5"] + +pick_post = SelectionAction( + name="Pick Post ID", + selections=post_ids, + title="Choose a Post ID", + prompt_message="Select a post > ", +) + +fetch_post = ShellAction( + name="Fetch Post via curl", + command_template="curl https://jsonplaceholder.typicode.com/posts/{}", +) + +async def get_post_title(last_result): + return json.loads(last_result).get("title", "No title found") + +post_flow = ChainedAction( + name="Fetch and Parse Post", + actions=[pick_post, fetch_post, get_post_title], + auto_inject=True, +) + +async def hello(): + print("๐Ÿ‘‹ Hello from Falyx!") + return "Hello Complete!" + +async def some_work(): + await asyncio.sleep(2) + print("Work Finished!") + return "Work Complete!" + +work_action = Action( + name="Work Action", + action=some_work, +) """ TEMPLATE_CONFIG = """\ -- key: B - description: Build the project - action: tasks.build - aliases: [build] - spinner: true +# falyx.yaml โ€” Config-driven CLI definition +# Define your commands here and point to Python callables in tasks.py +title: Sample CLI Project +prompt: + - ["#61AFEF bold", "FALYX > "] +columns: 3 +welcome_message: "๐Ÿš€ Welcome to your new CLI project!" +exit_message: "๐Ÿ‘‹ See you next time!" +commands: + - key: S + description: Say Hello + action: tasks.hello + aliases: [hi, hello] + tags: [example] -- key: T - description: Run tests - action: tasks.test - aliases: [test] - spinner: true + - key: P + description: Get Post Title + action: tasks.post_flow + aliases: [submit] + preview_before_confirm: true + confirm: true + tags: [demo, network] + + - key: G + description: Do Some Work + action: tasks.work_action + aliases: [work] + spinner: true + spinner_message: "Working..." """ GLOBAL_TEMPLATE_TASKS = """\ @@ -33,10 +91,12 @@ async def cleanup(): """ GLOBAL_CONFIG = """\ -- key: C - description: Cleanup temp files - action: tasks.cleanup - aliases: [clean, cleanup] +title: Global Falyx Config +commands: + - key: C + description: Cleanup temp files + action: tasks.cleanup + aliases: [clean, cleanup] """ console = Console(color_system="auto") @@ -56,7 +116,7 @@ def init_project(name: str = ".") -> None: tasks_path.write_text(TEMPLATE_TASKS) config_path.write_text(TEMPLATE_CONFIG) - print(f"โœ… Initialized Falyx project in {target}") + console.print(f"โœ… Initialized Falyx project in {target}") def init_global() -> None: diff --git a/falyx/io_action.py b/falyx/io_action.py index 7ec7350..2938e00 100644 --- a/falyx/io_action.py +++ b/falyx/io_action.py @@ -16,6 +16,7 @@ Common usage includes shell-like filters, input transformers, or any tool that needs to consume input from another process or pipeline. """ import asyncio +import shlex import subprocess import sys from typing import Any @@ -183,13 +184,13 @@ class ShellAction(BaseIOAction): Designed for quick integration with shell tools like `grep`, `ping`, `jq`, etc. - โš ๏ธ Warning: - Be cautious when using ShellAction with untrusted user input. Since it uses - `shell=True`, unsanitized input can lead to command injection vulnerabilities. - Avoid passing raw user input directly unless the template or use case is secure. + โš ๏ธ Security Warning: + By default, ShellAction uses `shell=True`, which can be dangerous with unsanitized input. + To mitigate this, set `safe_mode=True` to use `shell=False` with `shlex.split()`. Features: - Automatically handles input parsing (str/bytes) + - `safe_mode=True` disables shell interpretation and runs with `shell=False` - Captures stdout and stderr from shell execution - Raises on non-zero exit codes with stderr as the error - Result is returned as trimmed stdout string @@ -199,11 +200,15 @@ class ShellAction(BaseIOAction): name (str): Name of the action. command_template (str): Shell command to execute. Must include `{}` to include input. If no placeholder is present, the input is not included. + safe_mode (bool): If True, runs with `shell=False` using shlex parsing (default: False). """ - def __init__(self, name: str, command_template: str, **kwargs): + def __init__( + self, name: str, command_template: str, safe_mode: bool = False, **kwargs + ): super().__init__(name=name, **kwargs) self.command_template = command_template + self.safe_mode = safe_mode def from_input(self, raw: str | bytes) -> str: if not isinstance(raw, (str, bytes)): @@ -215,7 +220,11 @@ class ShellAction(BaseIOAction): async def _run(self, parsed_input: str) -> str: # Replace placeholder in template, or use raw input as full command command = self.command_template.format(parsed_input) - result = subprocess.run(command, shell=True, text=True, capture_output=True) + if self.safe_mode: + args = shlex.split(command) + result = subprocess.run(args, capture_output=True, text=True) + else: + result = subprocess.run(command, shell=True, text=True, capture_output=True) if result.returncode != 0: raise RuntimeError(result.stderr.strip()) return result.stdout.strip() @@ -225,14 +234,18 @@ class ShellAction(BaseIOAction): async def preview(self, parent: Tree | None = None): label = [f"[{OneColors.GREEN_b}]โš™ ShellAction[/] '{self.name}'"] + label.append(f"\n[dim]Template:[/] {self.command_template}") + label.append( + f"\n[dim]Safe mode:[/] {'Enabled' if self.safe_mode else 'Disabled'}" + ) if self.inject_last_result: label.append(f" [dim](injects '{self.inject_into}')[/dim]") - if parent: - parent.add("".join(label)) - else: - self.console.print(Tree("".join(label))) + tree = parent.add("".join(label)) if parent else Tree("".join(label)) + if not parent: + self.console.print(tree) def __str__(self): return ( - f"ShellAction(name={self.name!r}, command_template={self.command_template!r})" + f"ShellAction(name={self.name!r}, command_template={self.command_template!r}, " + f"safe_mode={self.safe_mode})" ) diff --git a/falyx/prompt_utils.py b/falyx/prompt_utils.py index 6f316b2..2a5ddd6 100644 --- a/falyx/prompt_utils.py +++ b/falyx/prompt_utils.py @@ -1,3 +1,4 @@ +# Falyx CLI Framework โ€” (c) 2025 rtj.dev LLC โ€” MIT Licensed from falyx.options_manager import OptionsManager diff --git a/falyx/protocols.py b/falyx/protocols.py index df6431b..456f237 100644 --- a/falyx/protocols.py +++ b/falyx/protocols.py @@ -1,3 +1,4 @@ +# Falyx CLI Framework โ€” (c) 2025 rtj.dev LLC โ€” MIT Licensed from __future__ import annotations from typing import Any, Protocol diff --git a/falyx/select_file_action.py b/falyx/select_file_action.py index e1ddf8b..95b58a3 100644 --- a/falyx/select_file_action.py +++ b/falyx/select_file_action.py @@ -1,3 +1,4 @@ +# Falyx CLI Framework โ€” (c) 2025 rtj.dev LLC โ€” MIT Licensed from __future__ import annotations import csv @@ -33,8 +34,30 @@ class FileReturnType(Enum): TOML = "toml" YAML = "yaml" CSV = "csv" + TSV = "tsv" XML = "xml" + @classmethod + def _get_alias(cls, value: str) -> str: + aliases = { + "yml": "yaml", + "txt": "text", + "file": "path", + "filepath": "path", + } + return aliases.get(value, value) + + @classmethod + def _missing_(cls, value: object) -> FileReturnType: + if isinstance(value, str): + normalized = value.lower() + alias = cls._get_alias(normalized) + for member in cls: + if member.value == alias: + return member + valid = ", ".join(member.value for member in cls) + raise ValueError(f"Invalid FileReturnType: '{value}'. Must be one of: {valid}") + class SelectFileAction(BaseAction): """ @@ -42,7 +65,7 @@ class SelectFileAction(BaseAction): - file content (as text, JSON, CSV, etc.) - or the file path itself. - Supported formats: text, json, yaml, toml, csv, xml. + Supported formats: text, json, yaml, toml, csv, tsv, xml. Useful for: - dynamically loading config files @@ -72,7 +95,7 @@ class SelectFileAction(BaseAction): prompt_message: str = "Choose > ", style: str = OneColors.WHITE, suffix_filter: str | None = None, - return_type: FileReturnType = FileReturnType.PATH, + return_type: FileReturnType | str = FileReturnType.PATH, console: Console | None = None, prompt_session: PromptSession | None = None, ): @@ -83,9 +106,14 @@ class SelectFileAction(BaseAction): self.prompt_message = prompt_message self.suffix_filter = suffix_filter self.style = style - self.return_type = return_type self.console = console or Console(color_system="auto") self.prompt_session = prompt_session or PromptSession() + self.return_type = self._coerce_return_type(return_type) + + def _coerce_return_type(self, return_type: FileReturnType | str) -> FileReturnType: + if isinstance(return_type, FileReturnType): + return return_type + return FileReturnType(return_type) def get_options(self, files: list[Path]) -> dict[str, SelectionOption]: value: Any @@ -106,6 +134,10 @@ class SelectFileAction(BaseAction): with open(file, newline="", encoding="UTF-8") as csvfile: reader = csv.reader(csvfile) value = list(reader) + elif self.return_type == FileReturnType.TSV: + with open(file, newline="", encoding="UTF-8") as tsvfile: + reader = csv.reader(tsvfile, delimiter="\t") + value = list(reader) elif self.return_type == FileReturnType.XML: tree = ET.parse(file, parser=ET.XMLParser(encoding="UTF-8")) root = tree.getroot() @@ -183,7 +215,7 @@ class SelectFileAction(BaseAction): if len(files) > 10: file_list.add(f"[dim]... ({len(files) - 10} more)[/]") except Exception as error: - tree.add(f"[bold red]โš ๏ธ Error scanning directory: {error}[/]") + tree.add(f"[{OneColors.DARK_RED_b}]โš ๏ธ Error scanning directory: {error}[/]") if not parent: self.console.print(tree) diff --git a/falyx/selection.py b/falyx/selection.py index 071e06a..321fa51 100644 --- a/falyx/selection.py +++ b/falyx/selection.py @@ -216,7 +216,7 @@ async def prompt_for_index( console = console or Console(color_system="auto") if show_table: - console.print(table) + console.print(table, justify="center") selection = await prompt_session.prompt_async( message=prompt_message, @@ -318,7 +318,7 @@ async def select_key_from_dict( prompt_session = prompt_session or PromptSession() console = console or Console(color_system="auto") - console.print(table) + console.print(table, justify="center") return await prompt_for_selection( selections.keys(), @@ -343,7 +343,7 @@ async def select_value_from_dict( prompt_session = prompt_session or PromptSession() console = console or Console(color_system="auto") - console.print(table) + console.print(table, justify="center") selection_key = await prompt_for_selection( selections.keys(), diff --git a/falyx/selection_action.py b/falyx/selection_action.py index bc7f30a..1482349 100644 --- a/falyx/selection_action.py +++ b/falyx/selection_action.py @@ -28,7 +28,7 @@ class SelectionAction(BaseAction): selections: list[str] | set[str] | tuple[str, ...] | dict[str, SelectionOption], *, title: str = "Select an option", - columns: int = 2, + columns: int = 5, prompt_message: str = "Select > ", default_selection: str = "", inject_last_result: bool = False, @@ -186,7 +186,7 @@ class SelectionAction(BaseAction): if len(self.selections) > 10: sub.add(f"[dim]... ({len(self.selections) - 10} more)[/]") else: - tree.add("[bold red]Invalid selections type[/]") + tree.add(f"[{OneColors.DARK_RED_b}]Invalid selections type[/]") return tree.add(f"[dim]Default:[/] '{self.default_selection or self.last_result}'") diff --git a/falyx/signal_action.py b/falyx/signal_action.py index cf14bcf..4dc36cf 100644 --- a/falyx/signal_action.py +++ b/falyx/signal_action.py @@ -1,3 +1,4 @@ +# Falyx CLI Framework โ€” (c) 2025 rtj.dev LLC โ€” MIT Licensed from falyx.action import Action from falyx.signals import FlowSignal diff --git a/falyx/version.py b/falyx/version.py index 6852ddf..9eb734d 100644 --- a/falyx/version.py +++ b/falyx/version.py @@ -1 +1 @@ -__version__ = "0.1.22" +__version__ = "0.1.23" diff --git a/pyproject.toml b/pyproject.toml index 62e5543..69e6c87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "falyx" -version = "0.1.22" +version = "0.1.23" description = "Reliable and introspectable async CLI action framework." authors = ["Roland Thomas Jr "] license = "MIT"