/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2025-2026, Leafscale, LLC - https://www.leafscale.com Project: zygctl Filename: main.reef Authors: Chris Tusa License: Description: Administrative CLI for zyginit service manager ******************************************************************************/ module zygctl import sys.args as args import sys.env as env import sys.process as process import net.unix as unix import io.file import io.dir import core.str import version extern "C" fn zyginit_symlink(target: string, linkpath: string): int extern "C" fn zyginit_isatty(fd: int): int fn client_should_be_plain(): bool if env.has_env("NO_COLOR") return true end if if zyginit_isatty(1) == 0 return true end if let term = env.get_env_or("TERM", "") if str.length(term) == 0 return true end if if term == "dumb" return true end if return false end client_should_be_plain fn SOCKET_PATH(): string return env.get_env_or("ZYGINIT_SOCKET", "/var/run/zyginit.sock") end SOCKET_PATH fn CONFIG_DIR(): string return env.get_env_or("ZYGINIT_CONFIG_DIR", "/etc/zyginit") end CONFIG_DIR proc main() let argc = args.count() if argc < 2 print_usage() return end if let command = args.get(1) // Local commands (no daemon needed) if command == "help" or command == "--help" or command == "-h" print_usage() return end if if command == "version" or command == "--version" println("zygctl " + version.VERSION()) return end if // Local commands that operate on the filesystem directly if command == "enable" if argc < 3 println("error: usage: zygctl enable ") return end if cmd_enable(args.get(2)) return end if if command == "disable" if argc < 3 println("error: usage: zygctl disable ") return end if cmd_disable(args.get(2)) return end if // single/multi are forwarded to zyginit's socket — handled there. // (Earlier versions of zygctl had local stubs for these; removed // now that the daemon implements runlevel transitions.) // TTY-aware dispatch for status and list: // When stdout is not a TTY (piped/redirected), or when NO_COLOR is set, // append PLAIN so the daemon sends plain text without ANSI escapes. if command == "status" mut req = "status" if client_should_be_plain() req = "status plain" end if if argc >= 3 req = str.concat(req, " ") req = str.concat(req, args.get(2)) end if req = str.concat(req, "\n") let fd = unix.unix_connect(SOCKET_PATH()) if fd < 0 println("zygctl: cannot connect to zyginit at " + SOCKET_PATH()) println("zygctl: is zyginit running?") return end if unix.unix_send(fd, req) let resp = unix.unix_recv(fd, 4096) unix.unix_close(fd) if str.length(resp) > 0 print(resp) end if return end if if command == "list" mut req = "list" if client_should_be_plain() req = "list plain" end if req = str.concat(req, "\n") let fd = unix.unix_connect(SOCKET_PATH()) if fd < 0 println("zygctl: cannot connect to zyginit at " + SOCKET_PATH()) println("zygctl: is zyginit running?") return end if unix.unix_send(fd, req) let resp = unix.unix_recv(fd, 4096) unix.unix_close(fd) if str.length(resp) > 0 print(resp) end if return end if // Build the message to send to zyginit mut message = command if argc >= 3 message = str.concat(message, " ") message = str.concat(message, args.get(2)) end if message = str.concat(message, "\n") // Connect to zyginit socket let sock_path = SOCKET_PATH() let fd = unix.unix_connect(sock_path) if fd < 0 println("zygctl: cannot connect to zyginit at " + sock_path) println("zygctl: is zyginit running?") return end if // Send command unix.unix_send(fd, message) // Read response let response = unix.unix_recv(fd, 4096) unix.unix_close(fd) if str.length(response) > 0 print(response) end if end main // ============================================================================ // Local commands — enable/disable (filesystem operations, no daemon needed) // ============================================================================ proc cmd_enable(name: string) let config_dir = CONFIG_DIR() let toml_path = str.concat(str.concat(config_dir, "/"), str.concat(name, ".toml")) if not file.fileExists(toml_path) println("error: no service definition found: " + toml_path) return end if let enabled_dir = str.concat(config_dir, "/enabled.d") // Create enabled.d/ if it does not exist if not dir.dir_exists(enabled_dir) if not dir.create_dir(enabled_dir) println("error: cannot create " + enabled_dir) return end if end if let link_path = str.concat(str.concat(enabled_dir, "/"), name) if file.fileExists(link_path) println(name + " is already enabled") return end if // Create symlink: enabled.d/ -> ../.toml let target = str.concat("../", str.concat(name, ".toml")) let rc = zyginit_symlink(target, link_path) if rc < 0 println("error: failed to create symlink for " + name) return end if println(name + " enabled") end cmd_enable proc cmd_disable(name: string) let config_dir = CONFIG_DIR() let enabled_dir = str.concat(config_dir, "/enabled.d") let link_path = str.concat(str.concat(enabled_dir, "/"), name) if not file.fileExists(link_path) println(name + " is not enabled") return end if if file.deleteFile(link_path) println(name + " disabled") else println("error: failed to remove " + link_path) end if end cmd_disable // ============================================================================ // Usage // ============================================================================ proc print_usage() println("zygctl — zyginit service control") println("") println("Usage: zygctl [argument]") println("") println("Commands:") println(" status [name] Show service status (all or one)") println(" start Start a service") println(" stop Stop a service") println(" restart Restart a service") println(" enable Enable a service (symlink in enabled.d/)") println(" disable Disable a service (remove from enabled.d/)") println(" reload Reload service configuration") println(" replace [--wait=N]") println(" Replace running zyginit via execve(/sbin/init);") println(" keeps services alive across the swap") println(" log Show service output log") println(" list List all service definitions") println(" halt Cleanly stop all services and halt the system") println(" reboot Cleanly stop all services and reboot") println(" poweroff Cleanly stop all services and power off") println(" single Transition to single-user (recovery) mode") println(" multi Transition to multi-user mode") println(" version Show version") println(" help Show this help") println("") println("Environment:") println(" ZYGINIT_SOCKET Socket path (default: /var/run/zyginit.sock)") println(" ZYGINIT_CONFIG_DIR Config directory (default: /etc/zyginit)") end print_usage end module