scry-run 0.1.0__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.
- scry_run/__init__.py +102 -0
- scry_run/backends/__init__.py +6 -0
- scry_run/backends/base.py +65 -0
- scry_run/backends/claude.py +404 -0
- scry_run/backends/frozen.py +85 -0
- scry_run/backends/registry.py +72 -0
- scry_run/cache.py +441 -0
- scry_run/cli/__init__.py +137 -0
- scry_run/cli/apps.py +396 -0
- scry_run/cli/cache.py +342 -0
- scry_run/cli/config_cmd.py +84 -0
- scry_run/cli/env.py +27 -0
- scry_run/cli/init.py +375 -0
- scry_run/cli/run.py +71 -0
- scry_run/config.py +141 -0
- scry_run/console.py +52 -0
- scry_run/context.py +298 -0
- scry_run/generator.py +698 -0
- scry_run/home.py +60 -0
- scry_run/logging.py +171 -0
- scry_run/meta.py +1852 -0
- scry_run/packages.py +175 -0
- scry_run-0.1.0.dist-info/METADATA +282 -0
- scry_run-0.1.0.dist-info/RECORD +26 -0
- scry_run-0.1.0.dist-info/WHEEL +4 -0
- scry_run-0.1.0.dist-info/entry_points.txt +2 -0
scry_run/cli/apps.py
ADDED
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""App management commands for scry-run."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import shutil
|
|
6
|
+
import subprocess
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.prompt import Confirm
|
|
15
|
+
|
|
16
|
+
from scry_run.home import get_apps_dir, get_app_dir
|
|
17
|
+
from scry_run.packages import ensure_scry_run_installed, get_venv_python, get_installed_packages
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_all_apps() -> list[str]:
|
|
23
|
+
"""Get list of all app names."""
|
|
24
|
+
apps_dir = get_apps_dir()
|
|
25
|
+
if not apps_dir.exists():
|
|
26
|
+
return []
|
|
27
|
+
return sorted([
|
|
28
|
+
d.name for d in apps_dir.iterdir()
|
|
29
|
+
if d.is_dir() and (d / "app.py").exists()
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def extract_description_from_app(app_dir: Path) -> str | None:
|
|
34
|
+
"""Extract the description from an app's app.py file.
|
|
35
|
+
|
|
36
|
+
The description is stored in the module docstring and class docstring.
|
|
37
|
+
Returns the full description text (may be multi-line).
|
|
38
|
+
"""
|
|
39
|
+
app_file = app_dir / "app.py"
|
|
40
|
+
if not app_file.exists():
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
content = app_file.read_text()
|
|
44
|
+
|
|
45
|
+
# Try to extract from module docstring (first triple-quoted string)
|
|
46
|
+
# Format: """
|
|
47
|
+
# {description}
|
|
48
|
+
#
|
|
49
|
+
# Generated by scry-run...
|
|
50
|
+
# """
|
|
51
|
+
match = re.search(r'^"""[\s\n]*(.*?)(?:\n\nGenerated by scry-run|""")', content, re.MULTILINE | re.DOTALL)
|
|
52
|
+
if match:
|
|
53
|
+
return match.group(1).strip()
|
|
54
|
+
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@click.command("list")
|
|
59
|
+
def list_apps() -> None:
|
|
60
|
+
"""List all scry-run apps."""
|
|
61
|
+
apps = get_all_apps()
|
|
62
|
+
|
|
63
|
+
if not apps:
|
|
64
|
+
console.print("[dim]No apps found.[/dim]")
|
|
65
|
+
console.print(f"[dim]Create one with:[/dim] [cyan]scry-run init[/cyan]")
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
table = Table(show_header=True, header_style="bold")
|
|
69
|
+
table.add_column("Name")
|
|
70
|
+
table.add_column("Description")
|
|
71
|
+
table.add_column("Path")
|
|
72
|
+
|
|
73
|
+
for app_name in apps:
|
|
74
|
+
app_dir = get_app_dir(app_name)
|
|
75
|
+
description = extract_description_from_app(app_dir) or "[dim]—[/dim]"
|
|
76
|
+
table.add_row(app_name, description, str(app_dir))
|
|
77
|
+
|
|
78
|
+
console.print(table)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@click.command("which")
|
|
82
|
+
@click.argument("app_name")
|
|
83
|
+
def which_app(app_name: str) -> None:
|
|
84
|
+
"""Print command to run app in its environment.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
scry-run which myapp
|
|
88
|
+
$(scry-run which myapp) --help
|
|
89
|
+
"""
|
|
90
|
+
app_dir = get_app_dir(app_name)
|
|
91
|
+
app_file = app_dir / "app.py"
|
|
92
|
+
|
|
93
|
+
if not app_file.exists():
|
|
94
|
+
console.print(f"[red]Error:[/red] App '{app_name}' not found")
|
|
95
|
+
raise SystemExit(1)
|
|
96
|
+
|
|
97
|
+
# Print the full command to run the app
|
|
98
|
+
click.echo(f"uv run --directory {app_dir} python {app_file}")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@click.command("rm")
|
|
102
|
+
@click.argument("app_name")
|
|
103
|
+
@click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
|
|
104
|
+
def rm_app(app_name: str, force: bool) -> None:
|
|
105
|
+
"""Remove an scry-run app.
|
|
106
|
+
|
|
107
|
+
This permanently deletes the app directory and all its contents.
|
|
108
|
+
|
|
109
|
+
Example:
|
|
110
|
+
scry-run rm myapp
|
|
111
|
+
scry-run rm myapp --force
|
|
112
|
+
"""
|
|
113
|
+
app_dir = get_app_dir(app_name)
|
|
114
|
+
|
|
115
|
+
if not app_dir.exists():
|
|
116
|
+
console.print(f"[red]Error:[/red] App '{app_name}' not found")
|
|
117
|
+
raise SystemExit(1)
|
|
118
|
+
|
|
119
|
+
if not force:
|
|
120
|
+
if not Confirm.ask(f"[yellow]Delete app '{app_name}' and all its contents?[/yellow]"):
|
|
121
|
+
console.print("[dim]Aborted.[/dim]")
|
|
122
|
+
raise SystemExit(0)
|
|
123
|
+
|
|
124
|
+
shutil.rmtree(app_dir)
|
|
125
|
+
console.print(f"[green]Removed app '{app_name}'[/green]")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@click.command("reset")
|
|
129
|
+
@click.argument("app_name")
|
|
130
|
+
@click.option("--force", "-f", is_flag=True, help="Skip confirmation prompt")
|
|
131
|
+
def reset_app(app_name: str, force: bool) -> None:
|
|
132
|
+
"""Reset an app to initial state, preserving name and description.
|
|
133
|
+
|
|
134
|
+
This removes the generated code and cache, then reinitializes the app
|
|
135
|
+
as if 'scry-run init' was run with the same name and description.
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
scry-run reset myapp
|
|
139
|
+
scry-run reset myapp --force
|
|
140
|
+
"""
|
|
141
|
+
from scry_run.cli.init import MAIN_TEMPLATE, to_class_name
|
|
142
|
+
|
|
143
|
+
app_dir = get_app_dir(app_name)
|
|
144
|
+
|
|
145
|
+
if not app_dir.exists():
|
|
146
|
+
console.print(f"[red]Error:[/red] App '{app_name}' not found")
|
|
147
|
+
raise SystemExit(1)
|
|
148
|
+
|
|
149
|
+
# Extract description before deleting
|
|
150
|
+
description = extract_description_from_app(app_dir)
|
|
151
|
+
if not description:
|
|
152
|
+
description = f"A {app_name} application powered by LLM code generation"
|
|
153
|
+
|
|
154
|
+
if not force:
|
|
155
|
+
console.print(f"[yellow]This will reset app '{app_name}':[/yellow]")
|
|
156
|
+
console.print(f" - Remove all generated code")
|
|
157
|
+
console.print(f" - Clear the cache")
|
|
158
|
+
console.print(f" - Keep name: [cyan]{app_name}[/cyan]")
|
|
159
|
+
console.print(f" - Keep description: [cyan]{description}[/cyan]")
|
|
160
|
+
if not Confirm.ask("Continue?"):
|
|
161
|
+
console.print("[dim]Aborted.[/dim]")
|
|
162
|
+
raise SystemExit(0)
|
|
163
|
+
|
|
164
|
+
# Remove existing directory
|
|
165
|
+
shutil.rmtree(app_dir)
|
|
166
|
+
|
|
167
|
+
# Recreate app directory
|
|
168
|
+
app_dir.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
|
|
170
|
+
# Create virtual environment and install scry-run
|
|
171
|
+
ensure_scry_run_installed(app_dir)
|
|
172
|
+
|
|
173
|
+
class_name = to_class_name(app_name)
|
|
174
|
+
|
|
175
|
+
# Create app.py
|
|
176
|
+
app_file = app_dir / "app.py"
|
|
177
|
+
app_content = MAIN_TEMPLATE.format(
|
|
178
|
+
name=app_name,
|
|
179
|
+
description=description,
|
|
180
|
+
class_name=class_name,
|
|
181
|
+
)
|
|
182
|
+
app_file.write_text(app_content)
|
|
183
|
+
app_file.chmod(app_file.stat().st_mode | 0o111) # Make executable
|
|
184
|
+
|
|
185
|
+
# Create empty cache.json
|
|
186
|
+
cache_file = app_dir / "cache.json"
|
|
187
|
+
cache_file.write_text(json.dumps({}))
|
|
188
|
+
|
|
189
|
+
# Create logs directory
|
|
190
|
+
logs_dir = app_dir / "logs"
|
|
191
|
+
logs_dir.mkdir(exist_ok=True)
|
|
192
|
+
|
|
193
|
+
console.print(f"[green]Reset app '{app_name}' successfully[/green]")
|
|
194
|
+
console.print(f" [dim]Description:[/dim] {description}")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _format_size(size_bytes: int) -> str:
|
|
198
|
+
"""Format a size in bytes to human-readable format."""
|
|
199
|
+
if size_bytes < 1024:
|
|
200
|
+
return f"{size_bytes} B"
|
|
201
|
+
elif size_bytes < 1024 * 1024:
|
|
202
|
+
return f"{size_bytes / 1024:.1f} KB"
|
|
203
|
+
else:
|
|
204
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _format_timestamp(ts: float | None) -> str:
|
|
208
|
+
"""Format a timestamp to human-readable format."""
|
|
209
|
+
if ts is None:
|
|
210
|
+
return "—"
|
|
211
|
+
dt = datetime.fromtimestamp(ts)
|
|
212
|
+
return dt.strftime("%Y-%m-%d %H:%M")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _get_python_version(app_dir: Path) -> str | None:
|
|
216
|
+
"""Get the Python version in the app's venv."""
|
|
217
|
+
python_path = get_venv_python(app_dir)
|
|
218
|
+
if not python_path.exists():
|
|
219
|
+
return None
|
|
220
|
+
try:
|
|
221
|
+
result = subprocess.run(
|
|
222
|
+
[str(python_path), "--version"],
|
|
223
|
+
capture_output=True,
|
|
224
|
+
text=True,
|
|
225
|
+
)
|
|
226
|
+
if result.returncode == 0:
|
|
227
|
+
# Output is "Python 3.12.12"
|
|
228
|
+
return result.stdout.strip().replace("Python ", "")
|
|
229
|
+
except Exception:
|
|
230
|
+
pass
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _get_cache_stats(cache_file: Path) -> dict:
|
|
235
|
+
"""Get statistics about the cache file."""
|
|
236
|
+
stats = {
|
|
237
|
+
"methods": 0,
|
|
238
|
+
"total_lines": 0,
|
|
239
|
+
"size_bytes": 0,
|
|
240
|
+
"last_cached": None,
|
|
241
|
+
"entries": [],
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if not cache_file.exists():
|
|
245
|
+
return stats
|
|
246
|
+
|
|
247
|
+
stats["size_bytes"] = cache_file.stat().st_size
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
with open(cache_file, "r", encoding="utf-8") as f:
|
|
251
|
+
data = json.load(f)
|
|
252
|
+
|
|
253
|
+
for class_name, methods in data.items():
|
|
254
|
+
for attr_name, entry in methods.items():
|
|
255
|
+
stats["methods"] += 1
|
|
256
|
+
code = entry.get("code", "")
|
|
257
|
+
stats["total_lines"] += code.count("\n") + 1
|
|
258
|
+
stats["entries"].append({
|
|
259
|
+
"class": class_name,
|
|
260
|
+
"attr": attr_name,
|
|
261
|
+
"type": entry.get("code_type", "method"),
|
|
262
|
+
"created_at": entry.get("created_at"),
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
# Track most recent cache entry
|
|
266
|
+
created_at = entry.get("created_at")
|
|
267
|
+
if created_at:
|
|
268
|
+
try:
|
|
269
|
+
ts = datetime.fromisoformat(created_at).timestamp()
|
|
270
|
+
if stats["last_cached"] is None or ts > stats["last_cached"]:
|
|
271
|
+
stats["last_cached"] = ts
|
|
272
|
+
except ValueError:
|
|
273
|
+
pass
|
|
274
|
+
except (json.JSONDecodeError, KeyError):
|
|
275
|
+
pass
|
|
276
|
+
|
|
277
|
+
return stats
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _get_venv_size(venv_path: Path) -> int:
|
|
281
|
+
"""Get total size of venv directory."""
|
|
282
|
+
if not venv_path.exists():
|
|
283
|
+
return 0
|
|
284
|
+
total = 0
|
|
285
|
+
for f in venv_path.rglob("*"):
|
|
286
|
+
if f.is_file():
|
|
287
|
+
total += f.stat().st_size
|
|
288
|
+
return total
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
@click.command("info")
|
|
292
|
+
@click.argument("app_name")
|
|
293
|
+
def info_app(app_name: str) -> None:
|
|
294
|
+
"""Show detailed information about an app.
|
|
295
|
+
|
|
296
|
+
Displays paths, cache statistics, environment info, and timestamps.
|
|
297
|
+
|
|
298
|
+
Example:
|
|
299
|
+
scry-run info myapp
|
|
300
|
+
"""
|
|
301
|
+
app_dir = get_app_dir(app_name)
|
|
302
|
+
app_file = app_dir / "app.py"
|
|
303
|
+
cache_file = app_dir / "cache.json"
|
|
304
|
+
venv_path = app_dir / ".venv"
|
|
305
|
+
|
|
306
|
+
if not app_dir.exists():
|
|
307
|
+
console.print(f"[red]Error:[/red] App '{app_name}' not found")
|
|
308
|
+
raise SystemExit(1)
|
|
309
|
+
|
|
310
|
+
# Get description
|
|
311
|
+
description = extract_description_from_app(app_dir) or "[dim]No description[/dim]"
|
|
312
|
+
|
|
313
|
+
# Get cache stats
|
|
314
|
+
cache_stats = _get_cache_stats(cache_file)
|
|
315
|
+
|
|
316
|
+
# Get installed packages
|
|
317
|
+
packages = get_installed_packages(app_dir)
|
|
318
|
+
# Filter out scry-run itself and common built-in packages for display
|
|
319
|
+
user_packages = [p for p in packages if not p.startswith(("pip==", "setuptools==", "wheel=="))]
|
|
320
|
+
|
|
321
|
+
# Get Python version
|
|
322
|
+
python_version = _get_python_version(app_dir) or "—"
|
|
323
|
+
|
|
324
|
+
# Get timestamps
|
|
325
|
+
app_created = app_file.stat().st_ctime if app_file.exists() else None
|
|
326
|
+
app_modified = app_file.stat().st_mtime if app_file.exists() else None
|
|
327
|
+
|
|
328
|
+
# Build output
|
|
329
|
+
lines = []
|
|
330
|
+
|
|
331
|
+
# Description section
|
|
332
|
+
lines.append("[bold]Description[/bold]")
|
|
333
|
+
# Indent each line of the description
|
|
334
|
+
for line in description.split("\n"):
|
|
335
|
+
lines.append(f" {line}")
|
|
336
|
+
lines.append("")
|
|
337
|
+
|
|
338
|
+
# Paths section
|
|
339
|
+
lines.append("[bold]Paths[/bold]")
|
|
340
|
+
lines.append(f" [dim]App directory[/dim] {app_dir}")
|
|
341
|
+
lines.append(f" [dim]Main file[/dim] {app_file}")
|
|
342
|
+
lines.append(f" [dim]Cache file[/dim] {cache_file}")
|
|
343
|
+
lines.append("")
|
|
344
|
+
|
|
345
|
+
# Cache section
|
|
346
|
+
lines.append("[bold]Cache[/bold]")
|
|
347
|
+
if cache_stats["methods"] > 0:
|
|
348
|
+
lines.append(f" [dim]Methods[/dim] {cache_stats['methods']}")
|
|
349
|
+
lines.append(f" [dim]Total lines[/dim] {cache_stats['total_lines']}")
|
|
350
|
+
lines.append(f" [dim]Cache size[/dim] {_format_size(cache_stats['size_bytes'])}")
|
|
351
|
+
|
|
352
|
+
# Show method breakdown
|
|
353
|
+
if cache_stats["entries"]:
|
|
354
|
+
method_list = ", ".join(
|
|
355
|
+
f"{e['attr']}" for e in cache_stats["entries"][:5]
|
|
356
|
+
)
|
|
357
|
+
if len(cache_stats["entries"]) > 5:
|
|
358
|
+
method_list += f", ... (+{len(cache_stats['entries']) - 5} more)"
|
|
359
|
+
lines.append(f" [dim]Cached[/dim] {method_list}")
|
|
360
|
+
else:
|
|
361
|
+
lines.append(" [dim]No cached methods[/dim]")
|
|
362
|
+
lines.append("")
|
|
363
|
+
|
|
364
|
+
# Environment section
|
|
365
|
+
lines.append("[bold]Environment[/bold]")
|
|
366
|
+
if venv_path.exists():
|
|
367
|
+
venv_size = _get_venv_size(venv_path)
|
|
368
|
+
lines.append(f" [dim]Venv[/dim] {venv_path}")
|
|
369
|
+
lines.append(f" [dim]Venv size[/dim] {_format_size(venv_size)}")
|
|
370
|
+
lines.append(f" [dim]Python[/dim] {python_version}")
|
|
371
|
+
lines.append(f" [dim]Packages[/dim] {len(user_packages)}")
|
|
372
|
+
|
|
373
|
+
# Show top packages (excluding scry-run internals)
|
|
374
|
+
display_packages = [p.split("==")[0] for p in user_packages if not p.startswith("scry-run")][:5]
|
|
375
|
+
if display_packages:
|
|
376
|
+
pkg_list = ", ".join(display_packages)
|
|
377
|
+
if len(user_packages) > 5:
|
|
378
|
+
pkg_list += f", ... (+{len(user_packages) - 5} more)"
|
|
379
|
+
lines.append(f" [dim]Top packages[/dim] {pkg_list}")
|
|
380
|
+
else:
|
|
381
|
+
lines.append(" [dim]No virtual environment[/dim]")
|
|
382
|
+
lines.append("")
|
|
383
|
+
|
|
384
|
+
# Timestamps section
|
|
385
|
+
lines.append("[bold]Timestamps[/bold]")
|
|
386
|
+
lines.append(f" [dim]Created[/dim] {_format_timestamp(app_created)}")
|
|
387
|
+
lines.append(f" [dim]Last modified[/dim] {_format_timestamp(app_modified)}")
|
|
388
|
+
lines.append(f" [dim]Last cached[/dim] {_format_timestamp(cache_stats['last_cached'])}")
|
|
389
|
+
|
|
390
|
+
# Print as panel
|
|
391
|
+
panel = Panel(
|
|
392
|
+
"\n".join(lines),
|
|
393
|
+
title=f"[bold]{app_name}[/bold]",
|
|
394
|
+
border_style="blue",
|
|
395
|
+
)
|
|
396
|
+
console.print(panel)
|