///|
pub(all) struct CliOutcome {
exit_code : Int
stdout : String
stderr : String
}
///|
priv struct ParsedNew {
help : Bool
json : Bool
options : ProjectOptions?
}
///|
priv struct ParsedDoctor {
help : Bool
json : Bool
fix : Bool
options : DoctorOptions
}
///|
priv struct ParsedAddPlatform {
help : Bool
json : Bool
options : AddPlatformOptions?
}
///|
priv struct ParsedPackage {
help : Bool
json : Bool
options : PackageReportOptions
}
///|
fn root_usage() -> String {
(
#|MoUI command-line tools
#|
#|Usage:
#| moui new [options]
#| moui add platform [options]
#| moui doctor [options]
#| moui package [options]
#| moui dev [package] [options]
#| moui build [options]
#| moui run [options]
#| moui devices [options]
#| moui verify [options]
#| moui config [args]
#| moui --version
#|
#|Commands:
#| new Create an independent MoUI project.
#| add platform Add platforms to an existing project.
#| doctor Check the selected platform toolchains.
#| package Package a desktop/web app as a distributable artifact,
#| or plan a unified release manifest (legacy mode).
#| dev Build, watch, and restart a MoUI project.
#| build Build an app artifact for any platform (desktop in
#| place, mobile APK / .app / .hap).
#| run Run an app: desktop builds and launches in place,
#| web serves through the dev runner, mobile builds,
#| installs, and launches on a device or emulator.
#| devices List connected mobile devices, emulators, and the
#| desktop host.
#| verify Verify embedded-runtime evidence.
#| config Get/set MoUI CLI configuration values (XDG-backed).
#|
#|Options:
#| -h, --help Show help.
#| --version Show the CLI version and default MoUI dependency version.
)
}
///|
fn new_usage() -> String {
(
#|Usage: moui new [options]
#|
#|Options:
#| --id ID Project identifier. Defaults to the target name.
#| --module OWNER/NAME MoonBit module. Defaults to local/.
#| --title TITLE Display title. Defaults from the project id.
#| --template NAME App skeleton: counter (default) or hello.
#| --moui-version VERSION MoUI dependency version written into moon.mod.
#| Defaults to 0.2.2. Independent from CLI version.
#| --platform PLATFORM Add a target platform; may be repeated.
#| Values: web, macos, windows, linux, android,
#| ios, harmonyos, or all.
#| --bundle-id ID Required when a mobile platform is selected.
#| --json Emit machine-readable output.
#| -h, --help Show this help.
)
}
///|
fn doctor_usage() -> String {
(
#|Usage: moui doctor [options]
#|
#|Options:
#| --project-root PATH Project root. Defaults to the current directory.
#| --platform PLATFORM Check one platform; may be repeated.
#| --all Check every supported platform.
#| --fix Apply safe local remediations (moon.work/README).
#| --json Emit machine-readable output.
#| -h, --help Show this help.
)
}
///|
fn add_usage() -> String {
(
#|Usage: moui add platform [options]
#|
#|Add one or more platforms to an existing MoUI project.
#|
#|Options:
#| --project-root PATH Project root. Defaults to the current directory.
#| --platform PLATFORM Additional platform; may be repeated.
#| --bundle-id ID Required when adding the first mobile platform.
#| --json Emit machine-readable output.
#| -h, --help Show this help.
)
}
///|
fn package_usage() -> String {
(
#|Usage: moui package [options]
#|
#|Validate project metadata and plan platform artifacts. The default keeps
#|the legacy inventory report; --manifest or --output emits schema 1 JSON.
#|
#|Options:
#| --project-root PATH Project root. Defaults to the current directory.
#| --platform PLATFORM Package one platform; may be repeated.
#| --version VERSION Release version override.
#| --output DIR Artifact output directory.
#| --manifest PATH Write the unified moui-package.json manifest.
#| --build Mark the plan for a host build (never claims built).
#| --dry-run Validate and print the plan without writing files.
#| --json Emit machine-readable output.
#| -h, --help Show this help.
)
}
///|
fn stdout_outcome(text : String) -> CliOutcome {
{ exit_code: 0, stdout: text, stderr: "", }
}
///|
fn failed_outcome(
message : String,
json : Bool,
usage : String,
exit_code? : Int = 2,
) -> CliOutcome {
let stderr = if json {
let value : Json = {
"schemaVersion": 1,
"ok": false,
"error": { "kind": "command", "message": message },
}
value.stringify(indent=2) + "\n"
} else if usage == "" {
"error: " + message + "\n"
} else {
"error: " + message + "\n\n" + usage
}
{ exit_code, stdout: "", stderr, }
}
// CLI argument helpers (`json_requested`, `option_parts`, `option_value`,
// `flag_without_value`, `parse_string_option`, `parse_string_list`) now live
// in `cli_args.mbt`.
///|
fn parse_new_args(
args : Array[String],
cwd : String,
host : HostPlatform,
) -> Result[ParsedNew, String] {
let target : Ref[String?] = Ref(None)
let id : Ref[String?] = Ref(None)
let module_name : Ref[String?] = Ref(None)
let title : Ref[String?] = Ref(None)
let moui_version : Ref[String?] = Ref(None)
let template : Ref[String?] = Ref(None)
let bundle_id : Ref[String?] = Ref(None)
let platforms : Array[String] = []
let help = Ref(false)
let json = Ref(false)
let mut index = 0
while index < args.length() {
let argument = args[index]
let (name, inline) = option_parts(argument)
match name {
"--id"
| "--module"
| "--title"
| "--template"
| "--moui-version"
| "--platform"
| "--bundle-id" => {
let (value, next) = match option_value(args, index, inline, name) {
Ok(result) => result
Err(message) => return Err(message)
}
match name {
"--id" => id.val = Some(value)
"--module" => module_name.val = Some(value)
"--title" => title.val = Some(value)
"--template" => template.val = Some(value)
"--moui-version" => moui_version.val = Some(value)
"--platform" => platforms.push(value)
"--bundle-id" => bundle_id.val = Some(value)
_ => ()
}
index = next
}
"--json" => {
match flag_without_value(inline, name) {
Ok(_) => ()
Err(message) => return Err(message)
}
json.val = true
index += 1
}
"-h" | "--help" => {
match flag_without_value(inline, name) {
Ok(_) => ()
Err(message) => return Err(message)
}
help.val = true
index += 1
}
_ if argument.has_prefix("-") =>
return Err("unknown option for moui new: " + name)
_ => {
match target.val {
None => target.val = Some(argument)
Some(_) => return Err("moui new accepts exactly one target path")
}
index += 1
}
}
}
if help.val {
return Ok({ help: true, json: json.val, options: None, })
}
guard target.val is Some(raw_target) else {
return Err("missing target path for moui new")
}
let resolved_target = resolve_path(cwd, raw_target)
let project_id = id.val.unwrap_or_else(() => project_id_from_path(raw_target))
let selected = match normalize_platforms(host, platforms) {
Ok(value) => value
Err(message) => return Err(message)
}
let template_name = template.val.unwrap_or("counter").to_lower()
if template_name != "counter" && template_name != "hello" {
return Err(
"unsupported template: " + template_name + " (expected counter|hello)",
)
}
let options : ProjectOptions = {
target: resolved_target,
id: project_id,
module_name: module_name.val.unwrap_or("local/" + project_id),
title: title.val.unwrap_or_else(() => default_title(project_id)),
moui_version: moui_version.val.unwrap_or(default_moui_dependency_version),
moui_skia_renderer_version: default_moui_skia_renderer_dependency_version,
template: template_name,
platforms: selected,
bundle_id: bundle_id.val,
}
match validate_project_options(options) {
Ok(_) => Ok({ help: false, json: json.val, options: Some(options), })
Err(message) => Err(message)
}
}
///|
fn parse_doctor_args(
args : Array[String],
cwd : String,
) -> Result[ParsedDoctor, String] {
let project_root = Ref(cwd)
let platforms : Array[String] = []
let all = Ref(false)
let help = Ref(false)
let json = Ref(false)
let fix = Ref(false)
let mut index = 0
while index < args.length() {
let argument = args[index]
let (name, inline) = option_parts(argument)
match name {
"--project-root" | "--platform" => {
let (value, next) = match option_value(args, index, inline, name) {
Ok(result) => result
Err(message) => return Err(message)
}
if name == "--project-root" {
project_root.val = resolve_path(cwd, value)
} else {
platforms.push(value)
}
index = next
}
"--all" | "--fix" | "--json" | "-h" | "--help" => {
match flag_without_value(inline, name) {
Ok(_) => ()
Err(message) => return Err(message)
}
match name {
"--all" => all.val = true
"--fix" => fix.val = true
"--json" => json.val = true
"-h" | "--help" => help.val = true
_ => ()
}
index += 1
}
_ => return Err("unknown option for moui doctor: " + name)
}
}
Ok({
help: help.val,
json: json.val,
fix: fix.val,
options: { project_root: project_root.val, platforms, all: all.val, },
})
}
///|
fn parse_add_platform_args(
args : Array[String],
cwd : String,
) -> Result[ParsedAddPlatform, String] {
let project_root = Ref(cwd)
let platforms : Array[String] = []
let bundle_id : Ref[String?] = Ref(None)
let help = Ref(false)
let json = Ref(false)
let mut index = 0
while index < args.length() {
let argument = args[index]
let (name, inline) = option_parts(argument)
match name {
"--project-root" | "--platform" | "--bundle-id" => {
let (value, next) = match option_value(args, index, inline, name) {
Ok(result) => result
Err(message) => return Err(message)
}
match name {
"--project-root" => project_root.val = resolve_path(cwd, value)
"--platform" => platforms.push(value)
"--bundle-id" => bundle_id.val = Some(value)
_ => ()
}
index = next
}
"--json" | "-h" | "--help" => {
match flag_without_value(inline, name) {
Ok(_) => ()
Err(message) => return Err(message)
}
if name == "--json" {
json.val = true
} else {
help.val = true
}
index += 1
}
_ if argument.has_prefix("-") =>
return Err("unknown option for moui add platform: " + name)
_ => {
platforms.push(argument)
index += 1
}
}
}
if help.val {
return Ok({ help: true, json: json.val, options: None, })
}
if platforms.is_empty() {
return Err("moui add platform requires at least one platform")
}
Ok({
help: false,
json: json.val,
options: Some({
project_root: project_root.val,
platforms,
bundle_id: bundle_id.val,
}),
})
}
///|
fn parse_package_args(
args : Array[String],
cwd : String,
) -> Result[ParsedPackage, String] {
let project_root = Ref(cwd)
let help = Ref(false)
let json = Ref(false)
let platforms : Ref[Array[String]] = Ref([])
let version : Ref[String?] = Ref(None)
let output_dir : Ref[String?] = Ref(None)
let manifest_path : Ref[String?] = Ref(None)
let build = Ref(false)
let dry_run = Ref(false)
let mut index = 0
while index < args.length() {
let argument = args[index]
let (name, inline) = option_parts(argument)
match name {
"--platform" => {
let (value, next) = match option_value(args, index, inline, name) {
Ok(result) => result
Err(message) => return Err(message)
}
platforms.val.push(value)
index = next
}
"--version" | "--output" | "--manifest" => {
let (value, next) = match option_value(args, index, inline, name) {
Ok(result) => result
Err(message) => return Err(message)
}
match name {
"--version" => version.val = Some(value)
"--output" => output_dir.val = Some(resolve_path(cwd, value))
"--manifest" => manifest_path.val = Some(resolve_path(cwd, value))
_ => ()
}
index = next
}
"--build" | "--dry-run" => {
match flag_without_value(inline, name) {
Ok(_) => ()
Err(message) => return Err(message)
}
if name == "--build" {
build.val = true
} else {
dry_run.val = true
}
index += 1
}
"--project-root" => {
let (value, next) = match option_value(args, index, inline, name) {
Ok(result) => result
Err(message) => return Err(message)
}
project_root.val = resolve_path(cwd, value)
index = next
}
"--json" | "-h" | "--help" => {
match flag_without_value(inline, name) {
Ok(_) => ()
Err(message) => return Err(message)
}
if name == "--json" {
json.val = true
} else {
help.val = true
}
index += 1
}
_ => return Err("unknown option for moui package: " + name)
}
}
Ok({
help: help.val,
json: json.val,
options: {
project_root: project_root.val,
platforms: platforms.val,
version: version.val,
output_dir: output_dir.val,
manifest_path: manifest_path.val,
build: build.val,
dry_run: dry_run.val,
},
})
}
///|
fn run_new(args : Array[String], cwd : String) -> CliOutcome {
let json = json_requested(args)
let parsed = match parse_new_args(args, cwd, detect_host_platform()) {
Ok(value) => value
Err(message) => return failed_outcome(message, json, new_usage())
}
if parsed.help {
return stdout_outcome(new_usage())
}
guard parsed.options is Some(options) else {
return failed_outcome(
"new command options are unavailable",
parsed.json,
"",
)
}
match generate_project(options) {
Ok(files) =>
if parsed.json {
let value : Json = {
"schemaVersion": 1,
"ok": true,
"command": "new",
"target": options.target,
"files": files,
}
stdout_outcome(value.stringify(indent=2) + "\n")
} else {
stdout_outcome(
"Created MoUI project at " +
options.target +
"\nGenerated \{files.length()} files.\n",
)
}
Err(message) => failed_outcome(message, parsed.json, "")
}
}
///|
fn run_doctor_command(args : Array[String], cwd : String) -> CliOutcome {
let json = json_requested(args)
let parsed = match parse_doctor_args(args, cwd) {
Ok(value) => value
Err(message) => return failed_outcome(message, json, doctor_usage())
}
if parsed.help {
return stdout_outcome(doctor_usage())
}
let fixes : Array[String] = if parsed.fix {
match apply_doctor_fixes(parsed.options) {
Ok(value) => value
Err(message) => return failed_outcome(message, parsed.json, "")
}
} else {
[]
}
match run_doctor(parsed.options) {
Ok(report) => doctor_report_outcome(report, parsed.json, fixes~)
Err(message) => failed_outcome(message, parsed.json, "")
}
}
///|
fn doctor_report_outcome(
report : DoctorReport,
json : Bool,
fixes? : Array[String] = [],
) -> CliOutcome {
let text = if json {
let root = @json.parse(report.to_json_text()) catch {
_ =>
return {
exit_code: if report.ok {
0
} else {
1
},
stdout: report.to_json_text(),
stderr: "",
}
}
guard root is Object(fields) else {
return {
exit_code: if report.ok {
0
} else {
1
},
stdout: report.to_json_text(),
stderr: "",
}
}
fields["fixes"] = fixes.to_json()
Json::object(fields).stringify(indent=2) + "\n"
} else {
let builder = StringBuilder()
builder.write_string(report.to_human_text())
if !fixes.is_empty() {
builder.write_string("Applied fixes:\n")
for action in fixes {
builder.write_string("- " + action + "\n")
}
}
builder.to_string()
}
{ exit_code: if report.ok { 0 } else { 1 }, stdout: text, stderr: "", }
}
///|
fn run_add_platform(args : Array[String], cwd : String) -> CliOutcome {
let json = json_requested(args)
let parsed = match parse_add_platform_args(args, cwd) {
Ok(value) => value
Err(message) => return failed_outcome(message, json, add_usage())
}
if parsed.help {
return stdout_outcome(add_usage())
}
guard parsed.options is Some(options) else {
return failed_outcome(
"add platform options are unavailable",
parsed.json,
"",
)
}
match add_platforms_to_project(options) {
Ok((added, files)) =>
if parsed.json {
let value : Json = {
"schemaVersion": 1,
"ok": true,
"command": "add platform",
"projectRoot": options.project_root,
"addedPlatforms": added,
"files": files,
}
stdout_outcome(value.stringify(indent=2) + "\n")
} else {
stdout_outcome(
"Added platforms: " +
added.join(", ") +
"\nWrote \{files.length()} files under " +
options.project_root +
"\n",
)
}
Err(message) => failed_outcome(message, parsed.json, "")
}
}
///|
fn run_package_command(args : Array[String], cwd : String) -> CliOutcome {
let json = json_requested(args)
let parsed = match parse_package_args(args, cwd) {
Ok(value) => value
Err(message) => return failed_outcome(message, json, package_usage())
}
if parsed.help {
return stdout_outcome(package_usage())
}
if parsed.options.manifest_path is Some(_) ||
parsed.options.output_dir is Some(_) ||
parsed.options.build ||
parsed.options.dry_run ||
!parsed.options.platforms.is_empty() ||
parsed.options.version is Some(_) {
match package_manifest_outcome(parsed.options, parsed.json) {
Ok(outcome) => return outcome
Err(message) => return failed_outcome(message, parsed.json, "")
}
}
if parsed.json {
match package_report_json(parsed.options) {
Ok(text) => stdout_outcome(text)
Err(message) => failed_outcome(message, true, "")
}
} else {
match package_report_text(parsed.options) {
Ok(text) => stdout_outcome(text)
Err(message) => failed_outcome(message, false, "")
}
}
}
///|
fn version_output(json : Bool) -> CliOutcome {
if json {
let value : Json = {
"schemaVersion": 1,
"ok": true,
"command": "version",
"version": cli_version(),
"defaultMouiDependencyVersion": default_moui_dependency_version,
"note": "CLI version and default wzzc-dev/moui dependency version are independent.",
}
stdout_outcome(value.stringify(indent=2) + "\n")
} else {
stdout_outcome(
"moui " +
cli_version() +
"\ndefault moui dependency: wzzc-dev/moui@" +
default_moui_dependency_version +
"\n(CLI version and dependency version are independent)\n",
)
}
}
///|
pub fn run_cli(args : Array[String], cwd : String) -> CliOutcome {
if args.is_empty() {
return stdout_outcome(root_usage())
}
match args[0] {
"-h" | "--help" => stdout_outcome(root_usage())
"--version" =>
if args.length() == 1 {
version_output(false)
} else if args.length() == 2 && args[1] == "--json" {
version_output(true)
} else {
failed_outcome(
"--version only accepts the optional --json flag",
json_requested(args),
root_usage(),
)
}
"new" => run_new(args[1:].to_owned(), cwd)
"build" =>
match parse_build_args(args[1:].to_owned(), cwd, detect_host_platform()) {
Ok(parsed) => run_build(parsed, detect_host_platform())
Err(message) =>
failed_outcome(message, json_requested(args), build_usage())
}
"add" =>
if args.length() == 1 ||
(args.length() == 2 && (args[1] == "-h" || args[1] == "--help")) {
stdout_outcome(add_usage())
} else if args[1] == "platform" {
run_add_platform(args[2:].to_owned(), cwd)
} else {
failed_outcome(
"unknown moui add command: " + args[1],
json_requested(args),
add_usage(),
)
}
"doctor" => run_doctor_command(args[1:].to_owned(), cwd)
"package" => {
let rest = args[1:].to_owned()
if package_build_requested(rest) {
run_package_build(rest, cwd)
} else {
run_package_command(rest, cwd)
}
}
"dev" =>
match parse_dev_args(args[1:].to_owned(), cwd) {
Ok(parsed) => run_dev(parsed)
Err(message) =>
failed_outcome(message, json_requested(args), dev_usage())
}
"run" =>
match parse_run_args(args[1:].to_owned(), cwd, detect_host_platform()) {
Ok(parsed) => run_run(parsed, cwd, detect_host_platform())
Err(message) =>
failed_outcome(message, json_requested(args), run_usage())
}
"devices" =>
match parse_devices_args(args[1:].to_owned()) {
Ok(parsed) => run_devices(parsed, detect_host_platform())
Err(message) =>
failed_outcome(message, json_requested(args), devices_usage())
}
"verify" =>
match
parse_verify_args(args[1:].to_owned(), cwd, detect_host_platform()) {
Ok(parsed) => run_verify(parsed, detect_host_platform())
Err(message) =>
failed_outcome(message, json_requested(args), verify_usage())
}
"config" =>
match parse_config_args(args[1:].to_owned()) {
Ok(parsed) => run_config(parsed, detect_host_platform())
Err(message) =>
failed_outcome(message, json_requested(args), config_usage())
}
command =>
failed_outcome(
"unknown moui command: " + command,
json_requested(args),
root_usage(),
)
}
}