/****************************************************************************** __ ____ __ / / ___ ____ _/ __/_____________ _/ /__ / / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \ / /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/ /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ (C)opyright 2026, Leafscale, LLC - https://www.leafscale.com Project: repoman Filename: src/setup.reef Authors: Chris Tusa License: Description: First-time host bootstrap wizard (cmd_setup) ******************************************************************************/ module setup import core.str import core.result_generic as rg import io.console as console import io.dir as iodir import sys.env import sys.flag as flag import sys.process as p import incus import config import profile export type Environment fn detect_environment(home_dir: string): Environment fn detect_host_lan_ip(): string type Stage fn plan_stages(env: Environment): [Stage] fn apply_stage(stage: Stage, env: Environment, reg: config.Registry): rg.Result[config.Registry, string] fn cmd_setup(argv: [string]): int end export // Snapshot of the host state that `setup` cares about. type Environment = struct home_dir: string user: string host_lan_ip: string incus_reachable: bool repoman_project_present: bool end Environment // Stage is a planned action with a description for the user. type Stage = struct id: string description: string is_change: bool end Stage fn detect_environment(home_dir: string): Environment let user: string = env.get_env_or("USER", "") let lan_ip: string = detect_host_lan_ip() // incus reachable? let incus_pid: int = incus.process_run_silent("incus", ["version"]) mut incus_ok: bool = false if incus_pid >= 0 incus_ok = p.process_wait(incus_pid) == 0 end if // repoman project (only if incus is up) mut project_ok: bool = false if incus_ok let pe = incus.project_present("repoman") if rg.is_ok(pe) project_ok = rg.unwrap_ok(pe) end if end if return Environment { home_dir: home_dir, user: user, host_lan_ip: lan_ip, incus_reachable: incus_ok, repoman_project_present: project_ok } end detect_environment fn detect_host_lan_ip(): string let cap = incus.process_run_capture("ip", ["-4", "addr", "show", "br0"]) if cap.exit_code != 0 return "" end if // Look for the first occurrence of "inet " and grab the IPv4 token after it // until the next slash or space. `cap.stdout` is small, hand-roll the scan. let s: string = cap.stdout let n: int = str.length(s) let needle: string = "inet " let needle_len: int = 5 mut i: int = 0 while i + needle_len <= n if str.substring(s, i, needle_len) == needle let start: int = i + needle_len mut end_idx: int = start while end_idx < n and s[end_idx] != '/' and s[end_idx] != ' ' end_idx = end_idx + 1 end while return str.substring(s, start, end_idx - start) end if i = i + 1 end while return "" end detect_host_lan_ip fn plan_stages(env: Environment): [Stage] mut stages: [Stage] = new [Stage](2) stages[0] = Stage { id: "incus_project", description: "ensure Incus project 'repoman' exists", is_change: not env.repoman_project_present } stages[1] = Stage { id: "registry_defaults", description: "write registry defaults (schema 3, [host].lan_ip)", is_change: true } return stages end plan_stages fn apply_stage(stage: Stage, env: Environment, reg: config.Registry): rg.Result[config.Registry, string] if stage.id == "incus_project" if env.repoman_project_present return @Result[config.Registry, string].Ok(reg) end if let r = incus.project_ensure("repoman", false) if rg.is_err(r) return @Result[config.Registry, string].Err(rg.unwrap_err(r)) end if return @Result[config.Registry, string].Ok(reg) end if if stage.id == "registry_defaults" let new_host = config.Host { lan_ip: env.host_lan_ip } let new_reg = config.Registry { schema: 3, host: new_host, output: reg.output, defaults: reg.defaults, projects: reg.projects } // Ensure the user-profile-shadow dir exists so users can drop overrides // there without mkdir ceremony. iodir.create_dir_all is idempotent. let profiles_d: string = profile.user_dir(env.home_dir) let _md: bool = iodir.create_dir_all(profiles_d) return @Result[config.Registry, string].Ok(new_reg) end if return @Result[config.Registry, string].Err("unknown stage id: " + stage.id) end apply_stage fn cmd_setup(argv: [string]): int let parser: flag.FlagParser = flag.flag_parser_from(argv) flag.application(parser, "repoman setup") flag.description(parser, "First-time host bootstrap: incus project, registry, host LAN IP detection") let _f1 = flag.bool_flag(parser, "non-interactive", '\0', false, "accept all defaults without prompting") if not flag.parse(parser) console.printErr("repoman: error: " + flag.error(parser)) return 2 end if let non_interactive: bool = flag.get_bool(parser, "non-interactive") let home: string = env.get_env_or("HOME", "") if str.length(home) == 0 console.printErr("repoman: error: HOME is not set") return 3 end if let env_snap = detect_environment(home) if not env_snap.incus_reachable console.printErr("repoman: error: 'incus' is not installed or not on PATH") return 3 end if // Plan & display let stages = plan_stages(env_snap) println("") println("repoman setup plan:") let n: int = stages.length() mut i: int = 0 while i < n let st = stages[i] mut marker: string = " " if st.is_change marker = " * " end if println(marker + "[" + st.id + "] " + st.description) i = i + 1 end while println("") println("* = will change; otherwise no-op") println("") if not non_interactive let proceed = console.confirm("proceed?") if not proceed println("aborted") return 4 end if end if // Load registry (or init) let reg_r = config.load_or_init(home) if rg.is_err(reg_r) console.printErr("repoman: error: " + rg.unwrap_err(reg_r)) return 3 end if mut reg: config.Registry = rg.unwrap_ok(reg_r) // Apply stages mut k: int = 0 while k < n let st = stages[k] println("==> [" + st.id + "] " + st.description) let r = apply_stage(st, env_snap, reg) if rg.is_err(r) console.printErr("repoman: error: " + rg.unwrap_err(r)) return 1 end if reg = rg.unwrap_ok(r) k = k + 1 end while // Persist registry let cfg_path: string = config.registry_path(home) let saved = config.save(reg, cfg_path) if rg.is_err(saved) console.printErr("repoman: error: " + rg.unwrap_err(saved)) return 1 end if println("") println("setup complete.") println("") println(" next: repoman profile install --all (install vendor profile library)") println(" repoman new ") println(" repoman list") return 0 end cmd_setup end module