/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2026, Leafscale, LLC - https://www.leafscale.com Project: repoman Filename: src/incus.reef Authors: Chris Tusa License: Description: Wrappers over the incus CLI (launch, delete, profile ops, device add with shift) ******************************************************************************/ module incus import core.str import sys.process as p import sys.fd as fd import core.result_generic as rg import core.convert as convert export type CaptureResult fn process_run_capture(program: string, args: [string]): CaptureResult fn process_run_silent(program: string, args: [string]): int fn validate_name(name: string): bool fn project_ensure(project: string, verbose: bool): rg.Result[bool, string] fn project_present(project: string): rg.Result[bool, string] fn container_exists(project: string, name: string, verbose: bool): rg.Result[bool, string] fn launch(project: string, name: string, image: string, profiles: [string]): rg.Result[bool, string] fn device_add_disk(project: string, name: string, dev: string, src: string, dst: string): rg.Result[bool, string] fn set_env_var(project: string, name: string, key: string, val: string): rg.Result[bool, string] fn restart(project: string, name: string): rg.Result[bool, string] fn container_state(project: string, name: string): rg.Result[string, string] fn delete_container(project: string, name: string): rg.Result[bool, string] fn delete_profile(project: string, name: string): rg.Result[bool, string] fn host_uid(): rg.Result[string, string] fn profile_exists(project: string, name: string): rg.Result[bool, string] fn profile_create_or_edit(project: string, name: string, yaml: string): rg.Result[bool, string] fn profile_get(project: string, name: string): rg.Result[string, string] fn device_add_disk_opts(project: string, name: string, dev: string, src: string, dst: string, opts: [string]): rg.Result[bool, string] end export // Spawn a program with both stdout and stderr redirected to /dev/null in the // child. Used for existence-check probes where the upstream command's error // output would confuse the user (e.g., `incus project show foo` writing // "Error: Project not found" to stderr when foo doesn't exist — we already // know that, the missing-ness is the signal we wanted). // // Implementation: fork → in child, open /dev/null and dup2 it over fd 1/2, // then exec the program. Returns the child PID; caller waits as usual. fn process_run_silent(program: string, args: [string]): int let pid: int = p.process_fork() if pid < 0 return -1 end if if pid == 0 let dev_null: int = fd.fd_open("/dev/null", fd.O_WRONLY(), 0) if dev_null >= 0 let _o: int = fd.fd_dup2(dev_null, fd.STDOUT()) let _e: int = fd.fd_dup2(dev_null, fd.STDERR()) let _c: int = fd.fd_close(dev_null) end if let _x: int = p.process_run_exec(program, args) p.exit_now(127) end if return pid end process_run_silent type CaptureResult = struct exit_code: int stdout: string end CaptureResult // Spawn `program args` and capture the child's stdout to a string. // stderr passes through to parent terminal. Returns exit code + stdout. // On any setup failure (fork, pipe, exec), exit_code is non-zero and // stdout is empty. fn process_run_capture(program: string, args: [string]): CaptureResult let pipe_fds: [int] = fd.fd_pipe() if pipe_fds.length() != 2 return CaptureResult { exit_code: -1, stdout: "" } end if let read_fd: int = pipe_fds[0] let write_fd: int = pipe_fds[1] let pid: int = p.process_fork() if pid < 0 let _r: int = fd.fd_close(read_fd) let _w: int = fd.fd_close(write_fd) return CaptureResult { exit_code: -1, stdout: "" } end if if pid == 0 // Child: dup write end of pipe over stdout, close both ends, exec. let _o: int = fd.fd_dup2(write_fd, fd.STDOUT()) let _c1: int = fd.fd_close(read_fd) let _c2: int = fd.fd_close(write_fd) let _x: int = p.process_run_exec(program, args) p.exit_now(127) end if // Parent: close write end, read until EOF, wait. let _cw: int = fd.fd_close(write_fd) mut output: string = "" let chunk_size: int = 4096 mut keep_reading: bool = true while keep_reading let chunk: string = fd.fd_read(read_fd, chunk_size) if str.length(chunk) == 0 keep_reading = false else output = output + chunk end if end while let _cr: int = fd.fd_close(read_fd) let exit: int = p.process_wait(pid) return CaptureResult { exit_code: exit, stdout: output } end process_run_capture // Spawn `program args` with the given string written to the child's stdin. // stdout/stderr inherit the parent terminal. Returns exit code. // On any setup failure (fork, pipe, exec), returns non-zero. fn process_run_with_stdin(program: string, args: [string], input: string): int let pipe_fds: [int] = fd.fd_pipe() if pipe_fds.length() != 2 return -1 end if let read_fd: int = pipe_fds[0] let write_fd: int = pipe_fds[1] let pid: int = p.process_fork() if pid < 0 let _r: int = fd.fd_close(read_fd) let _w: int = fd.fd_close(write_fd) return -1 end if if pid == 0 // Child: dup read end of pipe over stdin, close both ends, exec. let _i: int = fd.fd_dup2(read_fd, fd.STDIN()) let _c1: int = fd.fd_close(read_fd) let _c2: int = fd.fd_close(write_fd) let _x: int = p.process_run_exec(program, args) p.exit_now(127) end if // Parent: close read end, write input, close write, wait. let _cr: int = fd.fd_close(read_fd) let _wn: int = fd.fd_write(write_fd, input) let _cw: int = fd.fd_close(write_fd) return p.process_wait(pid) end process_run_with_stdin // Query incus for the container's state. Uses `incus list --project

// --format csv -c s ` which outputs just the STATE column ("RUNNING", // "STOPPED", etc.) for matching containers. Empty output (no match) means // the container doesn't exist in this project; we return "MISSING". fn container_state(project: string, name: string): rg.Result[string, string] let cap: CaptureResult = process_run_capture("incus", [ "list", "--project", project, "--format", "csv", "-c", "s", name ]) if cap.exit_code != 0 return @Result[string, string].Err("incus list exited with code " + convert.to_string(cap.exit_code)) end if // Trim trailing newline + whitespace from stdout. let trimmed: string = str.trim_ws(cap.stdout) if str.length(trimmed) == 0 return @Result[string, string].Ok("MISSING") end if return @Result[string, string].Ok(trimmed) end container_state // Get the current process's effective UID as a decimal string. Used by // `repoman shell` to pass --user to incus exec so the in-container shell // runs as the host user (matched against claude-share's UID-mapping). // Reef stdlib doesn't expose getuid() publicly today, so we shell out to // `id -u` and parse stdout. fn host_uid(): rg.Result[string, string] let cap: CaptureResult = process_run_capture("id", ["-u"]) if cap.exit_code != 0 return @Result[string, string].Err("id -u exited with code " + convert.to_string(cap.exit_code)) end if let trimmed: string = str.trim_ws(cap.stdout) if str.length(trimmed) == 0 return @Result[string, string].Err("id -u returned empty output") end if return @Result[string, string].Ok(trimmed) end host_uid fn is_lower_alnum_or_hyphen(c: char): bool if c >= 'a' and c <= 'z' return true end if if c >= '0' and c <= '9' return true end if if c == '-' return true end if return false end is_lower_alnum_or_hyphen fn validate_name(name: string): bool let n: int = str.length(name) if n == 0 return false end if if n > 63 return false end if if name[0] == '-' return false end if mut i: int = 0 while i < n if not is_lower_alnum_or_hyphen(name[i]) return false end if i = i + 1 end while return true end validate_name // Run `incus `. Returns Ok(true) on exit 0, Err with a brief diagnostic // on non-zero. Stderr inherits the parent terminal. fn run_incus(args: [string]): rg.Result[bool, string] let pid: int = p.process_run("incus", args) if pid < 0 return @Result[bool, string].Err("failed to spawn 'incus' (is it installed?)") end if let exit: int = p.process_wait(pid) if exit == 0 return @Result[bool, string].Ok(true) end if return @Result[bool, string].Err("incus exited with code " + convert.to_string(exit)) end run_incus fn project_ensure(project: string, verbose: bool): rg.Result[bool, string] // `incus project show ` exits 0 if it exists, non-0 otherwise. // Quiet mode silences the YAML dump on success and the "Error: // Project not found" stderr on failure; verbose mode passes through. mut pid: int = -1 if verbose pid = p.process_run("incus", ["project", "show", project]) else pid = process_run_silent("incus", ["project", "show", project]) end if let exit: int = p.process_wait(pid) if exit == 0 return @Result[bool, string].Ok(true) end if // Create with all features disabled so the project is a pure container // namespace and shares images/profiles/networks/storage with the default // project. Without this, profiles like 'claude-share' that live in the // default project would be invisible to containers in this project. return run_incus([ "project", "create", project, "-c", "features.images=false", "-c", "features.profiles=false", "-c", "features.networks=false", "-c", "features.storage.volumes=false", "-c", "features.storage.buckets=false" ]) end project_ensure fn project_present(project: string): rg.Result[bool, string] let pid: int = process_run_silent("incus", ["project", "show", project]) if pid < 0 return @Result[bool, string].Err("failed to spawn 'incus project show'") end if let exit: int = p.process_wait(pid) if exit == 0 return @Result[bool, string].Ok(true) end if return @Result[bool, string].Ok(false) end project_present fn container_exists(project: string, name: string, verbose: bool): rg.Result[bool, string] // `incus info --project

` exits 0 if it exists. // Quiet silences both info dump and error output; verbose passes through. mut pid: int = -1 if verbose pid = p.process_run("incus", ["info", "--project", project, name]) else pid = process_run_silent("incus", ["info", "--project", project, name]) end if let exit: int = p.process_wait(pid) if exit == 0 return @Result[bool, string].Ok(true) end if return @Result[bool, string].Ok(false) end container_exists fn launch(project: string, name: string, image: string, profiles: [string]): rg.Result[bool, string] let pn: int = profiles.length() // argv shape: ["launch", "--project", project, ("--profile" P)*, image, name] mut args: [string] = new [string](3 + 2 * pn + 2) args[0] = "launch" args[1] = "--project" args[2] = project mut i: int = 0 while i < pn args[3 + i * 2] = "--profile" args[3 + i * 2 + 1] = profiles[i] i = i + 1 end while args[3 + 2 * pn] = image args[3 + 2 * pn + 1] = name return run_incus(args) end launch fn device_add_disk(project: string, name: string, dev: string, src: string, dst: string): rg.Result[bool, string] return run_incus([ "config", "device", "add", "--project", project, name, dev, "disk", "source=" + src, "path=" + dst ]) end device_add_disk // Like device_add_disk but with extra options like ["shift=true", // "readonly=true"] appended to the incus argv. fn device_add_disk_opts(project: string, name: string, dev: string, src: string, dst: string, opts: [string]): rg.Result[bool, string] let on: int = opts.length() // Final argv: ["config", "device", "add", "--project", P, NAME, DEV, "disk", // "source=...", "path=...", opts...] mut args: [string] = new [string](10 + on) args[0] = "config" args[1] = "device" args[2] = "add" args[3] = "--project" args[4] = project args[5] = name args[6] = dev args[7] = "disk" args[8] = "source=" + src args[9] = "path=" + dst mut i: int = 0 while i < on args[10 + i] = opts[i] i = i + 1 end while return run_incus(args) end device_add_disk_opts fn set_env_var(project: string, name: string, key: string, val: string): rg.Result[bool, string] return run_incus([ "config", "set", "--project", project, name, "environment." + key + "=" + val ]) end set_env_var fn restart(project: string, name: string): rg.Result[bool, string] return run_incus(["restart", "--project", project, name]) end restart fn delete_container(project: string, name: string): rg.Result[bool, string] return run_incus(["delete", "--project", project, "--force", name]) end delete_container fn delete_profile(project: string, name: string): rg.Result[bool, string] return run_incus(["profile", "delete", "--project", project, name]) end delete_profile // Returns Ok(true) if the named profile exists in the given project, // Ok(false) otherwise. Errors only on subprocess failure. fn profile_exists(project: string, name: string): rg.Result[bool, string] let pid: int = process_run_silent("incus", [ "profile", "show", "--project", project, name ]) if pid < 0 return @Result[bool, string].Err("failed to spawn 'incus profile show'") end if let exit: int = p.process_wait(pid) if exit == 0 return @Result[bool, string].Ok(true) end if return @Result[bool, string].Ok(false) end profile_exists // Idempotent: ensure the profile exists with the given YAML body. // Step 1: try `incus profile create` (no-op error if exists). // Step 2: `incus profile edit` reads stdin and replaces the profile body. fn profile_create_or_edit(project: string, name: string, yaml: string): rg.Result[bool, string] // Step 1: create (silent on failure — profile may already exist) let create_pid: int = process_run_silent("incus", [ "profile", "create", "--project", project, name ]) let _ce: int = p.process_wait(create_pid) // Step 2: edit with YAML on stdin let edit_exit: int = process_run_with_stdin("incus", [ "profile", "edit", "--project", project, name ], yaml) if edit_exit == 0 return @Result[bool, string].Ok(true) end if return @Result[bool, string].Err("incus profile edit exited with code " + convert.to_string(edit_exit)) end profile_create_or_edit // Returns the YAML body of the named profile (the same output as // `incus profile show `). Errors if the profile doesn't // exist or the subprocess fails. fn profile_get(project: string, name: string): rg.Result[string, string] let cap: CaptureResult = process_run_capture("incus", [ "profile", "show", "--project", project, name ]) if cap.exit_code != 0 return @Result[string, string].Err("incus profile show exited " + convert.to_string(cap.exit_code)) end if return @Result[string, string].Ok(cap.stdout) end profile_get end module