"""Deployment screen, final deployment pipeline and LaunchAgent setup.""" import os import plistlib import subprocess import threading import tkinter as tk from pathlib import Path from tkinter import ttk, messagebox, filedialog from knoe import screen as ui from knoe.core.env import PROJECT_ROOT class DeployScreenMixin: """Deployment screen, final deployment pipeline and LaunchAgent setup.""" def _render_deploy_page(self): self._render_title("Deploy", y=40) self._render_paragraph( "Preparing to deploy. We will verify steps and perform actions as needed.", y=90, ) # Render simple list of steps (static view). Runtime updates can redraw as needed. y = 140 left = 56 if not hasattr(self, "deploy_steps"): self.deploy_steps = [ {"name": "Build Prole macOS app", "status": "pending"}, {"name": "Check Docker is running", "status": "pending"}, {"name": "Install Prole Tools.app", "status": "pending"}, ] for step in self.deploy_steps: # status circle (pending empty) self._canvas_items.append( ui.canvas_oval(self, left, y, left + 18, y + 18, outline="#b0b0b0") ) self._canvas_items.append( ui.canvas_text( self, left + 26, y - 2, step["name"], fill="#1d1d1f", font=("Helvetica", 12), ) ) y += 26 def on_deploy(self): if getattr(self, "_deploy_running", False): return self._deploy_running = True try: if self.deploy_button: self.deploy_button.configure(state="disabled") except Exception: pass def worker(): err = None try: self.ensure_prole_env() self.reload_env_from_shell() except Exception as e: err = f"Failed to load environment: {e}" try: self._ensure_prole_directories() if "Install" in self.prole_cfg_data: self.prole_cfg_data["Install"]["STATUS"] = "Deployed" self._save_prole_cfg() except Exception as e: err = err or f"Failed to prepare Prole directories: {e}" if err: self.safe_after(lambda: messagebox.showerror("Deploy", err)) else: self.safe_after(self.open_drag_install_window) def _finish(): self._deploy_running = False try: if self.deploy_button: self.deploy_button.configure(state="normal") except Exception: pass self.safe_after(_finish) threading.Thread(target=worker, daemon=True).start() def on_launch(self): app_path = self._get_prole_dist_dir() / "Prole Tools.app" if app_path.exists(): try: subprocess.Popen(["open", str(app_path)]) except Exception: pass else: try: messagebox.showerror( "Launch", f"App not found at {app_path}. Please build it first." ) except Exception: pass def run_final_deployment(self): initial_dir = str(self._resolve_env_dir("PROLE_DATA", "data")) try: if not Path(initial_dir).exists(): initial_dir = str(Path.home()) except Exception: initial_dir = str(Path.home()) export_base = filedialog.askdirectory( initialdir=initial_dir, title="Select deployment export directory" ) if not export_base: self.safe_after( lambda: ( self.bg_canvas.itemconfig( self._save_deployment_status_label, text="Save canceled.", fill="#6e6e73", ) if hasattr(self, "_save_deployment_status_label") and self.bg_canvas.winfo_exists() else None ) ) return export_base = str(Path(export_base).expanduser()) docker_export_dir = str(Path(export_base) / "docker-import") helm_export_dir = str(Path(export_base) / "helm-chart") kustomize_export_dir = str(Path(export_base) / "kustomize") def worker(): self.safe_after( lambda: ( self._save_deployment_button.configure(state="disabled") if hasattr(self, "_save_deployment_button") and self._save_deployment_button.winfo_exists() else None ) ) self.safe_after( lambda: ( self.bg_canvas.itemconfig( self._save_deployment_status_label, text="Saving deployment...", fill="blue", ) if hasattr(self, "_save_deployment_status_label") and self.bg_canvas.winfo_exists() else None ) ) # Use the deploy console on the services screen for visible output console = getattr(self, "_cnpg_deploy_console", None) # Fall back to install_consoles tab if available script_name = "final_deployment.sh" if not console: console = getattr(self, "install_consoles", {}).get(script_name) if ( hasattr(self, "install_consoles") and script_name in self.install_consoles ): self.safe_after( lambda: ( self.install_tabs.select( self.install_consoles[script_name].master ) if hasattr(self, "install_tabs") and self.install_tabs.winfo_exists() else None ) ) env = os.environ.copy() env["PROLE_HOME"] = str(PROJECT_ROOT) env["DOCKER_IMPORT_DIR"] = docker_export_dir env["HELM_CHART_DIR"] = helm_export_dir env["KUSTOMIZE_DIR"] = kustomize_export_dir if console: console.clear() console.write("========================================\n") console.write("Save Deployment\n") console.write("========================================\n\n") console.write(f"Export base: {export_base}\n") console.write(f"Docker images: {docker_export_dir}\n") console.write(f"Helm chart: {helm_export_dir}\n") console.write(f"Kustomize: {kustomize_export_dir}\n\n") def _on_line(line): if console: console.write(line) # Stage 1: Docker export, Helm chart, Kustomize if console: console.write("==> Stage 1: Exporting deployment artifacts...\n\n") rc = self.controller.run_script( script_name, args=[ "--docker-export", "--helm-chart", "--kustomize", "--output-dir", export_base, ], env=env, on_line=_on_line, ) if rc != 0: if console: console.write(f"\n[ERROR] Artifact export failed with code {rc}\n") self.safe_after( lambda: ( self.bg_canvas.itemconfig( self._save_deployment_status_label, text=f"Failed with code {rc}", fill="#ff3b30", ) if hasattr(self, "_save_deployment_status_label") and self.bg_canvas.winfo_exists() else None ) ) self.safe_after( lambda: ( self._save_deployment_button.configure(state="normal") if hasattr(self, "_save_deployment_button") and self._save_deployment_button.winfo_exists() else None ) ) return # Stage 2: Prepare k3s pipeline (OpenTofu + ArgoCD) if console: console.write( "\n==> Stage 2: Preparing k3s pipeline (OpenTofu + ArgoCD)...\n\n" ) self.safe_after( lambda: ( self.bg_canvas.itemconfig( self._save_deployment_status_label, text="Preparing k3s pipeline...", fill="blue", ) if hasattr(self, "_save_deployment_status_label") and self.bg_canvas.winfo_exists() else None ) ) pipeline_rc = self.controller.run_script( script_name, args=["--prepare-k3s-pipeline"], env=env, on_line=_on_line ) if pipeline_rc != 0: if console: console.write( f"\n[WARN] k3s pipeline preparation failed with code {pipeline_rc}\n" ) console.write( "Deployment artifacts were saved, but pipeline staging had issues.\n" ) if console: console.write("\n========================================\n") console.write("Save Deployment Complete\n") console.write("========================================\n") console.write(f"\nArtifacts: {export_base}\n") console.write( f"Pipeline: {PROJECT_ROOT / 'deploy' / 'opentofu' / 'k3s'}\n" ) console.write( f"ArgoCD: {PROJECT_ROOT / 'deploy' / 'opentofu' / 'k3s' / 'argocd'}\n" ) if rc == 0 and pipeline_rc == 0: self.safe_after( lambda: ( self.bg_canvas.itemconfig( self._save_deployment_status_label, text="Deployment saved successfully.", fill="#34c759", ) if hasattr(self, "_save_deployment_status_label") and self.bg_canvas.winfo_exists() else None ) ) elif rc == 0: self.safe_after( lambda: ( self.bg_canvas.itemconfig( self._save_deployment_status_label, text="Saved (pipeline warnings)", fill="#ff9500", ) if hasattr(self, "_save_deployment_status_label") and self.bg_canvas.winfo_exists() else None ) ) else: self.safe_after( lambda: ( self.bg_canvas.itemconfig( self._save_deployment_status_label, text=f"Failed with code {rc}", fill="#ff3b30", ) if hasattr(self, "_save_deployment_status_label") and self.bg_canvas.winfo_exists() else None ) ) self.safe_after( lambda: ( self._save_deployment_button.configure(state="normal") if hasattr(self, "_save_deployment_button") and self._save_deployment_button.winfo_exists() else None ) ) threading.Thread(target=worker, daemon=True).start() def run_build_a_bao(self): def worker(): self.safe_after( lambda: ( self._build_a_bao_button.configure(state="disabled") if hasattr(self, "_build_a_bao_button") and self._build_a_bao_button.winfo_exists() else None ) ) self.safe_after( lambda: ( self.bg_canvas.itemconfig( self._build_a_bao_status_label, text="Saving secrets to OpenBao...", fill="blue", ) if hasattr(self, "_build_a_bao_status_label") and self.bg_canvas.winfo_exists() else None ) ) script_name = "build-a-bao.sh" if ( hasattr(self, "install_consoles") and script_name in self.install_consoles ): self.safe_after( lambda: ( self.install_tabs.select( self.install_consoles[script_name].master ) if hasattr(self, "install_tabs") and self.install_tabs.winfo_exists() else None ) ) env = os.environ.copy() env["PROLE_HOME"] = str(PROJECT_ROOT) env["PROLE_SERVICE"] = str(PROJECT_ROOT) env["NAMESPACE"] = (self.db_namespace.get() or "").strip() console = getattr(self, "install_consoles", {}).get(script_name) if console: console.clear() console.write(f"Running {script_name} ...\n") def _on_line(line): if console: console.write(line) rc = self.controller.run_script(script_name, env=env, on_line=_on_line) if rc == 0: self._secrets_finalized = True self.safe_after( lambda: ( self.bg_canvas.itemconfig( self._build_a_bao_status_label, text="Secrets saved to OpenBao.", fill="#34c759", ) if hasattr(self, "_build_a_bao_status_label") and self.bg_canvas.winfo_exists() else None ) ) else: self.safe_after( lambda: ( self.bg_canvas.itemconfig( self._build_a_bao_status_label, text=f"Failed with code {rc}", fill="#ff3b30", ) if hasattr(self, "_build_a_bao_status_label") and self.bg_canvas.winfo_exists() else None ) ) self.safe_after( lambda: ( self._build_a_bao_button.configure(state="normal") if hasattr(self, "_build_a_bao_button") and self._build_a_bao_button.winfo_exists() else None ) ) threading.Thread(target=worker, daemon=True).start() def create_deploy_screen(self): """Create the Deploy screen""" frame = tk.Frame(self.content_area, bg="#1a1a1a") self.screens["deploy"] = frame # Title title = ttk.Label(frame, text="Build and Deploy", style="Title.TLabel") title.pack(pady=(0, 30)) # Instructions instructions = tk.Label( frame, text="Build and deploy Prole services to k3d cluster", bg="#1a1a1a", fg="#aaaaaa", font=("Helvetica", 11), ) instructions.pack(pady=(0, 20)) # Environment selector env_frame = tk.Frame(frame, bg="#1a1a1a") env_frame.pack(pady=(0, 10), fill="x") env_label = tk.Label( env_frame, text="Target Environment:", bg="#1a1a1a", fg="#dddddd", font=("Helvetica", 11), ) env_label.pack(side="left", padx=(0, 10)) self.deploy_environment = tk.StringVar(value="Dev") env_combo = ttk.Combobox( env_frame, textvariable=self.deploy_environment, values=["Dev", "Service", "Prod"], state="readonly", width=18, ) env_combo.pack(side="left") env_help = tk.Label( env_frame, text="Dev = local k3d (knoe-dev-cluster), Service = remote k3s at pi.prole.org:6443, Prod = stretch of prole-service-cluster", bg="#1a1a1a", fg="#888888", font=("Helvetica", 9), ) env_help.pack(side="left", padx=(12, 0)) # Deploy button deploy_btn = tk.Button( frame, text="Build and Deploy", command=self.start_deployment, bg="#4a9eff", fg="white", activebackground="#3a8eef", font=("Helvetica", 16, "bold"), padx=40, pady=20, cursor="hand2", relief="flat", ) deploy_btn.pack(pady=20) # Progress frame progress_frame = tk.Frame(frame, bg="#1a1a1a") progress_frame.pack(fill="both", expand=True, pady=20) # Progress list # Initialize deploy steps; some names are updated dynamically when deployment starts self.deploy_steps = [ {"name": "Build ProleStatus macOS app", "status": "pending"}, {"name": "Check Docker is running", "status": "pending"}, {"name": "Check or configure container registry", "status": "pending"}, {"name": "Ensure target cluster", "status": "pending"}, {"name": "Build knoe-db Docker image", "status": "pending"}, {"name": "Tag Docker image for registry", "status": "pending"}, {"name": "Push image to registry", "status": "pending"}, {"name": "Import image to k3d cluster (Dev only)", "status": "pending"}, ] self.deploy_widgets = {} for step in self.deploy_steps: self.create_deploy_step_widget(progress_frame, step) def _create_page_deploy(self): f = self._page_container() ttk.Label(f, text="Deploy", style="Title.TLabel").pack( anchor="w", padx=24, pady=(24, 6) ) ttk.Label( f, text="Preparing to deploy. We will verify steps and perform actions as needed.", style="Body.TLabel", wraplength=800, ).pack(anchor="w", padx=24) # Reuse existing deploy steps UI but on light theme self.deploy_steps = [ {"name": "Build Prole macOS app", "status": "pending"}, {"name": "Check Docker is running", "status": "pending"}, {"name": "Check or configure container registry", "status": "pending"}, {"name": "Ensure target cluster", "status": "pending"}, {"name": "Build knoe-db Docker image", "status": "pending"}, {"name": "Tag Docker image for registry", "status": "pending"}, {"name": "Push image to registry", "status": "pending"}, {"name": "Import image to k3d cluster (Dev only)", "status": "pending"}, { "name": "Install LaunchAgent for port-forwards (Dev)", "status": "pending", }, ] self.deploy_widgets = {} container = ttk.Frame(f) container.pack(fill="both", expand=True, padx=16, pady=8) for step in self.deploy_steps: self._create_deploy_row(container, step) # self._register_page('deploy', f) def create_deploy_step_widget(self, parent, step): """Create a widget for a deployment step""" step_frame = tk.Frame(parent, bg="#2a2a2a", relief="flat") step_frame.pack(fill="x", pady=5, padx=10) # Status indicator status_canvas = tk.Canvas( step_frame, width=30, height=30, bg="#2a2a2a", highlightthickness=0 ) status_canvas.pack(side="left", padx=15, pady=15) # Step name name_label = tk.Label( step_frame, text=step["name"], bg="#2a2a2a", fg="#ffffff", font=("Helvetica", 11), anchor="w", ) name_label.pack(side="left", fill="x", expand=True, padx=10) # Status text status_label = tk.Label( step_frame, text="Pending", bg="#2a2a2a", fg="#aaaaaa", font=("Helvetica", 10), ) status_label.pack(side="right", padx=15) self.deploy_widgets[step["name"]] = { "canvas": status_canvas, "label": status_label, "step": step, } # Draw initial pending state self.update_deploy_step_status(step["name"], "pending") def _create_deploy_row(self, parent, step): row = ttk.Frame(parent) row.pack(fill="x", pady=6) canvas = tk.Canvas(row, width=20, height=20, highlightthickness=0) canvas.pack(side="left", padx=8) lbl = ttk.Label(row, text=step["name"], style="Body.TLabel") lbl.pack(side="left") status = ttk.Label(row, text="Pending", style="Dim.TLabel") status.pack(side="right", padx=8) self.deploy_widgets[step["name"]] = { "canvas": canvas, "label": status, "step": step, } self._draw_status(canvas, "pending") def update_deploy_step_status(self, step_name, status): """Update the status of a deployment step""" widget = self.deploy_widgets[step_name] canvas = widget["canvas"] label = widget["label"] step = widget["step"] step["status"] = status canvas.delete("all") if status == "pending": ui.canvas_oval_on(canvas, 5, 5, 25, 25, outline="#666", width=2) label.configure(text="Pending", fg="#aaaaaa") elif status == "running": ui.canvas_oval_on( canvas, 5, 5, 25, 25, outline="#ffaa00", width=2, fill="#ffaa00" ) label.configure(text="Running...", fg="#ffaa00") elif status == "completed": ui.canvas_oval_on( canvas, 5, 5, 25, 25, outline="#28a745", width=2, fill="#28a745" ) ui.canvas_text_on( canvas, 15, 15, "✓", fill="white", font=("Helvetica", 16, "bold") ) label.configure(text="Completed", fg="#28a745") elif status == "error": ui.canvas_oval_on( canvas, 5, 5, 25, 25, outline="#dc3545", width=2, fill="#dc3545" ) ui.canvas_text_on( canvas, 15, 15, "✗", fill="white", font=("Helvetica", 16, "bold") ) label.configure(text="Error", fg="#dc3545") elif status == "skipped": ui.canvas_oval_on( canvas, 5, 5, 25, 25, outline="#666", width=2, fill="#444444" ) label.configure(text="Skipped", fg="#888888") def start_deployment(self): """Start the deployment process""" threading.Thread(target=self.run_deployment, daemon=True).start() def generate_prole_properties(self, env: str): """Dynamically generate prole-tools-app/prole.properties based on environment""" props_path = Path("prole-tools-app/prole.properties") env_key = (env or "").strip().lower() dev_mode = env_key == "dev" content = f"""# Prole default endpoints (dynamically generated by install.py) # UI assets icon=img/proleIcon.png background=img/proleLogoSepia.png # Dev port-forward supervision pf.enabled={str(dev_mode).lower()} """ if dev_mode: content += """# Service endpoints (7 traffic lights) svc.1.name=K3D svc.1.host=localhost svc.1.port=6443 svc.2.name=Prometheus svc.2.host=localhost svc.2.port=9090 svc.3.name=Grafana svc.3.host=localhost svc.3.port=3000 svc.4.name=OpenBAO svc.4.host=localhost svc.4.port=8200 svc.5.name=PostgreSQL svc.5.host=localhost svc.5.port=5432 svc.6.name=Kong svc.6.host=localhost svc.6.port=8000 svc.7.name=CertManager svc.7.host=localhost svc.7.port=9402 """ else: content += """# Service/Prod endpoint mappings are intentionally not persisted here. # Live service endpoints are discovered from the running cluster. """ content += f"""# Kerberos configuration kerberos.enabled={str(self.kerberos_enabled.get()).lower()} kerberos.realm={self.kerberos_realm.get()} kerberos.user={self.kerberos_user.get()} kerberos.kdc={self.kerberos_kdc.get()} """ props_path.write_text(content) print(f"Generated {props_path} for {env} environment") def run_deployment(self): """Run the deployment steps""" try: # Capture environment selection and prepare dynamic labels env_selected = (self.deploy_environment.get() or "").strip().lower() env = {"dev": "Dev", "service": "Service", "prod": "Prod"}.get( env_selected, "Dev" ) # Update step labels to reflect environment self.deploy_widgets["Ensure target cluster"]["step"][ "name" ] = f"Ensure target cluster ({env})" # Use the correct way to update the label text in the UI self.deploy_widgets["Ensure target cluster"][ "label" ].master.winfo_children()[1].configure( text=f"Ensure target cluster ({env})" ) # Step 0: Generate prole.properties self.generate_prole_properties(env) # Step 1: Build Prole macOS app self.update_deploy_step_status("Build Prole macOS app", "running") self.build_prole_app() self.update_deploy_step_status("Build Prole macOS app", "completed") # Step 1: Check Docker self.update_deploy_step_status("Check Docker is running", "running") if not self.check_docker_running(): self.update_deploy_step_status("Check Docker is running", "error") messagebox.showerror( "Error", "Docker is not running. Please start Docker Desktop." ) return self.update_deploy_step_status("Check Docker is running", "completed") # Step 2: Registry discovery/config self.update_deploy_step_status( "Check or configure container registry", "running" ) self.registry_url = self.ensure_registry_available(env) self.update_deploy_step_status( "Check or configure container registry", "completed" ) # Step 3: Ensure target cluster as per environment self.update_deploy_step_status("Ensure target cluster", "running") self.create_or_select_cluster(env) self.update_deploy_step_status("Ensure target cluster", "completed") # Step 3: Build Docker image self.update_deploy_step_status("Build knoe-db Docker image", "running") self.build_docker_image() self.update_deploy_step_status("Build knoe-db Docker image", "completed") # Step 4: Tag image self.update_deploy_step_status("Tag Docker image for registry", "running") self.tag_docker_image() self.update_deploy_step_status("Tag Docker image for registry", "completed") # Step 5: Push to registry self.update_deploy_step_status("Push image to registry", "running") self.push_docker_image() self.update_deploy_step_status("Push image to registry", "completed") # Step 6: Import to k3d (Dev only) if env == "Dev": self.update_deploy_step_status( "Import image to k3d cluster (Dev only)", "running" ) self.import_k3d_image(cluster_name="knoe-dev-cluster") self.update_deploy_step_status( "Import image to k3d cluster (Dev only)", "completed" ) else: self.update_deploy_step_status( "Import image to k3d cluster (Dev only)", "skipped" ) # Step 7: Install LaunchAgent for port-forwards (Dev) if env == "Dev": self.update_deploy_step_status( "Install LaunchAgent for port-forwards (Dev)", "running" ) ok = self.install_launchagent_port_forwards() if ok: self.update_deploy_step_status( "Install LaunchAgent for port-forwards (Dev)", "completed" ) else: self.update_deploy_step_status( "Install LaunchAgent for port-forwards (Dev)", "error" ) return else: self.update_deploy_step_status( "Install LaunchAgent for port-forwards (Dev)", "skipped" ) messagebox.showinfo("Success", "Deployment completed successfully!") except Exception as e: messagebox.showerror("Error", f"Deployment failed: {str(e)}") def install_launchagent_port_forwards(self) -> bool: """Create/update the user LaunchAgent and helper script to manage kubectl port-forwards. - Helper script: ~/Library/Application Support/Prole/bin/prole-kpf.sh - LaunchAgent: ~/Library/LaunchAgents/org.prole.knoe-db.kpf-dev.plist """ try: home = Path.home() bin_dir = home / "Library" / "Application Support" / "Prole" / "bin" run_dir = home / "Library" / "Application Support" / "Prole" / "run" plist_path = ( home / "Library" / "LaunchAgents" / "org.prole.knoe-db.kpf-dev.plist" ) script_path = bin_dir / "prole-kpf.sh" bin_dir.mkdir(parents=True, exist_ok=True) run_dir.mkdir(parents=True, exist_ok=True) plist_path.parent.mkdir(parents=True, exist_ok=True) helper_script = """#!/bin/sh set -eu LABEL="org.prole.knoe-db.kpf-dev" PLIST="$HOME/Library/LaunchAgents/$LABEL.plist" RUNDIR="$HOME/Library/Application Support/Prole/run" PIDFILE="$RUNDIR/kpf.pids" ensure_rundir() { mkdir -p "$RUNDIR" } list_cmds() { # Enumerate ProleCommands array using PlistBuddy if /usr/libexec/PlistBuddy -c "Print :ProleCommands" "$PLIST" >/dev/null 2>&1; then i=0 while true; do if ! val=$(/usr/libexec/PlistBuddy -c "Print :ProleCommands:$i" "$PLIST" 2>/dev/null); then break fi echo "$val" i=$((i+1)) done fi } start() { ensure_rundir : > "$PIDFILE" IFS='\n' for cmd in $(list_cmds); do [ -z "$cmd" ] && continue (sh -lc "$cmd") & echo $! >> "$PIDFILE" done wait } stop() { if [ -f "$PIDFILE" ]; then while read -r pid; do [ -z "$pid" ] && continue kill "$pid" 2>/dev/null || true done < "$PIDFILE" rm -f "$PIDFILE" fi } status() { if [ ! -f "$PIDFILE" ]; then echo "not running" exit 3 fi alive=0 total=0 while read -r pid; do [ -z "$pid" ] && continue total=$((total+1)) if kill -0 "$pid" 2>/dev/null; then alive=$((alive+1)); fi done < "$PIDFILE" echo "$alive/$total running" } case "${1:-}" in start) start ;; stop) stop ;; restart) stop; start ;; status) status ;; *) echo "Usage: $0 {start|stop|restart|status}" >&2; exit 2 ;; esac """ script_path.write_text(helper_script) os.chmod(script_path, 0o755) # Default commands for Dev environment (k3d) default_cmds = [ "kubectl port-forward -n monitoring svc/kps-kube-prometheus-stack-prometheus 9090", "kubectl -n kubernetes-dashboard port-forward svc/kubernetes-dashboard-kong-proxy 8443:443", "kubectl port-forward svc/knoe-db-rw 5432:5432 --address 0.0.0.0", "kubectl port-forward -n monitoring svc/kps-grafana 3000:80 --address 0.0.0.0", ] # Create plist dict from plistlib import dumps as plist_dumps plist_dict = { "Label": "org.prole.knoe-db.kpf-dev", "RunAtLoad": True, "KeepAlive": True, # Ensure PATH has common locations for kubectl "EnvironmentVariables": { "PATH": os.environ.get("PATH", "/usr/local/bin:/usr/bin:/bin") }, "StandardOutPath": str( home / "Library" / "Logs" / "org.prole.knoe-db.kpf-dev.out.log" ), "StandardErrorPath": str( home / "Library" / "Logs" / "org.prole.knoe-db.kpf-dev.err.log" ), "ProgramArguments": [str(script_path), "start"], "ProleCommands": default_cmds, } plist_bytes = plist_dumps(plist_dict) plist_path.write_bytes(plist_bytes) # Reload the agent uid = os.getuid() # Try to kickstart first subprocess.run( [ "launchctl", "kickstart", "-k", f"gui/{uid}/org.prole.knoe-db.kpf-dev", ], capture_output=True, ) # Bootout and bootstrap to ensure it's loaded, then kickstart subprocess.run( [ "launchctl", "bootout", f"gui/{uid}", f"gui/{uid}/org.prole.knoe-db.kpf-dev", ], capture_output=True, ) subprocess.run( ["launchctl", "bootstrap", f"gui/{uid}", str(plist_path)], check=True ) subprocess.run( [ "launchctl", "kickstart", "-k", f"gui/{uid}/org.prole.knoe-db.kpf-dev", ], check=True, ) return True except Exception as e: print(f"LaunchAgent setup failed: {e}") return False def _draw_status(self, canvas, status): ui.canvas_clear(canvas) if status == "success": ui.canvas_oval_on(canvas, 2, 2, 18, 18, fill="#34c759", outline="") ui.canvas_line_on(canvas, 5, 10, 9, 14, fill="white", width=2) ui.canvas_line_on(canvas, 9, 14, 16, 6, fill="white", width=2) elif status == "running": ui.canvas_oval_on(canvas, 2, 2, 18, 18, fill="#ffd60a", outline="") elif status == "error": ui.canvas_oval_on(canvas, 2, 2, 18, 18, fill="#ff3b30", outline="") else: ui.canvas_oval_on(canvas, 2, 2, 18, 18, outline="#b0b0b0") # --------------- Dependency helpers ---------------