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 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
@@ -0,0 +1,9 @@
1
+ """
2
+ Entry point for ry-next when run as a module.
3
+
4
+ Usage: python -m ry_next
5
+ """
6
+ from .app import run
7
+
8
+ if __name__ == "__main__":
9
+ run()
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()