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