// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
// `mctl serve`, the half of uvicorn's `--reload` a compiled language needs a tool for: a server
// cannot re-import its modules, so something outside it has to rebuild and restart it. Reading the
// command line and telling what changed are here, without touching the disk; the binary owns the
// processes.
///|
/// How long to wait between looks at the sources, in milliseconds (← uvicorn's `--reload-delay`,
/// 0.25 seconds).
pub let reload_delay : Int32 = 250
///|
/// Which directories are watched when none are named (← uvicorn's `--reload-dir`, the current
/// directory).
pub let reload_dirs : Array[String] = ["."]
///|
/// What `mctl serve` was asked to do: the package to build and run, whether to rebuild it on change,
/// where and how often to look, and the arguments that go to the server.
pub(all) struct Serve {
pkg : String
reload : Bool
dirs : Array[String]
delay : Int32
args : Array[String]
}
///|
/// Read `mctl serve [package] [--reload] [--reload-dir dir]... [--reload-delay seconds] [-- args]`.
/// Everything after `--` is the server's own.
pub fn parse_serve(argv : ArrayView[String]) -> Serve raise {
let mut pkg : String? = None
let mut reload = false
let dirs : Array[String] = []
let mut delay = reload_delay
let args : Array[String] = []
let mut i = 0
let value = (flag : String) => {
guard i + 1 < argv.length() else { fail("\{flag} needs a value") }
i = i + 1
argv[i]
}
while i < argv.length() {
match argv[i] {
"--" => {
args.push_iter(argv[i + 1:].iter())
break
}
"--reload" => reload = true
"--reload-dir" => dirs.push(value("--reload-dir"))
"--reload-delay" => {
let seconds = value("--reload-delay")
let parsed = @json.parse(seconds) catch {
_ => fail("--reload-delay takes seconds, not \{seconds}")
}
guard parsed is Number(n, ..) && n > 0.0 else {
fail("--reload-delay takes seconds, not \{seconds}")
}
delay = (n * 1000.0).to_int()
}
flag if flag.has_prefix("--") => fail("mctl serve: unknown flag \{flag}")
name => {
guard pkg is None else { fail("mctl serve: one package, not two") }
pkg = Some(name)
}
}
i = i + 1
}
{
pkg: pkg.unwrap_or("."),
reload,
dirs: if dirs.length() == 0 {
reload_dirs
} else {
dirs
},
delay,
args,
}
}
///|
/// Whether a save to this file calls for a rebuild: MoonBit source, or the manifests that say how
/// it is built.
pub fn is_watched(name : String) -> Bool {
name.has_suffix(".mbt") ||
name.has_suffix(".mbti") ||
name == "moon.pkg" ||
name == "moon.mod"
}
///|
/// Whether a directory is left out of the walk: what the build itself writes, and anything hidden.
pub fn is_skipped(dir : String) -> Bool {
let parts = dir.split("/").collect()
let base = parts[parts.length() - 1].to_owned()
base == "_build" ||
base == "target" ||
(base.has_prefix(".") && base != "." && base != "..")
}
///|
/// The executable a `moon run --build-only` built, from what it prints.
pub fn built_binary(output : String) -> String? {
let json = @json.parse(output) catch { _ => return None }
guard json is Object(fields) else { return None }
guard fields.get("artifacts_path") is Some(Array(paths)) else { return None }
match paths {
[String(path), ..] => Some(path)
_ => None
}
}