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/executor.py
ADDED
@@ -0,0 +1,475 @@
|
|
1
|
+
"""
|
2
|
+
Command executor with multiple language support.
|
3
|
+
|
4
|
+
Purpose: Execute commands via subprocess without shell escaping issues.
|
5
|
+
Supports shell, python, subprocess, and other language execution.
|
6
|
+
No YAML knowledge, just pure execution.
|
7
|
+
"""
|
8
|
+
import subprocess
|
9
|
+
import sys
|
10
|
+
import os
|
11
|
+
import json
|
12
|
+
from typing import Dict, Any, List, Optional
|
13
|
+
from dataclasses import dataclass
|
14
|
+
from io import StringIO
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class ExecutionResult:
|
19
|
+
"""Result of executing a command."""
|
20
|
+
success: bool
|
21
|
+
stdout: str = ""
|
22
|
+
stderr: str = ""
|
23
|
+
returncode: int = 0
|
24
|
+
captured_var: Optional[str] = None
|
25
|
+
modifications: Optional[Dict[str, Any]] = None
|
26
|
+
|
27
|
+
|
28
|
+
class Executor:
|
29
|
+
"""
|
30
|
+
Execute commands in various modes without shell escaping issues.
|
31
|
+
|
32
|
+
Key innovation: Pass strings directly to subprocess, no encoding needed.
|
33
|
+
"""
|
34
|
+
|
35
|
+
def __init__(self, context: Any = None):
|
36
|
+
"""
|
37
|
+
Initialize executor with execution context.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
context: ExecutionContext object or dict with variables
|
41
|
+
"""
|
42
|
+
# Accept both ExecutionContext objects and dicts
|
43
|
+
if hasattr(context, 'to_dict'):
|
44
|
+
# It's an ExecutionContext, convert to dict for internal use
|
45
|
+
self.context = context.to_dict()
|
46
|
+
else:
|
47
|
+
self.context = context or {}
|
48
|
+
self.dry_run = False
|
49
|
+
self.debug = False
|
50
|
+
|
51
|
+
def execute_step(self, step: Dict[str, Any], extra_env: Dict[str, str] = None) -> ExecutionResult:
|
52
|
+
"""
|
53
|
+
Execute a single step based on its type.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
step: Step definition with type and code/command
|
57
|
+
extra_env: Additional environment variables
|
58
|
+
|
59
|
+
Returns:
|
60
|
+
ExecutionResult
|
61
|
+
"""
|
62
|
+
# Merge environment
|
63
|
+
env = step.get('env', {})
|
64
|
+
if extra_env:
|
65
|
+
env.update(extra_env)
|
66
|
+
|
67
|
+
if 'shell' in step:
|
68
|
+
return self.execute_shell(step['shell'], env,
|
69
|
+
interactive=step.get('interactive', False),
|
70
|
+
capture_file=step.get('_capture_file'))
|
71
|
+
elif 'python' in step:
|
72
|
+
return self.execute_python(step['python'])
|
73
|
+
elif 'subprocess' in step:
|
74
|
+
return self.execute_subprocess(step['subprocess'])
|
75
|
+
elif 'ruby' in step:
|
76
|
+
return self.execute_ruby(step['ruby'])
|
77
|
+
else:
|
78
|
+
raise ValueError(f"Unknown step type: {step.keys()}")
|
79
|
+
|
80
|
+
def execute_shell(self, command: str, env: Dict[str, str] = None,
|
81
|
+
interactive: bool = False, capture_file: str = None) -> ExecutionResult:
|
82
|
+
"""
|
83
|
+
Execute shell command.
|
84
|
+
|
85
|
+
Args:
|
86
|
+
command: Shell command to execute
|
87
|
+
env: Additional environment variables
|
88
|
+
interactive: If True, preserve TTY for interactive tools
|
89
|
+
capture_file: If provided with interactive, redirect output here
|
90
|
+
"""
|
91
|
+
if self.dry_run:
|
92
|
+
return ExecutionResult(
|
93
|
+
success=True,
|
94
|
+
stdout=f"[DRY RUN] Would execute: {command}"
|
95
|
+
)
|
96
|
+
|
97
|
+
# Merge environment
|
98
|
+
exec_env = os.environ.copy()
|
99
|
+
if env:
|
100
|
+
exec_env.update(env)
|
101
|
+
|
102
|
+
# Add context vars to environment
|
103
|
+
for key, value in self.context.items():
|
104
|
+
if isinstance(value, (str, int, float, bool)):
|
105
|
+
exec_env[f'RY_{key.upper()}'] = str(value)
|
106
|
+
|
107
|
+
try:
|
108
|
+
if interactive:
|
109
|
+
# Interactive mode - preserve TTY
|
110
|
+
if capture_file:
|
111
|
+
# Pass capture file as environment variable
|
112
|
+
exec_env['RY_CAPTURE_FILE'] = capture_file
|
113
|
+
|
114
|
+
# Run WITHOUT capture_output to preserve TTY
|
115
|
+
result = subprocess.run(
|
116
|
+
command,
|
117
|
+
shell=True,
|
118
|
+
env=exec_env
|
119
|
+
)
|
120
|
+
|
121
|
+
return ExecutionResult(
|
122
|
+
success=result.returncode == 0,
|
123
|
+
stdout="", # Empty - output went to TTY or file
|
124
|
+
stderr="",
|
125
|
+
returncode=result.returncode
|
126
|
+
)
|
127
|
+
else:
|
128
|
+
# Normal mode with capture
|
129
|
+
result = subprocess.run(
|
130
|
+
command,
|
131
|
+
shell=True,
|
132
|
+
capture_output=True,
|
133
|
+
text=True,
|
134
|
+
env=exec_env
|
135
|
+
)
|
136
|
+
|
137
|
+
return ExecutionResult(
|
138
|
+
success=result.returncode == 0,
|
139
|
+
stdout=result.stdout,
|
140
|
+
stderr=result.stderr,
|
141
|
+
returncode=result.returncode
|
142
|
+
)
|
143
|
+
except Exception as e:
|
144
|
+
return ExecutionResult(
|
145
|
+
success=False,
|
146
|
+
stderr=str(e),
|
147
|
+
returncode=1
|
148
|
+
)
|
149
|
+
|
150
|
+
def execute_relay(self, target: str, args: List[str], env: Dict[str, str] = None) -> ExecutionResult:
|
151
|
+
"""
|
152
|
+
Relay execution to native command.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
target: Target binary path (e.g., /usr/bin/git)
|
156
|
+
args: Command arguments to pass
|
157
|
+
env: Additional environment variables
|
158
|
+
|
159
|
+
Returns:
|
160
|
+
ExecutionResult
|
161
|
+
"""
|
162
|
+
if self.dry_run:
|
163
|
+
return ExecutionResult(
|
164
|
+
success=True,
|
165
|
+
stdout=f"[DRY RUN] Would relay to: {target} {' '.join(args)}"
|
166
|
+
)
|
167
|
+
|
168
|
+
# Merge environment
|
169
|
+
exec_env = os.environ.copy()
|
170
|
+
if env:
|
171
|
+
exec_env.update(env)
|
172
|
+
|
173
|
+
# Add context vars to environment
|
174
|
+
for key, value in self.context.items():
|
175
|
+
if isinstance(value, (str, int, float, bool)):
|
176
|
+
exec_env[f'RY_{key.upper()}'] = str(value)
|
177
|
+
|
178
|
+
try:
|
179
|
+
# Direct execution without shell, preserves TTY
|
180
|
+
result = subprocess.run(
|
181
|
+
[target] + args,
|
182
|
+
env=exec_env,
|
183
|
+
capture_output=False, # Direct TTY passthrough
|
184
|
+
text=True
|
185
|
+
)
|
186
|
+
|
187
|
+
return ExecutionResult(
|
188
|
+
success=result.returncode == 0,
|
189
|
+
stdout="", # Output went directly to TTY
|
190
|
+
stderr="",
|
191
|
+
returncode=result.returncode
|
192
|
+
)
|
193
|
+
except FileNotFoundError:
|
194
|
+
return ExecutionResult(
|
195
|
+
success=False,
|
196
|
+
stderr=f"Command not found: {target}",
|
197
|
+
returncode=127
|
198
|
+
)
|
199
|
+
except Exception as e:
|
200
|
+
return ExecutionResult(
|
201
|
+
success=False,
|
202
|
+
stderr=str(e),
|
203
|
+
returncode=1
|
204
|
+
)
|
205
|
+
|
206
|
+
def execute_python(self, code: str) -> ExecutionResult:
|
207
|
+
"""
|
208
|
+
Execute Python code with context.
|
209
|
+
|
210
|
+
Code has access to context variables and can modify them.
|
211
|
+
Modifications are tracked and returned in the ExecutionResult.
|
212
|
+
"""
|
213
|
+
if self.dry_run:
|
214
|
+
return ExecutionResult(
|
215
|
+
success=True,
|
216
|
+
stdout="[DRY RUN] Would execute Python code"
|
217
|
+
)
|
218
|
+
|
219
|
+
# Helper to safely copy dict/list values
|
220
|
+
def safe_copy(value, default):
|
221
|
+
if isinstance(value, dict):
|
222
|
+
return value.copy()
|
223
|
+
elif isinstance(value, list):
|
224
|
+
return value.copy()
|
225
|
+
return default
|
226
|
+
|
227
|
+
# Store original values to detect modifications
|
228
|
+
original_flags = safe_copy(self.context.get('flags'), {})
|
229
|
+
original_arguments = safe_copy(self.context.get('arguments'), {})
|
230
|
+
original_env = safe_copy(self.context.get('env'), {})
|
231
|
+
original_captured = safe_copy(self.context.get('captured'), {})
|
232
|
+
|
233
|
+
# Prepare execution environment with mutable references
|
234
|
+
exec_globals = {
|
235
|
+
'sys': sys,
|
236
|
+
'os': os,
|
237
|
+
'subprocess': subprocess,
|
238
|
+
'json': json,
|
239
|
+
'flags': safe_copy(self.context.get('flags'), {}),
|
240
|
+
'arguments': safe_copy(self.context.get('arguments'), {}),
|
241
|
+
'env': safe_copy(self.context.get('env'), {}),
|
242
|
+
'captured': safe_copy(self.context.get('captured'), {}),
|
243
|
+
'remaining_args': self.context.get('remaining_args', []),
|
244
|
+
'positionals': safe_copy(self.context.get('positionals'), []),
|
245
|
+
'remaining': safe_copy(self.context.get('remaining'), []),
|
246
|
+
'context': self.context
|
247
|
+
}
|
248
|
+
|
249
|
+
# Capture stdout/stderr
|
250
|
+
old_stdout = sys.stdout
|
251
|
+
old_stderr = sys.stderr
|
252
|
+
stdout_capture = StringIO()
|
253
|
+
stderr_capture = StringIO()
|
254
|
+
|
255
|
+
try:
|
256
|
+
sys.stdout = stdout_capture
|
257
|
+
sys.stderr = stderr_capture
|
258
|
+
|
259
|
+
exec(code, exec_globals)
|
260
|
+
|
261
|
+
# Track all modifications
|
262
|
+
modifications = {}
|
263
|
+
|
264
|
+
# Check for flag modifications
|
265
|
+
if 'flags' in exec_globals and exec_globals['flags'] != original_flags:
|
266
|
+
modifications['flags'] = exec_globals['flags']
|
267
|
+
|
268
|
+
# Check for argument modifications
|
269
|
+
if 'arguments' in exec_globals and exec_globals['arguments'] != original_arguments:
|
270
|
+
modifications['arguments'] = exec_globals['arguments']
|
271
|
+
|
272
|
+
# Check for environment modifications
|
273
|
+
if 'env' in exec_globals and exec_globals['env'] != original_env:
|
274
|
+
modifications['env'] = exec_globals['env']
|
275
|
+
|
276
|
+
# Check for captured variable modifications
|
277
|
+
if 'captured' in exec_globals and exec_globals['captured'] != original_captured:
|
278
|
+
modifications['captured'] = exec_globals['captured']
|
279
|
+
|
280
|
+
# Check for positionals modifications
|
281
|
+
if 'positionals' in exec_globals and exec_globals['positionals'] != self.context.get('positionals', []):
|
282
|
+
modifications['positionals'] = exec_globals['positionals']
|
283
|
+
|
284
|
+
# Check for remaining modifications
|
285
|
+
if 'remaining' in exec_globals and exec_globals['remaining'] != self.context.get('remaining', []):
|
286
|
+
modifications['remaining'] = exec_globals['remaining']
|
287
|
+
|
288
|
+
return ExecutionResult(
|
289
|
+
success=True,
|
290
|
+
stdout=stdout_capture.getvalue(),
|
291
|
+
stderr=stderr_capture.getvalue(),
|
292
|
+
returncode=0,
|
293
|
+
modifications=modifications if modifications else None
|
294
|
+
)
|
295
|
+
except SystemExit as e:
|
296
|
+
# Even on exit, track modifications
|
297
|
+
modifications = {}
|
298
|
+
if 'flags' in exec_globals and exec_globals['flags'] != original_flags:
|
299
|
+
modifications['flags'] = exec_globals['flags']
|
300
|
+
if 'arguments' in exec_globals and exec_globals['arguments'] != original_arguments:
|
301
|
+
modifications['arguments'] = exec_globals['arguments']
|
302
|
+
if 'env' in exec_globals and exec_globals['env'] != original_env:
|
303
|
+
modifications['env'] = exec_globals['env']
|
304
|
+
if 'captured' in exec_globals and exec_globals['captured'] != original_captured:
|
305
|
+
modifications['captured'] = exec_globals['captured']
|
306
|
+
|
307
|
+
return ExecutionResult(
|
308
|
+
success=e.code == 0,
|
309
|
+
stdout=stdout_capture.getvalue(),
|
310
|
+
stderr=stderr_capture.getvalue(),
|
311
|
+
returncode=e.code or 0,
|
312
|
+
modifications=modifications if modifications else None
|
313
|
+
)
|
314
|
+
except Exception as e:
|
315
|
+
return ExecutionResult(
|
316
|
+
success=False,
|
317
|
+
stdout=stdout_capture.getvalue(),
|
318
|
+
stderr=stderr_capture.getvalue() + str(e),
|
319
|
+
returncode=1,
|
320
|
+
modifications=None
|
321
|
+
)
|
322
|
+
finally:
|
323
|
+
sys.stdout = old_stdout
|
324
|
+
sys.stderr = old_stderr
|
325
|
+
|
326
|
+
def execute_subprocess(self, config: Dict[str, Any]) -> ExecutionResult:
|
327
|
+
"""
|
328
|
+
Execute with fine-grained subprocess control.
|
329
|
+
|
330
|
+
Args:
|
331
|
+
config: {
|
332
|
+
'cmd': ['git', 'commit', '-m', 'message'],
|
333
|
+
'env': {'KEY': 'value'},
|
334
|
+
'cwd': '/path/to/dir',
|
335
|
+
'stdin': 'input data',
|
336
|
+
'capture': 'stdout' # or 'stderr' or 'both'
|
337
|
+
}
|
338
|
+
"""
|
339
|
+
if self.dry_run:
|
340
|
+
return ExecutionResult(
|
341
|
+
success=True,
|
342
|
+
stdout=f"[DRY RUN] Would execute: {config.get('cmd', [])}"
|
343
|
+
)
|
344
|
+
|
345
|
+
cmd = config.get('cmd', [])
|
346
|
+
if not cmd:
|
347
|
+
return ExecutionResult(success=False, stderr="No command specified")
|
348
|
+
|
349
|
+
# Prepare subprocess arguments
|
350
|
+
kwargs = {
|
351
|
+
'capture_output': config.get('capture', True),
|
352
|
+
'text': True
|
353
|
+
}
|
354
|
+
|
355
|
+
if 'env' in config:
|
356
|
+
exec_env = os.environ.copy()
|
357
|
+
exec_env.update(config['env'])
|
358
|
+
kwargs['env'] = exec_env
|
359
|
+
|
360
|
+
if 'cwd' in config:
|
361
|
+
kwargs['cwd'] = config['cwd']
|
362
|
+
|
363
|
+
if 'stdin' in config:
|
364
|
+
kwargs['input'] = config['stdin']
|
365
|
+
|
366
|
+
try:
|
367
|
+
result = subprocess.run(cmd, **kwargs)
|
368
|
+
|
369
|
+
return ExecutionResult(
|
370
|
+
success=result.returncode == 0,
|
371
|
+
stdout=result.stdout if result.stdout else "",
|
372
|
+
stderr=result.stderr if result.stderr else "",
|
373
|
+
returncode=result.returncode
|
374
|
+
)
|
375
|
+
except Exception as e:
|
376
|
+
return ExecutionResult(
|
377
|
+
success=False,
|
378
|
+
stderr=str(e),
|
379
|
+
returncode=1
|
380
|
+
)
|
381
|
+
|
382
|
+
def execute_ruby(self, code: str) -> ExecutionResult:
|
383
|
+
"""Execute Ruby code."""
|
384
|
+
if self.dry_run:
|
385
|
+
return ExecutionResult(
|
386
|
+
success=True,
|
387
|
+
stdout="[DRY RUN] Would execute Ruby code"
|
388
|
+
)
|
389
|
+
|
390
|
+
# Write context as JSON for Ruby to read
|
391
|
+
context_json = json.dumps(self.context)
|
392
|
+
|
393
|
+
# Ruby wrapper to load context
|
394
|
+
ruby_code = f"""
|
395
|
+
require 'json'
|
396
|
+
context = JSON.parse('{context_json}')
|
397
|
+
flags = context['flags'] || {{}}
|
398
|
+
env = context['env'] || {{}}
|
399
|
+
|
400
|
+
{code}
|
401
|
+
"""
|
402
|
+
|
403
|
+
try:
|
404
|
+
result = subprocess.run(
|
405
|
+
['ruby', '-e', ruby_code],
|
406
|
+
capture_output=True,
|
407
|
+
text=True
|
408
|
+
)
|
409
|
+
|
410
|
+
return ExecutionResult(
|
411
|
+
success=result.returncode == 0,
|
412
|
+
stdout=result.stdout,
|
413
|
+
stderr=result.stderr,
|
414
|
+
returncode=result.returncode
|
415
|
+
)
|
416
|
+
except FileNotFoundError:
|
417
|
+
return ExecutionResult(
|
418
|
+
success=False,
|
419
|
+
stderr="Ruby not found",
|
420
|
+
returncode=127
|
421
|
+
)
|
422
|
+
except Exception as e:
|
423
|
+
return ExecutionResult(
|
424
|
+
success=False,
|
425
|
+
stderr=str(e),
|
426
|
+
returncode=1
|
427
|
+
)
|
428
|
+
|
429
|
+
def show_execution_plan(self, steps: List[Dict[str, Any]]) -> str:
|
430
|
+
"""
|
431
|
+
Show what would be executed (for --ry-run mode).
|
432
|
+
|
433
|
+
Returns formatted execution plan.
|
434
|
+
"""
|
435
|
+
plan = ["=== Execution Plan ==="]
|
436
|
+
|
437
|
+
for i, step in enumerate(steps, 1):
|
438
|
+
if 'shell' in step:
|
439
|
+
cmd = step['shell'].strip().replace('\n', ' ')[:80]
|
440
|
+
plan.append(f"{i}. Shell: {cmd}{'...' if len(step['shell']) > 80 else ''}")
|
441
|
+
|
442
|
+
elif 'python' in step:
|
443
|
+
lines = step['python'].strip().split('\n')
|
444
|
+
first_line = lines[0][:60] if lines else "<empty>"
|
445
|
+
plan.append(f"{i}. Python: {first_line}{'...' if len(lines) > 1 else ''}")
|
446
|
+
|
447
|
+
elif 'subprocess' in step:
|
448
|
+
cmd = step['subprocess'].get('cmd', [])
|
449
|
+
plan.append(f"{i}. Subprocess: {' '.join(cmd)}")
|
450
|
+
|
451
|
+
elif 'ruby' in step:
|
452
|
+
lines = step['ruby'].strip().split('\n')
|
453
|
+
first_line = lines[0][:60] if lines else "<empty>"
|
454
|
+
plan.append(f"{i}. Ruby: {first_line}{'...' if len(lines) > 1 else ''}")
|
455
|
+
|
456
|
+
elif 'relay' in step and step['relay'] == 'native':
|
457
|
+
target = self.context.get('target', 'native-command')
|
458
|
+
plan.append(f"{i}. Relay: {target}")
|
459
|
+
|
460
|
+
elif 'require' in step:
|
461
|
+
plan.append(f"{i}. Require: {step['require']}")
|
462
|
+
if 'error' in step:
|
463
|
+
plan.append(f" Error if missing: {step['error'][:60]}...")
|
464
|
+
|
465
|
+
elif 'capture' in step:
|
466
|
+
plan.append(f"{i}. Capture: {step['capture']}")
|
467
|
+
if 'shell' in step:
|
468
|
+
cmd = step['shell'].strip()[:60]
|
469
|
+
plan.append(f" Via shell: {cmd}...")
|
470
|
+
|
471
|
+
elif 'error' in step:
|
472
|
+
plan.append(f"{i}. Error: {step['error']}")
|
473
|
+
|
474
|
+
return "\n".join(plan)
|
475
|
+
|
ry_tool/installer.py
ADDED
@@ -0,0 +1,176 @@
|
|
1
|
+
"""
|
2
|
+
Library installer and manager.
|
3
|
+
|
4
|
+
Purpose: Install libraries from various sources to user directory.
|
5
|
+
Handles copying library files and dependencies.
|
6
|
+
"""
|
7
|
+
import os
|
8
|
+
import shutil
|
9
|
+
import json
|
10
|
+
from pathlib import Path
|
11
|
+
from typing import Dict
|
12
|
+
from .loader import LibraryLoader, LibraryConfig
|
13
|
+
|
14
|
+
|
15
|
+
class LibraryInstaller:
|
16
|
+
"""Handles library installation to user directory."""
|
17
|
+
|
18
|
+
def __init__(self):
|
19
|
+
"""Initialize installer with user install directory."""
|
20
|
+
xdg_data = os.environ.get("XDG_DATA_HOME", Path.home() / ".local/share")
|
21
|
+
self.install_dir = Path(xdg_data) / "ry-next" / "libraries"
|
22
|
+
self.install_dir.mkdir(parents=True, exist_ok=True)
|
23
|
+
|
24
|
+
# Track installed libraries
|
25
|
+
self.installed_file = self.install_dir.parent / "installed.json"
|
26
|
+
self.installed = self._load_installed()
|
27
|
+
|
28
|
+
def _load_installed(self) -> Dict:
|
29
|
+
"""Load installed libraries tracking."""
|
30
|
+
if self.installed_file.exists():
|
31
|
+
with open(self.installed_file) as f:
|
32
|
+
return json.load(f)
|
33
|
+
return {}
|
34
|
+
|
35
|
+
def _save_installed(self):
|
36
|
+
"""Save installed libraries tracking."""
|
37
|
+
with open(self.installed_file, 'w') as f:
|
38
|
+
json.dump(self.installed, f, indent=2)
|
39
|
+
|
40
|
+
def install_local(self, library: LibraryConfig) -> bool:
|
41
|
+
"""
|
42
|
+
Install from local file to user directory.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
library: LibraryConfig from a local source
|
46
|
+
|
47
|
+
Returns:
|
48
|
+
True if successful
|
49
|
+
"""
|
50
|
+
target_dir = self.install_dir / library.name
|
51
|
+
target_dir.mkdir(exist_ok=True)
|
52
|
+
|
53
|
+
try:
|
54
|
+
# Copy library file
|
55
|
+
target_yaml = target_dir / f"{library.name}.yaml"
|
56
|
+
shutil.copy2(library.path, target_yaml)
|
57
|
+
|
58
|
+
# If library is in directory format, copy additional files
|
59
|
+
if library.path.parent.name == library.name:
|
60
|
+
source_dir = library.path.parent
|
61
|
+
|
62
|
+
# Copy lib/ directory if exists
|
63
|
+
lib_dir = source_dir / 'lib'
|
64
|
+
if lib_dir.exists():
|
65
|
+
target_lib = target_dir / 'lib'
|
66
|
+
if target_lib.exists():
|
67
|
+
shutil.rmtree(target_lib)
|
68
|
+
shutil.copytree(lib_dir, target_lib)
|
69
|
+
|
70
|
+
# Copy meta.yaml if exists
|
71
|
+
meta_file = source_dir / 'meta.yaml'
|
72
|
+
if meta_file.exists():
|
73
|
+
shutil.copy2(meta_file, target_dir / 'meta.yaml')
|
74
|
+
|
75
|
+
# Copy README.md if exists
|
76
|
+
readme = source_dir / 'README.md'
|
77
|
+
if readme.exists():
|
78
|
+
shutil.copy2(readme, target_dir / 'README.md')
|
79
|
+
|
80
|
+
# Update installed tracking
|
81
|
+
self.installed[library.name] = {
|
82
|
+
'version': library.metadata.get('version', '0.0.0'),
|
83
|
+
'source': str(library.path),
|
84
|
+
'type': library.type
|
85
|
+
}
|
86
|
+
self._save_installed()
|
87
|
+
|
88
|
+
return True
|
89
|
+
|
90
|
+
except Exception as e:
|
91
|
+
print(f"Installation failed: {e}")
|
92
|
+
return False
|
93
|
+
|
94
|
+
def install_from_registry(self, library_name: str, version: str = None) -> bool:
|
95
|
+
"""
|
96
|
+
Install from online registry.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
library_name: Name of library to install
|
100
|
+
version: Optional version constraint
|
101
|
+
|
102
|
+
Returns:
|
103
|
+
True if successful
|
104
|
+
"""
|
105
|
+
# For now, try to find in local development directories
|
106
|
+
loader = LibraryLoader()
|
107
|
+
|
108
|
+
try:
|
109
|
+
# Try to load from existing sources
|
110
|
+
library = loader.load(library_name)
|
111
|
+
|
112
|
+
# Install to user directory
|
113
|
+
return self.install_local(library)
|
114
|
+
|
115
|
+
except FileNotFoundError:
|
116
|
+
# Future: Download from online registry
|
117
|
+
print(f"Library '{library_name}' not found in local sources")
|
118
|
+
print("Online registry support coming soon!")
|
119
|
+
return False
|
120
|
+
|
121
|
+
def uninstall(self, library_name: str) -> bool:
|
122
|
+
"""
|
123
|
+
Uninstall a library.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
library_name: Name of library to remove
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
True if successful
|
130
|
+
"""
|
131
|
+
library_dir = self.install_dir / library_name
|
132
|
+
|
133
|
+
if not library_dir.exists():
|
134
|
+
print(f"Library '{library_name}' is not installed")
|
135
|
+
return False
|
136
|
+
|
137
|
+
try:
|
138
|
+
shutil.rmtree(library_dir)
|
139
|
+
|
140
|
+
# Update tracking
|
141
|
+
if library_name in self.installed:
|
142
|
+
del self.installed[library_name]
|
143
|
+
self._save_installed()
|
144
|
+
|
145
|
+
print(f"Uninstalled {library_name}")
|
146
|
+
return True
|
147
|
+
|
148
|
+
except Exception as e:
|
149
|
+
print(f"Failed to uninstall: {e}")
|
150
|
+
return False
|
151
|
+
|
152
|
+
def list_installed(self) -> Dict[str, Dict]:
|
153
|
+
"""
|
154
|
+
List installed libraries.
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
Dictionary of installed libraries with metadata
|
158
|
+
"""
|
159
|
+
return self.installed
|
160
|
+
|
161
|
+
def update(self, library_name: str) -> bool:
|
162
|
+
"""
|
163
|
+
Update an installed library.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
library_name: Name of library to update
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
True if successful
|
170
|
+
"""
|
171
|
+
if library_name not in self.installed:
|
172
|
+
print(f"Library '{library_name}' is not installed")
|
173
|
+
return False
|
174
|
+
|
175
|
+
# Re-install from source
|
176
|
+
return self.install_from_registry(library_name)
|