|
root / src / setup.reef
setup.reef Reef 252 lines 7.8 KB
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
/******************************************************************************
                __               ____                __   
               / /   ___  ____ _/ __/_____________ _/ /__ 
              / /   / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
             / /___/  __/ /_/ / __(__  ) /__/ /_/ / /  __/
            /_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/ 

    (C)opyright 2026, Leafscale, LLC -  https://www.leafscale.com

    Project: repoman
   Filename: src/setup.reef
    Authors: Chris Tusa <chris.tusa@leafscale.com>
    License: <see LICENSE file included with this source code>
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 <name>")
    println("        repoman list")
    return 0
end cmd_setup

end module