ry-tool 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ry_tool/__init__.py +27 -0
- ry_tool/__main__.py +9 -0
- ry_tool/_cli.py +244 -0
- ry_tool/app.py +420 -0
- ry_tool/context.py +297 -0
- ry_tool/executor.py +475 -0
- ry_tool/installer.py +176 -0
- ry_tool/loader.py +280 -0
- ry_tool/matcher.py +233 -0
- ry_tool/parser.py +248 -0
- ry_tool/template.py +306 -0
- ry_tool/utils.py +396 -0
- ry_tool-1.0.1.dist-info/METADATA +112 -0
- ry_tool-1.0.1.dist-info/RECORD +16 -0
- ry_tool-1.0.1.dist-info/WHEEL +4 -0
- ry_tool-1.0.1.dist-info/entry_points.txt +3 -0
ry_tool/__init__.py
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
"""
|
2
|
+
ry-next: Next generation command augmentation framework.
|
3
|
+
|
4
|
+
Clean architecture with semantic command understanding.
|
5
|
+
"""
|
6
|
+
|
7
|
+
__version__ = "2.0.0-alpha"
|
8
|
+
|
9
|
+
from .parser import CommandParser, ParsedCommand
|
10
|
+
from .executor import Executor, ExecutionResult
|
11
|
+
from .context import ExecutionContext
|
12
|
+
from .template import TemplateProcessor
|
13
|
+
from .loader import LibraryLoader, LibraryConfig
|
14
|
+
from .matcher import CommandMatcher, MatchResult
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
'CommandParser',
|
18
|
+
'ParsedCommand',
|
19
|
+
'Executor',
|
20
|
+
'ExecutionResult',
|
21
|
+
'ExecutionContext',
|
22
|
+
'TemplateProcessor',
|
23
|
+
'LibraryLoader',
|
24
|
+
'LibraryConfig',
|
25
|
+
'CommandMatcher',
|
26
|
+
'MatchResult',
|
27
|
+
]
|
ry_tool/__main__.py
ADDED
ry_tool/_cli.py
ADDED
@@ -0,0 +1,244 @@
|
|
1
|
+
"""
|
2
|
+
Lightweight CLI framework for ry.
|
3
|
+
A mini framework tailored for ry's command parsing needs.
|
4
|
+
"""
|
5
|
+
|
6
|
+
import sys
|
7
|
+
from typing import Dict, List, Callable, Optional
|
8
|
+
from dataclasses import dataclass
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class Command:
|
13
|
+
"""Represents a CLI command."""
|
14
|
+
|
15
|
+
name: str
|
16
|
+
handler: Callable
|
17
|
+
help: str
|
18
|
+
requires_arg: bool = False
|
19
|
+
arg_name: str = "arg"
|
20
|
+
arg_help: str = ""
|
21
|
+
|
22
|
+
|
23
|
+
class CLI:
|
24
|
+
"""Lightweight CLI framework for ry."""
|
25
|
+
|
26
|
+
def __init__(self, name: str = "ry", description: str = ""):
|
27
|
+
self.name = name
|
28
|
+
self.description = description
|
29
|
+
self.commands: Dict[str, Command] = {}
|
30
|
+
self.default_handler: Optional[Callable] = None
|
31
|
+
self.global_flags: Dict[str, bool] = {} # Track global flags
|
32
|
+
|
33
|
+
def command(
|
34
|
+
self,
|
35
|
+
name: str,
|
36
|
+
help: str = "",
|
37
|
+
requires_arg: bool = False,
|
38
|
+
arg_name: str = "arg",
|
39
|
+
arg_help: str = "",
|
40
|
+
):
|
41
|
+
"""Decorator to register a command."""
|
42
|
+
|
43
|
+
def decorator(func: Callable):
|
44
|
+
self.commands[name] = Command(
|
45
|
+
name=name,
|
46
|
+
handler=func,
|
47
|
+
help=help,
|
48
|
+
requires_arg=requires_arg,
|
49
|
+
arg_name=arg_name,
|
50
|
+
arg_help=arg_help,
|
51
|
+
)
|
52
|
+
return func
|
53
|
+
|
54
|
+
return decorator
|
55
|
+
|
56
|
+
def default(self, func: Callable):
|
57
|
+
"""Decorator to register the default handler for non-command arguments."""
|
58
|
+
self.default_handler = func
|
59
|
+
return func
|
60
|
+
|
61
|
+
def run(self, argv: Optional[List[str]] = None):
|
62
|
+
"""Parse arguments and run the appropriate command."""
|
63
|
+
if argv is None:
|
64
|
+
argv = sys.argv
|
65
|
+
|
66
|
+
# No arguments - show help
|
67
|
+
if len(argv) < 2:
|
68
|
+
self.show_help()
|
69
|
+
sys.exit(0)
|
70
|
+
|
71
|
+
# Parse global flags first
|
72
|
+
self.global_flags = {}
|
73
|
+
filtered_argv = [argv[0]]
|
74
|
+
|
75
|
+
for arg in argv[1:]:
|
76
|
+
if arg == '--ry-run':
|
77
|
+
self.global_flags['ry_run'] = True
|
78
|
+
else:
|
79
|
+
filtered_argv.append(arg)
|
80
|
+
|
81
|
+
# If only global flags were provided, show help
|
82
|
+
if len(filtered_argv) < 2:
|
83
|
+
self.show_help()
|
84
|
+
sys.exit(0)
|
85
|
+
|
86
|
+
first_arg = filtered_argv[1]
|
87
|
+
remaining_args = filtered_argv[2:] if len(filtered_argv) > 2 else []
|
88
|
+
|
89
|
+
# Check for help
|
90
|
+
if first_arg in ["-h", "--help"]:
|
91
|
+
self.show_help()
|
92
|
+
sys.exit(0)
|
93
|
+
|
94
|
+
# Check if it's a registered command
|
95
|
+
if first_arg in self.commands:
|
96
|
+
cmd = self.commands[first_arg]
|
97
|
+
|
98
|
+
# Check if command requires an argument
|
99
|
+
if cmd.requires_arg and not remaining_args:
|
100
|
+
print(f"Error: {first_arg} requires an argument", file=sys.stderr)
|
101
|
+
print(
|
102
|
+
f"Usage: {self.name} {first_arg} <{cmd.arg_name}>", file=sys.stderr
|
103
|
+
)
|
104
|
+
sys.exit(1)
|
105
|
+
|
106
|
+
# Call the handler
|
107
|
+
try:
|
108
|
+
if cmd.requires_arg:
|
109
|
+
result = cmd.handler(remaining_args[0], *remaining_args[1:])
|
110
|
+
else:
|
111
|
+
result = cmd.handler(*remaining_args)
|
112
|
+
|
113
|
+
# Handle result
|
114
|
+
if isinstance(result, bool):
|
115
|
+
sys.exit(0 if result else 1)
|
116
|
+
elif isinstance(result, int):
|
117
|
+
sys.exit(result)
|
118
|
+
else:
|
119
|
+
sys.exit(0)
|
120
|
+
except KeyboardInterrupt:
|
121
|
+
sys.exit(130)
|
122
|
+
except Exception as e:
|
123
|
+
print(f"Error: {e}", file=sys.stderr)
|
124
|
+
sys.exit(1)
|
125
|
+
|
126
|
+
# Not a command - try default handler
|
127
|
+
elif self.default_handler:
|
128
|
+
try:
|
129
|
+
result = self.default_handler(first_arg, *remaining_args)
|
130
|
+
if isinstance(result, bool):
|
131
|
+
sys.exit(0 if result else 1)
|
132
|
+
elif isinstance(result, int):
|
133
|
+
sys.exit(result)
|
134
|
+
else:
|
135
|
+
sys.exit(0)
|
136
|
+
except KeyboardInterrupt:
|
137
|
+
sys.exit(130)
|
138
|
+
except Exception as e:
|
139
|
+
print(f"Error: {e}", file=sys.stderr)
|
140
|
+
sys.exit(1)
|
141
|
+
|
142
|
+
else:
|
143
|
+
print(f"Unknown command: {first_arg}", file=sys.stderr)
|
144
|
+
print(f"Try: {self.name} --help", file=sys.stderr)
|
145
|
+
sys.exit(1)
|
146
|
+
|
147
|
+
def show_help(self):
|
148
|
+
"""Display auto-generated help message."""
|
149
|
+
lines = []
|
150
|
+
|
151
|
+
# Header
|
152
|
+
lines.append(f"{self.name} - {self.description}")
|
153
|
+
lines.append("")
|
154
|
+
|
155
|
+
# Usage
|
156
|
+
lines.append("Usage:")
|
157
|
+
if self.default_handler:
|
158
|
+
lines.append(
|
159
|
+
f" {self.name} <library> [args...] Execute library command"
|
160
|
+
)
|
161
|
+
lines.append(
|
162
|
+
f" {self.name} <file.yaml> [args...] Execute from YAML file"
|
163
|
+
)
|
164
|
+
lines.append(
|
165
|
+
f" {self.name} --ry-run <library> [args...] Show execution plan"
|
166
|
+
)
|
167
|
+
lines.append("")
|
168
|
+
|
169
|
+
# Group commands by type
|
170
|
+
user_commands = {}
|
171
|
+
dev_commands = {}
|
172
|
+
|
173
|
+
for name, cmd in sorted(self.commands.items()):
|
174
|
+
if name.startswith("--dev-"):
|
175
|
+
dev_commands[name] = cmd
|
176
|
+
else:
|
177
|
+
user_commands[name] = cmd
|
178
|
+
|
179
|
+
# User commands
|
180
|
+
if user_commands:
|
181
|
+
lines.append("Package Management:")
|
182
|
+
for name, cmd in user_commands.items():
|
183
|
+
# Format command line
|
184
|
+
if cmd.requires_arg:
|
185
|
+
usage = f"{self.name} {name} <{cmd.arg_name}>"
|
186
|
+
else:
|
187
|
+
usage = f"{self.name} {name}"
|
188
|
+
# Align help text
|
189
|
+
lines.append(f" {usage:<40} {cmd.help}")
|
190
|
+
lines.append("")
|
191
|
+
|
192
|
+
# Developer commands
|
193
|
+
if dev_commands:
|
194
|
+
lines.append("Developer Commands:")
|
195
|
+
for name, cmd in dev_commands.items():
|
196
|
+
if cmd.requires_arg:
|
197
|
+
usage = f"{self.name} {name} <{cmd.arg_name}>"
|
198
|
+
else:
|
199
|
+
usage = f"{self.name} {name}"
|
200
|
+
lines.append(f" {usage:<40} {cmd.help}")
|
201
|
+
lines.append("")
|
202
|
+
|
203
|
+
# Examples
|
204
|
+
lines.append("Examples:")
|
205
|
+
lines.append(f" {self.name} hello.yaml world --name Alice")
|
206
|
+
lines.append(f" {self.name} git commit -m 'feat: add feature'")
|
207
|
+
lines.append(f" {self.name} --ry-run deploy.yaml production")
|
208
|
+
lines.append(f" {self.name} --list")
|
209
|
+
|
210
|
+
print("\n".join(lines))
|
211
|
+
|
212
|
+
|
213
|
+
# Utility decorators for common CLI patterns
|
214
|
+
def requires_git_repo(func):
|
215
|
+
"""Decorator to ensure command runs in a git repository."""
|
216
|
+
from functools import wraps
|
217
|
+
|
218
|
+
@wraps(func)
|
219
|
+
def wrapper(*args, **kwargs):
|
220
|
+
import subprocess
|
221
|
+
try:
|
222
|
+
subprocess.run(['git', 'rev-parse', '--git-dir'],
|
223
|
+
capture_output=True, check=True)
|
224
|
+
return func(*args, **kwargs)
|
225
|
+
except subprocess.CalledProcessError:
|
226
|
+
print("Error: Not in a git repository", file=sys.stderr)
|
227
|
+
return 1
|
228
|
+
return wrapper
|
229
|
+
|
230
|
+
|
231
|
+
def requires_file(file_path: str):
|
232
|
+
"""Decorator to ensure required file exists."""
|
233
|
+
def decorator(func):
|
234
|
+
from functools import wraps
|
235
|
+
|
236
|
+
@wraps(func)
|
237
|
+
def wrapper(*args, **kwargs):
|
238
|
+
from pathlib import Path
|
239
|
+
if not Path(file_path).exists():
|
240
|
+
print(f"Error: Required file not found: {file_path}", file=sys.stderr)
|
241
|
+
return 1
|
242
|
+
return func(*args, **kwargs)
|
243
|
+
return wrapper
|
244
|
+
return decorator
|
ry_tool/app.py
ADDED
@@ -0,0 +1,420 @@
|
|
1
|
+
"""
|
2
|
+
Main application for ry-next.
|
3
|
+
|
4
|
+
Purpose: Wire together all core modules to execute augmented commands.
|
5
|
+
No package management, just pure command augmentation.
|
6
|
+
"""
|
7
|
+
import sys
|
8
|
+
import os
|
9
|
+
import tempfile
|
10
|
+
import hashlib
|
11
|
+
from pathlib import Path
|
12
|
+
|
13
|
+
from ._cli import CLI
|
14
|
+
from .parser import CommandParser
|
15
|
+
from .loader import LibraryLoader
|
16
|
+
from .matcher import CommandMatcher
|
17
|
+
from .executor import Executor
|
18
|
+
from .template import TemplateProcessor
|
19
|
+
from .installer import LibraryInstaller
|
20
|
+
from .utils import ContextFactory
|
21
|
+
|
22
|
+
|
23
|
+
# Create the CLI app
|
24
|
+
app = CLI(name="ry-next", description="Next generation command augmentation")
|
25
|
+
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
# Note: --ry-run is handled as a global flag in CLI
|
30
|
+
|
31
|
+
|
32
|
+
@app.command("--version", help="Show version")
|
33
|
+
def show_version():
|
34
|
+
"""Show ry-next version."""
|
35
|
+
from . import __version__
|
36
|
+
print(f"ry-next {__version__}")
|
37
|
+
return True
|
38
|
+
|
39
|
+
|
40
|
+
@app.command("--list", help="List available libraries")
|
41
|
+
def list_libraries(installed: bool = False, verbose: bool = False):
|
42
|
+
"""List libraries with source information."""
|
43
|
+
loader = LibraryLoader()
|
44
|
+
|
45
|
+
if installed:
|
46
|
+
# Show only installed libraries
|
47
|
+
installer = LibraryInstaller()
|
48
|
+
installed_libs = installer.list_installed()
|
49
|
+
if installed_libs:
|
50
|
+
print("Installed libraries:")
|
51
|
+
for name, info in installed_libs.items():
|
52
|
+
print(f" • {name} (v{info.get('version', '0.0.0')})")
|
53
|
+
else:
|
54
|
+
print("No libraries installed")
|
55
|
+
return True
|
56
|
+
|
57
|
+
# Show all available libraries
|
58
|
+
if verbose:
|
59
|
+
print("Available libraries by source:")
|
60
|
+
for path in loader.library_paths:
|
61
|
+
libs = loader.list_from_path(path)
|
62
|
+
if libs:
|
63
|
+
# Identify source type
|
64
|
+
if "docs_next" in str(path):
|
65
|
+
source = "📁 workspace"
|
66
|
+
elif ".local/share" in str(path):
|
67
|
+
source = "👤 user"
|
68
|
+
elif ".cache" in str(path):
|
69
|
+
source = "🌐 cached"
|
70
|
+
else:
|
71
|
+
source = "📦 system"
|
72
|
+
|
73
|
+
print(f"\n{source} ({path}):")
|
74
|
+
for lib in libs:
|
75
|
+
print(f" • {lib}")
|
76
|
+
else:
|
77
|
+
libraries = loader.list_available()
|
78
|
+
if libraries:
|
79
|
+
print("Available libraries:")
|
80
|
+
for lib in libraries:
|
81
|
+
print(f" • {lib}")
|
82
|
+
else:
|
83
|
+
print("No libraries found")
|
84
|
+
print("Add libraries to:")
|
85
|
+
print(" • ./docs_next/libraries/")
|
86
|
+
print(" • ~/.local/share/ry-next/libraries/")
|
87
|
+
|
88
|
+
return True
|
89
|
+
|
90
|
+
|
91
|
+
@app.command("--install", help="Install a library")
|
92
|
+
def install_library(library_name: str):
|
93
|
+
"""
|
94
|
+
Install library from registry or local source.
|
95
|
+
|
96
|
+
Examples:
|
97
|
+
ry-next --install git # From registry
|
98
|
+
ry-next --install ./my-lib.yaml # From local file
|
99
|
+
"""
|
100
|
+
installer = LibraryInstaller()
|
101
|
+
loader = LibraryLoader()
|
102
|
+
|
103
|
+
try:
|
104
|
+
if library_name.endswith('.yaml'):
|
105
|
+
# Install from local file
|
106
|
+
library = loader.load_file(Path(library_name))
|
107
|
+
if installer.install_local(library):
|
108
|
+
print(f"✅ Installed {library.name} from {library_name}")
|
109
|
+
else:
|
110
|
+
print(f"❌ Failed to install {library_name}")
|
111
|
+
return False
|
112
|
+
else:
|
113
|
+
# Install from registry/available sources
|
114
|
+
if installer.install_from_registry(library_name):
|
115
|
+
print(f"✅ Installed {library_name}")
|
116
|
+
else:
|
117
|
+
return False
|
118
|
+
except Exception as e:
|
119
|
+
print(f"❌ Installation failed: {e}")
|
120
|
+
return False
|
121
|
+
|
122
|
+
return True
|
123
|
+
|
124
|
+
@app.command("--uninstall", help="Uninstall a library")
|
125
|
+
def uninstall_library(library_name: str):
|
126
|
+
"""Uninstall an installed library."""
|
127
|
+
installer = LibraryInstaller()
|
128
|
+
return installer.uninstall(library_name)
|
129
|
+
|
130
|
+
# Default handler for library execution
|
131
|
+
@app.default
|
132
|
+
def execute_library(library_name: str, *args):
|
133
|
+
"""
|
134
|
+
Execute a library command.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
library_name: Name of library or path to YAML file
|
138
|
+
args: Command and arguments to execute
|
139
|
+
"""
|
140
|
+
# Check for --ry-run flag from CLI global flags
|
141
|
+
ry_run = app.global_flags.get('ry_run', False)
|
142
|
+
|
143
|
+
# Check for ry-help flag early (avoid conflict with native --help)
|
144
|
+
if '--ry-help' in args:
|
145
|
+
# Remove help flag from args
|
146
|
+
args = [a for a in args if a != '--ry-help']
|
147
|
+
show_help = True
|
148
|
+
else:
|
149
|
+
show_help = False
|
150
|
+
|
151
|
+
# Initialize components
|
152
|
+
parser = CommandParser()
|
153
|
+
loader = LibraryLoader()
|
154
|
+
matcher = CommandMatcher()
|
155
|
+
|
156
|
+
# Load library
|
157
|
+
try:
|
158
|
+
if library_name.endswith('.yaml'):
|
159
|
+
# Direct YAML file
|
160
|
+
library = loader.load_file(Path(library_name))
|
161
|
+
else:
|
162
|
+
# Library by name
|
163
|
+
library = loader.load(library_name)
|
164
|
+
except FileNotFoundError:
|
165
|
+
print(f"Library not found: {library_name}", file=sys.stderr)
|
166
|
+
print(f"Available libraries: {', '.join(loader.list_available())}", file=sys.stderr)
|
167
|
+
return False
|
168
|
+
except Exception as e:
|
169
|
+
print(f"Failed to load library: {e}", file=sys.stderr)
|
170
|
+
return False
|
171
|
+
|
172
|
+
# Handle help request
|
173
|
+
if show_help:
|
174
|
+
help_text = parser.generate_help(library, args[0] if args else None)
|
175
|
+
print(help_text)
|
176
|
+
return True
|
177
|
+
|
178
|
+
# Setup library environment using factory
|
179
|
+
library_env = ContextFactory.create_library_env(library)
|
180
|
+
|
181
|
+
# Parse command
|
182
|
+
parsed = parser.parse_with_command_schema(
|
183
|
+
list(args),
|
184
|
+
library.commands.get(args[0] if args else '', {})
|
185
|
+
)
|
186
|
+
|
187
|
+
# Match to handler
|
188
|
+
match_result = matcher.match(parsed, library)
|
189
|
+
|
190
|
+
if not match_result.matched:
|
191
|
+
print(f"No matching command: {match_result.reason}", file=sys.stderr)
|
192
|
+
# Show available commands
|
193
|
+
if library.commands:
|
194
|
+
print(f"Available commands in {library.name}:", file=sys.stderr)
|
195
|
+
for cmd in library.commands.keys():
|
196
|
+
if cmd != '*': # Skip catch-all
|
197
|
+
print(f" • {cmd}", file=sys.stderr)
|
198
|
+
return False
|
199
|
+
|
200
|
+
# Check if this is an augmentation/relay command
|
201
|
+
handler = match_result.handler
|
202
|
+
|
203
|
+
# Create executor with library environment
|
204
|
+
executor_context = {**match_result.context.to_dict(), 'env': library_env}
|
205
|
+
executor = Executor(context=executor_context)
|
206
|
+
|
207
|
+
# Handle relay/augmentation mode
|
208
|
+
# Check for relay in handler directly OR in augment section (for conditional handlers)
|
209
|
+
has_relay = ('relay' in handler and handler['relay'] == 'native') or \
|
210
|
+
('augment' in handler and 'relay' in handler['augment'] and handler['augment']['relay'] == 'native')
|
211
|
+
|
212
|
+
if has_relay:
|
213
|
+
# This is an augmentation command
|
214
|
+
target = library.target or parsed.command
|
215
|
+
|
216
|
+
# Execute before hooks if present
|
217
|
+
if 'augment' in handler and 'before' in handler['augment']:
|
218
|
+
template_processor = TemplateProcessor(match_result.context)
|
219
|
+
before_steps = template_processor.process_recursive(handler['augment']['before'])
|
220
|
+
|
221
|
+
if ry_run:
|
222
|
+
print("# Before hooks:")
|
223
|
+
for step in before_steps:
|
224
|
+
print(f" {step}")
|
225
|
+
else:
|
226
|
+
for step in before_steps:
|
227
|
+
# Handle special directives
|
228
|
+
if 'require' in step:
|
229
|
+
# Check requirement
|
230
|
+
value = match_result.context.get(step['require'])
|
231
|
+
if not value:
|
232
|
+
error = step.get('error', f"Requirement not met: {step['require']}")
|
233
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
234
|
+
return False
|
235
|
+
continue
|
236
|
+
|
237
|
+
if 'error' in step:
|
238
|
+
# Show error and exit
|
239
|
+
print(f"ERROR: {step['error']}", file=sys.stderr)
|
240
|
+
return False
|
241
|
+
|
242
|
+
# Normal execution
|
243
|
+
result = executor.execute_step(step, library_env)
|
244
|
+
|
245
|
+
# Apply any modifications from the step
|
246
|
+
if result.modifications:
|
247
|
+
match_result.context.apply_modifications(result.modifications)
|
248
|
+
|
249
|
+
# Show output from before hooks
|
250
|
+
if result.stderr:
|
251
|
+
print(result.stderr, end='', file=sys.stderr)
|
252
|
+
if result.stdout:
|
253
|
+
print(result.stdout, end='')
|
254
|
+
if not result.success:
|
255
|
+
return False
|
256
|
+
|
257
|
+
# Relay to native command
|
258
|
+
if ry_run:
|
259
|
+
print(f"# Relay to: {target} {' '.join(match_result.context.remaining_args)}")
|
260
|
+
else:
|
261
|
+
result = executor.execute_relay(target, match_result.context.remaining_args, library_env)
|
262
|
+
if not result.success:
|
263
|
+
return False
|
264
|
+
|
265
|
+
# Execute after hooks if present
|
266
|
+
if 'augment' in handler and 'after' in handler['augment']:
|
267
|
+
template_processor = TemplateProcessor(match_result.context)
|
268
|
+
after_steps = template_processor.process_recursive(handler['augment']['after'])
|
269
|
+
|
270
|
+
if ry_run:
|
271
|
+
print("# After hooks:")
|
272
|
+
for step in after_steps:
|
273
|
+
print(f" {step}")
|
274
|
+
else:
|
275
|
+
for step in after_steps:
|
276
|
+
# Handle special directives
|
277
|
+
if 'require' in step:
|
278
|
+
value = match_result.context.get(step['require'])
|
279
|
+
if not value:
|
280
|
+
error = step.get('error', f"Requirement not met: {step['require']}")
|
281
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
282
|
+
return False
|
283
|
+
continue
|
284
|
+
|
285
|
+
if 'error' in step:
|
286
|
+
print(f"ERROR: {step['error']}", file=sys.stderr)
|
287
|
+
return False
|
288
|
+
|
289
|
+
# Normal execution
|
290
|
+
result = executor.execute_step(step, library_env)
|
291
|
+
|
292
|
+
# Apply any modifications from the step
|
293
|
+
if result.modifications:
|
294
|
+
match_result.context.apply_modifications(result.modifications)
|
295
|
+
|
296
|
+
# Show output from after hooks (typically to stderr for info messages)
|
297
|
+
if result.stderr:
|
298
|
+
print(result.stderr, end='', file=sys.stderr)
|
299
|
+
if result.stdout:
|
300
|
+
print(result.stdout, end='')
|
301
|
+
if not result.success:
|
302
|
+
return False
|
303
|
+
|
304
|
+
return True
|
305
|
+
|
306
|
+
# Normal execution mode (not augmentation)
|
307
|
+
# Get execution steps
|
308
|
+
steps = matcher.get_execution_plan(match_result)
|
309
|
+
|
310
|
+
# Process templates in steps
|
311
|
+
template_processor = TemplateProcessor(match_result.context)
|
312
|
+
processed_steps = template_processor.process_recursive(steps)
|
313
|
+
|
314
|
+
if ry_run:
|
315
|
+
# Show execution plan
|
316
|
+
plan = executor.show_execution_plan(processed_steps)
|
317
|
+
print(plan)
|
318
|
+
return True
|
319
|
+
|
320
|
+
# Execute steps
|
321
|
+
for i, step in enumerate(processed_steps):
|
322
|
+
try:
|
323
|
+
# Handle special directives
|
324
|
+
if 'require' in step:
|
325
|
+
# Check requirement
|
326
|
+
value = match_result.context.get(step['require'])
|
327
|
+
if not value:
|
328
|
+
error = step.get('error', f"Requirement not met: {step['require']}")
|
329
|
+
print(f"ERROR: {error}", file=sys.stderr)
|
330
|
+
return False
|
331
|
+
continue
|
332
|
+
|
333
|
+
if 'capture' in step:
|
334
|
+
# Execute and capture variable
|
335
|
+
var_name = step['capture']
|
336
|
+
exec_step = {k: v for k, v in step.items() if k != 'capture'}
|
337
|
+
|
338
|
+
# Handle interactive capture with tempfile
|
339
|
+
if exec_step.get('interactive'):
|
340
|
+
# Create temp file with hash-based name
|
341
|
+
temp_dir = tempfile.gettempdir()
|
342
|
+
hash_name = hashlib.md5(f"ry-{var_name}-{i}".encode()).hexdigest()[:12]
|
343
|
+
capture_file = os.path.join(temp_dir, f"ry-capture-{hash_name}")
|
344
|
+
exec_step['_capture_file'] = capture_file
|
345
|
+
|
346
|
+
# Execute with TTY preserved
|
347
|
+
result = executor.execute_step(exec_step, library_env)
|
348
|
+
|
349
|
+
if result.success and os.path.exists(capture_file):
|
350
|
+
# Read captured value from file
|
351
|
+
with open(capture_file, 'r') as f:
|
352
|
+
value = f.read().strip()
|
353
|
+
os.unlink(capture_file) # Clean up
|
354
|
+
|
355
|
+
match_result.context.set(f'captured.{var_name}', value)
|
356
|
+
executor.context['captured'][var_name] = value
|
357
|
+
else:
|
358
|
+
print(f"Failed to capture {var_name} (interactive)", file=sys.stderr)
|
359
|
+
if not os.path.exists(capture_file):
|
360
|
+
print(f"Capture file not created: {capture_file}", file=sys.stderr)
|
361
|
+
return False
|
362
|
+
else:
|
363
|
+
# Normal capture from stdout
|
364
|
+
result = executor.execute_step(exec_step, library_env)
|
365
|
+
if result.success:
|
366
|
+
value = result.stdout.strip()
|
367
|
+
match_result.context.set(f'captured.{var_name}', value)
|
368
|
+
executor.context['captured'][var_name] = value
|
369
|
+
else:
|
370
|
+
print(f"Failed to capture {var_name}", file=sys.stderr)
|
371
|
+
if result.stderr:
|
372
|
+
print(result.stderr, file=sys.stderr)
|
373
|
+
return False
|
374
|
+
|
375
|
+
# Re-process remaining steps with updated context
|
376
|
+
if i + 1 < len(processed_steps):
|
377
|
+
template_processor = TemplateProcessor(match_result.context)
|
378
|
+
processed_steps[i+1:] = template_processor.process_recursive(steps[i+1:])
|
379
|
+
|
380
|
+
continue
|
381
|
+
|
382
|
+
if 'error' in step:
|
383
|
+
# Show error and exit
|
384
|
+
print(f"ERROR: {step['error']}", file=sys.stderr)
|
385
|
+
return False
|
386
|
+
|
387
|
+
# Normal execution (may be interactive without capture)
|
388
|
+
result = executor.execute_step(step, library_env)
|
389
|
+
|
390
|
+
# Apply any modifications from the step
|
391
|
+
if result.modifications:
|
392
|
+
match_result.context.apply_modifications(result.modifications)
|
393
|
+
# Re-create template processor with updated context for remaining steps
|
394
|
+
template_processor = TemplateProcessor(match_result.context)
|
395
|
+
|
396
|
+
# Handle output (skip for interactive mode as it goes directly to TTY)
|
397
|
+
if not step.get('interactive'):
|
398
|
+
if result.stdout:
|
399
|
+
print(result.stdout, end='')
|
400
|
+
if result.stderr:
|
401
|
+
print(result.stderr, end='', file=sys.stderr)
|
402
|
+
|
403
|
+
# Check for failure
|
404
|
+
if not result.success:
|
405
|
+
return False
|
406
|
+
|
407
|
+
except Exception as e:
|
408
|
+
print(f"Execution error: {e}", file=sys.stderr)
|
409
|
+
return False
|
410
|
+
|
411
|
+
return True
|
412
|
+
|
413
|
+
|
414
|
+
def run():
|
415
|
+
"""Entry point for the CLI."""
|
416
|
+
app.run()
|
417
|
+
|
418
|
+
|
419
|
+
if __name__ == "__main__":
|
420
|
+
run()
|