feat: Add new tools and configurations for Castle platform

- Introduced `uv.lock` for dependency management with various packages including `pytest`, `colorama`, and `pluggy`.
- Added `pyrightconfig.json` for Python type checking configuration.
- Expanded `recommendations.md` with detailed scaling recommendations and project management strategies.
- Created shared `ruff.toml` for consistent linting across projects.
- Developed `Castle Tools` with various utilities including Android backup, browser automation, document conversion, and search tools.
- Implemented `backup-collect` and `schedule` tools for system administration tasks.
- Enhanced `search` functionality with indexing and querying capabilities using Tantivy.
- Added comprehensive documentation for each tool, including usage examples and installation instructions.
This commit is contained in:
2026-02-20 16:41:19 -08:00
parent 0d35ac9ffd
commit f39a551aad
152 changed files with 21197 additions and 83 deletions

View File

@@ -0,0 +1,186 @@
"""castle gateway - manage the Caddy reverse proxy gateway."""
from __future__ import annotations
import argparse
import shutil
import subprocess
from castle_cli.config import GENERATED_DIR, CastleConfig, ensure_dirs, load_config
def _find_dashboard_dist(config: CastleConfig) -> str | None:
"""Find the dashboard dist/ directory if it exists."""
dist = config.root / "dashboard" / "dist"
if dist.exists() and (dist / "index.html").exists():
return str(dist)
return None
def _generate_caddyfile(config: CastleConfig) -> str:
"""Generate Caddyfile content from castle config."""
lines = [f":{config.gateway.port} {{"]
# Reverse proxy for each component with proxy.caddy and expose.http
for name, manifest in config.components.items():
if not (manifest.proxy and manifest.proxy.caddy and manifest.proxy.caddy.enable):
continue
if not (manifest.expose and manifest.expose.http):
continue
caddy = manifest.proxy.caddy
http = manifest.expose.http
path_prefix = caddy.path_prefix or f"/{name}"
port = http.internal.port
host = http.internal.host or "localhost"
lines.append(f" handle_path {path_prefix}/* {{")
lines.append(f" reverse_proxy {host}:{port}")
lines.append(" }")
lines.append("")
# Dashboard SPA at root (must come after more-specific handle_path rules)
dashboard_dist = _find_dashboard_dist(config)
if dashboard_dist:
lines.append(" handle {")
lines.append(f" root * {dashboard_dist}")
lines.append(" try_files {path} /index.html")
lines.append(" file_server")
lines.append(" }")
else:
# Fallback: serve from generated directory
fallback = GENERATED_DIR / "dashboard"
lines.append(" handle / {")
lines.append(f" root * {fallback}")
lines.append(" file_server")
lines.append(" }")
lines.append("}")
return "\n".join(lines)
def _write_generated_files(config: CastleConfig) -> None:
"""Write generated Caddyfile."""
ensure_dirs()
caddyfile_path = GENERATED_DIR / "Caddyfile"
caddyfile_path.write_text(_generate_caddyfile(config))
print(f" Generated {caddyfile_path}")
dashboard_dist = _find_dashboard_dist(config)
if dashboard_dist:
print(f" Dashboard: {dashboard_dist}")
else:
print(" Dashboard: dist/ not found, using fallback")
def run_gateway(args: argparse.Namespace) -> int:
"""Manage the Caddy gateway."""
if not args.gateway_command:
print("Usage: castle gateway {start|stop|reload|status}")
return 1
config = load_config()
if args.gateway_command in ("start", "reload") and getattr(args, "dry_run", False):
return _gateway_dry_run(config)
if args.gateway_command == "start":
return _gateway_start(config)
elif args.gateway_command == "stop":
return _gateway_stop()
elif args.gateway_command == "reload":
return _gateway_reload(config)
elif args.gateway_command == "status":
return _gateway_status()
return 1
def _gateway_dry_run(config: CastleConfig) -> int:
"""Print generated Caddyfile and gateway unit without applying."""
from castle_cli.commands.service import _generate_gateway_unit
print("# Caddyfile")
print(_generate_caddyfile(config))
print()
print("# castle-gateway.service")
print(_generate_gateway_unit(config))
return 0
def _gateway_start(config: CastleConfig) -> int:
"""Generate config and start Caddy."""
if not shutil.which("caddy"):
print("Error: caddy is not installed.")
print("Install with: sudo apt install caddy")
return 1
print("Generating gateway configuration...")
_write_generated_files(config)
caddyfile = GENERATED_DIR / "Caddyfile"
print(f"\nStarting Caddy on port {config.gateway.port}...")
result = subprocess.run(
["caddy", "start", "--config", str(caddyfile), "--adapter", "caddyfile"],
)
if result.returncode == 0:
print(f"Gateway running at http://localhost:{config.gateway.port}")
else:
print("Failed to start gateway.")
return result.returncode
def _gateway_stop() -> int:
"""Stop Caddy."""
if not shutil.which("caddy"):
print("Error: caddy is not installed.")
return 1
result = subprocess.run(["caddy", "stop"])
if result.returncode == 0:
print("Gateway stopped.")
return result.returncode
def _gateway_reload(config: CastleConfig) -> int:
"""Regenerate config and reload Caddy."""
print("Regenerating gateway configuration...")
_write_generated_files(config)
caddyfile = GENERATED_DIR / "Caddyfile"
result = subprocess.run(
["caddy", "reload", "--config", str(caddyfile), "--adapter", "caddyfile"],
)
if result.returncode == 0:
print("Gateway reloaded.")
else:
print("Failed to reload gateway. Is it running?")
return result.returncode
def _gateway_status() -> int:
"""Show gateway status."""
if not shutil.which("caddy"):
print("Gateway: not installed")
return 1
result = subprocess.run(
["pgrep", "-x", "caddy"],
capture_output=True,
)
if result.returncode == 0:
print("Gateway: running")
caddyfile = GENERATED_DIR / "Caddyfile"
if caddyfile.exists():
print(f" Config: {caddyfile}")
else:
print("Gateway: stopped")
return 0