diff --git a/castle.yaml b/castle.yaml index 972cdae..0294a6c 100644 --- a/castle.yaml +++ b/castle.yaml @@ -1,42 +1,43 @@ gateway: port: 9000 - programs: central-context: description: Content storage API useful to get context into my data dir from anywhere on the LAN + behavior: daemon source: components/central-context stack: python-fastapi - behavior: daemon notification-bridge: description: Desktop notification forwarder. This taps into your notifications system on the desktop and will forward all notifications the the central context server. + behavior: daemon source: components/notification-bridge stack: python-fastapi - behavior: daemon + install_extras: + - linux castle-api: description: Castle API + behavior: daemon source: castle-api stack: python-fastapi - behavior: daemon protonmail: description: ProtonMail email sync via Bridge + behavior: tool source: components/protonmail stack: python-cli - behavior: tool backup-collect: description: Collect files from various sources into backup directory + behavior: tool source: components/backup-collect stack: python-cli - behavior: tool system_dependencies: - rsync castle-app: description: Castle web app + behavior: frontend source: app stack: react-vite - behavior: frontend build: commands: - - pnpm @@ -45,92 +46,91 @@ programs: - dist/ devbox-connect: description: SSH tunnel manager with auto-reconnect + behavior: tool source: components/devbox-connect stack: python-cli - behavior: tool mbox2eml: description: MBOX to EML email converter + behavior: tool source: components/mbox2eml stack: python-cli - behavior: tool android-backup: description: Backup Android device using ADB + behavior: tool source: components/android-backup stack: python-cli - behavior: tool system_dependencies: - adb browser: description: Browse the web using natural language via browser-use + behavior: tool source: components/browser stack: python-cli - behavior: tool docx-extractor: description: Extract content and metadata from Word .docx files + behavior: tool source: components/docx-extractor stack: python-cli - behavior: tool system_dependencies: - pandoc docx2md: description: Convert Word .docx files to Markdown + behavior: tool source: components/docx2md stack: python-cli - behavior: tool system_dependencies: - pandoc gpt: description: OpenAI text generation utility + behavior: tool source: components/gpt stack: python-cli - behavior: tool html2text: description: Convert HTML content to plain text + behavior: tool source: components/html2text stack: python-cli - behavior: tool md2pdf: description: Convert Markdown files to PDF + behavior: tool source: components/md2pdf stack: python-cli - behavior: tool system_dependencies: - pandoc - texlive-latex-base mdscraper: description: Combine text files into a single markdown document + behavior: tool source: components/mdscraper stack: python-cli - behavior: tool pdf-extractor: description: Extract content and metadata from PDF files + behavior: tool source: components/pdf-extractor stack: python-cli - behavior: tool pdf2md: description: Convert PDF files to Markdown + behavior: tool source: components/pdf2md stack: python-cli - behavior: tool system_dependencies: - pandoc - poppler-utils schedule: description: Manage systemd user timers and scheduled tasks + behavior: tool source: components/schedule stack: python-cli - behavior: tool search: description: Manage self-contained searchable collections of files + behavior: tool source: components/search stack: python-cli - behavior: tool text-extractor: description: Extract content and metadata from text files + behavior: tool source: components/text-extractor stack: python-cli - behavior: tool - services: castle-gateway: description: Caddy reverse proxy gateway @@ -143,22 +143,20 @@ services: - /home/payne/.castle/generated/Caddyfile - --adapter - caddyfile - manage: - systemd: - exec_reload: caddy reload --config /home/payne/.castle/generated/Caddyfile - --adapter caddyfile expose: http: internal: port: 9000 health_path: / + manage: + systemd: + exec_reload: caddy reload --config /home/payne/.castle/generated/Caddyfile + --adapter caddyfile central-context: component: central-context run: runner: python program: central-context - manage: - systemd: {} expose: http: internal: @@ -167,18 +165,13 @@ services: proxy: caddy: path_prefix: /central-context + manage: + systemd: {} notification-bridge: component: notification-bridge run: runner: python program: notification-bridge - defaults: - env: - CENTRAL_CONTEXT_URL: http://localhost:9001 - BUCKET_NAME: notifications - PORT: '9002' - manage: - systemd: {} expose: http: internal: @@ -187,13 +180,18 @@ services: proxy: caddy: path_prefix: /notifications + manage: + systemd: {} + defaults: + env: + CENTRAL_CONTEXT_URL: http://localhost:9001 + BUCKET_NAME: notifications + PORT: '9002' castle-api: component: castle-api run: runner: python program: castle-api - manage: - systemd: {} expose: http: internal: @@ -202,24 +200,24 @@ services: proxy: caddy: path_prefix: /api - + manage: + systemd: {} castle-mqtt: description: MQTT broker for inter-node mesh coordination run: runner: container image: eclipse-mosquitto:2 ports: - 1883: 1883 + '1883': 1883 volumes: - /data/castle/castle-mqtt/config:/mosquitto/config - /data/castle/castle-mqtt/data:/mosquitto/data - manage: - systemd: {} expose: http: internal: port: 1883 - + manage: + systemd: {} jobs: protonmail-sync: component: protonmail @@ -230,12 +228,12 @@ jobs: - protonmail - sync schedule: '*/5 * * * *' + manage: + systemd: {} defaults: env: PROTONMAIL_USERNAME: paul@payne.io PROTONMAIL_API_KEY: ${secret:PROTONMAIL_API_KEY} - manage: - systemd: {} backup-collect: component: backup-collect description: Collect files from various sources into backup directory @@ -244,11 +242,11 @@ jobs: argv: - backup-collect schedule: 0 2 * * * + manage: + systemd: {} defaults: env: DBACKUP: /data/backup - manage: - systemd: {} backup-data: description: Nightly restic backup of /data to /storage run: @@ -256,9 +254,9 @@ jobs: argv: - backup-data schedule: 30 3 * * * + manage: + systemd: {} defaults: env: RESTIC_REPOSITORY: /storage/restic-data RESTIC_PASSWORD_FILE: /home/payne/.config/restic-password - manage: - systemd: {} diff --git a/cli/src/castle_cli/commands/sync.py b/cli/src/castle_cli/commands/sync.py index 640238d..8665778 100644 --- a/cli/src/castle_cli/commands/sync.py +++ b/cli/src/castle_cli/commands/sync.py @@ -86,8 +86,13 @@ def run_sync(args: argparse.Namespace) -> int: continue if (source_dir / "pyproject.toml").exists(): print(f"\nInstalling {name}...") + prog = config.programs.get(svc.component or "") if svc.component else None + extras = prog.install_extras if prog else [] + pkg_spec = str(source_dir) + if extras: + pkg_spec += "[" + ",".join(extras) + "]" result = subprocess.run( - [uv_path, "tool", "install", "--editable", str(source_dir), "--force"], + [uv_path, "tool", "install", "--editable", pkg_spec, "--force"], capture_output=True, text=True, ) @@ -122,8 +127,12 @@ def _try_install( if (source_dir / "pyproject.toml").exists(): print(f"\nInstalling {name}...") + extras = getattr(comp, "install_extras", []) + pkg_spec = str(source_dir) + if extras: + pkg_spec += "[" + ",".join(extras) + "]" result = subprocess.run( - [uv_path, "tool", "install", "--editable", str(source_dir), "--force"], + [uv_path, "tool", "install", "--editable", pkg_spec, "--force"], capture_output=True, text=True, ) diff --git a/core/src/castle_core/config.py b/core/src/castle_core/config.py index 6d2c48c..20ae245 100644 --- a/core/src/castle_core/config.py +++ b/core/src/castle_core/config.py @@ -134,7 +134,11 @@ def load_config(root: Path | None = None) -> CastleConfig: # Support both "programs:" and legacy "components:" key programs_data = data.get("programs") or data.get("components") or {} for name, comp_data in programs_data.items(): - programs[name] = _parse_program(name, comp_data) + prog = _parse_program(name, comp_data) + # Resolve relative source paths to absolute + if prog.source and not Path(prog.source).is_absolute(): + prog.source = str(root / prog.source) + programs[name] = prog services: dict[str, ServiceSpec] = {} for name, svc_data in data.get("services", {}).items(): @@ -230,7 +234,14 @@ def save_config(config: CastleConfig) -> None: if config.programs: data["programs"] = {} for name, spec in config.programs.items(): - data["programs"][name] = _spec_to_yaml_dict(spec) + d = _spec_to_yaml_dict(spec) + # Store relative source paths in YAML + if d.get("source") and Path(d["source"]).is_absolute(): + try: + d["source"] = str(Path(d["source"]).relative_to(config.root)) + except ValueError: + pass # not under root — keep absolute + data["programs"][name] = d if config.services: data["services"] = {} diff --git a/core/src/castle_core/manifest.py b/core/src/castle_core/manifest.py index 4a3c6e7..871a43b 100644 --- a/core/src/castle_core/manifest.py +++ b/core/src/castle_core/manifest.py @@ -185,6 +185,7 @@ class ProgramSpec(BaseModel): stack: str | None = None system_dependencies: list[str] = Field(default_factory=list) + install_extras: list[str] = Field(default_factory=list) version: str | None = None build: BuildSpec | None = None diff --git a/core/src/castle_core/stacks.py b/core/src/castle_core/stacks.py index ea257c1..852fe03 100644 --- a/core/src/castle_core/stacks.py +++ b/core/src/castle_core/stacks.py @@ -139,8 +139,11 @@ class PythonHandler(StackHandler): async def install(self, name: str, comp: ProgramSpec, root: Path) -> ActionResult: src = _source_dir(comp, root) + pkg_spec = str(src) + if comp.install_extras: + pkg_spec += "[" + ",".join(comp.install_extras) + "]" rc, output = await _run( - ["uv", "tool", "install", "--editable", str(src), "--force"], src + ["uv", "tool", "install", "--editable", pkg_spec, "--force"], src ) return ActionResult( component=name, action="install", status="ok" if rc == 0 else "error", output=output