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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
|
/******************************************************************************
__ ____ __
/ / ___ ____ _/ __/_____________ _/ /__
/ / / _ \/ __ `/ /_/ ___/ ___/ __ `/ / _ \
/ /___/ __/ /_/ / __(__ ) /__/ /_/ / / __/
/_____/\___/\__,_/_/ /____/\___/\__,_/_/\___/
(C)opyright 2026, Leafscale, LLC - https://www.leafscale.com
Project: repoman
Filename: src/incus.reef
Authors: Chris Tusa <chris.tusa@leafscale.com>
License: <see LICENSE file included with this source code>
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 <p>
// --format csv -c s <name>` 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 <args>`. 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 <name>` 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 <p> <name>` 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 <project> <name>`). 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
|