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
|